X86-64和ARM64用戶棧的結構 (4) ---__libc_start_main到main

_start到__libc_start_main函數之間的棧

1、x86-64

X86-64的寄存器相對於X86有擴展,主要不同體現在:

  • 通用寄存器:X86-64有16個64bit通用寄存器
  • 狀態寄存器:1個64bit狀態寄存器RFLAGS,僅僅低32bit被使用
  • 指令寄存器:1個64bit指令寄存器RIP
  • MMX寄存器:8個64bitMMX寄存器,16個128bitXMM寄存器。當使用這些寄存器時,數據的地址必須對齊到64bit、128bit。

16個64bit寄存器 爲:RAX,RBX,RCX,RDX,RDI,RSI,RBP,RSP,R8,R9,R10,R11,R12,R13,R14,R15
在X86-64架構的處理器上,Windows和Linux的函數調用規則是不一樣。

1.1、Stack Frame

Linux使用System V Application Binary Interface的函數調用規則。在《System V Applocation Binary Interface》中3.2.2 The Stack Frame中寫道:
In addition to registers, each function has a frame on the run-time stack. This stack grows downwards from high addresses. Figure 3.3 shows the stack organization. The end of the input argument area shall be aligned on a 16 (32 or 64, if __m256 or __m512 is passed on stack) byte boundary. In other words, the value (%rsp + 8) is always a multiple of 16 (32 or 64) when control is transferred to the function entry point. The stack pointer, %rsp, always points to the end of the latest allocated stack frame.
X86-64和ARM64用戶棧的結構 (4) ---__libc_start_main到main

在輸入參數的結尾處rsp必須對齊到16字節,當調用函數時,首先rsp會減8,rip會壓棧,在棧中佔8個字節,然後rip指向另一個函數的entry point,也即控制轉移到了函數的entry point。由於rip壓棧了,rsp+8應該是16字節對齊。

至於爲什麼需要16字節對齊?查了一些資料發現和Sreaming SIMD Extensions(SSE)有關,它是一組CPU指令,用於像信號處理、科學計算或者3D圖形計算一樣的應用(SSE入門)。SIMD 也是幾個單詞的首寫字母組成的: Single Instruction, Multiple Data。 一個指令發出後,同一時刻被放到不同的數據上執行。16個128bit XMM寄存器可以被SSE指令操控,SSE利用這些寄存器可以同時做多個數據的運算,從而加快運算速度。但是數據被裝進XMM寄存器時,要求數據的地址需要16字節對齊,而數據經常會在棧上分配,因此只有要求棧以16字節對齊,才能更好的支持數據的16字節對齊。

1.2、Parameter Passing

當參數的數目小於7個時,使用rdi,rsi, rdx, rcx, r8 and r9傳遞參數,大於等於7個時使用stack傳參數。具體的規則見《System V Applocation Binary Interface》中3.2.3 Parameter Passing
X86-64和ARM64用戶棧的結構 (4) ---__libc_start_main到main

  • rax 作爲函數返回值使用。
  • rsp 棧指針寄存器,指向棧頂。
  • rdi,rsi,rdx,rcx,r8,r9 用作函數參數,依次對應第1參數,第2參數...
  • rbx,rbp,r12,r13,r14,r15 用作數據存儲,遵循被調用者(callee)使用規則,簡單說就是隨便用,調用子函數之前要備份它,以防他被修改
  • r10,r11 用作數據存儲,遵循調用者(caller)使用規則,簡單說就是使用之前要先保存原值

1.3、_start函數

0000000000000540 <_start>:
 540:   31 ed                 xor    %ebp,%ebp
 542:   49 89 d1              mov    %rdx,%r9
 545:   5e                    pop    %rsi
 546:   48 89 e2              mov    %rsp,%rdx
 549:   48 83 e4 f0           and    $0xfffffffffffffff0,%rsp
 54d:   50                    push   %rax
 54e:   54                    push   %rsp
 54f:   4c 8d 05 da 02 00 00  lea    0x2da(%rip),%r8      # 830 <__libc_csu_fini>
 556:   48 8d 0d 63 02 00 00  lea    0x263(%rip),%rcx     # 7c0 <__libc_csu_init>
 55d:   48 8d 3d 2c 02 00 00  lea    0x22c(%rip),%rdi     # 790 <main>
 564:   ff 15 76 0a 20 00     callq  *0x200a76(%rip)      # 200fe0 <__libc_start_main@GLIBC_2.2.5>
 56a:   f4                    hlt
 56b:   0f 1f 44 00 00        nopl   0x0(%rax,%rax,1)

