程式的執行可以理解為連續的函式呼叫,每一個用戶態(用戶態指的是CPU指令集權限ring 0,用戶只能訪問常用CPU指令集,在應用程式中運行)行程都對應一個呼叫堆疊結構,當一個函式執行完畢后,會自動回到原先呼叫函式的位置(call指令)的下一步命令并執行,堆疊結構的作用是保存函式回傳地址、傳遞函式引數、記錄本地變數、臨時保存函式背景關系(背景關系,也就是執行函式所需要的相關資訊),
暫存器:
暫存器分配:
暫存器是處理器加工資料和運行程式的重要載體,暫存器在程式執行中中負責存盤資料和指令,因此函式呼叫與暫存器有重要聯系,
32位CPU所含有的暫存器有:
8個32位通用暫存器,其中包含4個資料暫存器(EAX、EBX、ECX、EDX)、2個變址暫存器(ESI和EDI)和2個指標暫存器(ESP和EBP)
6個段暫存器(ES、CS、SS、DS、FS、GS)
1個指令指標暫存器(EIP)
1個標志暫存器(EFLAGS)
最初的8086平臺使用16位暫存器,每個暫存器都有具體特定的用途,但隨著32位暫存器的出現,32位暫存器采用平臺尋址方式,因此對特殊暫存器沒有過多要求,但由于歷史原因,16位暫存器的名字被保存,EAX,EBX,ECX,EDX,ESI,EDI這六個暫存器通常作為通用暫存器使用,但是部分指令會有特定的源暫存器或者目的暫存器(比如%eax通常用于保存函式回傳值),因此為避免兼容性問題,ABI規范各個暫存器的作用,EAX通常用于保存函式回傳值,EBX用于存盤基地址,ECX是計數器,重復前綴指令(REP,X86匯編指令,使指定指令重復n次,但只能指定一條陳述句)和LOOP指令(回圈指令,能夠執行代碼塊)的內定計數器,回圈重復執行次數將保留在cx中,EDX一般用來儲存整數除法中的余數部分(當函式體中包含除法時,EAX保留整數部分,EDX則負責保存余數部分,乘除關系一般都與EAX、EDX有關),而EDI、ESI則通常用于儲存函式引數
EIP指令暫存器通常指向下一條待執行的指令地址(代碼段內的偏移量),每完成一潭訓編指令,EIP的值就會增加,ESP指向當前函式的堆疊幀結構的堆疊頂位置,EBP則始終指向當前函式的堆疊幀結構的堆疊底位置,同時注意EIP暫存器不能通過尋常方式訪問到(無法獲得opcode)
在Intel CPU中,通常將EBP暫存器作為堆疊幀指標暫存器,存盤基地址,對于函式引數,偏移量為正值,對于區域變數,偏移量為負值
暫存器的使用原則:
主調函式指的是呼叫其他函式的函式,被調函式指的是被其他函式呼叫的函式
主調函式一般使用eax、ecx、edx暫存器作為主調函式保存暫存器,當函式呼叫時,若主調函式希望保持這些暫存器的值,則必須在呼叫前顯式地將其保存在堆疊中;被調函式可以覆寫這些暫存器,而不會破壞主調函式所需的資料,被調函式一般使用ebx、edi、esi作為被調函式保存暫存器,被調函式在覆寫這些暫存器的值時,必須先將暫存器原值壓入堆疊中保存起來,并在函式回傳前從堆疊中恢復其原值,因為主調函式可能也在使用這些暫存器,此外,被調函式必須保持暫存器%ebp和%esp,并在函式回傳后將其恢復到呼叫前的值,亦即必須恢復主調函式的堆疊幀,
堆疊幀結構:
函式的呼叫通常是嵌套的,在同一時刻,堆疊中會有多個函式的資訊,每一個未執行完成的函式都有一個連續獨立的區域即堆疊幀,堆疊幀是堆疊的一個邏輯片段,當函式呼叫時,邏輯堆疊幀被壓入堆疊中,當函式回傳時,堆疊幀從堆疊中彈出,堆疊幀主要儲存函式引數、函式內區域變數以及回傳前一堆疊幀所需要的資訊
堆疊幀的作用:
1、保存主調函式的區域變數
2、向被調函式傳遞引數
3、回傳被調函式的回傳值
4、回傳函式的回傳地址(即當被呼叫函式執行完成時應當執行的下一條指令)
堆疊幀的邊界由堆疊幀基暫存器EBP和堆疊頂暫存器ESP來界定,EBP位于堆疊底,高地址,在堆疊幀內位置固定,ESP位于堆疊頂,低地址,位置隨著出堆疊和入堆疊而發生變化,因而資料訪問通常通過EBP來進行(通過偏移量來訪問)
ESP指向堆疊頂,EBP一般指向堆疊幀的開始位置
現在在假定有一個程式:A()——>B()——>C()
那么A中元素包括A函式的區域變數,傳給函式B的引數、B函式的回傳值、執行完B的下一條指令的地址
B中元素包括B函式的區域變數、傳給函式C的引數、C函式的回傳值、執行完C的下一條指令的地址
C中元素包括C函式的區域變數
因此:
(1)被調函式的引數和回傳值保存在主調函式的堆疊幀中
(2)以堆疊幀為單位,那么C函式堆疊幀位于堆疊頂,ESP暫存器指向C函式堆疊幀的堆疊頂(即整個堆疊的堆疊頂),而EBP暫存器則指向C函式堆疊幀的起始位置
(3)同時,因為主調函式尚未執行完成,所以被調函式的堆疊幀并不能覆寫主調函式的堆疊幀,只能通過push和pop指令實作呼叫
(4)堆疊的生長方向為由高地址到低地址,資料填充則是由低地址到高地址
接下來再介紹幾個匯編指令:
(1)call指令:執行call指令時,會將EIP的值通過push壓入堆疊中(因為EIP保存的是CPU即將執行的下一條指令的地址,所以這一步就對應前面說的保存函式回傳地址,解釋了為什么函式的回傳地址保存在主調函式中),然后將EIP的值修改為被調函式的值,則當call執行完成后,將自動呼叫目標函式(被調函式)
(2)ret指令:將call指令中壓入堆疊中的EIP的值(回傳地址)pop回到EIP中,則ret指令執行完成后,將執行主調函式的下一條指令
(3)push指令:將ESP暫存器減去八,將ESP暫存器向高地址移動,從而開辟新的空間,然后把運算元復制到ESP所指的位置上
在AT&T格式下:
sub $8 %esp
mov 源運算元 esp
(4)pop指令:將ESP暫存器的所存的值傳到指定位置,然后將ESP暫存器加上八
在AT&T格式下:
mov %esp 目標運算元
add $8 %esp
(5)leave指令:跟在ret指令后面,作用是交換esp和ebp的值
以下面一段程式為例:
#include <stdio.h>
int sum(int a, int b)
{
int s = a + b;
return s;
}
int main(int argc, char *argv[])
{
int n = sum(1, 2);
return 0;
}
通過gdb除錯后:
0x0000000000400540 <+0>:push %rbp 0x0000000000400541 <+1>:mov %rsp,%rbp 0x0000000000400544 <+4>:sub $0x20,%rsp 0x0000000000400548 <+8>:mov %edi,-0x14(%rbp) 0x000000000040054b <+11>:mov %rsi,-0x20(%rbp) 0x000000000040054f <+15>:mov $0x2,%esi 0x0000000000400554 <+20>:mov $0x1,%edi 0x0000000000400559 <+25>:callq 0x400526 <sum> 0x000000000040055e <+30>:mov %eax,-0x4(%rbp) 0x0000000000400561 <+33>:mov -0x4(%rbp),%eax 0x0000000000400564 <+36>:mov %eax,%esi 0x0000000000400566 <+38>:mov $0x400604,%edi 0x0000000000400575 <+53>:mov $0x0,%eax 0x000000000040057a<+58>:leaveq0x000000000040057b <+59>:retq
現在開始執行第一條指令:
0x0000000000400540 <+0>:push %rbp
push %rbp:push指令將rsp暫存器減8開辟新的空間后,將rbp的值壓入堆疊中,此時rbp的值為呼叫main函式的函式的幀基地址,push rbp的原因是main函式需要rbp暫存器存盤自己的幀基地址,
但是又不能覆寫呼叫main函式的函式的幀基地址,因此通過push指令開辟八個位元組的空間來存盤呼叫main函式的函式的幀基地址,所以目前為止,main函式的堆疊幀中只有呼叫main函式的函式的幀基地址
同時在這條指令之前,代碼還沒有到main函式,從這條指令開始進入main函式
0x0000000000400541 <+1>:mov %rsp,%rbp
將rsp暫存器的值賦值給rbp,使rbp和rsp指向同一個位置,即main函式堆疊幀的起始位置
0x0000000000400544 <+4>:sub $0x20,%rsp
將rsp暫存器減去32位元組,使其指向更低位置,這是為了給main函式中區域變數和臨時變數預留空間,這里注意的是,當程式開始運行時,作業系統會自動為程式分配32位元組空間,但具體使用多少由rsp暫存器決定
另外,當該指令執行完后,main函式的空間就全部分配完成,分別是存盤主調函式的8位元組和預留的32位元組
0x0000000000400548 <+8> :mov %rdi,-0x14(%rbp) #保存main函式的第1個引數
0x000000000040054b <+11>:mov %rsi,-0x20(%rbp) #保存main函式的第2個引數
0x000000000040054f <+15>:mov $0x2,%rsi #sum函式的第2個引數放入esi暫存器
0x0000000000400554 <+20>:mov $0x1,%rdi #sum函式的第1個引數放入edi暫存器
前兩條指令的目的是保存rdi和rsi的值,因為在呼叫main函式時,rdi和rsi分別保存了argc和argv兩個引數,而接下來要呼叫sum函式,則為了防止rdi和rsi中的數值被覆寫,就提前將他們存入堆疊幀中
通過rbp加偏移量的方式
后兩條指令的目的是傳遞sum函式的實參,將rsi和rdi分別賦值為2和1,同時這里有一條規定,就是函式引數保存默認暫存器順序為rdi、rsi、rdx,,,
0x0000000000400559 <+25>:callq 0x400526 <sum>
使用call指令,如上文提到一般,call指令先將rip的值壓入堆疊中保存起來,也就是0x40055e 這個地址,這里會將rsp的值減8來開辟新的空間,然后將rip的值修改為目標函式的值,
也就是call指令的運算元0x400526,執行完成后
跳轉到sum函式
0x0000000000400526 <+0>:push %rbp # 保存main函式的rbp的值入堆疊 0x0000000000400527 <+1>:mov %rsp,%rbp # 修改當前rbp的值為當前的堆疊頂 0x000000000040052a <+4>:mov %edi,-0x14(%rbp) # 把第1個引數放入臨時變數 0x000000000040052d <+7>:mov %esi,-0x18(%rbp) # 把第2個引數放入臨時變數 0x0000000000400530 <+10>:mov -0x14(%rbp),%edx # 將第1個臨時變數放入到 edx 當中 0x0000000000400533 <+13>:mov -0x18(%rbp),%eax # 將第2個臨時變數放入到 eax 當中 0x0000000000400536 <+16>:add %edx, %eax # 進行加法計算, 結果保存在 eax 當中 0x0000000000400538 <+18>:mov %eax,-0x4(%rbp) # 將 eax 的值保存到臨時變數中 0x000000000040053b <+21>:mov -0x4(%rbp),%eax # 將臨時變數的值放入到 eax 暫存器當中 0x000000000040053e <+24>:pop %rbp # 出堆疊, 恢復main函式的 rbp 的值 0x000000000040053f <+25>:retq # 函式回傳
這里要注意一點就是之所以sum函式沒有修改rsp的值來預留空間是因為sum是最后一個被呼叫的函式,他沒有使用call指令,也就是說沒有將rip的值壓入堆疊中,
不需要修改rsp的值,也就是它預留的空間為堆疊中的所有剩余空間
然后繼續執行 retq 指令, 該指令把 rsp 指向的堆疊單元當中的 0x40055e 取出給 rip 暫存器, 同時 rsp 加8, 這樣,
rip 暫存器 中的值就變成了 main 函式中呼叫 sum 的 call 指令的下一條指令, 于是回傳到 main 函式中繼續執行.
繼續執行 main 函式中的:
mov %eax,-0x4(%rbp) # 把sum函式的回傳值賦給變數n
該指令是把 rax 暫存器當中的值(sum函式回傳值), 放入到 rbp-4 所指的記憶體, 也就是變數 n 所在的位置,繼續執行程式結束
轉載請註明出處,本文鏈接:https://www.uj5u.com/qita/551568.html
標籤:其他
上一篇:【經濟機器是如何運行的】30分鐘看懂經濟的本質(無數大佬推薦)建議收藏!
下一篇:返回列表
