В языках программирования, таких как C и C ++, люди часто ссылаются на статическое и динамическое распределение памяти. Я понимаю концепцию, но фраза «Вся память была выделена (зарезервирована) во время компиляции» всегда смущает меня.
Компиляция, насколько я понимаю, преобразует код высокого уровня C / C ++ в машинный язык и выводит исполняемый файл. Как распределяется память в скомпилированном файле? Разве память не всегда выделяется в ОЗУ всеми средствами управления виртуальной памятью?
Разве распределение памяти по определению не является концепцией времени выполнения?
Если я сделаю статически размещенную переменную размером 1 КБ в своем коде C / C ++, увеличит ли это размер исполняемого файла на ту же величину?
Это одна из страниц, где фраза используется под заголовком «Статическое распределение».
источник
Ответы:
Память, выделенная во время компиляции, означает, что компилятор разрешает во время компиляции, где определенные вещи будут выделяться внутри карты памяти процесса.
Например, рассмотрим глобальный массив:
Компилятор знает во время компиляции размер массива и размер
int
, поэтому он знает весь размер массива во время компиляции. Также глобальная переменная имеет статическую длительность хранения по умолчанию: она размещается в области статической памяти пространства памяти процесса (раздел .data / .bss). Учитывая эту информацию, компилятор решает во время компиляции, по какому адресу этой статической области памяти будет массив .Конечно, адреса памяти - это виртуальные адреса. Программа предполагает, что она имеет собственное пространство памяти (например, от 0x00000000 до 0xFFFFFFFF). Вот почему компилятор может делать предположения типа «Хорошо, массив будет по адресу 0x00A33211». Во время выполнения эти адреса преобразуются в реальные / аппаратные адреса MMU и ОС.
Значение инициализированного статического хранилища вещей немного отличается. Например:
В нашем первом примере компилятор только решил, где будет размещен массив, сохранив эту информацию в исполняемом файле.
В случае вещей, инициализированных значением, компилятор также вводит начальное значение массива в исполняемый файл и добавляет код, который сообщает загрузчику программы, что после выделения массива при запуске программы массив должен быть заполнен этими значениями.
Вот два примера сборки, сгенерированной компилятором (GCC4.8.1 с целью x86):
C ++ код:
Выходная сборка:
Как видите, значения непосредственно вводятся в сборку. В массиве
a
компилятор генерирует нулевую инициализацию 16 байтов, потому что стандарт говорит, что статические хранимые вещи должны быть по умолчанию инициализированы нулем:Я всегда предлагаю людям разобрать свой код, чтобы посмотреть, что на самом деле делает компилятор с кодом C ++. Это относится от классов хранения / продолжительности (как этот вопрос) к расширенной оптимизации компилятора. Вы могли бы поручить вашему компилятору сгенерировать сборку, но есть замечательные инструменты, чтобы сделать это в Интернете дружественным образом. Мой любимый это GCC Explorer .
источник
Память, выделенная во время компиляции, просто означает, что больше не будет выделяться во время выполнения - нет вызовов для malloc, новых или других методов динамического выделения. У вас будет фиксированный объем использования памяти, даже если вам не нужна вся эта память все время.
Память не используется до времени выполнения, но непосредственно перед началом выполнения ее распределение обрабатывается системой.
Простое объявление статического кода не увеличит размер вашего исполняемого файла более чем на несколько байтов. Объявление его с ненулевым начальным значением будет (для удержания этого начального значения). Скорее, компоновщик просто добавляет эту сумму в 1 КБ к требованию к памяти, которое системный загрузчик создает для вас непосредственно перед выполнением.
источник
static int i[4] = {2 , 3 , 5 ,5 }
, он увеличится на исполняемый размер на 16 байт. Вы сказали: «Простое объявление статического значения не приведет к увеличению размера исполняемого файла более чем на несколько байтов. Объявление его с начальным значением, отличным от нуля». Объявление его с начальным значением будет иметь значение.Память, выделенная во время компиляции, означает, что при загрузке программы некоторая часть памяти будет выделена немедленно, а размер и (относительная) позиция этого выделения определяются во время компиляции.
Эти 3 переменные «выделяются во время компиляции», это означает, что компилятор вычисляет их размер (который является фиксированным) во время компиляции. Переменная
a
будет смещением в памяти, скажем, указывая на адрес 0,b
будет указывать на адреса 33 иc
34 (при условии отсутствия оптимизации выравнивания). Таким образом, выделение 1 КБ статических данных не увеличит размер вашего кода , поскольку он просто изменит смещение внутри него. Фактическое пространство будет выделено во время загрузки .Реальное выделение памяти всегда происходит во время выполнения, потому что ядру необходимо отслеживать его и обновлять свои внутренние структуры данных (сколько памяти выделяется для каждого процесса, страниц и т. Д.). Разница в том, что компилятор уже знает размер всех данных, которые вы собираетесь использовать, и он выделяется, как только ваша программа выполняется.
Помните также, что мы говорим об относительных адресах . Реальный адрес, где будет расположена переменная, будет другим. Во время загрузки ядро зарезервирует некоторую память для процесса, скажем, по адресу
x
, и все жестко закодированные адреса, содержащиеся в исполняемом файле, будут увеличены наx
байты, так что переменнаяa
в примере будет по адресуx
, b по адресуx+33
и скоро.источник
Добавление в стек переменных, занимающих N байтов, не обязательно увеличивает размер корзины на N байтов. Фактически он будет добавлять только несколько байтов большую часть времени.
Давайте начнем с примера того, как добавление 1000 символов в ваш код будет линейно увеличивать размер корзины.
Если 1k - строка из тысячи символов, которая объявлена так
и тогда
vim your_compiled_bin
вы должны были увидеть эту строку в мусорном ведре. В таком случае, да: исполняемый файл будет на 1 кб больше, потому что он содержит строку полностью.Однако, если вы размещаете массив
int
s,char
s илиlong
s в стеке и назначаете его в цикле, что-то вроде этоготогда, нет: это не увеличит корзину ...
1000*sizeof(int)
Распределение во время компиляции означает, что вы теперь поняли, что это означает (на основе ваших комментариев): скомпилированная корзина содержит информацию, необходимую системе, чтобы узнать, сколько памяти какая функция / блок понадобится при запуске, а также информация о размере стека, необходимого вашему приложению. Это то, что система выделит, когда выполнит ваш бин, и ваша программа станет процессом (ну, выполнение вашего бина - это процесс, который ... ну, вы понимаете, что я говорю).
Конечно, я не рисую полную картину здесь: корзина содержит информацию о том, какой большой стек будет на самом деле нужен корзине. Основываясь на этой информации (помимо прочего), система зарезервирует кусок памяти, называемый стеком, для которого программа получает вид свободного управления. Стековая память все еще выделяется системой, когда инициируется процесс (результат выполнения вашего бина). Затем процесс управляет памятью стека для вас. Когда функция или цикл (любой тип блока) вызывается / выполняется, переменные, локальные для этого блока, помещаются в стек и удаляются ( так сказать, память стека «освобождается» ) для использования другими функции / блоки. Так декларируя
int some_array[100]
добавит только несколько байтов дополнительной информации в корзину, которая сообщит системе, что для функции X потребуется100*sizeof(int)
+ некоторое дополнительное место для хранения.источник
i
не «освобождается» или то и другое. Если бы онi
находился в памяти, он просто был бы помещен в стек, что-то, что не освобождается в этом смысле слова, игнорируя этоi
илиc
будет храниться в регистрах все время. Конечно, все зависит от компилятора, а это значит, что он не такой уж черно-белый.free()
вызовов, но используемая ими стековая память свободна для использования другими функциями, когда функция, которую я перечислил, возвращает. Я удалил код, так как он может вводить некоторых в заблуждениеНа многих платформах все глобальные или статические выделения в каждом модуле будут консолидироваться компилятором в три или менее консолидированных распределения (одно для неинициализированных данных (часто называемых "bss"), одно для инициализированных записываемых данных (часто называемых "данными"). ) и один для постоянных данных («const»)), и все глобальные или статические распределения каждого типа в программе будут объединены компоновщиком в один глобальный для каждого типа. Например, предполагая, что
int
это четыре байта, модуль имеет только следующие статические распределения:он сказал бы компоновщику, что ему нужно 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. Операционная система или загрузчик будет выделять соответствующий объем пространства для каждой области, а затем добавлять начальный адрес этой области к каждой инструкции, которая в этом нуждается.
Операционная система выделит кусок памяти для хранения всех трех типов данных и даст приложению указатель на этот кусок памяти. Любой код, который использует статические или глобальные данные, будет разыменовывать их относительно этого указателя (во многих случаях указатель будет храниться в регистре в течение всего времени жизни приложения).
Изначально операционная система не будет выделять приложению какую-либо память, за исключением того, что содержит его двоичный код, но первое, что делает приложение, - это запрашивает подходящее распределение из операционной системы, которое оно всегда будет хранить в реестре.
Операционная система первоначально не будет выделять место для приложения, но приложение будет запрашивать подходящее распределение при запуске (как указано выше). Приложение будет содержать список инструкций с адресами, которые необходимо обновить, чтобы отразить, где была выделена память (как в первом стиле), но вместо того, чтобы приложение исправлялось загрузчиком ОС, приложение будет включать в себя достаточно кода для исправления самого себя. ,
Все четыре подхода имеют свои преимущества и недостатки. В любом случае, однако, компилятор объединит произвольное количество статических переменных в фиксированное небольшое количество запросов памяти, а компоновщик объединит все из них в небольшое количество консолидированных распределений. Несмотря на то, что приложению придется получать часть памяти от операционной системы или загрузчика, именно компилятор и компоновщик отвечают за выделение отдельных фрагментов из этого большого блока всем отдельным переменным, которые в этом нуждаются.
источник
Суть вашего вопроса заключается в следующем: «Как распределяется память» в скомпилированном файле? Разве память не всегда выделяется в ОЗУ всеми средствами управления виртуальной памятью? Разве распределение памяти по определению не является концепцией времени выполнения? »
Я думаю, что проблема в том, что в распределении памяти участвуют две разные концепции. По своей сути, выделение памяти - это процесс, с помощью которого мы говорим «этот элемент данных хранится в этом конкретном фрагменте памяти». В современной компьютерной системе это включает в себя два этапа:
Последний процесс является чисто во время выполнения, но первый может быть выполнен во время компиляции, если данные имеют известный размер и требуется фиксированное их количество. Вот в основном, как это работает:
Компилятор видит исходный файл, содержащий строку, которая выглядит примерно так:
Он производит вывод для ассемблера, который инструктирует его зарезервировать память для переменной 'c'. Это может выглядеть так:
Когда ассемблер работает, он сохраняет счетчик, который отслеживает смещения каждого элемента с начала сегмента памяти (или «секции»). Это похоже на части очень большой «структуры», которая содержит все во всем файле, в настоящее время ей не выделено никакой фактической памяти, и она может находиться где угодно. Это отмечает в таблице, что
_c
конкретное смещение (скажем, 510 байт от начала сегмента), а затем увеличивает его счетчик на 4, поэтому следующая такая переменная будет иметь значение (например, 514 байт). Для любого кода, которому нужен адрес_c
, он просто помещает 510 в выходной файл и добавляет примечание, что для вывода нужен адрес сегмента, который содержит_c
добавление к нему позже.Компоновщик берет все выходные файлы ассемблера и проверяет их. Он определяет адрес для каждого сегмента, чтобы они не перекрывались, и добавляет необходимые смещения, чтобы инструкции по-прежнему ссылались на правильные элементы данных. В случае неинициализированной памяти, как это занято
c
(ассемблеру сказали, что память будет неинициализирована из-за того, что компилятор поместил ее в сегмент «.bss», который является именем, зарезервированным для неинициализированной памяти), в выводе он включает поле заголовка, которое сообщает операционной системе сколько нужно зарезервировать. Он может быть перемещен (и обычно так и есть), но обычно предназначен для более эффективной загрузки по одному конкретному адресу памяти, и ОС попытается загрузить его по этому адресу. На данный момент у нас есть довольно хорошее представление о том, какой виртуальный адрес будет использоватьсяc
.Физический адрес не будет определен до тех пор, пока не будет запущена программа. Однако с точки зрения программиста физический адрес на самом деле не имеет значения - мы никогда даже не узнаем, что это такое, потому что ОС обычно не говорит никому, она может часто меняться (даже во время работы программы), и Основная цель ОС - все равно абстрагироваться.
источник
Исполняемый файл описывает, какое пространство выделить статическим переменным. Это распределение выполняется системой при запуске исполняемого файла. Таким образом, ваша статическая переменная 1 КБ не увеличит размер исполняемого файла на 1 КБ:
Если, конечно, вы не укажете инициализатор:
Таким образом, в дополнение к «машинному языку» (т. Е. Инструкциям процессора) исполняемый файл содержит описание требуемой структуры памяти.
источник
Память может быть распределена разными способами:
Теперь ваш вопрос - что такое «память, выделенная во время компиляции». Определенно, это просто неверно сформулированное высказывание, которое должно относиться либо к двоичному выделению сегмента, либо к выделению стека, либо в некоторых случаях даже к распределению кучи, но в этом случае распределение скрыто от глаз программиста при вызове невидимого конструктора. Или, возможно, человек, который сказал, что просто хотел сказать, что память не выделяется в куче, но не знал о выделении стека или сегмента (или не хотел вдаваться в подробности такого рода).
Но в большинстве случаев человек просто хочет сказать, что объем выделяемой памяти известен во время компиляции .
Размер двоичного файла будет изменяться только тогда, когда память зарезервирована в коде или сегменте данных вашего приложения.
источник
.data
и.bss
.Ты прав. Память фактически выделяется (выгружается) во время загрузки, т.е. когда исполняемый файл заносится в (виртуальную) память. Память также может быть инициализирована в этот момент. Компилятор просто создает карту памяти. [Кстати, пространство стека и кучи также выделяется во время загрузки!]
источник
Я думаю, тебе нужно немного отступить. Память выделяется во время компиляции .... Что это может означать? Может ли это означать, что память на чипах, которые еще не были изготовлены, для компьютеров, которые еще не были спроектированы, каким-то образом резервируется? Нет, путешествие во времени, нет компиляторов, которые могут манипулировать вселенной.
Таким образом, это должно означать, что компилятор генерирует инструкции для выделения этой памяти каким-либо образом во время выполнения. Но если вы посмотрите на это с правильной стороны, компилятор генерирует все инструкции, так в чем же может быть разница. Разница в том, что компилятор принимает решение, и во время выполнения ваш код не может изменить или изменить свои решения. Если он решил, что ему нужно 50 байтов во время компиляции, во время выполнения вы не можете заставить его принять решение выделить 60 - это решение уже принято.
источник
Если вы изучите программирование на ассемблере, вы увидите, что вам нужно выделить сегменты для данных, стека, кода и т. Д. Сегмент данных - это место, где живут ваши строки и числа. Сегмент кода - это место, где живет ваш код. Эти сегменты встроены в исполняемую программу. Конечно, размер стека также важен ... вы бы не хотели переполнения стека !
Таким образом, если ваш сегмент данных имеет длину 500 байт, ваша программа имеет область размером 500 байт. Если вы измените сегмент данных на 1500 байт, размер программы будет на 1000 байт больше. Данные собраны в фактическую программу.
Это то, что происходит, когда вы компилируете языки более высокого уровня. Фактическая область данных выделяется, когда она компилируется в исполняемую программу, увеличивая размер программы. Программа также может запрашивать память на лету, и это динамическая память. Вы можете запросить память из ОЗУ, и ЦП предоставит ее вам для использования, вы можете отпустить ее, и ваш сборщик мусора вернет ее обратно в ЦП. При необходимости, он может быть перенесен на жесткий диск хорошим менеджером памяти. Эти функции - то, что предоставляют языки высокого уровня.
источник
Я хотел бы объяснить эти понятия с помощью нескольких диаграмм.
Это правда, что память не может быть выделена во время компиляции, наверняка. Но то, что происходит на самом деле во время компиляции.
Вот и объяснение. Скажем, например, программа имеет четыре переменные x, y, z и k. Теперь, во время компиляции, он просто создает карту памяти, где определяется местоположение этих переменных относительно друг друга. Эта диаграмма проиллюстрирует это лучше.
А теперь представьте, в памяти не работает ни одна программа. Это я показываю большим пустым прямоугольником.
Далее выполняется первый экземпляр этой программы. Вы можете визуализировать это следующим образом. Это время, когда фактически выделяется память.
Когда запущен второй экземпляр этой программы, память будет выглядеть следующим образом.
И третий ..
Так далее и тому подобное.
Я надеюсь, что эта визуализация хорошо объясняет эту концепцию.
источник
В принятом ответе дано очень хорошее объяснение. На всякий случай я опубликую ссылку, которую я нашел полезной. https://www.tenouk.com/ModuleW.html
источник