Порядок размещения локальной переменной в стеке - PullRequest
26 голосов
/ 09 июля 2009

Посмотрите на эти две функции:

void function1() {
    int x;
    int y;
    int z;
    int *ret;
}

void function2() {
    char buffer1[4];
    char buffer2[4];
    char buffer3[4];
    int *ret;
}

Если я разбью на function1() в gdb и выведу адреса переменных, я получу это:

(gdb) p &x  
$1 = (int *) 0xbffff380
(gdb) p &y
$2 = (int *) 0xbffff384
(gdb) p &z
$3 = (int *) 0xbffff388
(gdb) p &ret
$4 = (int **) 0xbffff38c

Если я делаю то же самое в function2(), я получаю это:

(gdb) p &buffer1
$1 = (char (*)[4]) 0xbffff388
(gdb) p &buffer2
$2 = (char (*)[4]) 0xbffff384
(gdb) p &buffer3
$3 = (char (*)[4]) 0xbffff380
(gdb) p &ret
$4 = (int **) 0xbffff38c

Вы заметите, что в обеих функциях ret хранится ближе всего к вершине стека. В function1() за ним следуют z, y и, наконец, x. В function2(), ret сопровождается buffer1, затем buffer2 и buffer3. Почему изменился порядок хранения? Мы используем одинаковый объем памяти в обоих случаях (4 байта int с против 4 байтов char массивов), поэтому это не может быть проблемой заполнения. Какие могут быть причины для такого переупорядочения, и, кроме того, возможно ли, взглянув на код C, заранее определить, как будут упорядочены локальные переменные?

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

Для справки я использую GCC 4.0.1 в Mac OS 10.5.7

Ответы [ 9 ]

16 голосов
/ 09 июля 2009

Понятия не имею , почему GCC организовывает свой стек так, как он это делает (хотя я думаю, что вы можете взломать его источник или эту статью и узнать) рассказать вам, как гарантировать порядок определенных переменных стека, если по какой-то причине вам это нужно. Просто поместите их в структуру:

void function1() {
    struct {
        int x;
        int y;
        int z;
        int *ret;
    } locals;
}

Если память мне не изменяет, спецификация гарантирует, что &ret > &z > &y > &x. Я оставил свой K & R на работе, поэтому не могу процитировать главу и стих.

9 голосов
/ 10 июля 2009

Итак, я провел еще несколько экспериментов, и вот что я нашел. Кажется, он основан на том, является ли каждая переменная массивом. Учитывая этот вход:

void f5() {
        int w;
        int x[1];
        int *ret;
        int y;
        int z[1];
}

Я получаю это в GDB:

(gdb) p &w
$1 = (int *) 0xbffff4c4
(gdb) p &x
$2 = (int (*)[1]) 0xbffff4c0
(gdb) p &ret 
$3 = (int **) 0xbffff4c8
(gdb) p &y
$4 = (int *) 0xbffff4cc
(gdb) p &z
$5 = (int (*)[1]) 0xbffff4bc

В этом случае int s и указатели обрабатываются первыми, последние объявляются сверху стека и сначала объявляются ближе к низу. Затем массивы обрабатываются в обратном направлении, чем раньше объявление, тем выше в стеке. Я уверен, что есть веская причина для этого. Интересно, что это такое.

8 голосов
/ 09 июля 2009

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

5 голосов
/ 09 июля 2009

Обычно это связано с проблемами выравнивания.

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

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

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

Это все о том, что проще (переводит как «самый быстрый») для процессора захватить.

2 голосов
/ 17 сентября 2017

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

[...] Структура хранилища для параметров [function] не указана. (C11 6.9.1p9)

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

Стандарт C не содержит одиночного упоминания слова «стек»; вполне возможно сделать, например, реализацию C без стеков, выделяя каждую запись активации из кучи (хотя тогда можно было бы понять, что они образуют стек).

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

0 голосов
/ 09 июля 2009

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

0 голосов
/ 09 июля 2009

Интересно, что если вы добавите дополнительный int * ret2 в function1, то в моей системе порядок будет правильным, тогда как он не в порядке только для 3 локальных переменных. Я предполагаю, что это упорядочено таким образом, чтобы отразить стратегию распределения регистров, которая будет использоваться. Либо это, либо это произвольно.

0 голосов
/ 09 июля 2009

Это также может быть проблемой безопасности?

int main()
{
    int array[10];
    int i;
    for (i = 0; i <= 10; ++i)
    {
        array[i] = 0;
    }
}

Если массив в стеке ниже, чем i, этот код будет зацикливаться бесконечно (поскольку он ошибочно обращается и обнуляет массив [10], то есть i). Если поместить массив выше в стек, попытки получить доступ к памяти за пределами стека с большей вероятностью коснутся нераспределенной памяти и произойдут сбой, чем вызовут неопределенное поведение.

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

0 голосов
/ 09 июля 2009

Я предполагаю, что это как-то связано с тем, как данные загружаются в регистры. Возможно, с массивами символов компилятор творит чудеса, чтобы делать вещи параллельно, и это как-то связано с положением в памяти, чтобы легко загружать данные в регистры. Попробуйте компилировать с разными уровнями оптимизации и попробуйте использовать int buffer1[1].

...