跟據上述彙編,其實也就做了一件事,調用__libc_start_main函數,並向其傳遞了7個參數:

  • r9傳遞 rdx
  • r8傳遞 __libc_csu_fini
  • rcx傳遞 __libc_csu_init
  • rdx傳遞 argv
  • rsi傳遞 argc
  • rdi傳遞 main
  • 棧傳遞 rsp的值

上述彙編有幾句比較晦澀:

  • and $0xfffffffffffffff0,%rsp的目的是使rsp對齊到16字節。
  • push %rax 爲了在調用__libc_start_main之前,幫助rsp對齊到16字節,%rax入棧無其它意義。顯然,這一句執行後,rsp還沒有對齊到16字節,下一句彙編執行後就將對齊到16字節。
  • push %rsp, rsp的值入棧,這時將rsp的值傳遞給__libc_start_main函數,且使rsp對齊到16字節。

執行_start的第一條指令時,rsp的值是多少呢?誰設置的呢?rsp的值是bprm->p,Linux內核設置的,在上面的內容中有介紹。下圖結合了Linux Kernel和_start設置的棧。其實_start來自glibc,在x86-64平臺上,可以在文件sysdeps/x86_64/start.S中找到代碼。這段代碼的目的很單純,只是給函數__libc_start_main準備參數。函數__libc_start_main同樣來自glibc,它定義在文件csu/libc-start.c中。
X86-64和ARM64用戶棧的結構 (4) ---__libc_start_main到main
函數__libc_start_main的原型如下:

