2012-11-02 6 views
10

Rozważmy następujący prosty program:Dlaczego prolog funkcji x86-64 przydziela mniejszy stos niż zmienne lokalne?

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

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

     return 0; 
} 

skompilowana z GCC 4.7.0 na x86-64 maszyny. Demontaż główny() z GDB otrzymujemy:

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  

Dlaczego sub RSP z tylko 0x98 = 152d, gdy bufor jest 256 bajtów? Kiedy przenoszę dane do bufora [0], po prostu wydaje się, że używają danych spoza przydzielonej ramki stosu i używają referencji rbp, więc jaki jest nawet punkt subspsp, 0x98?

Kolejne pytanie, co robią te linie?

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

Dlaczego trzeba zapisać EDI, a nie RDI? Widzę jednak, że przesuwa to poza maksymalny zakres przydzielonego bufora w kodzie C. Interesujące jest również to, że różnica pomiędzy tymi dwiema zmiennymi jest tak duża. Ponieważ EDI ma tylko 4 bajty, dlaczego potrzebna jest 12-bajtowa separacja dla dwóch zmiennych?

+2

12-bajtowe oddzielenie wynika z wyrównania. 'rsi' ma 8 bajtów, więc dopełnienie jest potrzebne, aby zachować wyrównanie do 8 bajtów. Ale nie mogę mówić za niedostateczne przydzielenie stosu. – Mysticial

+0

To prawdopodobnie zapisuje EDI i RSI tylko dlatego, że nie jest wymagane, aby zapisać je przez dzwoniącego? Ale nadal sposób, w jaki są zapisywane, wydaje się dziwny. – csstudent2233

+0

co się stanie, gdy skompilujesz go za pomocą 'gcc -s' (w celu uzyskania wyjścia zespołu) - ponieważ jeśli nie masz włączonego debugowania w kompilacji, twoje wyniki gdb mogą być nieparzyste – KevinDTimm

Odpowiedz

15

The x86-64 ABI used by Linux (i niektórych innych systemów operacyjnych, chociaż szczególnie nie Windows, który ma swój własny inny ABI) definiuje „czerwonej strefy” 128 bajtów poniżej wskaźnika stosu, który nie jest gwarantowana być dotykane przez sygnału lub programy obsługi przerw. (Patrz rys. 3.3 i §3.2.2.)

Funkcja liścia (czyli taka, która nie wywołuje niczego innego) może w związku z tym korzystać z tego obszaru, aby cokolwiek chce - nie robi nic takiego jak call, który dane na wskaźniku stosu; i dowolny sygnał lub przerywnik będzie podążać za ABI i upuść wskaźnik stosu o co najmniej dodatkowe 128 bajtów przed przechowywaniem czegokolwiek.

(kodowanie Krótsze instrukcje są dostępne dla podpisanych 8-bitowych przemieszczeń, więc punkt czerwonej strefie jest to, że zwiększa ilość lokalnych danych, że funkcja liści można uzyskać dostęp za pomocą tych krótszych instrukcji.)

To co tu się dzieje.

Ale ... ten kod nie korzysta z tych krótszych kodowań (używa przesunięć od rbp zamiast rsp). Dlaczego nie? To również całkowicie niepotrzebnie oszczędza edi i rsi - pytasz, dlaczego oszczędzasz edi zamiast rdi, ale dlaczego w ogóle to oszczędzasz?

Odpowiedź jest taka, że ​​kompilator generuje naprawdę zgnijący kod, ponieważ nie włączono optymalizacji. Jeśli włączysz żadnej optymalizacji, cała funkcja jest prawdopodobne, aby zwinąć w dół do:

mov eax, 0 
ret 

bo tak naprawdę wszystko to musi zrobić: buffer[] jest lokalny, więc zmiany wprowadzone do niego nigdy nie będzie widoczny na cokolwiek innego, więc można go zoptymalizować; poza tym, cała funkcja musi zrobić, to powrócić 0.


Oto lepszy przykład.Funkcja ta jest kompletna bzdura, ale używa podobnej tablicy, podczas gdy robi wystarczająco dużo, aby upewnić się, że rzeczy nie wszystkie zostaną zoptymalizowane z dala:

$ 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]; 
} 

kompilowane z niektórych optymalizacji można zobaczyć podobnym zastosowaniu czerwonej strefie , ale tym razem to naprawdę nie wykorzystać przesunięcia od 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  

teraz dostosować go bardzo nieznacznie, wkładając telefon do innej funkcji, tak że jest foo() nie jest już funkcją bramy:

$ 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]; 
} 

Teraz czerwona linia nie może być używany, więc można zobaczyć coś więcej jak ty pierwotnie zakładano:

$ 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  

(Zauważ, że tmp[200] był w zakresie podpisanego 8 -bitowe przesunięcie w pierwszym przypadku, ale nie występuje w tym przypadku).

+0

Doskonała przykładowa odpowiedź. Zwłaszcza spostrzeżenie, że przy optymalizacji powinno po prostu ustawić 'eax = 0' i powrócić. –

+0

Może przechowuje edi zamiast rsi, ponieważ jest int, nie długo. Nie jest to jednak konieczne, jak mówisz. –

Powiązane problemy