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

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

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 . В buffer1 function2() ret следует buffer1 , затем buffer2 и buffer3 . Почему порядок хранения изменился? Мы используем тот же объем памяти в обоих случаях (4 байта int s vs 4 байтовых массива char ), поэтому это не может быть проблемой заполнения. Какие причины могут быть для этого переупорядочения, и, кроме того, возможно ли это, посмотрев на код С, заранее определить, как будут упорядочены локальные переменные?

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

  • Есть ли способ получить ссылочный адрес?
  • Почему освобожденная структура в C все еще имеет данные?
  • g ++ размер массива без предупреждения?
  • В чем разница между оператором присваивания и конструктором копирования?
  • Размер байт в памяти - Java
  • C Управление памятью
  • Определить размер кучи приложения в Android
  • Когда Fragment заменяется и помещается в задний стек (или удаляется), он остается в памяти?
  • Как обращаться с realloc при сбое из-за памяти?
  • AngularJS - $ destroy удаляет прослушиватели событий?
  • Использование памяти в C #
  • Давайте будем гением компьютера.