Почему пролог x86-64 GCC пролог выделяет меньше стека, чем локальные переменные?

Рассмотрим следующую простую программу:

int main(int argc, char **argv)
{
        char buffer[256];

        buffer[0] = 0x41;
        buffer[128] = 0x41;
        buffer[255] = 0x41;

        return 0;
}

Скомпилирован с GCC 4.7.0 на машине x86-64. Разборка main() с помощью GDB дает:

0x00000000004004cc <+0>:     push   rbp
0x00000000004004cd <+1>:     mov    rbp,rsp
0x00000000004004d0 <+4>:     sub    rsp,0x98
0x00000000004004d7 <+11>:    mov    DWORD PTR [rbp-0x104],edi
0x00000000004004dd <+17>:    mov    QWORD PTR [rbp-0x110],rsi
0x00000000004004e4 <+24>:    mov    BYTE PTR [rbp-0x100],0x41
0x00000000004004eb <+31>:    mov    BYTE PTR [rbp-0x80],0x41
0x00000000004004ef <+35>:    mov    BYTE PTR [rbp-0x1],0x41
0x00000000004004f3 <+39>:    mov    eax,0x0
0x00000000004004f8 <+44>:    leave  
0x00000000004004f9 <+45>:    ret    

Почему это sub rsp только с 0x98 = 152d, когда буфер составляет 256 байт? Когда я перемещаю данные в буфер [0], он просто использует данные за пределами выделенного фрейма стека и использует rbp для ссылки, так что даже точка sub rsp, 0x98?

Другой вопрос, что делают эти строки?

0x00000000004004d7 <+11>:    mov    DWORD PTR [rbp-0x104],edi
0x00000000004004dd <+17>:    mov    QWORD PTR [rbp-0x110],rsi

Почему EDI, а не RDI необходимо сохранить? Я вижу, что он перемещает это вне максимального диапазона выделенного буфера в коде C. Интересно также, почему дельта между двумя переменными настолько велика. Поскольку EDI составляет всего 4 байта, для чего требуется разделение по 12 байт для двух переменных?

Ответ 1

x86-64 ABI, используемая Linux (и некоторые другие ОС, хотя, в частности, не Windows, у которой есть свой собственный ABI), определяет "красную зону" 128 байт ниже указателя стека, который гарантированно не будет затронут сигналами или обработчиками прерываний. (См. Рис. 3.3 и п. 3.2.2.)

Функция листа (то есть одна, которая не вызывает ничего другого), поэтому может использовать эту область для чего угодно - она ​​не делает ничего подобного call, который помещает данные в указатель стека; и любой обработчик сигнала или прерывания будет следовать за ABI и удалять указатель стека хотя бы на дополнительные 128 байтов, прежде чем хранить что-либо.

(Короткие кодировки команд доступны для подписанных 8-битных перемещений, поэтому точкой красной зоны является то, что она увеличивает количество локальных данных, доступ к которым может использовать функция листа, используя эти более короткие инструкции.)

Что здесь происходит.

Но... этот код не использует эти более короткие кодировки (он использует смещения от rbp, а не rsp). Почему нет? Он также сохраняет ненужные значения edi и rsi - вы спрашиваете, почему он сохраняет edi вместо rdi, но почему он вообще сохраняет его?

Ответ заключается в том, что компилятор генерирует действительно мутный код, потому что оптимизация не включена. Если вы включите какую-либо оптимизацию, вся ваша функция, скорее всего, упадет до:

mov eax, 0
ret

потому что это действительно все, что ему нужно сделать: buffer[] является локальным, поэтому внесенные в него изменения никогда не будут видны никому другому, поэтому их можно оптимизировать; кроме того, все, что нужно сделать, это вернуть 0.


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

$ cat test.c
int foo(char *bar)
{
    char tmp[256];
    int i;

    for (i = 0; bar[i] != 0; i++)
      tmp[i] = bar[i] + i;

    return tmp[1] + tmp[200];
}

Скомпилированный с некоторой оптимизацией, вы можете увидеть подобное использование красной зоны, кроме этого времени он действительно использует смещения от rsp:

