Заказ локального распределения переменных в стеке - программирование
Подтвердить что ты не робот

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

Взгляните на эти две функции:

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 массивов), поэтому это не может быть проблемой заполнения. Какие причины могут быть для этого переупорядочения, и, кроме того, возможно ли это, посмотрев на код С, заранее определить, как будут упорядочены локальные переменные?

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

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

4b9b3361

Ответ 1

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

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

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

Ответ 2

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

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 и указатели обрабатываются первым, последним объявленным в верхней части стека и сначала объявленным ближе к основанию. Затем массивы обрабатываются в противоположном направлении, чем раньше декларация, самая высокая в стеке. Я уверен, что для этого есть веская причина. Интересно, что это такое.

Ответ 3

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

Ответ 4

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

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

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

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

Это все о том, что проще всего (переводит на "самый быстрый" ) для захвата процессора.

Ответ 5

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

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

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

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

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

Ответ 6

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

Ответ 7

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

Ответ 8

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

Ответ 9

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

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

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

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