int __libc_start_main(
         (int (*main) (int, char**, char**),
         int argc,
         char **argv,
         __typeof (main) init,
         void (*fini) (void),
         void (*rtld_fini) (void),
         void* stack_end)

《How statically linked programs run on Linux 》中介紹了__libc_start_main的作用:

  • Figure out where the environment variables are on the stack.
  • Prepare the auxiliary vector, if required.
  • Initialize thread-specific functionality (pthreads, TLS, etc.)
  • Perform some security-related bookkeeping (this is not really a separate step, but is trickled all through the function).
  • Initialize libc itself.
  • Call the program initialization function through the passed pointer (init).
  • Register the program finalization function (fini) for execution on exit.
  • Call main(argc, argv, envp)
  • Call exit with the result of main as the exit code.

2、ARM64

2.1 工具鏈

sudo apt-get install gcc-aarch64-linux-gnu
sudo apt install gcc-arm-linux-gnueabi

2.2、 adr,ldr,adrp 指令

2.2.1 adr

主要用於形成pc相對地址,把相對地址load到寄存器中,使用方法爲:

adr <xd>, <label>

當前指令到label的偏移 offset_to_label 加上PC的值,然後將結果賦值給xd。offset_to_label可以是個負數,實際在執行過程中會將offse_to_label擴展成64爲有符號數。但是ARM指令的長度是固定爲32bit,offset_to_label最多隻能爲21位,也即可以尋PC +/-1MB的範圍。

經常會被編譯器轉換成add或sub指令:

add  <xd>,[PC, #offset_to_label]
sub  <xd>,[PC, #-offset_to_label]

2.2.2 ldr

這個指令的本質作用是把地址中的數據加載到寄存器中,根據地址的表達形式不同可以分爲幾種情況:

ldr <xd>, <label>

將程序label處的數據load到xd中,label是一個地址。指令記錄的不是label的絕對地址,是當前指令到label的偏移,記作offset_to_labe,l和adr指令描述中的 offset_to_label 有所不同。在彙編時,彙編器會計算當前指令到label的偏移量(以字節爲單位),然後將偏移量右移兩位得到 offset_to_label 。在執行執行指令時效果如下:

xd <===  [PC + (offset_to_label << 2)]

另外幾種如下:

ldr <Xt>,[<Xn|SP>],#<simm> post_index
ldr <Xt>,[<Xn|SP>,#<simm>]! pre_index
ldr <Xt>,[<Xn|SP>,#<pimm>] unsigned_offset

2.2.3 adrp

該指令在ARMv8中首次被設計出來,是ARM指令集的一個重大創新,可以減少指令條數以及訪存的次數。有幾篇博客介紹了該指令的作用,但是沒有講清楚,如《ARM指令淺析2(adrp、b)》《彙編七、ADRP指令》
指令的使用方式爲:

adrp <Rd>, <label>

adrp就是address page 的簡寫,這裏的page指的是大小爲4KB的連續內存,和操作系統中的頁不是一回事。該指令的作用是將label所在頁且4KB對其的頁基地址放入寄存器Rd中。Labe表示的地址肯定在這個頁基地址確定的頁內。要想徹底搞懂這個指令的作用,還需要從指令彙編的過程和譯碼的過程進行分析。

adrp指令彙編

也就是將這個指令變成二進制機器碼的過程,根據ARM文檔,adrp指令的二進制格式爲:
X86-64和ARM64用戶棧的結構 (4) ---__libc_start_main到main
32bit中的21bit immhi和immlo是由lable的地址(L)和當前指令所在的地址計算來的,第一步獲取label和當前指令所在頁的頁基地址,兩者相減得到差值;第二步將差值右移12位,再取低21位作爲immhi:immlo。在進行指令彙編的時候,數據和指令在最終的二進制文件中的位置都確定了,當然也可以確定當前指令在所在的頁基地址和lable所在的頁基地址。
X86-64和ARM64用戶棧的結構 (4) ---__libc_start_main到main
如上圖所示,在彙編時 immhi:immlo=(pageoffset_to_label>>12)&0x1FFFFF,Rd也是確定的,就可以形成一條二進制機器碼指令。

adrp 二進制指令譯碼

在cpu執行adrp 機器碼指令時,可以根據PC和機器碼指令中的immhi:immlo找到label所在頁的基地址。在adrp指令發明後,對二進制文件的映射提出了一個要求,即二進制文件映射的虛擬地址必須4K對齊。在CPU執行adrp的機器碼時,PC時已知的,根據PC就可以計算出label所在頁的基地址:Rd=(PC & 0xFFFFFFFFFFFF0000) + (immhi:immlo << 12).
X86-64和ARM64用戶棧的結構 (4) ---__libc_start_main到main
到這裏adrp指令的前前後後基本上也就介紹完了,還值得一提的是,獲取label所在頁的基地址本身沒有什麼用,所以一般在adrp指令的後面都會在跟一條add指令:add Rd, Rd,offset_inpage, label所在的地址就在寄存器Rd中了,就可以使用load指令加載label處的數據了;或者直接使用ldr Rd, [Rd, #offset_inpage]加載label處的數據。

adrp的優勢是什麼? ARM是RISC指令集,每個指令都是等長的32bit,這32bit能容下的東西很有限,一個尋址指令除去本身的操作碼,留給地址的bit位就沒幾個了,而有了adrp指令,相對尋址能力大大提升,可以尋址距離PC 4GB遠的數據,既可以尋址PC前的4GB範圍,也可以尋址PC後的4GB範圍,因爲immhi:immlo是21bit,offset_inpage是12bit,21+12=33。

2.3、_start 函數

在glibc的 sysdeps/aarch64/start.S中有_start函數,經過簡單的處理如下所示:

_start:
    /* Create an initial frame with 0 LR and FP */
1:  mov  x29, #0
2:  mov  x30, #0
    /* Setup rtld_fini in argument register */
3:  mov  x5, x0
    /* Load argc and a pointer to argv */
4:  ldr  x1, [sp, #0]
5:  add  x2, sp, #8
    /* Setup stack limit in argument register */
6:  mov  x6, sp
7:  adrp x0, :got:main
8:  ldr  x0, [x0, #:got_lo12:main]
9:  adrp x3, :got:__libc_csu_init
10: ldr  x3, [x3, #:got_lo12:__libc_csu_init]
11: adrp x4, :got:__libc_csu_fini
12: ldr  x4, [x4, #:got_lo12:__libc_csu_fini]
    /* __libc_start_main (main, argc, argv, init, fini, rtld_fini,
                  stack_end) */
    /* Let the libc call main and exit with its return code.  */
13: bl  __libc_start_main
    /* should never get here....*/
14: bl  abort

上面的彙編,1~2行表示情況LR(Link Register) 和FP(Frame Pointer); 第4行是將argc傳遞給x1;第5行是將argv傳遞給x2,這裏的argc和argv就是我們平時寫的C程序int main(int argc, char *argv[])函數的兩個參數;其餘幾行類似,都是使用寄存器傳遞參數。ARM64的_start函數和X86-64的_start函數目的是一樣的,都是調用__libc_start_mian函數,該函數的聲明爲:

__libc_start_main (int (*main) (int, char **, char **),
                   int argc,
                   char *argv,
                   void (*init) (void),
                   void (*fini) (void),
                   void (*rtld_fini) (void),
                   void *stack_end);

其中寄存器傳遞的參數爲:

x0   main
x1   argc
x2   argv
x3   init
x4   fini
x5   rtld_fini
x6   stack_end 

_start函數的作用如下圖所示,下圖的上半部分是Linux Kernel完成的和平臺無關的設置,建立起了用戶棧最初的部分,SP指向棧頂,棧中存放傳遞給__libc_start_main函數的參數argc和argv,Linux Kernel在這一點完成將用戶的參數傳遞給用戶程序的角色,同時也將棧的控制權轉移給libc,而libc的__libc_start_main函數在將棧的控制權完成轉移給用戶的main函數之前,還會做一些額外的工作,發揮一些額外的作用
X86-64和ARM64用戶棧的結構 (4) ---__libc_start_main到main

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章