В языках программирования, таких как C и C ++, люди часто ссылаются на статическое и динамическое распределение памяти. Я понимаю концепцию, но фраза «Вся память была выделена (зарезервирована) во время компиляции» всегда смущает меня.
Компиляция, насколько я понимаю, преобразует код высокого уровня C / C ++ в машинный язык и выводит исполняемый файл. Как распределяется память в скомпилированном файле? Разве память не всегда выделяется в ОЗУ всеми средствами управления виртуальной памятью?
Разве распределение памяти по определению не является концепцией времени выполнения?
Если я сделаю статически выделенную переменную размером 1 КБ в своем коде C / C ++, увеличит ли это размер исполняемого файла на ту же величину?
Это одна из страниц, где фраза используется под заголовком «Статическое распределение».
Память, выделенная во время компиляции, означает, что компилятор разрешает во время компиляции, где определенные вещи будут выделяться внутри карты памяти процесса.
Например, рассмотрим глобальный массив:
int array[100];
Компилятор знает во время компиляции размер массива и размер int
, так что он знает весь размер массива во время компиляции. Также глобальная переменная имеет статическую длительность хранения по умолчанию: она размещается в области статической памяти пространства памяти процесса (раздел .data / .bss). Учитывая эту информацию, во время компиляции компилятор решает, в каком адресе этой статической области памяти будет находиться массив.
Конечно, адреса памяти являются виртуальными адресами. Программа предполагает, что она имеет собственное пространство памяти (например, от 0x00000000 до 0xFFFFFFFF). Вот почему компилятор может делать предположения типа «Хорошо, массив будет по адресу 0x00A33211». Во время выполнения эти адреса преобразуются в реальные / аппаратные адреса MMU и ОС.
Значение инициализированного статического хранилища несколько отличается. Например:
int array[] = { 1 , 2 , 3 , 4 };
В нашем первом примере компилятор только решил, где будет размещен массив, сохранив эту информацию в исполняемом файле.
В случае вещей, инициализированных значением, компилятор также вводит начальное значение массива в исполняемый файл и добавляет код, который сообщает загрузчику программы, что после выделения массива при запуске программы массив должен быть заполнен этими значениями.
Вот два примера сборки, сгенерированной компилятором (GCC4.8.1 с целью x86):
Код C ++:
int a[4];
int b[] = { 1 , 2 , 3 , 4 };
int main()
{}
Выходная сборка:
a:
.zero 16
b:
.long 1
.long 2
.long 3
.long 4
main:
pushq %rbp
movq %rsp, %rbp
movl $0, %eax
popq %rbp
ret
Как видите, значения напрямую вводятся в сборку. В массиве a
компилятор генерирует нулевую инициализацию 16 байтов, потому что стандарт говорит, что статические хранимые вещи должны быть по умолчанию инициализированы нулем:
8.5.9 (инициализаторы) [Примечание]:
Каждый объект статической длительности хранения инициализируется нулями в
запуск программы перед любой другой инициализацией. В некоторых
случаев, дополнительная инициализация выполняется позже.
Я всегда предлагаю людям разобрать свой код, чтобы посмотреть, что на самом деле делает компилятор с кодом C ++. Это относится от классов хранения / продолжительности (как этот вопрос) к расширенной оптимизации компилятора. Вы можете поручить вашему компилятору сгенерировать сборку, но есть замечательные инструменты, чтобы сделать это в Интернете дружественным образом. Мой любимый GCC Explorer.
Память, выделенная во время компиляции, просто означает, что больше не будет выделяться во время выполнения — нет вызовов для malloc, новых или других методов динамического выделения. У вас будет фиксированный объем использования памяти, даже если вам не нужна вся эта память все время.
Разве распределение памяти по определению не является концепцией времени выполнения?
Память не в использовании до начала выполнения, но непосредственно перед началом выполнения, его распределение обрабатывается системой.
Если я сделаю статически выделенную переменную размером 1 КБ в своем коде C / C ++, увеличит ли это размер исполняемого файла на ту же величину?
Простое объявление статического кода не увеличит размер вашего исполняемого файла более чем на несколько байтов. Объявление его с начальным значением, которое является ненулевым, будет (чтобы держать это начальное значение). Скорее, компоновщик просто добавляет этот объем в 1 КБ к требованию к памяти, которое системный загрузчик создает для вас непосредственно перед выполнением.
Память, выделенная во время компиляции, означает, что при загрузке программы некоторая часть памяти будет выделена немедленно, а размер и (относительная) позиция этого выделения определяются во время компиляции.
char a[32];
char b;
char c;
Эти 3 переменные «выделяются во время компиляции», это означает, что компилятор вычисляет их размер (который является фиксированным) во время компиляции. Переменная a
будет смещение в памяти, скажем, указывая на адрес 0, b
укажет на адрес 33 и c
на 34 (предположим, нет оптимизации выравнивания). Так, выделение 1 КБ статических данных не увеличит размер вашего кода, так как он просто изменит смещение внутри него. Фактическое пространство будет выделено во время загрузки.
Реальное выделение памяти всегда происходит во время выполнения, потому что ядру необходимо отслеживать его и обновлять свои внутренние структуры данных (сколько памяти выделяется для каждого процесса, страниц и т. Д.). Разница в том, что компилятор уже знает размер всех данных, которые вы собираетесь использовать, и он выделяется, как только ваша программа выполняется.
Помните также, что мы говорим о относительные адреса. Реальный адрес, где будет расположена переменная, будет другим. Во время загрузки ядро зарезервирует некоторую память для процесса, скажем, по адресу x
и все жестко закодированные адреса, содержащиеся в исполняемом файле, будут увеличены на x
байтов, так что переменная a
в примере будет по адресу x
, б по адресу x+33
и так далее.
Добавление в стек переменных, занимающих N байт, не обязательно увеличивает размер бина на N байт. Фактически он будет добавлять только несколько байтов большую часть времени.
Давайте начнем с примера того, как добавить 1000 символов в ваш код будут увеличить размер корзины линейным способом.
Если 1k — строка из тысячи символов, которая объявлена так
const char *c_string = "Here goes a thousand chars...999";//implicit \0 at end
и вы тогда должны были vim your_compiled_bin
, вы на самом деле сможете увидеть эту строку в корзине где-то. В таком случае, да: исполняемый файл будет на 1 кб больше, потому что он содержит строку полностью.
Если, однако, вы выделяете массив int
s, char
с или long
s в стеке и назначить его в цикле, что-то вроде этого
int big_arr[1000];
for (int i=0;i<1000;++i) big_arr[i] = some_computation_func(i);
тогда нет: это не увеличит мусорное ведро … на 1000*sizeof(int)
Распределение во время компиляции означает то, что вы теперь поняли (на основе ваших комментариев): скомпилированная корзина содержит информацию, необходимую системе, чтобы знать, сколько памяти понадобится функции / блоку при ее выполнении, а также информацию о размер стека, который требуется вашему приложению. Это то, что система выделит, когда выполнит ваш бин, и ваша программа станет процессом (ну, выполнение вашего бина — это процесс, который … ну, вы понимаете, что я говорю).
Конечно, я не рисую полную картину здесь: корзина содержит информацию о том, какой большой стек будет на самом деле нужен корзине. Основываясь на этой информации (помимо прочего), система зарезервирует часть памяти, называемую стеком, для которой программа получает вид свободного управления. Стековая память все еще выделяется системой, когда инициируется процесс (результат выполнения вашего бина). Затем процесс управляет памятью стека для вас. Когда функция или цикл (любой тип блока) вызывается / выполняется, переменные, локальные для этого блока, помещаются в стек и удаляются (память стека «Освобождены» так сказать) для использования другими функциями / блоками. Так декларируя int some_array[100]
добавит в корзину только несколько байтов дополнительной информации, которая сообщит системе, что для функции X потребуется 100*sizeof(int)
+ немного места для бухгалтерии дополнительно.
На многих платформах все глобальные или статические выделения в каждом модуле будут объединены компилятором в три или менее консолидированных распределения (одно для неинициализированных данных (часто называемых «bss»), одно для инициализированных записываемых данных (часто называемых «данные»). ) и один для постоянных данных («const»)), и все глобальные или статические размещения каждого типа в программе будут объединены компоновщиком в одно глобальное для каждого типа. Например, предполагая int
4 байта, модуль имеет следующие статические распределения:
int a;
const int b[6] = {1,2,3,4,5,6};
char c[200];
const int d = 23;
int e[4] = {1,2,3,4};
int f;
он сказал бы компоновщику, что ему нужно 208 байтов для bss, 16 байтов для «данных» и 28 байтов для «const». Кроме того, любая ссылка на переменную будет заменена селектором области и смещением, поэтому a, b, c, d и e будут заменены на bss + 0, const + 0, bss + 4, const + 24, data +0 или bss + 204 соответственно.
Когда программа связана, все области bss из всех модулей объединяются вместе; аналогично данным и константным областям. Для каждого модуля адрес любых относительных к bss переменных будет увеличен на размер областей bss всех предыдущих модулей (опять же, аналогично данным и const). Таким образом, когда компоновщик завершен, любая программа будет иметь одно распределение bss, одно распределение данных и одно постоянное распределение.
Когда программа загружена, в зависимости от платформы обычно происходит одна из четырех вещей:
Исполняемый файл будет указывать, сколько байтов ему нужно для каждого типа данных и — для области инициализированных данных, где может быть найдено начальное содержимое. Он также будет включать в себя список всех инструкций, которые используют адрес bss, data или const. Операционная система или загрузчик выделит соответствующий объем пространства для каждой области, а затем добавит начальный адрес этой области к каждой инструкции, которая в этом нуждается.
Операционная система выделит кусок памяти для хранения всех трех типов данных и даст приложению указатель на этот кусок памяти. Любой код, который использует статические или глобальные данные, будет разыменовывать их относительно этого указателя (во многих случаях указатель будет храниться в регистре в течение всего времени жизни приложения).
Изначально операционная система не будет выделять приложению какую-либо память, за исключением того, что содержит ее двоичный код, но первое, что делает приложение, — это запрашивает подходящее распределение из операционной системы, которое оно всегда будет хранить в реестре.
Операционная система изначально не будет выделять место для приложения, но приложение будет запрашивать подходящее распределение при запуске (как указано выше). Приложение будет содержать список инструкций с адресами, которые необходимо обновить, чтобы отразить, где была выделена память (как в первом стиле), но вместо того, чтобы приложение исправлялось загрузчиком ОС, приложение будет включать в себя достаточно кода для исправления самого себя. ,
Все четыре подхода имеют свои преимущества и недостатки. В любом случае, однако, компилятор объединит произвольное количество статических переменных в фиксированное небольшое количество запросов памяти, а компоновщик объединит все из них в небольшое количество консолидированных распределений. Даже если приложению придется получать часть памяти от операционной системы или загрузчика, именно компилятор и компоновщик отвечают за выделение отдельных частей из этого большого блока всем отдельным переменным, которые в этом нуждаются.
Суть вашего вопроса заключается в следующем: «Как распределяется память» в скомпилированном файле? Разве память не всегда выделяется в ОЗУ со всеми средствами управления виртуальной памятью? Разве распределение памяти по определению не является концепцией времени выполнения? »
Я думаю, что проблема заключается в том, что в распределении памяти участвуют две разные концепции. По своей сути, выделение памяти — это процесс, с помощью которого мы говорим «этот элемент данных хранится в этом конкретном фрагменте памяти». В современной компьютерной системе это включает в себя два этапа:
Последний процесс является чисто во время выполнения, но первый может быть выполнен во время компиляции, если данные имеют известный размер и требуется фиксированное их количество. Вот в основном, как это работает:
Компилятор видит исходный файл, содержащий строку, которая выглядит примерно так:
int c;
Он производит вывод для ассемблера, который инструктирует его зарезервировать память для переменной ‘c’. Это может выглядеть так:
global _c
section .bss
_c: resb 4
Когда ассемблер работает, он сохраняет счетчик, который отслеживает смещения каждого элемента с начала сегмента памяти (или «секции»). Это похоже на части очень большой «структуры», которая содержит все во всем файле, в настоящее время ей не выделено никакой фактической памяти, и она может находиться где угодно. Это отмечает в таблице, что _c
имеет конкретное смещение (скажем, 510 байт от начала сегмента), а затем увеличивает его счетчик на 4, поэтому следующая такая переменная будет иметь (например, 514 байт). Для любого кода, который нуждается в адресе _c
он просто помещает 510 в выходной файл и добавляет примечание, что для вывода нужен адрес сегмента, который содержит _c
добавив к этому позже.
Компоновщик берет все выходные файлы ассемблера и проверяет их. Он определяет адрес для каждого сегмента, чтобы они не перекрывались, и добавляет необходимые смещения, чтобы инструкции по-прежнему ссылались на правильные элементы данных. В случае неинициализированной памяти, как это занято c
(ассемблеру сказали, что память будет неинициализирована из-за того, что компилятор поместил ее в сегмент «.bss», который является именем, зарезервированным для неинициализированной памяти), в выводе он включает поле заголовка, которое сообщает операционной системе сколько нужно зарезервировать. Он может быть перемещен (и обычно так и есть), но обычно предназначен для более эффективной загрузки по одному конкретному адресу памяти, и ОС попытается загрузить его по этому адресу. На данный момент у нас есть довольно хорошее представление о том, какой виртуальный адрес будет использоваться c
,
Физический адрес не будет определен до тех пор, пока не будет запущена программа. Однако, с точки зрения программиста, физический адрес на самом деле не имеет значения — мы даже никогда не узнаем, что это такое, потому что ОС обычно не говорит никому, она может часто меняться (даже во время работы программы), и Основная цель ОС — все равно абстрагироваться.
Исполняемый файл описывает, какое пространство выделить статическим переменным. Это распределение выполняется системой, когда вы запускаете исполняемый файл. Таким образом, ваша статическая переменная размером 1 КБ не увеличит размер исполняемого файла на 1 КБ:
static char[1024];
Если, конечно, вы не укажете инициализатор:
static char[1024] = { 1, 2, 3, 4, ... };
Таким образом, в дополнение к «машинному языку» (то есть инструкциям ЦП) исполняемый файл содержит описание требуемой структуры памяти.
Память может быть распределена разными способами:
Теперь ваш вопрос — что такое «память, выделенная во время компиляции». Определенно, это просто неверно сформулированное высказывание, которое должно относиться либо к двоичному выделению сегмента, либо к выделению стека, либо в некоторых случаях даже к распределению кучи, но в этом случае распределение скрыто от глаз программиста с помощью вызова невидимого конструктора. Или, возможно, человек, который сказал, что просто хотел сказать, что память не выделяется в куче, но не знал о выделении стека или сегмента (или не хотел вдаваться в подробности такого рода).
Но в большинстве случаев человек просто хочет сказать, что объем выделяемой памяти известен во время компиляции.
Размер двоичного файла будет изменяться только тогда, когда память зарезервирована в коде или сегменте данных вашего приложения.