2016-08-27 8 views
3

Я в процессе написания компилятора исключительно как опыт обучения. В настоящее время я изучаю кадры стека, компилируя простой код на языке C++, а затем изучая выходное asm, созданное gcc 4.9.2 для Windows x86.gcc x86 Выравнивание стека Windows

мой простой C++ код

#include <iostream> 

using namespace std; 

int globalVar; 

void testStackStuff(void); 
void testPassingOneInt32(int v); 
void forceStackFrameCreation(int v); 

int main() 
{ 
    globalVar = 0; 

    testStackStuff(); 

    std::cout << globalVar << std::endl; 
} 

void testStackStuff(void) 
{ 
    testPassingOneInt32(666); 
} 

void testPassingOneInt32(int v) 
{ 
    globalVar = globalVar + v; 

    forceStackFrameCreation(v); 
} 

void forceStackFrameCreation(int v) 
{ 
    globalVar = globalVar + v; 
} 

Хорошо, когда скомпилирован с -mpreferred-стек-граница = 4 я ожидал увидеть стек выровненный до 16 байт (технически это выровненных до 16 байтов но с дополнительными 16 байтами неиспользуемого пространства стека). Пролог для основного, изготовленного gcc:

22      .loc 1 12 0 
23      .cfi_startproc 
24 0000 8D4C2404  lea ecx, [esp+4] 
25      .cfi_def_cfa 1, 0 
26 0004 83E4F0   and esp, -16 
27 0007 FF71FC   push DWORD PTR [ecx-4] 
28 000a 55    push ebp 
29      .cfi_escape 0x10,0x5,0x2,0x75,0 
30 000b 89E5   mov ebp, esp 
31 000d 51    push ecx 
32      .cfi_escape 0xf,0x3,0x75,0x7c,0x6 
33 000e 83EC14   sub esp, 20 
34      .loc 1 12 0 
35 0011 E8000000  call ___main 
35  00 
36      .loc 1 13 0 
37 0016 C7050000  mov DWORD PTR _globalVar, 0 
38      .loc 1 15 0 
39 0020 E8330000  call __Z14testStackStuffv 

линия 26 раундов esp до ближайшей границы 16 байт.

линии 27, 28 и 31 толкать в общей сложности 12 байтов в стек, а затем

линии 33 вычитает еще 20 байт из особ, что в общей сложности 32 байт!

Почему?

строка 39 затем вызывает testStackStuff.

ПРИМЕЧАНИЕ - этот вызов вызывает адрес возврата (4 байта).

Теперь давайте посмотрим на пролог для testStackStuff, имея в виду, что стек теперь находится на 4 байта ближе к следующей границе 16 байтов.

67 0058 55    push ebp 
68      .cfi_def_cfa_offset 8 
69      .cfi_offset 5, -8 
70 0059 89E5   mov ebp, esp 
71      .cfi_def_cfa_register 5 
72 005b 83EC18   sub esp, 24 
73      .loc 1 22 0 
74 005e C704249A  mov DWORD PTR [esp], 666 

линия 67 подталкивает еще 4 байта (теперь 8 байт к границе).

строка 72 вычитает еще 24 байта (всего 32 байта).

В этот момент стопка теперь выравнивается правильно на границе 16 байтов. Но почему кратное 2?

Если я сменил флаги компилятора на -mpreferred-stack-border = 5, я бы ожидал, что стек выровнён до 32 байт, но снова gcc, кажется, создает фреймы стека, выровненные в 64 байта, в два раза превышающую сумму, которую я ожидал.

Вводных для основных

