Что означает "память, выделенная во время компиляции"?

В языках программирования, таких как C и С++, люди часто ссылаются на распределение статической и динамической памяти. Я понимаю концепцию, но фраза "Вся память была выделена (зарезервирована) во время компиляции" всегда меня смущает.

Компиляция, как я понимаю, преобразует код высокого уровня C/С++ в машинный язык и выводит исполняемый файл. Как выделяется память в скомпилированном файле? Разве память не всегда распределяется в ОЗУ со всеми файлами управления виртуальной памятью?

Не является ли выделение памяти по определению концепцией времени выполнения?

Если я создаю статически выделенную переменную 1 Кбайт в моем коде на C/С++, это увеличит размер исполняемого файла на ту же сумму?

Это одна из страниц, где фраза используется под заголовком "Статическое распределение".

Назад к основам: распределение памяти, переход вниз по истории

Ответ 1

Память, выделенная во время компиляции, означает, что компилятор решает во время компиляции, когда определенные вещи будут выделены внутри карты памяти процесса.

Например, рассмотрим глобальный массив:

int array[100];

Компилятор знает во время компиляции размер массива и размер int, поэтому он знает весь размер массива во время компиляции. Также глобальная переменная имеет статическую продолжительность хранения по умолчанию: она выделяется в области статической памяти пространства памяти процесса (.data/.bss). Учитывая эту информацию, компилятор решает во время компиляции в каком адресе этой области статической памяти массив будет.

Конечно, адреса памяти - это виртуальные адреса. Программа предполагает, что у нее есть собственное пространство памяти (например, от 0x00000000 до 0xFFFFFFFF). Именно поэтому компилятор мог делать такие предположения, как "Хорошо, массив будет по адресу 0x00A33211". Во время выполнения адреса обращаются к реальным/аппаратным адресам с помощью MMU и ОС.

Значение инициализированного статического хранения вещей немного отличается. Например:

int array[] = { 1 , 2 , 3 , 4 };

В нашем первом примере компилятор только решил, где будет выделен массив, сохраняя эту информацию в исполняемом файле.
В случае инициализированных значениями компилятор также вводит начальное значение массива в исполняемый файл и добавляет код, который сообщает загрузчику программы, что после распределения массива при запуске программы массив должен быть заполнен этими значениями.

Вот два примера сборки, сгенерированной компилятором (GCC4.8.1 с мишенью x86):

Код С++:

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 (Инициализаторы) [Примечание]:
Каждый объект статической продолжительности хранения инициализируется нулем при запуск программы перед любой другой инициализацией. В некоторых случаи, дополнительная инициализация выполняется позже.

Я всегда предлагаю людям разобрать их код, чтобы узнать, что делает компилятор с кодом С++. Это относится к классам хранения/продолжительности (например, к этому вопросу) к передовой оптимизации компилятора. Вы можете поручить своему компилятору сгенерировать сборку, но есть замечательные инструменты для этого в Интернете в дружеской манере. Мой любимый GCC Explorer.

Ответ 2

Память, выделенная во время компиляции, просто означает, что во время выполнения не будет никакого дальнейшего выделения - нет вызовов для malloc, новых или других методов динамического выделения. У вас будет фиксированный объем использования памяти, даже если вам не нужна вся эта память все время.

Не является ли выделение памяти по определению концепцией времени выполнения?

Память не используется до запуска, но непосредственно перед запуском, начиная с ее выделения, обрабатывается системой.

Если я создаю статически выделенную переменную 1 Кбайт в моем коде на C/С++, это увеличит размер исполняемого файла на ту же сумму?

Просто объявление статического не увеличит размер вашего исполняемого файла более чем на несколько байтов. Объявление его с исходным значением, отличным от нуля, будет (для того, чтобы сохранить это начальное значение). Скорее, компоновщик просто добавляет эту сумму 1 КБ к требованию памяти, которое системный загрузчик создает для вас непосредственно перед выполнением.

Ответ 3

Память, выделенная во время компиляции, означает, что когда вы загружаете программу, некоторая часть памяти будет немедленно выделена, а размер и (относительная) позиция этого распределения определяется во время компиляции.

char a[32];
char b;
char c;

Эти 3 переменные "распределяются во время компиляции", это означает, что компилятор вычисляет их размер (который является фиксированным) во время компиляции. Переменная a будет смещением в памяти, скажем, указывая на адрес 0, b будет указывать на адрес 33 и c на 34 (не предполагая оптимизации выравнивания). Таким образом, выделение 1Kb статических данных не увеличит размер вашего кода, так как оно просто изменит смещение внутри него. Фактическое пространство будет выделено во время загрузки.

