Зависит ли хранение локальных переменных в памяти стека от использования их значений в вызовах функций?
Выполняя простые упражнения с языком программирования "C" и, более конкретно, с указателями, я заметил следующую аномалию (я знаю, что это не аномалия, это просто мое непонимание) относительно инициализированных локальных переменных.
Когда я определяю и инициализирую пару переменных и пытаюсь напечатать адрес (через функцию «printf()») первой и последней переменных, я ожидаю, что последовательность адресов будет соответствовать порядку перечисления переменные. Первая переменная имеет наибольший адрес, а последняя переменная занимает (адрес блока памяти первой переменной, minuns N блоков памяти, где N — количество оставшихся переменных, кроме первой). Как это:
v1 = memory block 10;
v2 = memory block 09;
v3 = memory block 08;
v4 = memory block 07;
И когда я заставляю программу печатать только адреса v1 и v4, я ожидаю, что она напечатает:
Address of v1 is block 10;
Address of v4 is block 07;
Здесь доходит до так называемой «аномалии». Когда программа печатает адреса этих переменных, она на самом деле печатает:
Address of v1 is block10; (as it should be)
Address of v4 is block09; (isn't v2 supposed to be stored here?)
Вот пример кода:
#include <stdio.h>
int main()
{
char a = 1, b = 23, c = 123, d = 12;
printf("address of a: %p\naddress of d: %p", &a, &d);
return 0;
}
Результат:
address of a: 0x7fffa86fc724
address of d: 0x7fffa86fc725
Теперь, если я добавлю третью переменную в качестве аргумента в функцию «printf()», адрес первой переменной останется прежним, все три переменные будут совместно использовать соседние блоки памяти, следуя порядку определения переменных. Возьмем указанные выше четыре переменные v1, v2, v3 и v4. Если я напечатаю адреса v1, v3 и v4, я получу следующий результат:
v1 = block10;
v3 = block09;
v4 = block08;
Порядок перечисления адресов переменных в списке аргументов функции "printf()" не влияет на порядок адресов переменных. Я вижу, что программа по-прежнему следует порядку определения переменных - переменная, которая определена первой, будет занимать самый старший адрес, а каждая следующая переменная, переданная в качестве аргумента функции, будет занимать место в памяти, соседнее с памятью. расположение предыдущей переменной в зависимости от порядка определения.
Пример кода:
#include <stdio.h>
int main()
{
char a = 1, b = 23, c = 123, d = 12;
printf("address of c: %p\naddress of a: %p\naddress of d: %p", &c, &a, &d);
return 0;
}
Вывод будет:
address of c: 0x7ffd970e27d5
address of a: 0x7ffd970e27d4
address of d: 0x7ffd970e27d6
Кроме того, если переменные передаются хотя бы один раз в качестве аргументов для вызова функции (другая функция, кроме «printf()»), то печать только адреса v1 и v4 приведет к результату, который я изначально ожидал.
Пример кода:
#include <stdio.h>
int main()
{
char a, b, c, d;
scanf(" %hhi %hhi %hhi %hhi", &a, &b, &c, &d);
printf("address of c: %p\naddress of a: %p\naddress of d: %p", &c, &a, &d);
return 0;
}
Вывод будет:
address of a: 0x7ffeaac3c4a4
address of d: 0x7ffeaac3c4a7
Таким образом, я прихожу к выводу, что в памяти стека хранятся только те переменные, которые передаются в качестве аргумента вызову функции. Что происходит, а главное - почему так происходит?
Компилятор "выбрасывает" из программы (в процессе компиляции) переменные, которые, несмотря на то, что они инициализированы каким-то значением, не используются ни одной функцией в качестве аргументов?
Преобразованный комментарий в ответ.
Фундаментальная проблема здесь, я боюсь, заключается в вашем предположении, что для того, чтобы «понять» C, вам каким-то образом нужно понять, как локальные переменные располагаются во фрейме стека. Но если вы не делаете что-то достаточно экзотическое, вам не нужно это понимать! Это полностью зависит от компилятора, как устроен кадр стека, компилятор может сделать это по-другому без всякой причины завтра, и конкретный выбор, который делает компилятор, вас вообще не касается, потому что он не влияет на то, как работает ваша программа. бежит. (Опять же, если вы не делаете что-то экзотическое.)
@SteveSummit Учитывая, насколько легко написать Undefined Behavior на C, я думаю, что особенности компилятора и сгенерированной сборки (даже только для одной платформы) касаются каждого программиста на C. Гораздо легче избежать ошибок, когда вы понимаете возможную механику, стоящую за этими ошибками, по сравнению с механикой правильного кода C.
@hyde Зная, что на машине, основанной на стеке, локальные переменные (во всяком случае, нерегистровые) располагаются как смещения от указателя кадра, смежные друг с другом и такими вещами, как предыдущий указатель кадра и адрес возврата, действительно полезный. Но ОП, похоже, пытался понять, как, учитывая int a, b, c;
, предсказать, будут ли a
, b
и c
на самом деле соседними, или в каком порядке. Возможно, я должен был сказать «точно понимать, как расположены локальные переменные».
Оптимизатор может оптимизировать до тех пор, пока поведение программы не изменится (правило «как если»), как указано в стандарте C (именно поэтому Undefined Behavior настолько опасен, что позволяет компилятору «оптимизировать» неожиданным образом).
Например, локальная переменная может находиться в регистре, если только вы не получите ее адрес, что заставит компилятор поместить ее в память, чтобы у нее был адрес.
volatile
заставляет компилятор предполагать, что каждый доступ к переменной (чтение или запись) может иметь побочный эффект, поэтому отключает многие оптимизации и принудительно помещает переменную в память (стек для локальной переменной). Это должно решить вашу проблему.
C ничего не знает о стеке. Как хранятся автоматические переменные, зависит от реализации.
Компилятору не нужно хранить какие-либо переменные или, если они существуют, хранить их в определенном месте. Наблюдаемое поведение программы должно быть точно таким же, как абстрактная семантика. Как это делается внутри, зависит от реализации за одним исключением: volatile объекты.
Так что обсуждать можно только конкретную реализацию. Реализация может создавать другой код, когда используются разные параметры компиляции (например, уровень оптимизации).
Таким образом, я прихожу к выводу, что только переменные, которые передаются в качестве аргумента вызову функции, хранятся в стеке объем памяти. Что происходит, а главное - почему происходит подобное это?
Ваш вывод неверен. Пример:
void foo(int x)
{
int z = x;
printf("%d\n", z);
}
int main(void)
{
int y = 5;
foo(y);
}
Код:
.string "%d\n"
foo:
mov esi, edi
xor eax, eax
mov edi, OFFSET FLAT:.LC0
jmp printf
main:
push rax
mov edi, 5
call foo
xor eax, eax
pop rdx
ret
Я ожидаю, что последовательность адресов будет соответствовать порядку перечисления переменных.
Почему? Такой гарантии никто не давал, ни книга K&R, ни стандарт C. Кроме того, указатель стека может либо увеличиваться, либо уменьшаться в зависимости от процессора, и C позволяет это сделать - на самом деле C ничего не говорит о стеке. Я даже программировал C на крошечных микроконтроллерах без стека.
все три переменные будут совместно использовать соседние блоки памяти в соответствии с порядком определения переменных
Опять же, никто не давал такой гарантии. Это просто одно конкретное поведение на одном конкретном компиляторе для одной конкретной системы.
Таким образом, я прихожу к выводу, что в памяти стека хранятся только те переменные, которые передаются в качестве аргумента вызову функции. Что происходит, а главное - почему так происходит?
Все переменные, используемые вашей программой, должны где-то храниться. Их нельзя выделить в воздухе.
Что касается того, что происходит, когда вы передаете переменные в функцию, они передаются в соответствии с ABI (абстрактным двоичным интерфейсом) для определенной системы, который указывает, какие параметры складываются/хранятся в регистрах и в каком порядке, или если стек выполняется вызывающим или вызываемым пользователем и т. д. Опять же, это сильно зависит от системы и выходит за рамки самого C.
Основное заблуждение здесь заключается в том, что нельзя исследовать одну конкретную программу для конкретной системы, сгенерированную одним конкретным компилятором, а затем делать выводы о том, как работают программы на языке C в целом.
Однако вы можете сделать выводы о том, как компилятор x организует данные и кадры стека в соответствии с ABI y для системы z. Если это то, что вас интересует, в вашем вопросе необходимо упомянуть эти детали.
Спасибо! Добавление ключевого слова «volatile» к объявлению переменной заставило программу выдать ожидаемый результат.