1. 程式人生 > >程序執行空間分析

程序執行空間分析

7.1 程序執行空間

程式編譯連結成功後,要執行;由自己的虛地址空間,對映到實體地址空間進行執行; 可執行檔案的虛地址空間,也就是程序執行空間,是怎麼劃分的?

linux-64bit機器為例:

  • 地址64bit,標識範圍:0x0000000000000000 ~ 0xFFFFFFFFFFFFFFFF

  • text segment: 程式碼段 二進位制程式碼;從虛擬記憶體地址00400000開始,這個地址是固定的。使用pmap PID可以檢視到;

  • data segment: 存放已經初始化的全域性變數,靜態變數

  • BSS segment: 未初始化的全域性變數,會初始化預設值;

  • heap 段: c++動態記憶體分配運算子new,delete c動態記憶體分配函式:malloc alloc free

  • Memeory mapping region:mmap系統呼叫可將檔案對映入程序memory map地址空間; 使用ldd命令,我們可以看到程序依賴的庫檔案,這些庫檔案編譯過程中都要關聯到此虛擬地址空間;

  • stack 段: 函式區域性變數,函式呼叫等都儲存在stack區,地址由高到低的分配記憶體;

  • 使用pmap PID 或 cat /proc/PID/maps 檢視程序空間: maps檔案格式: 地址:庫在程序裡地址範圍 許可權:虛擬記憶體的許可權,r=讀,w=寫,x=,s=共享,p=私有; 偏移量:庫在程序裡地址範圍 裝置:映像檔案的主裝置號和次裝置號; 節點:映像檔案的節點號; 路徑: 映像檔案的路徑 每項都與一個vm_area_struct結構成員對應

7.2 x86-64暫存器體系和彙編指令

7.2.1 暫存器體系

  • 呼叫者保護:主函式作為呼叫者,呼叫子函式時,某些暫存器中的資料需要壓棧保護,以便於恢復,不被覆蓋;這個動作由主函式完成;對應的暫存器叫caller save

  • 被呼叫者保護:callee save 子函式壓棧保護暫存器中內容;

  • %rax 通常用於儲存函式呼叫的返回結果,同時也用於乘法和除法指令中。在imul 指令中,兩個64位的乘法最多會產生128位的結果,需要 %rax 與 %rdx 共同儲存乘法結果,在div 指令中被除數是128 位的,同樣需要%rax 與 %rdx 共同儲存被除數。

  • %rsp 是堆疊指標暫存器,通常會指向棧頂位置,堆疊的 pop 和push 操作就是通過改變 %rsp 的值即移動堆疊指標的位置來實現的。

  • %rbp 是棧幀指標,用於標識當前棧幀的起始位置

  • %rdi, %rsi, %rdx, %rcx,%r8, %r9 六個暫存器用於儲存函式呼叫時的6個引數(如果有6個或6個以上引數的話)。

7.2.2 常用匯編命令

7.3 函式呼叫執行進位制

7.3.1 函式棧幀

注意,這是棧區,存在函式呼叫時記憶體分佈。也就是所謂的函式呼叫棧; 

7.3.2 函式呼叫

呼叫者:引數從右向左依次壓棧 -》 函式返回地址(函式名)壓棧 -》跳轉到子函式的首地址執行程式碼 -》 被呼叫者: 上一個函式棧幀rbp指標壓棧 -》同時修改當前棧幀指標為當前地址 -》 繼續執行子函式 彙編程式碼示例:

...   # 引數壓棧
call FUNC  # 儲存返回地址,並跳轉到子函式 FUNC 處執行;兩個功能合併;
...  # 函式呼叫的返回位置

FUNC:  # 子函式入口
pushq %rbp  # 儲存舊的幀指標,相當於建立新的棧幀
movq  %rsp, %rbp  # 讓 %rbp 指向新棧幀的起始位置
subq  $N, %rsp  # 在新棧幀中預留一些空位,供子程式使用,用 (%rsp+K) 或 (%rbp-K) 的形式引用空位

7.3.3 函式返回

思考:當函式呼叫結束,如何恢復呼叫者函式棧空間,並進行執行? 1、rbp-》rsp: 也就是棧頂指標恢復到rbp處 2、rsp中的內容 -》上一個rbp:利用棧頂指標中的內容,恢復呼叫函式rbp 3、%rsp此時指向返回地址,取出來,呼叫函式繼續執行;引數由編譯器自動釋放; 4、函式返回值儲存在:%rax 

上面的過程,實際對應的指令leave和ret:

leave 指令:恢復主函式的棧幀; 執行完後,%rsp正好指向,主函式返回地址處;%rbp指向主函式棧幀起始位置; 分解出來實現程式碼:

movq %rbp, %rsp    # 使 %rsp 和 %rbp 指向同一位置,即子棧幀的起始處
popq %rbp # 將棧中儲存的父棧幀的 %rbp 的值賦值給 %rbp,並且 %rsp 上移一個位置指向父棧幀的結尾處

