В тексте, который я читаю, говорится:
Глобальные переменные расположены в исполняемом образе, поэтому использование количества глобальных переменных увеличит размер исполняемого образа.
Вопросы:
Что означает «находится в исполняемом образе»? Из того, что я прочитал, глобальные переменные расположены в разделе «данные» исполняемого файла. Я предполагаю, что инструкции по инициализации локальных переменных хранятся в разделе «текст». Так почему же инструкции по инициализации локальных переменных не занимают примерно столько же места, сколько глобальные переменные?
Под исполняемым файлом имеется в виду исполняемый файл, загруженный в память, или исполняемый файл, который находится только в энергонезависимой памяти? Будут ли глобальные переменные занимать больше места для исполняемого файла, который не загружен в ОЗУ?
Есть ли книги или краткие ресурсы для чтения, к которым я могу обратиться, которые помогут мне с такими концепциями более низкого уровня?
Я ожидал, что инструкции по инициализации локальных переменных займут в исполняемом файле такой же объем места, как и глобальные переменные. Рассмотрим следующую программу:
#include <stdlib.h>
int global_var = 10
int main(void){
int local_var = 20;
return EXIT_SUCCESS;
}
При преобразовании в исполняемый файл машинного уровня (при условии, что он не загружен в память/не является процессом), я предполагаю, что и определение, и инициализация global_var и local_var будут закодированы как код машинного уровня, хотя и в разных разделах (данные и текст) исполняемого файла. Так почему же global_var будет занимать больше места?
SO — это сайт вопросов и ответов. Обратите внимание, что вопрос стоит в единственном числе, а не во множественном. Если у вас больше одного вопроса, нужно вынести их в отдельные посты. Кроме того, вопросы, в которых нас просят порекомендовать или найти книги, учебные пособия или другие внешние ресурсы, в рекомендациях справочного центра специально помечены как не относящиеся к теме. Возможно, вам захочется просмотреть эти рекомендации перед публикацией следующего поста.
Я бы предположил, что инициализация локальной переменной по-другому влияет на размер исполняемого файла. Начальные значения глобальных переменных могут быть представлены как блок данных в исполняемом файле, который может быть загружен/скопирован как блок в хранилище инициализированных глобальных переменных с помощью стартового кода. Локальные переменные могут быть инициализированы исполняемым кодом, который включает «непосредственные» значения и рассредоточен в соответствующих местах по разделам кода исполняемого файла.
@AviBerger, когда вы говорите «исполняемый файл», вы имеете в виду исполняемый файл, который находится в постоянном хранилище, или тот, который загружается в оперативную память? Также не могли бы вы уточнить последнюю часть: «Локальные жители могут быть… исполняемым файлом»?
На самом деле я оставил это в основном двусмысленным и не вдавался в мелкие детали. Под исполняемым файлом я имел в виду файл, созданный компоновщиком и хранящийся в режиме ожидания на запоминающем устройстве. Исполняемый образ — это не одно и то же, хотя оно связано и может несколько отличаться в зависимости от конфигурации. Размер исполняемого файла может быть размером файла, размером изображения или размером загрузки. Здесь вы можете углубляться во всевозможные мелкие детали, и я не уверен, что они так же важны, как и общие концепции.
Для int local_var = 20; в main() main может иметь код на входе, который резервирует место во фрейме стека для локальных переменных. Это обеспечит хранение памяти времени выполнения для local_var. Тогда main может включать команду перемещения непосредственного значения 20 в память по этому адресу памяти (который был зарезервирован в стеке для local_var). ТАК что значение 20 здесь является частью кода и во время выполнения также копируется в память в пространство, установленное во время выполнения для переменной.
@AviBerger в неактивном двоичном/исполняемом/машинном коде пространство, занимаемое двоичным представлением чисел 10 и 20, должно быть одинаковым, поскольку они оба имеют тип «int»? Так не будет ли пространство, занимаемое инструкциями: 1. определение/инициализация глобальной переменной 2. инструкции по помещению local_var=20 в кадр стека, одинаковым? Есть ли способ представить число 20 в двоичном формате таким образом, чтобы оно занимало меньше места в неактивном исполняемом файле.
Да, значения 10 или 20 займут то же место. Остальная часть инструкции также занимает место. Пятьдесят таких переменных, которым присвоены разные значения, могут представлять 50 таких инструкций. Пятьдесят глобальных целочисленных переменных могут означать блок данных из 50 значений (одинаковое пространство для значений) и один цикл копирования блока в коде для перемещения значений на место. Пятьдесят инструкций разбросаны по всему коду вместо одного цикла в коде запуска. Или загрузчик может загрузить значения непосредственно на место без необходимости использования цикла. Существует много разных платформ, и детали могут различаться.
Однако чем отличаются эти функции воздействия. И функциональность является основной проблемой. Использование памяти или размер изображения — не единственные соображения. Важные соображения зависят от платформы и ситуации. Размер изображения и использование оперативной памяти также не одинаковы.





