再次探索:x86-64的站空間和棧幀結構

接上一篇文章《where the top of the stack is on x86》,這次我們關注x86-6下的戰(zhàn)陣結構和參數的存放規(guī)則,以及Linux和其他遵循System V AMD64 ABI調用約定的操作系統(tǒng)。

寄存器差異

之前的文章已經介紹了不同結構下通用寄存器的種類和作用,我們知道x86下只有8個通用寄存器分別是(eax, ebx, ecx, edx, ebp, esp, esi, edi),而x86-64增新了8個寄存器(r8, r9, r10, r11, r12, r13, r14, r15)。

參數傳遞

我們最關心的時x86-64結構下這些寄存器到底是如何存儲的,從ABI規(guī)則來看,函數開始的6個整型或者指針類型參數通過寄存器傳遞參數,分別保存在rdi, rsi, rdx, rcx,r8,r9中,從第7個參數開始,接下來的所有參數將通過棧傳遞。

分析一個棧幀實例

還是以典型的C程序為例,看下棧幀布局:

long myfunc(long a, long b, long c, long d,
            long e, long f, long g, long h)
{
    long xx = a * b * c * d * e * f * g * h;
    long yy = a + b + c + d + e + f + g + h;
    long zz = utilfunc(xx, yy, xx % yy);
    return zz + 20;
}

結合上面文章的分析,我們可以得到本函數的棧幀布局


stack x86-64.png

函數有8個參數,發(fā)現最后兩個參數的傳遞和x86是一致的,但是最后有兩個所謂"red zone",下面分析這個區(qū)域是神馬。

紅燈區(qū) (Red Zone)

來自System V AMD64 ABI的標準中的話:
The 128-byte area beyond the location pointed to by %rsp is considered to be reserved and shall not be modified by signal or interrupt handlers. Therefore, functions may use this area for temporary data that is not needed across function calls. In particular, leaf functions may use this area for their entire stack frame, rather than adjusting the stack pointer in the prologue and epilogue. This area is known as the red zone.

嘗試翻譯下 - “在%rsp指向的棧頂之后的128字節(jié)是被保留的——它不能被信號和終端處理程序使用。因此,函數可以在這個區(qū)域放一些臨時的數據。特別地,葉子函數可能會將這128字節(jié)的區(qū)域作為它的整個棧幀,而不是像往常一樣在進入函數和離開時靠移動棧指針獲取棧幀和釋放棧幀。這128字節(jié)被稱作紅色區(qū)域”

簡單點說,這個紅色區(qū)域(red zone)就是一個優(yōu)化。因為這個區(qū)域不會被信號或者中斷侵占,函數可以在不移動棧指針的情況下使用它存取一些臨時數據——于是兩個移動rsp的指令就被節(jié)省下來了。但是這個區(qū)域會被程序覆寫,文獻中描述說red zone最有用的時候是末端函數(葉子函數)使用的時候。

看起來還是不容易理解,回頭看上面myfunc函數,其引用的utilfunc就是一個葉子函數,查看utilfunc代碼

long utilfunc(long a, long b, long c)
{
    long xx = a + 2;
    long yy = b + 3;
    long zz = c + 4;
    long sum = xx + yy + zz;
  
    return xx * yy * zz + sum;
}

這個函數沒有用到??臻g存放參數,其結構為


yezi x86-64.png

可以看到這個葉子函數直接使用myfunc函數的128bytes的red zone空間存儲函數的所有的局部變量,最明顯的差異就是此時rsp指針不在遞減。

再看一個例子:

/*test.c*/
long test2(long a, long b, long c)  /* 葉子函數 */
{
    return a*b + c;
}
long test1(long a, long b)
{
    return test2(b, a, 3);
}
int main(int argc, char const *argv[])
{
    return test1(1, 2);
}

使用gcc進行編譯和反編譯

gcc test.c && objdump -d a.out

查看test2、test1、main函數的匯編結果

00000000004004d6 <test2>:
  4004d6:   55                      push   %rbp
  4004d7:   48 89 e5                mov    %rsp,%rbp
  4004da:   48 89 7d f8             mov    %rdi,-0x8(%rbp)
  4004de:   48 89 75 f0             mov    %rsi,-0x10(%rbp)
  4004e2:   48 89 55 e8             mov    %rdx,-0x18(%rbp)
  4004e6:   48 8b 45 f8             mov    -0x8(%rbp),%rax
  4004ea:   48 0f af 45 f0          imul   -0x10(%rbp),%rax
  4004ef:   48 89 c2                mov    %rax,%rdx
  4004f2:   48 8b 45 e8             mov    -0x18(%rbp),%rax
  4004f6:   48 01 d0                add    %rdx,%rax
  4004f9:   5d                      pop    %rbp
  4004fa:   c3                      retq   