ret 指令: %rsp指向返回地址取出,跳到返回地址處,繼續執行主函式指令; 呼叫引數由編譯器自動釋放;

7.4 函式呼叫例項分析

1、示例程式碼:test.c

int add(int a, int b, int c, int d, int e, int f, int g, int h) { // 8 個引數相加
  int sum = a + b + c + d + e + f + g + h;
  return sum;
}

int main(void) {
  int i = 10;
  int j = 20;
  int k = i + j; 
  int sum = add(11, 22,33, 44, 55, 66, 77, 88);
  int m = k; // 為了觀察 %rax Caller Save 暫存器的恢復
  return 0;
}

2、彙編程式碼分析

檢視彙編程式碼: (1) gcc -S test.c -o test.s (2) gdb test_bin -> disassemble 函式名 gdb的反彙編功能

  • main程式碼分析:
main:
.LFB3:
	pushq	%rbp  #主函式棧幀指標壓棧;被調函式必做的事情;main也是某個函式的子函式;
.LCFI2:
	movq	%rsp, %rbp  #建立子函式棧幀;起始地址;必做;
.LCFI3:
	subq	$40, %rsp  #棧幀分配40bytes空間;減法;棧是向下生長
.LCFI4:
	movl	$10, -4(%rbp)  # 立即數+ 基址定址:將10 -》(*(%rbp)-4)
	movl	$20, -8(%rbp)  # 同上,對應程式碼:j = 20;
	movl	-8(%rbp), %eax 
	addl	-4(%rbp), %eax # 進行加法運算,同時把結果存放再%rax中
	movl	%eax, -12(%rbp) # 這一步驗證了%rax是caller save 暫存器;在呼叫子函式前將結果壓棧
	movl	$88, 8(%rsp) # 第8個引數壓棧,使用%rsp上面的第8個空間
	movl	$77, (%rsp)  
	movl	$66, %r9d    # 小於6個引數壓棧進入暫存器
	movl	$55, %r8d
	movl	$44, %ecx
	movl	$33, %edx
	movl	$22, %esi
	movl	$11, %edi
	call	add        #呼叫子函式add:儲存呼叫函式返回地址,病進入到子函式
	movl	%eax, -16(%rbp)  #將add函式的返回值%eax儲存到棧中;int sum = add(...)
	movl	-12(%rbp), %eax  #將呼叫add之前,儲存的 k= i+j的結果,恢復
	movl	%eax, -20(%rbp)  #對應原始碼:int m = k 命令; 
	movl	$0, %eax  # return 0;所以返回值要置為0;返回到上一個主函式
	leave  # 恢復主函式棧幀;rbp,rsp
	ret    # 返回並跳轉到主函式呼叫處下一條指令處,繼續執行

  • add函式分析:

add:
.LFB2:
	pushq	%rbp  #主函式棧幀指標壓棧;剛進子函式必做的事情
.LCFI0:
	movq	%rsp, %rbp  #子函式棧幀起始地址賦值;必做;
.LCFI1:
	movl	%edi, -4(%rbp)  # 將引數入棧;這種操作,相當於push %edi,(%rsp)
	movl	%esi, -8(%rbp)  # 由於子程式中可能會用到引數的記憶體地址,這些引數放在暫存器中是無法取地址的,這裡把引數壓棧,印證了我們之前的猜想:暫存器中的引數,也要入棧的。
	movl	%edx, -12(%rbp)
	movl	%ecx, -16(%rbp)
	movl	%r8d, -20(%rbp)
	movl	%r9d, -24(%rbp)
	movl	-8(%rbp), %eax
	addl	-4(%rbp), %eax  
	addl	-12(%rbp), %eax
	addl	-16(%rbp), %eax
	addl	-20(%rbp), %eax
	addl	-24(%rbp), %eax
	addl	16(%rbp), %eax  #取第7個引數;為什麼是16(%rbp)? 參考下面的總結
	addl	24(%rbp), %eax
	movl	%eax, -28(%rbp) # sum=a+b+c...
	movl	-28(%rbp), %eax
	leave
	ret

3、總結說明

(1):在彙編程式中,如果使用的是64位通用暫存器的低32位,則暫存器以 ”e“ 開頭,比如 %eax,%ebx 等,對於 %r8-%r15,其低32 位是在64位寄存後加 “d” 來表示,比如 %r8d, %r9d (2):如果運算元是32 位的,則指令以 ”l“ 結尾,例如 movl $11, %esi,指令和暫存器都是32位的格式。如果運算元是64 位的,則指令以 q 結尾,例如 “movq %rsp, %rbp” (3): 程式執行到取第7個引數時,棧幀情況:原理,就是基於子函式的rbp來訪問主函式的記憶體區域,因為引數超過6個,只能放到記憶體區域來傳遞;

Reference

1、http://blog.chinaunix.net/uid-27119491-id-3325943.html 《x86-64 程序地址空間》同時參考: Chapter 15 - The Process Address Space, Linux kernel development.3rd.Edition