Все объекты занимают память. Если он инициализирован, его необходимо где-то сохранить.
Однако не все объявления объектов одинаковы. В вашем примере есть global_var, который должен иметь реализованное int объектное пространство где-то в вашей программе, которое можно изменить. Это требует места.
Но локальный local_var, скорее всего, будет оптимизирован вашим компилятором, поскольку компилятор может доказать себе, что он никогда не используется (или используется только в контексте, не требующем реального сохраненного int объекта где-либо в памяти).
Единственное, что необходимо, — это начальные значения, которые должны существовать где-то в программе. Опять же, поскольку local_var исключается, то же самое происходит и с требованием хранить его начальное значение.
Очевидно, это требует лучшего объяснения, поскольку тема сложна и упрощенные рассуждения неприменимы. То, как компиляторы оптимизируют код и как компоновщики связывают все это вместе, — это обширная тема. Я постараюсь сохранить это small.
Существует несколько способов хранения данных:
Этот тип хранилища используется для инициализации всего, что не доступно только для чтения. Например:
int main()
{
const char * constant_string = "Hello world!";
char mutable_string[] = "Hello world!";
}
Здесь у нас есть два объекта. Массив символов Hello world!\0 хранится в вашем исполняемом файле в разделе «Загрузка в постоянную память» исполняемого файла. В исполняемом файле существует только одна копия массива.
При выполнении main() создаются две переменные*. Первый просто напрямую ссылается на память только для чтения через указатель. Второй создает локальный массив в стеке** и копирует данные из постоянной памяти в локальную память стека.
* Maybe. Keep reading.
** Probably.
Вы, конечно, можете изменять локальный массив сколько угодно, но если вы попытаетесь изменить данные, доступные только для чтения, вы получите знакомый сбой и сгорание (поскольку система не позволяет вашей программе записывать в память, помеченную как прочитанная). -только).
⟶ At this point we have considered on-disk and in-memory constant data to be two separate things, but it need not be. On Windows, for example, an executable file is locked and memory-mapped, which makes read-only memory initialization very, very simple for the OS. That is a topic for another conversation, though.
Кроме того, важно отметить, что этот раздел используется для инициализации всех начальных значений в вашей программе, а не только глобальных переменных. В приведенном выше примере и constant_string, и mutable_string[] являются локальными переменными, но они инициализируются с использованием данных, доступных только для чтения.
Мы называем эти «глобальные переменные». В соответствии со стандартом глобальные переменные инициализируются нулевым значением, если только они не имеют явного начального значения. Еще раз: начальные значения должны где-то существовать. Это наше глобальное хранилище только для чтения. Незадолго до вызова main() ваши глобальные переменные инициализируются значениями, хранящимися в разделе данных, доступном только для чтения.
Я не знаю ни одного компилятора, который позволял бы исключать глобальные переменные по какой-либо причине. (По крайней мере, мне никогда не приходилось это делать, и я не интересовался, возможно ли это сделать.) Вы можете пометить глобальную переменную как static, сделав ее видимой только для отдельной единицы перевода (и, поэтому он не подлежит включению в таблицу символов объектного файла для использования компоновщиком), но он все равно будет существовать как объект, занимающий место в вашем конечном исполняемом файле, и снова как другой объект, занимающий место в памяти при выполнении вашей программы.
Однако в большинстве случаев люди не удосуживаются сделать даже это, и компилятор не может ничего сделать, кроме как проявить осторожность и сделать глобальную переменную доступной в таблице символов объектного файла - просто для того, чтобы другие единицы перевода имели доступ. к этому:
// some source file that needs access to `quux.c`’s `quuxlcoatl` variable
extern int quuxlcoatl;
// quux.c
int quuxlcoatl = 5;
Так откуда же это 5?
Правильный. Он объединяется с разделом данных вашего исполняемого файла, доступным только для чтения, и используется для инициализации глобальной переменной перед вызовом main().⁂
⁂ Simplified explanation. Wrinkles may apply.
Честно говоря, мы опускаем ряд различных видов «локального хранилища», но опять же, см. примечание ⁂.
Локальное хранилище стека — это место, где размещаются переменные в функциях.
int quuxify( int value )
{
int quesoso = value * quuxlcoatl;
Здесь у нас есть два объекта в стеке (*,⁑):
value помещается в стек до вызова quuxify()quesoso помещается в стек после вызова quuxify()Похоже, что оба имеют фактическое место для хранения (в стеке). Оба инициализируются некоторым значением.
Помните, что значение quuxlcoatl могло быть изменено какой-то другой частью вашей программы, но его исходное значение также существует в постоянной памяти.
* Maybe. Keep reading.
⁑ For the moment we will ignore concepts like passing-by-register and other calling conventions.
Он позволяет создавать и уничтожать локальные переменные вашей программы, как изменяемые, так и постоянные, по мере необходимости.
Сам стек представляет собой предварительно выделенное пространство, созданное до вызова main(), и его можно рассматривать как глобальное хранилище данных, работающее за вас.
⟶ The size of the stack created for your program (by the OS) can be modified, both by flags in the executable file and by requests directly to the OS, but it is always a single, large-ish block of memory that exists when your program is running.
I also think it is possible to have more than one stack in your program, but that is weird stuff, and you can’t quote me on it.
⟶ And since your program has full access rights to its stack memory, it is entirely possible to examine it from anywhere else in your program! This is how, for example, Object Pascal implements access to variables in a containing procedure from a nested procedure, by direct access to the caller’s stack space.
Доступно через семейство функций malloc() и free(). Я не буду ничего говорить об этом, поскольку считаю, что это очевидно и не вносит полезного вклада в дальнейшее рассмотрение данной темы.
tl;dr: Дополнительная память, запрашиваемая вашей программой у ОС, получаемая блоками. Не влияет на размер исполняемого файла.
Теперь мы переходим к странным и страшным вещам.
Просто потому, что вы написали и даже использовали переменную в своем коде, она не обязательно существует в конечном исполняемом коде!
Давайте вернемся к нашему последнему банальному примеру:
#include <stdio.h>
#include "quux.h"
int quuxify( int value )
{
int quesoso = value * quuxlcoatl;
int parangaricutirimicuaro = 7;
return quesoso + parangaricutirimicuaro;
}
int main(void)
{
printf( "%d\n", quuxify( 42 ) );
return 0;
}
Если бы вы были компилятором, вы бы заметили несколько вещей, которые можно сделать, чтобы улучшить код. Один из основных способов сделать это: не тратьте время на хранение и извлечение вещей, если в этом нет необходимости!
Во-первых, value не обязательно должен существовать в стеке! Ничто в коде этого не требует. (Нет необходимости получать указатель на value в функции.)
Следовательно, функция может использовать очень стандартное, очень обычное соглашение о вызовах, которое предполагает, что первые несколько значений аргументов будут находиться в регистрах ЦП.
⟶ A shocking number of different conventions for invoking functions exist. Specifics vary by platform, but the most common modern x86-specific version is cdecl, which by default passes as many arguments in CPU registers as possible before using the stack.
Таким образом, вызывающая сторона может просто вставить значение 42 в правильный регистр и вызвать функцию. Место для стека не требуется!
⟶ Keep in mind that the stack space exists whether it is used or not. This is just an optimization that keeps the stack from being used up as fast as it otherwise would be.
Мы можем наблюдать аналогичную характеристику локального объекта quesoso: нет никакой реальной необходимости создавать его в стеке. Фактически, поскольку value не используется после инициализации quesoso, нет смысла удерживать value. Просто умножьте его на глобальное значение и после этого назовите его quesoso!
Теперь мы исключили quesoso, чтобы он не существовал ни в каком локальном или глобальном хранилище. Он существует только в регистре процессора!
Глядя теперь на parangaricutirimicuaro (пара-что, сейчас?), мы видим, что его также можно исключить из фактического места хранения в стеке.
Но теперь другой вид удара молотка: тот 7, которым мы его инициализируем, также может исчезнуть. Нам не нужно хранить его в сегменте хранилища исполняемого файла, доступном только для чтения.
Как?
Это небольшое постоянное целое число, которое может быть непосредственно закодировано в скомпилированные машинные инструкции.
Наша функция теперь становится достаточно простой, и куча вещей, которые выглядят так, будто должны где-то существовать, на самом деле существуют только в нескольких словах кода.
Это подводит нас к последней (потенциальной) оптимизации: сама функция представляет собой всего лишь несколько небольших инструкций, которые даже не используют пространство стека.† Зачем создавать функцию? Просто вставьте объектный код функции туда, где она вызывается.
† Lack of use of stack space is not required to consider a function for inlining.
Таким образом, компоновщик может заметить, что функция существует, но на самом деле ничем не вызывается (ни в этой единице трансляции, ни в какой-либо другой), и, следовательно, исключить ее из окончательного исполняемого файла. (Может быть.)
Почему инструкции инициализации локальных переменных не влияют на размер исполняемого файла так же, как глобальные переменные?
Ответ: это не так просто. Они могут, а могут и нет.
И то, является ли что-либо «вероятным», во многом зависит от того, какие параметры вы используете для компиляции своей программы. Например, «отладочная» сборка будет работать значительно менее эффективно, чем «релизная».
Обобщения здесь просто неприменимы.
Глобальные переменные расположены в исполняемом образе, поэтому использование количества глобальных переменных увеличит размер исполняемого образа.
Исполняемый образ — это файл, в котором находится ваша программа. Для стандартного кода C, предназначенного для выполнения в среде Linux, это обычно файл ELF. Предоставленное вами утверждение верно, но не упоминает влияние локальных переменных на окончательное изображение.
Из того, что я прочитал, глобальные переменные расположены в разделе «данные» исполняемого файла.
Правильно, глобальные переменные хранятся в разделе data или bss файла ELF. В разделе data хранятся инициализированные глобальные переменные, а в разделе bss — неинициализированные.
Я предполагаю, что инструкции по инициализации локальных переменных хранятся в разделе «текст». Так почему же инструкции по инициализации локальных переменных не занимают примерно столько же места, сколько глобальные переменные?
Инструкции по инициализации локальных переменных всегда влияют на размер кода (раздел text), но не всегда могут влиять на размер конечного изображения. Это зависит от многих факторов, включая формат исполняемого файла, выравнивание и расположение.
Ниже приведен пример того, почему этот сценарий может произойти с 32-битным файлом ELF в Linux.
Давайте посмотрим на эти две программы:
// one.c
int a = 0xDEAD;
int main()
{
int c = 0xBABABABA;
int b = 0xCAFECAFE;
int d = 0xFFFFFFFF;
int e = 0x12345678;
return 0;
}
// two.c
int a = 0xDEAD;
int main()
{
return 0;
}
Вклеиваем сборку из них:
# one.c
08049000 <main>:
8049000: 55 push %ebp
8049001: 89 e5 mov %esp,%ebp
8049003: 83 ec 10 sub $0x10,%esp
8049006: e8 28 00 00 00 call 8049033 <__x86.get_pc_thunk.ax>
804900b: 05 f5 2f 00 00 add $0x2ff5,%eax
8049010: c7 45 fc ba ba ba ba movl $0xbabababa,-0x4(%ebp)
8049017: c7 45 f8 fe ca fe ca movl $0xcafecafe,-0x8(%ebp)
804901e: c7 45 f4 ff ff ff ff movl $0xffffffff,-0xc(%ebp)
8049025: c7 45 f0 78 56 34 12 movl $0x12345678,-0x10(%ebp)
804902c: b8 00 00 00 00 mov $0x0,%eax
8049031: c9 leave
8049032: c3 ret
# two.c
08049000 <main>:
8049000: 55 push %ebp
8049001: 89 e5 mov %esp,%ebp
8049003: e8 0c 00 00 00 call 8049014 <__x86.get_pc_thunk.ax>
8049008: 05 f8 2f 00 00 add $0x2ff8,%eax
804900d: b8 00 00 00 00 mov $0x0,%eax
8049012: 5d pop %ebp
8049013: c3 ret
Мы ясно видим, что основная функция программы (в данном случае весь раздел text) больше. И мы можем даже подтвердить это, посмотрев на размер раздела text в ELF-файле:
# one.c
Section Headers:
[Nr] Name Type Addr Off Size ES Flg Lk Inf Al
[ 2] .text PROGBITS 08049000 001000 000037 00 AX 0 0 1
# two.c
Section Headers:
[Nr] Name Type Addr Off Size ES Flg Lk Inf Al
[ 2] .text PROGBITS 08049000 001000 000018 00 AX 0 0 1
Давайте проверим размер каждой из двух программ (окончательное изображение) с помощью wc -c. Первая программа привела к изображению 13092 bytes, а вторая программа - к точно такому же изображению - 13092 bytes. Что происходит?
Причина этого противоречивого результата связана с выравниванием сегментов.
Каждый раздел связан с сегментом таблицы в файле ELF под названием Section to Segment mapping. Сегменты содержат данные, которые загружаются в память в неизмененном виде и используются во время выполнения, а разделы содержат данные для связывания и перемещения. В одном сегменте может быть несколько разделов, причем не обязательно одного типа.
В первой программе наша таблица Section to Segment mapping вместе с Program Headers (заголовками, описывающими сегменты) показана ниже:
# one.c
Program Headers:
Type Offset VirtAddr PhysAddr FileSiz MemSiz Flg Align
LOAD 0x000000 0x08048000 0x08048000 0x00118 0x00118 R 0x1000
LOAD 0x001000 0x08049000 0x08049000 0x00037 0x00037 R E 0x1000
LOAD 0x002000 0x0804a000 0x0804a000 0x0004c 0x0004c R 0x1000
LOAD 0x003000 0x0804c000 0x0804c000 0x00010 0x00010 RW 0x1000
NOTE 0x0000f4 0x080480f4 0x080480f4 0x00024 0x00024 R 0x4
GNU_STACK 0x000000 0x00000000 0x00000000 0x00000 0x00000 RW 0x10
Section to Segment mapping:
Segment Sections...
00 .note.gnu.build-id
01 .text
02 .eh_frame
03 .got.plt .data
04 .note.gnu.build-id
05
Что касается Program Headers: Offset — смещение сегмента в файле; VirtAddr и PhysAddr обозначают адреса, по которым этот сегмент должен быть загружен; FileSiz — размер сегмента внутри файла.
Обратите внимание на несколько вещей: раздел text сопоставлен с сегментом 01, а раздел data — с сегментом 03; FileSiz сегмента 01 равен 0x37, что соответствует значению, которое мы видели ранее; Offset сегментов в файле выровнены по 0x1000; сегмент 01 (раздела text) находится перед сегментом 03 (раздела data), который является последним сегментом в файле.
Такое выравнивание сегментов является причиной того, что хотя мы и увеличили размер раздела text, сам файл не увеличился. Байты 0x1000 в файле между сегментом 01 и сегментом, который следует за ним, останутся 0x1000 до тех пор, пока наш раздел text содержит менее 0x1000 байт — он будет просто дополнен нулями, пока не используется. Если у вас есть раздел text с 0xfff байтами и вы добавляете еще 4, то ваше окончательное изображение вырастет еще на 0x1000 байт, так как размер сегмента, содержащего раздел text, теперь будет 0x2000.
А поскольку раздел data хранится в последнем сегменте файла, его не нужно дополнять, а это означает, что увеличение его размера будет соответствовать увеличению размера конечного изображения.
[Упрощенный]
Такое выравнивание может помочь операционной системе быстрее загружать процесс, одновременно ограничивая привилегии чтения/записи/выполнения в соответствии с различными разделами (раздел data будет доступен для чтения/записи, а раздел text для чтения/выполнения).
Операционная система может выделять блоки памяти только размером 0x1000; эти блоки называются страницами. Каждую страницу можно настроить по мере необходимости для чтения/записи/выполнения. Если мы хотим, чтобы наш сегмент, содержащий data, был доступен для чтения/записи, а наш сегмент, содержащий text, был доступен для чтения/выполнения, мы не можем разместить их на одной странице.
Есть ли книги или краткие ресурсы для чтения, к которым я могу обратиться, которые помогут мне с такими концепциями более низкого уровня?
Я думаю, что лучший способ изучить эти концепции или любую другую концепцию — это практика. Выберите низкоуровневый проект, который вы хотите написать (например, драйвер оборудования, компилятор, операционную систему), и сделайте это. Это позволит вам понять эти концепции, а не только думать, что вы их понимаете. Изучение C, ассемблера, принципов работы компиляторов и формата файлов ELF — отличное начало.
так...
Они могут внести свой вклад, а могут и нет. В любом случае, они будут способствовать увеличению размера раздела text.
Посмотрите сборку исполняемого файла на сайте godbolt.org/z/s6611E17c — глобальная переменная присутствует в исполняемом файле, но локальная переменная помещается в стек.