Почему моя программа не переполняет стек, когда я выделяю массив символов размером 11 МБ, а верхний предел стека составляет 10 МБ?

У меня есть две простые программы на C++ и два вопроса. Я работаю в CentOS 5.2, и моя среда разработки выглядит следующим образом:

  • g++ (GCC) 4.1.2 20080704 (Red Hat 4.1.2-50)
  • Вывод "ulimit -s": 10240 (кбайт), то есть 10 МБ.

Программа №1:

основной.cpp:

int main(int argc, char * argv[])
{
    char buf[1024*1024*11] = {0};
    return 0;
}

(Составлено с помощью "g++ -g main.cpp")

Программа выделяет 1024*1024*11 байт (то есть 11 МБ) в стеке, но не падает. После того, как я изменил размер выделения на 1024*1024*12 (то есть 12 МБ), программа вылетает. Я думаю, что это должно быть вызвано переполнением стека. Но почему программа не аварийно завершает работу, когда размер выделения составляет 11 МБ, что также превышает верхний предел в 10 МБ??

Программа №2:

основной.cpp:

#include <iostream>

int main(int argc, char * argv[])
{
    char buf[1024*1024*11] = {0};

    std::cout << "*** separation ***" << std::endl;

    char buf2[1024*1024] = {0};

    return 0;
}

(Составлено с помощью "g++ -g main.cpp")

Эта программа приведет к сбою программы, поскольку она выделяет 12 МБ байтов в стеке. Однако, согласно файлу дампа ядра (см. ниже), сбой происходит на buf, а не на buf2. Разве не должен произойти сбой с buf2, потому что из программы №1 мы знаем, что выделение char buf[1024*1024*11] в порядке, поэтому после того, как мы выделим еще 1024*1024 байт, стек переполнится?

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

Приложение: Информация о дампе ядра, сгенерированная программой №2:

Core was generated by `./a.out'.
Program terminated with signal 11, Segmentation fault.
[New process 16433]
#0  0x08048715 in main () at main.cpp:5
5           char buf[1024*1024*11] = {0};

person yaobin    schedule 15.08.2011    source источник
comment
Аккуратный. Возможно, вы захотите попробовать это снова с -O0 на тот случай, если gcc распределит массивы забавным образом.   -  person Owen    schedule 15.08.2011
comment
Кроме того, я получаю segfault, когда запускаю первую программу с ограничением стека в 10 МБ. Не то чтобы это много говорит.   -  person Owen    schedule 15.08.2011


Ответы (4)


Вы ошибочно предполагаете, что выделение стека происходит там, где оно появляется в вашем коде. Каждый раз, когда у вас есть локальные переменные, размер которых известен во время компиляции, пространство для них будет выделено вместе при входе в функцию. Позже выделяются только локальные переменные динамического размера (VLA и alloca).

Кроме того, ошибка возникает, как только вы записываете в память, а не при ее первом выделении. Скорее всего, buf находится перед buf2 в стеке, поэтому переполнение происходит в buf, а не в buf2.

person Per Johansson    schedule 15.08.2011
comment
Позже выделяются только локальные переменные динамического размера, и почему это так? Кажется, нет никаких причин, по которым компилятор не должен резервировать стек в месте объявления переменной, а вместо этого делает это при входе в функцию. (Я узнал то же самое, но вопрос, почему? это, кажется, не имеет особого смысла.) - person Ingo Blackman; 06.12.2016
comment
@IngoBlackman Я не знаю очень подробного ответа, так устроен стек. Большинство функций создают кадр стека при входе в функцию; этот кадр стека содержит место для локальных переменных. Очевидно, что Alloca и VLA портят ситуацию, но они появились позднее. Вероятно, есть много причин сделать это таким образом, например. более легкая генерация машинного кода и более легкая отладка. Я уверен, что вы можете найти больше причин в какой-нибудь книге по дизайну ABI. - person Per Johansson; 06.12.2016

Чтобы проанализировать такого рода загадки, всегда полезно посмотреть на сгенерированный код. Я предполагаю, что ваша конкретная версия компилятора делает что-то другое, потому что мой segfaults с -O0, но не с -O1.

Из вашей программы №1 с помощью g++ a.c -g -O0, а затем objdump -S a.out

int main(int argc, char * argv[])
{
 8048484:       55                      push   %ebp
 8048485:       89 e5                   mov    %esp,%ebp

Это стандартный кадр стека. Здесь нечего смотреть.

 8048487:       83 e4 f0                and    $0xfffffff0,%esp

На всякий случай выровняйте стек до числа, кратного 16.

 804848a:       81 ec 30 00 b0 00       sub    $0xb00030,%esp

Выделите 0xB00030 байт пространства стека. То есть 1024*1024*11 + 48 байт. Доступа к памяти пока нет, так что не исключение. Дополнительные 48 байтов предназначены для внутреннего использования компилятором.

 8048490:       8b 45 0c                mov    0xc(%ebp),%eax
 8048493:       89 44 24 1c             mov    %eax,0x1c(%esp)   <--- SEGFAULTS

Первый раз, когда доступ к стеку выходит за пределы ulimit, поэтому он segfaults.

 8048497:       65 a1 14 00 00 00       mov    %gs:0x14,%eax

Это защита стека.

 804849d:       89 84 24 2c 00 b0 00    mov    %eax,0xb0002c(%esp)
 80484a4:       31 c0                   xor    %eax,%eax
    char buf[1024*1024*11] = {0};
 80484a6:       8d 44 24 2c             lea    0x2c(%esp),%eax
 80484aa:       ba 00 00 b0 00          mov    $0xb00000,%edx
 80484af:       89 54 24 08             mov    %edx,0x8(%esp)
 80484b3:       c7 44 24 04 00 00 00    movl   $0x0,0x4(%esp)
 80484ba:       00 
 80484bb:       89 04 24                mov    %eax,(%esp)
 80484be:       e8 d1 fe ff ff          call   8048394 <memset@plt>

Инициализировать массив, вызвав memset

    return 0;
 80484c3:       b8 00 00 00 00          mov    $0x0,%eax
}

Как вы можете видеть, segfault происходит при доступе к внутренним переменным, потому что они оказались ниже большого массива (они должны быть, потому что есть защита стека, чтобы обнаружить разрушение стека).

Если вы компилируете с оптимизацией, компилятор замечает, что вы не делаете ничего полезного с массивом, и оптимизирует его. Так что никакого сигсега.

Вероятно, ваша версия GCC немного перемудрила в режиме без оптимизации и удаляет массив. Мы можем проанализировать это дальше, если вы опубликуете вывод objdump -S a.out.

person rodrigo    schedule 16.08.2011

При определении локальных переменных в стеке нет реального выделения памяти, как это делается в куче. Выделение памяти в стеке состоит более просто в изменении адреса указателя стека (который будет использоваться вызываемыми функциями) для резервирования требуемой памяти.

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

person Didier Trosset    schedule 15.08.2011
comment
Возможно, вы пропустили фигурные скобки с инициализатором массива. Кстати, для меня он сбрасывает ядро ​​​​с первым примером. - person Maxim Egorushkin; 15.08.2011

Обе ваши программы в идеале должны выдавать segfault.

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

Нулевой уровень оптимизации, обозначенный при компиляции как -O0, указывает на отсутствие оптимизации вообще. Это также уровень оптимизации по умолчанию, с которым компилируется код. Вышеупомянутые программы при компиляции с -O0 выдают segfault.

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

person rango    schedule 12.07.2016