00000000004004fb <test1>:
  4004fb:   55                      push   %rbp
  4004fc:   48 89 e5                mov    %rsp,%rbp
  4004ff:   48 83 ec 10             sub    $0x10,%rsp
  400503:   48 89 7d f8             mov    %rdi,-0x8(%rbp)
  400507:   48 89 75 f0             mov    %rsi,-0x10(%rbp)
  40050b:   48 8b 4d f8             mov    -0x8(%rbp),%rcx
  40050f:   48 8b 45 f0             mov    -0x10(%rbp),%rax
  400513:   ba 03 00 00 00          mov    $0x3,%edx
  400518:   48 89 ce                mov    %rcx,%rsi
  40051b:   48 89 c7                mov    %rax,%rdi
  40051e:   e8 b3 ff ff ff          callq  4004d6 <test2>
  400523:   c9                      leaveq 
  400524:   c3                      retq   
0000000000400525 <main>:
  400525:   55                      push   %rbp
  400526:   48 89 e5                mov    %rsp,%rbp
  400529:   48 83 ec 10             sub    $0x10,%rsp
  40052d:   89 7d fc                mov    %edi,-0x4(%rbp)
  400530:   48 89 75 f0             mov    %rsi,-0x10(%rbp)
  400534:   be 02 00 00 00          mov    $0x2,%esi
  400539:   bf 01 00 00 00          mov    $0x1,%edi
  40053e:   e8 b8 ff ff ff          callq  4004fb <test1>
  400543:   c9                      leaveq 
  400544:   c3                      retq   
  400545:   66 2e 0f 1f 84 00 00    nopw   %cs:0x0(%rax,%rax,1)
  40054c:   00 00 00 
  40054f:   90                      nop

可以看到main函數和test1函數都執(zhí)行了rsp移動獲取棧幀空間:

4004ff: 48 83 ec 10             sub    $0x10,%rsp
....
400529: 48 83 ec 10             sub    $0x10,%rsp

而test2函數由于是葉子函數直接使用ebp/esp(此時它們兩個相等),其參數和局部變量直接使用red zone空間存儲,test2函數的棧幀空間布局如下:


yezi2 x86-64.png

關于ebp基地址指針的使用(原標題:節(jié)約通用寄存器)

其實很多時候,我們發(fā)現ebp指針并沒有使用,而僅僅使用esp指針就可以定位,并且DWARF(Debugging With Attributed Record Formats)調試信息格式支持處理無基址指針的方法(CFI)。這就是一些編譯器開始在高級優(yōu)化中省略基址指針了,這樣做可以縮減程序執(zhí)行的“預處理代碼”(prologue)和“后處理代碼”(epilogue),節(jié)省出來一個通用寄存器供程序使用(在x86架構有限的GPRs資源條件下非常有用)。GPRs:GeneralPurpose Registers(通用寄存器)。在x86 gcc下默認保留ebp指針,但是也提供了-fomit-frame-pointer優(yōu)化參數選項,對于是否推薦使用這個選項,爭議比較大,我們查閱了相關的資料:

總之,通過使用%rsp索引棧幀的方法避免了傳統(tǒng)的%rbp使用方法,這項技術節(jié)約掉了“預處理代碼”(prologue)和“后處理代碼”(epilogue)中的兩條指令,而且也空出來一個通用寄存器供給程序使用。

為了弄清楚,我又編寫了一個簡單的包含葉子函數的C程序,分別使用正常編譯和帶有-fomit-frame-pointer指令的編譯。
C程序為

#include <stdio.h>

int add(int a, int b)
{

        return a + b;
}

int main(int argc, char const *argv[])
{

        int sum = 0;

        sum = add(1,2);

        printf("%d\n",sum);

        return 0;
}

gcc反編譯得到:

0000000000400526 <add>:
  400526:   55                      push   %rbp
  400527:   48 89 e5                mov    %rsp,%rbp
  40052a:   89 7d fc                mov    %edi,-0x4(%rbp)
  40052d:   89 75 f8                mov    %esi,-0x8(%rbp)
  400530:   8b 55 fc                mov    -0x4(%rbp),%edx
  400533:   8b 45 f8                mov    -0x8(%rbp),%eax
  400536:   01 d0                   add    %edx,%eax
  400538:   5d                      pop    %rbp
  400539:   c3                      retq   

