Почему GCC вычитает неправильное значение указателю стека при распределении большого массива без последующих вызовов функций?

Действительно причудливая gcc quirk. Проверьте это:

main() { int a[100]; a[0]=1; }

создает эту сборку:

   0:   55                      push   %rbp
   1:   48 89 e5                mov    %rsp,%rbp
   4:   48 81 ec 18 01 00 00    sub    $0x118,%rsp
   b:   c7 85 70 fe ff ff 01    movl   $0x1,-0x190(%rbp)
  12:   00 00 00 
  15:   c9                      leaveq 
  16:   c3                      retq

В верхней части стека явно 400, так как его массив 100 * 4. Поэтому, когда он записывается в первую запись, он делает rbp-400 (строка 'b'). Хорошо. Но почему он вычитает 280 из указателя стека (строка "4" )? Не указывает ли это на середину массива?

Если после этого добавить вызов функции, gcc делает правильную вещь:

b() {}
main() { int a[100]; a[0]=1; b(); }

создает эту сборку:

0000000000000000 <b>:
   0:   55                      push   %rbp
   1:   48 89 e5                mov    %rsp,%rbp
   4:   c9                      leaveq 
   5:   c3                      retq   

0000000000000006 <main>:
   6:   55                      push   %rbp
   7:   48 89 e5                mov    %rsp,%rbp
   a:   48 81 ec 90 01 00 00    sub    $0x190,%rsp
  11:   c7 85 70 fe ff ff 01    movl   $0x1,-0x190(%rbp)
  18:   00 00 00 
  1b:   b8 00 00 00 00          mov    $0x0,%eax
  20:   e8 00 00 00 00          callq  25 <main+0x1f>
  25:   c9                      leaveq 
  26:   c3                      retq 

Здесь он правильно вычитает 400 (строка 'a').

Почему изменение при добавлении вызова функции? Является ли gcc просто ленивым и не делает это правильно, потому что это не имеет значения? Что происходит? Очевидно, это происходит только при компиляции для x86_64, но не для простого x86. Имеет ли это что-то странное с x86_64 "redzone"? Что происходит точно?

Ответ 1

Ваша догадка правильная. Это "красная зона". Красная зона - это пространство от rsp-128 до rsp, которое может использоваться функцией для локальных переменных и для временного хранения. Это пространство не тронуто обработчиками прерываний и исключений. Очевидно, что красная зона уничтожается вызовами функций, поэтому, если вызывается какая-либо функция, никакая локальная переменная не может находиться в красной зоне.

Красную зону можно использовать только в 64-битных Linux, BSD и Mac. Он недоступен в коде ядра.

Он может использоваться для оптимизации пространства, поскольку с красной зоной вы можете ссылаться на 512 байтов локальных переменных с короткими инструкциями на основе только rsp и ebp. Без красной зоны доступно только 384 байта. Доступ ко всем локальным переменным за пределами этого предела осуществляется с более длинным кодом или с дополнительными регистрами.

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

Ответ 2

ABI x86-64 задает "красную зону" из 128 байт за указатель стека, который можно использовать без изменения %rsp. В первом примере main() является листовой функцией, поэтому компилятор оптимизирует использование пространства стека - то есть нет вызовов функций, поэтому эта область не будет перезаписана.