Реальное распределение памяти всегда происходит во время выполнения, потому что ядру необходимо отслеживать его и обновлять свои внутренние структуры данных (сколько памяти выделено для каждого процесса, страниц и т.д.). Разница в том, что компилятор уже знает размер каждой информации, которую вы собираетесь использовать, и это выделяется, как только ваша программа будет выполнена.

Помните также, что мы говорим об относительных адресах. Реальный адрес, где будет располагаться переменная, будет отличаться. Во время загрузки ядро ​​резервирует некоторую память для процесса, скажем, по адресу x, и все жестко закодированные адреса, содержащиеся в исполняемом файле, будут увеличиваться на x байты, так что переменная a в примере будет находиться по адресу x, b по адресу x+33 и т.д.

Ответ 4

Добавление переменных в стек, которые занимают N байтов, не (обязательно) увеличивает размер буфера на N байтов. Фактически, он будет добавлять, но несколько байтов большую часть времени.
Давайте начнем с примера того, как добавление 1000 символов в ваш код увеличит размер буфера линейным способом.

Если 1k - строка, из тысячи символов, которая объявляется так

const char *c_string = "Here goes a thousand chars...999";//implicit \0 at end

и тогда вы должны были vim your_compiled_bin, вы действительно сможете увидеть эту строку в корзине где-нибудь. В этом случае да: исполняемый файл будет на 1 k больше, потому что он содержит строку в полном объеме.
Если, однако, вы выделяете массив из int s, char или long в стеке и назначаете его в цикле, что-то вдоль этих строк

int big_arr[1000];
for (int i=0;i<1000;++i) big_arr[i] = some_computation_func(i);

тогда нет: он не увеличит корзину... на 1000*sizeof(int)
Выделение во время компиляции означает, что вы поняли, что это означает (на основе ваших комментариев): скомпилированный bin содержит информацию, которую система должна знать, сколько памяти, какая функция/блок потребуется при ее выполнении, а также информацию о размер стека, который требуется вашему приложению. Это то, что система будет выделять при выполнении вашего бункера, и ваша программа станет процессом (ну, выполнение вашего бункера - это процесс, который... ну, вы получаете то, что я говорю).
Конечно, я не рисую полную картину здесь: в мусорном контейнере содержится информация о том, насколько большой стек, в котором он действительно понадобится. Основываясь на этой информации (среди прочего), система резервирует кусок памяти, называемый стеком, что программа получает свое свободное владение. Память стека по-прежнему выделяется системой, когда инициируется процесс (результат выполнения вашего бина). Затем процесс управляет памятью стека для вас. Когда функция или цикл (любой тип блока) вызывается/выполняется, переменные, локальные для этого блока, помещаются в стек, и они удаляются (так называемая память стека "освобождается" ) для использования другими функции/блоки. Таким образом, объявление int some_array[100] добавит только несколько байтов дополнительной информации в бункер, в котором говорится, что функция X потребует 100*sizeof(int) + некоторого дополнительного пространства для хранения.

Ответ 5

На многих платформах все глобальные или статические распределения в каждом модуле будут объединены компилятором в три или менее консолидированных распределения (один для неинициализированных данных (часто называемый "bss" ), один для инициализированных записываемых данных (часто называемых "данные" ) и один для постоянных данных ( "const" )), а все глобальные или статические распределения каждого типа внутри программы будут объединены компоновщиком в один глобальный для каждого типа. Например, если int - четыре байта, то в качестве его единственных статических распределений модуль имеет следующие значения:

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, данные +0 или bss + 204 соответственно.

Когда программа связана, все области bss из всех модулей объединяются вместе; а также данные и константные области. Для каждого модуля адрес любых bss-относительных переменных будет увеличен на размер всех областей bss предыдущих модулей (опять же, как и данные и const). Таким образом, когда компоновщик выполнен, любая программа будет иметь одно распределение bss, одно распределение данных и одно распределение const.