000000000040053a <main>:
  40053a:   55                      push   %rbp
  40053b:   48 89 e5                mov    %rsp,%rbp
  40053e:   48 83 ec 20             sub    $0x20,%rsp
  400542:   89 7d ec                mov    %edi,-0x14(%rbp)
  400545:   48 89 75 e0             mov    %rsi,-0x20(%rbp)
  400549:   c7 45 fc 00 00 00 00    movl   $0x0,-0x4(%rbp)
  400550:   be 02 00 00 00          mov    $0x2,%esi
  400555:   bf 01 00 00 00          mov    $0x1,%edi
  40055a:   e8 c7 ff ff ff          callq  400526 <add>
  40055f:   89 45 fc                mov    %eax,-0x4(%rbp)
  400562:   8b 45 fc                mov    -0x4(%rbp),%eax
  400565:   89 c6                   mov    %eax,%esi
  400567:   bf 04 06 40 00          mov    $0x400604,%edi
  40056c:   b8 00 00 00 00          mov    $0x0,%eax
  400571:   e8 8a fe ff ff          callq  400400 <printf@plt>
  400576:   b8 00 00 00 00          mov    $0x0,%eax
  40057b:   c9                      leaveq 
  40057c:   c3                      retq   
  40057d:   0f 1f 00                nopl   (%rax)

帶有指令的編譯和匯編碼

lic@ubuntu:~/Documents$ gcc -fomit-frame-pointer test2.c
lic@ubuntu:~/Documents$ objdump -d a.out

反匯編結果

0000000000400526 <add>:
  400526:   89 7c 24 fc             mov    %edi,-0x4(%rsp)
  40052a:   89 74 24 f8             mov    %esi,-0x8(%rsp)
  40052e:   8b 54 24 fc             mov    -0x4(%rsp),%edx
  400532:   8b 44 24 f8             mov    -0x8(%rsp),%eax
  400536:   01 d0                   add    %edx,%eax
  400538:   c3                      retq   

0000000000400539 <main>:
  400539:   48 83 ec 28             sub    $0x28,%rsp
  40053d:   89 7c 24 0c             mov    %edi,0xc(%rsp)
  400541:   48 89 34 24             mov    %rsi,(%rsp)
  400545:   c7 44 24 1c 00 00 00    movl   $0x0,0x1c(%rsp)
  40054c:   00 
  40054d:   be 02 00 00 00          mov    $0x2,%esi
  400552:   bf 01 00 00 00          mov    $0x1,%edi
  400557:   e8 ca ff ff ff          callq  400526 <add>
  40055c:   89 44 24 1c             mov    %eax,0x1c(%rsp)
  400560:   8b 44 24 1c             mov    0x1c(%rsp),%eax
  400564:   89 c6                   mov    %eax,%esi
  400566:   bf 04 06 40 00          mov    $0x400604,%edi
  40056b:   b8 00 00 00 00          mov    $0x0,%eax
  400570:   e8 8b fe ff ff          callq  400400 <printf@plt>
  400575:   b8 00 00 00 00          mov    $0x0,%eax
  40057a:   48 83 c4 28             add    $0x28,%rsp
  40057e:   c3                      retq   
  40057f:   90                      nop

發(fā)現不僅是葉子函數,mian函數也沒有了ebp指針,但是對于gcc下的elf-x86-64程序,其函數的入口是start函數,查看start函數的匯編結果:

0000000000400430 <_start>:
  400430:   31 ed                   xor    %ebp,%ebp
  400432:   49 89 d1                mov    %rdx,%r9
  400435:   5e                      pop    %rsi
  400436:   48 89 e2                mov    %rsp,%rdx
  400439:   48 83 e4 f0             and    $0xfffffffffffffff0,%rsp
  40043d:   50                      push   %rax
  40043e:   54                      push   %rsp
  40043f:   49 c7 c0 f0 05 40 00    mov    $0x4005f0,%r8
  400446:   48 c7 c1 80 05 40 00    mov    $0x400580,%rcx
  40044d:   48 c7 c7 39 05 40 00    mov    $0x400539,%rdi
  400454:   e8 b7 ff ff ff          callq  400410 <__libc_start_main@plt>
  400459:   f4                      hlt    
  40045a:   66 0f 1f 44 00 00       nopw   0x0(%rax,%rax,1)

這里的結果和一些網上的文章結果不一致,我這里的環(huán)境和對象分別為:

lic@ubuntu:~/Documents$ gcc --version
gcc (Ubuntu 5.4.0-6ubuntu1~16.04.12) 5.4.0 20160609
a.out:     file format elf64-x86-64

關于start函數我們將在另一篇介紹elf文件結構的文章中進行描述。

最后,依照windows x64 ABI,并不存在所謂的red zone,

?著作權歸作者所有,轉載或內容合作請聯系作者
【社區(qū)內容提示】社區(qū)部分內容疑似由AI輔助生成,瀏覽時請結合常識與多方信息審慎甄別。
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發(fā)布,文章內容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務。

友情鏈接更多精彩內容