23      .cfi_startproc 
24 0000 8D4C2404  lea ecx, [esp+4] 
25      .cfi_def_cfa 1, 0 
26 0004 83E4E0   and esp, -32 
27 0007 FF71FC   push DWORD PTR [ecx-4] 
28 000a 55    push ebp 
29      .cfi_escape 0x10,0x5,0x2,0x75,0 
30 000b 89E5   mov ebp, esp 
31 000d 51    push ecx 
32      .cfi_escape 0xf,0x3,0x75,0x7c,0x6 
33 000e 83EC34   sub esp, 52 
34      .loc 1 12 0 
35 0011 E8000000  call ___main 
35  00 
36      .loc 1 13 0 
37 0016 C7050000  mov DWORD PTR _globalVar, 0 
37  00000000 
37  0000 
38      .loc 1 15 0 
39 0020 E8330000  call __Z14testStackStuffv 

линия 26 раундов особ вплоть до границы байта ближайших 32

линия 27, 28 и 31 толчка в общей сложности 12 байт в стек, а затем

строка 33 вычитает еще 52 байта из esp, давая в общей сложности 64 байта!

и пролог для testStackStuff является

66      .cfi_startproc 
67 0058 55    push ebp 
68      .cfi_def_cfa_offset 8 
69      .cfi_offset 5, -8 
70 0059 89E5   mov ebp, esp 
71      .cfi_def_cfa_register 5 
72 005b 83EC38   sub esp, 56 
73      .loc 1 22 0 

(4 байта на стек с) называют __Z14testStackStuffv

(4 байта на стек с) нажимной EBP

(56 байт на стек с) sub esp, 56

всего 64 байта.

Кто-нибудь знает, почему gcc создает это дополнительное пространство для стека или я пропустил что-то очевидное?

Спасибо за любую помощь, которую вы можете предложить.

+0

*, но gcc, похоже, создает кадры стека, выровненные с 64 байтами *. Нет, он использовал 'и esp, -32'. Размер фрейма стека выглядит как 64 байта, но его выравнивание - только 32B. –

+0

related: http://stackoverflow.com/questions/38781118/why-is-gcc-generating-an-extra-return-address объясняет 'push DWORD PTR [ecx-4]' часть. –

ответ

1

Чтобы решить эту загадку, Вы должны смотреть на документацию НКУ, чтобы выяснить, какой именно аромат Application Binary Interface (ABI) он использует, а затем найти спецификацию этого ABI и читать Это. Если вы «в процессе написания компилятора просто как учебный опыт», вам это обязательно понадобится.

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

В старый стиль кадров стека, мы бы установили PUSH в стек, а затем вызываем функцию.

В новом стиле кадров стека EBP больше не используется (не уверен, почему он сохраняется и копируется из ESP больше), параметры помещаются в стек с определенным смещением относительно ESP, а затем вызывается функция. Об этом свидетельствует тот факт, что mov DWORD PTR [esp], 666 используется для передачи аргумента 666 для вызова testPassingOneInt32(666);.

+1

Это имеет смысл, в Интернете полно примеров, которые используют EBP со смещением для доступа к местным жителям и автоматическим переменным. Я поеду и найду ABI и опубликую больше информации после прочтения. Я отвечу на ваш ответ как правильный ответ, когда я это сделаю. – CascadeCoder

1

Для чего он делает push DWORD PTR [ecx-4] для копирования адреса возврата, см. this partial duplicate. IIRC, он создает полную копию пары return-address/saved-ebp.


но опять-таки GCC, кажется, производит кадры стека, выровненные 64 байт

Нет, он использовал and esp, -32. Размер фрейма стека выглядит как 64 байта, но его выравнивание - только 32B.

Я не уверен, почему он оставляет столько места в кадре стека. Не очень интересно догадаться, почему gcc -O0 делает то, что он делает, потому что он даже не пытается быть оптимальным.

Вы, очевидно, скомпилированы без оптимизации, что делает все это менее интересным. Это расскажет вам больше о gcc-компонентах и ​​о том, что было удобно для gcc, а не о том, что код, который он испускал, был необходим или полезен. Кроме того, используйте http://gcc.godbolt.org/, чтобы получить хороший выход ASM без директив CFI и других шумов. (Пожалуйста, убирайте блоки кода asm в своем вопросе с выходом из этого. Все шумы затрудняют их чтение.)