Когда программа загружается, в зависимости от платформы обычно происходит одна из четырех вещей:

  • В исполняемом файле указывается, сколько байтов требуется для каждого типа данных, и - для области инициализированных данных, где может быть найдено исходное содержимое. Он также будет содержать список всех инструкций, которые используют адрес bss-, data- или const-relative. Операционная система или загрузчик будет выделять соответствующее пространство для каждой области, а затем добавить начальный адрес этой области в каждую требуемую ему команду.

  • Операционная система выделяет кусок памяти для хранения всех трех видов данных и дает приложению указатель на этот кусок памяти. Любой код, который использует статические или глобальные данные, будет разыменовывать его относительно этого указателя (во многих случаях указатель будет храниться в регистре на время жизни приложения).

  • Операционная система изначально не выделяет какую-либо память для приложения, за исключением того, что хранит ее двоичный код, но первое, что приложение делает, это запросить подходящее выделение из операционной системы, которое оно будет навсегда войдите в реестр.

  • Операционная система изначально не выделяет пространство для приложения, но приложение будет запрашивать подходящее распределение при запуске (как указано выше). Приложение будет содержать список инструкций с адресами, которые необходимо обновить, чтобы отражать, где была выделена память (как и в первом стиле), но вместо того, чтобы иметь приложение, исправленное загрузчиком ОС, приложение будет содержать достаточно кода для исправления.

Все четыре подхода имеют свои преимущества и недостатки. Однако в каждом случае компилятор будет консолидировать произвольное количество статических переменных в фиксированное небольшое количество запросов на память, и компоновщик объединит все их в небольшое количество консолидированных распределений. Несмотря на то, что приложение должно будет получить кусок памяти из операционной системы или загрузчика, это компилятор и компоновщик, которые отвечают за выделение отдельных частей из этого большого куска всем тем отдельным переменным, которые в нем нуждаются.

Ответ 6

Ядро вашего вопроса заключается в следующем: "Как распределяется память" в скомпилированном файле? Не всегда ли распределено память в ОЗУ со всеми элементами управления виртуальной памятью? Не является ли выделение памяти по определению концепцией времени выполнения? "

Я думаю, проблема в том, что в распределении памяти есть две разные концепции. По своей сути выделение памяти - это процесс, по которому мы говорим "этот элемент данных хранится в этом конкретном куске памяти". В современной компьютерной системе это предполагает двухэтапный процесс:

  • Некоторая система используется для определения виртуального адреса, в котором будет сохранен элемент
  • Виртуальный адрес сопоставляется с физическим адресом

Последний процесс - это чисто время выполнения, но первое может быть сделано во время компиляции, если данные имеют известный размер и требуется фиксированное число из них. Здесь, в основном, как это работает:

  • Компилятор видит исходный файл, содержащий строку, которая выглядит примерно так:

    int c;
    
  • Он производит выходные данные для ассемблера, который инструктирует его зарезервировать память для переменной c. Это может выглядеть так:

    global _c
    section .bss
    _c: resb 4
    
  • Когда ассемблер запускается, он держит счетчик, который отслеживает смещения каждого элемента с начала сегмента "памяти" (или "секции" ). Это похоже на части очень большой "структуры", которая содержит все во всем файле, на котором в данный момент нет никакой реальной памяти, и может быть где угодно. В таблице отмечается, что _c имеет конкретное смещение (скажем, 510 байт от начала сегмента), а затем увеличивает его счетчик на 4, поэтому следующая такая переменная будет равна (например, 514 байтам). Для любого кода, которому нужен адрес _c, он просто помещает 510 в выходной файл и добавляет примечание о том, что на выходе нужен адрес сегмента, который содержит _c добавление к нему позже.

  • Компонент принимает все выходные файлы ассемблера и анализирует их. Он определяет адрес для каждого сегмента, так что он не будет перекрываться и добавляет необходимые смещения, чтобы инструкции по-прежнему ссылались на правильные элементы данных. В случае неинициализированной памяти, подобной занятой c (ассемблеру было сказано, что память будет неинициализирована из-за того, что компилятор помещает ее в сегмент ".bss", который является именем, зарезервированным для неинициализированной памяти) он включает поле заголовка в своем выходе, которое сообщает операционной системе, сколько необходимо зарезервировать. Он может быть перемещен (и обычно есть), но обычно предназначен для более эффективной загрузки на одном конкретном адресе памяти, и ОС будет пытаться загрузить его по этому адресу. На данный момент мы довольно хорошо знаем, что такое виртуальный адрес, который будет использоваться c.

  • Физический адрес фактически не будет определен до тех пор, пока программа не будет запущена. Однако с точки зрения программиста физический адрес на самом деле не имеет значения - мы даже не узнаем, что это такое, потому что ОС обычно не говорит никому, он может часто меняться (даже во время работы программы), и Основной целью ОС является абстракция этого в любом случае.

Ответ 7

Исполняемый файл описывает, какое пространство выделять для статических переменных. Это распределение выполняется системой при запуске исполняемого файла. Поэтому ваша статическая переменная 1kB не будет увеличивать размер исполняемого файла с помощью 1kB:

static char[1024];

Если вы, конечно, не указали инициализатор:

