Как инициализированные локальные переменные хранятся в памяти стека?

Зависит ли хранение локальных переменных в памяти стека от использования их значений в вызовах функций?

Выполняя простые упражнения с языком программирования "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

Таким образом, я прихожу к выводу, что в памяти стека хранятся только те переменные, которые передаются в качестве аргумента вызову функции. Что происходит, а главное - почему так происходит?

Компилятор "выбрасывает" из программы (в процессе компиляции) переменные, которые, несмотря на то, что они инициализированы каким-то значением, не используются ни одной функцией в качестве аргументов?

Спасибо! Добавление ключевого слова «volatile» к объявлению переменной заставило программу выдать ожидаемый результат.

Sandrious 23.11.2022 16:08

Преобразованный комментарий в ответ.

hyde 23.11.2022 16:14

Фундаментальная проблема здесь, я боюсь, заключается в вашем предположении, что для того, чтобы «понять» C, вам каким-то образом нужно понять, как локальные переменные располагаются во фрейме стека. Но если вы не делаете что-то достаточно экзотическое, вам не нужно это понимать! Это полностью зависит от компилятора, как устроен кадр стека, компилятор может сделать это по-другому без всякой причины завтра, и конкретный выбор, который делает компилятор, вас вообще не касается, потому что он не влияет на то, как работает ваша программа. бежит. (Опять же, если вы не делаете что-то экзотическое.)

Steve Summit 23.11.2022 16:24

@SteveSummit Учитывая, насколько легко написать Undefined Behavior на C, я думаю, что особенности компилятора и сгенерированной сборки (даже только для одной платформы) касаются каждого программиста на C. Гораздо легче избежать ошибок, когда вы понимаете возможную механику, стоящую за этими ошибками, по сравнению с механикой правильного кода C.

hyde 23.11.2022 17:21

@hyde Зная, что на машине, основанной на стеке, локальные переменные (во всяком случае, нерегистровые) располагаются как смещения от указателя кадра, смежные друг с другом и такими вещами, как предыдущий указатель кадра и адрес возврата, действительно полезный. Но ОП, похоже, пытался понять, как, учитывая int a, b, c;, предсказать, будут ли a, b и c на самом деле соседними, или в каком порядке. Возможно, я должен был сказать «точно понимать, как расположены локальные переменные».

Steve Summit 23.11.2022 17:25
Стоит ли изучать PHP в 2023-2024 годах?
Стоит ли изучать PHP в 2023-2024 годах?
Привет всем, сегодня я хочу высказать свои соображения по поводу вопроса, который я уже много раз получал в своем сообществе: "Стоит ли изучать PHP в...
Поведение ключевого слова "this" в стрелочной функции в сравнении с нормальной функцией
Поведение ключевого слова "this" в стрелочной функции в сравнении с нормальной функцией
В JavaScript одним из самых запутанных понятий является поведение ключевого слова "this" в стрелочной и обычной функциях.
Приемы CSS-макетирования - floats и Flexbox
Приемы CSS-макетирования - floats и Flexbox
Здравствуйте, друзья-студенты! Готовы совершенствовать свои навыки веб-дизайна? Сегодня в нашем путешествии мы рассмотрим приемы CSS-верстки - в...
Тестирование функциональных ngrx-эффектов в Angular 16 с помощью Jest
В системе управления состояниями ngrx, совместимой с Angular 16, появились функциональные эффекты. Это здорово и делает код определенно легче для...
Концепция локализации и ее применение в приложениях React ⚡️
Концепция локализации и ее применение в приложениях React ⚡️
Локализация - это процесс адаптации приложения к различным языкам и культурным требованиям. Это позволяет пользователям получить опыт, соответствующий...
Пользовательский скаляр GraphQL
Пользовательский скаляр GraphQL
Листовые узлы системы типов GraphQL называются скалярами. Достигнув скалярного типа, невозможно спуститься дальше по иерархии типов. Скалярный тип...
1
5
50
3
Перейти к ответу Данный вопрос помечен как решенный

Ответы 3

Ответ принят как подходящий

Оптимизатор может оптимизировать до тех пор, пока поведение программы не изменится (правило «как если»), как указано в стандарте C (именно поэтому Undefined Behavior настолько опасен, что позволяет компилятору «оптимизировать» неожиданным образом).

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

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

  1. C ничего не знает о стеке. Как хранятся автоматические переменные, зависит от реализации.

  2. Компилятору не нужно хранить какие-либо переменные или, если они существуют, хранить их в определенном месте. Наблюдаемое поведение программы должно быть точно таким же, как абстрактная семантика. Как это делается внутри, зависит от реализации за одним исключением: 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

https://godbolt.org/z/6Ye8WrM8M

Я ожидаю, что последовательность адресов будет соответствовать порядку перечисления переменных.

Почему? Такой гарантии никто не давал, ни книга K&R, ни стандарт C. Кроме того, указатель стека может либо увеличиваться, либо уменьшаться в зависимости от процессора, и C позволяет это сделать - на самом деле C ничего не говорит о стеке. Я даже программировал C на крошечных микроконтроллерах без стека.

все три переменные будут совместно использовать соседние блоки памяти в соответствии с порядком определения переменных

Опять же, никто не давал такой гарантии. Это просто одно конкретное поведение на одном конкретном компиляторе для одной конкретной системы.

  • Единственная гарантия, которую C дает в отношении смежного распределения, — это когда вы используете массивы.
  • Единственная гарантия C относительно порядка размещения — это когда вы используете переменные-члены внутри структуры. (Хотя не гарантируется, что они будут смежными.)

Таким образом, я прихожу к выводу, что в памяти стека хранятся только те переменные, которые передаются в качестве аргумента вызову функции. Что происходит, а главное - почему так происходит?

Все переменные, используемые вашей программой, должны где-то храниться. Их нельзя выделить в воздухе.

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


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

Однако вы можете сделать выводы о том, как компилятор x организует данные и кадры стека в соответствии с ABI y для системы z. Если это то, что вас интересует, в вашем вопросе необходимо упомянуть эти детали.

Другие вопросы по теме