$ gcc -m64 -O1 -c test.c
$ objdump -Mintel -d test.o

test.o:     file format elf64-x86-64


Disassembly of section .text:

0000000000000000 <foo>:
   0:   53                      push   rbx
   1:   48 81 ec 88 00 00 00    sub    rsp,0x88
   8:   0f b6 17                movzx  edx,BYTE PTR [rdi]
   b:   84 d2                   test   dl,dl
   d:   74 26                   je     35 <foo+0x35>
   f:   4c 8d 44 24 88          lea    r8,[rsp-0x78]
  14:   48 8d 4f 01             lea    rcx,[rdi+0x1]
  18:   4c 89 c0                mov    rax,r8
  1b:   89 c3                   mov    ebx,eax
  1d:   44 28 c3                sub    bl,r8b
  20:   89 de                   mov    esi,ebx
  22:   01 f2                   add    edx,esi
  24:   88 10                   mov    BYTE PTR [rax],dl
  26:   0f b6 11                movzx  edx,BYTE PTR [rcx]
  29:   48 83 c0 01             add    rax,0x1
  2d:   48 83 c1 01             add    rcx,0x1
  31:   84 d2                   test   dl,dl
  33:   75 e6                   jne    1b <foo+0x1b>
  35:   0f be 54 24 50          movsx  edx,BYTE PTR [rsp+0x50]
  3a:   0f be 44 24 89          movsx  eax,BYTE PTR [rsp-0x77]
  3f:   8d 04 02                lea    eax,[rdx+rax*1]
  42:   48 81 c4 88 00 00 00    add    rsp,0x88
  49:   5b                      pop    rbx
  4a:   c3                      ret    

Теперь немного измените его, вставив вызов другой функции, так что foo() уже не является листовой функцией:

$ cat test.c
extern void dummy(void);  /* ADDED */

int foo(char *bar)
{
    char tmp[256];
    int i;

    for (i = 0; bar[i] != 0; i++)
      tmp[i] = bar[i] + i;

    dummy();  /* ADDED */

    return tmp[1] + tmp[200];
}

Теперь красную зону нельзя использовать, поэтому вы видите что-то большее, как вы первоначально ожидалось:

$ gcc -m64 -O1 -c test.c
$ objdump -Mintel -d test.o

test.o:     file format elf64-x86-64


Disassembly of section .text:

0000000000000000 <foo>:
   0:   53                      push   rbx
   1:   48 81 ec 00 01 00 00    sub    rsp,0x100
   8:   0f b6 17                movzx  edx,BYTE PTR [rdi]
   b:   84 d2                   test   dl,dl
   d:   74 24                   je     33 <foo+0x33>
   f:   49 89 e0                mov    r8,rsp
  12:   48 8d 4f 01             lea    rcx,[rdi+0x1]
  16:   48 89 e0                mov    rax,rsp
  19:   89 c3                   mov    ebx,eax
  1b:   44 28 c3                sub    bl,r8b
  1e:   89 de                   mov    esi,ebx
  20:   01 f2                   add    edx,esi
  22:   88 10                   mov    BYTE PTR [rax],dl
  24:   0f b6 11                movzx  edx,BYTE PTR [rcx]
  27:   48 83 c0 01             add    rax,0x1
  2b:   48 83 c1 01             add    rcx,0x1
  2f:   84 d2                   test   dl,dl
  31:   75 e6                   jne    19 <foo+0x19>
  33:   e8 00 00 00 00          call   38 <foo+0x38>
  38:   0f be 94 24 c8 00 00    movsx  edx,BYTE PTR [rsp+0xc8]
  3f:   00 
  40:   0f be 44 24 01          movsx  eax,BYTE PTR [rsp+0x1]
  45:   8d 04 02                lea    eax,[rdx+rax*1]
  48:   48 81 c4 00 01 00 00    add    rsp,0x100
  4f:   5b                      pop    rbx
  50:   c3                      ret    

(Обратите внимание, что tmp[200] находился в диапазоне подписанного 8-битного смещения в первом случае, но не находится в этом.)