static char[1024] = { 1, 2, 3, 4, ... };

Таким образом, помимо "машинного языка" (т.е. инструкций CPU), исполняемый файл содержит описание требуемого макета памяти.

Ответ 8

Память может быть распределена по-разному:

  • в куче приложений (вся куча выделена для вашего приложения ОС при запуске программы)
  • в куче операционной системы (чтобы вы могли захватывать все больше и больше)
  • в мусорной корзине, контролируемой кучей (так же, как и выше)
  • в стеке (так что вы можете получить переполнение стека)
  • зарезервировано в сегменте кода/данных вашего двоичного файла (исполняемого файла)
  • в удаленном месте (файл, сеть - и вы получите дескриптор не указатель на эту память)

Теперь ваш вопрос - это то, что "память выделяется во время компиляции". Определенно это просто неверно сформулированное высказывание, которое должно ссылаться либо на распределение двоичных сегментов, либо на распределение стека, или в некоторых случаях даже на распределение кучи, но в этом случае выделение скрывается от глаз программиста невидимым вызовом конструктора. Или, возможно, тот, кто сказал, что просто хотел сказать, что память не выделена в кучу, но не знает о распределении стека или сегмента (или не хотела вдаваться в подробности).

Но в большинстве случаев человек просто хочет сказать, что объем выделенной памяти известен во время компиляции.

Двоичный размер будет изменяться только тогда, когда память зарезервирована в сегменте кода или данных вашего приложения.

Ответ 9

Вы правы. Память фактически распределяется (выгружается) во время загрузки, то есть когда исполняемый файл вводится в (виртуальную) память. Память также может быть инициализирована в этот момент. Компилятор просто создает карту памяти. [Кстати, пространства стека и кучи также выделяются во время загрузки!]

Ответ 10

Думаю, вам нужно немного отступить. Память, выделенная во время компиляции... Что это может означать? Может ли это означать, что память на чипах, которые еще не были созданы, для компьютеров, которые еще не были разработаны, каким-то образом зарезервированы? Нет. Нет, путешествие во времени, нет компиляторов, которые могут манипулировать Вселенной.

Таким образом, это должно означать, что компилятор генерирует инструкции для распределения памяти как-то во время выполнения. Но если вы посмотрите на него под прямым углом, компилятор генерирует все инструкции, поэтому какая разница. Разница в том, что компилятор решает, и во время выполнения ваш код не может изменять или изменять свои решения. Если он решил, что во время компиляции требуется 50 байт, во время выполнения вы не можете заставить его выбрать 60 - это решение уже принято.

Ответ 11

Если вы изучите программирование сборки, вы увидите, что вам нужно вырезать сегменты для данных, стека и кода и т.д. В сегменте данных находятся ваши строки и номера. Сегмент кода - это код вашего кода. Эти сегменты встроены в исполняемую программу. Конечно, размер стека также важен... вам не нужно переполнение стека!

Итак, если ваш сегмент данных составляет 500 байт, ваша программа имеет 500-байтовую область. Если вы измените сегмент данных на 1500 байт, размер программы будет на 1000 байт больше. Данные собираются в фактическую программу.

Это то, что происходит, когда вы скомпилируете языки более высокого уровня. Фактическая область данных выделяется, когда она компилируется в исполняемую программу, увеличивая размер программы. Программа также может запросить память "на лету", и это динамическая память. Вы можете запросить память из ОЗУ, и CPU даст вам ее использовать, вы можете отпустить ее, и ваш сборщик мусора выведет его обратно в CPU. Он может даже быть заменен на жесткий диск, если необходимо, хорошим менеджером памяти. Эти функции предоставляют вам языки высокого уровня.

Ответ 12

Я хотел бы объяснить эти понятия с помощью нескольких диаграмм.

Это верно, что память не может быть выделена во время компиляции. Но тогда то, что происходит на самом деле во время компиляции.

Вот объяснение. Скажем, например, программа имеет четыре переменные x, y, z и k. Теперь, во время компиляции, он просто создает карту памяти, где установлено местоположение этих переменных относительно друг друга. Эта диаграмма будет лучше иллюстрировать ее.

Теперь представьте, что в памяти не работает программа. Это я показываю большим пустым прямоугольником.

empty field

Затем выполняется первый экземпляр этой программы. Вы можете визуализировать его следующим образом. Это время, когда на самом деле выделена память.

first instance

Когда второй экземпляр этой программы запущен, память будет выглядеть следующим образом.

second instance

И третий...

third instance

Итак, и т.д.

Я надеюсь, что эта визуализация хорошо объясняет эту концепцию.