|
1. c語言的函數調用與對應的匯編代碼 1.1 調用規則
比如調用函數 function(parameter1, parameter2, parameter3)
Pascal調用規則 | _cdecl調用規則 | _stdcall調用規則 | PUSH parameter1
PUSH parameter2
PUSH parameter3
CALL function | PUSH parameter3
PUSH parameter2
PUSH parameter1
CALL function
ADD ESP, 0CH | PUSH parameter3
PUSH parameter2
PUSH parameter1
CALL function | 參數從左到右傳遞壓棧,由被調用函數清理堆棧 | 參數從右到左傳遞壓棧,由調用函數負責清理堆棧 | 參數從右到左傳遞壓棧,由被調用函數負責清理堆棧 | 用于Win16平臺 | C/C++調用標準 | Windows API 使用 |
1.2 匯編代碼 對調用的方式,舉一個例子,對c語言常用的printf:
printf(“%d", a);
解析成匯編代碼如:
PUSH a
PUSH OFFSET String "%d"
CALL printf
ADD ESP, 8
CALL指令和RET指令
段內調用
對CALL指令來說,其執行的步驟一般包括:
- 將IP壓入棧(即寄存器EIP的值,指向CALL指令之后的第一條指令)
- 將IP置為跳轉到的地址,開始執行
對應 的RET指令為:
段間調用
如果是段間函數調用,則CALL的執行過程一般是:
- 將CS(段地址)壓入棧
- 將IP(即EIP的值)壓入棧
- 將IP置為跳轉到的函數地址,開始執行
對應的RET指令執行步驟:
被調用函數的執行步驟
PUSH EBP ; 保存當前堆棧基址ebp,以作返回用
MOVE EBP, ESP ; 將當前esp的值賦給ebp,作為新的基址,即進入函數內部
SUB ESP, 0CCH ; 將esp往下移動一個范圍,開辟一片新的堆棧空間給當前函數使用
; 這是由于堆棧從高地址往低地址增長,所以,減一個值意味著開辟了
; 新的空間
................. ; 保存其他寄存器的值
.................
................. ; 恢復壓棧的其他寄存器的值
MOVE ESP, EBP ; 恢復esp的值為原來的堆棧棧頂值
POP EBP ; 恢復堆棧基址為原基址位置
RET
一般來說,函數的返回值會放在EAX寄存器中返回。
2. c語言特殊語句塊的匯編代碼
1)For循環的匯編代碼模板
mov <循環變量>, <初始值> ; 循環變量賦初值
jmp B ; 直接跳轉到循環控制測試部分代碼
A: (改動循環變量) ; 修改循環變量值的部分代碼
......
B: cmp <循環變量>, <限制變量> ; 將循環變量的值進行測試、跳轉
jge 跳出循環 ; 符合終止條件,則跳出循環體 ; (注意,這里的jl指令可以是其他的jge等,一具判斷條件而定)
(循環體代碼) ; 否則,進入循環體代碼執行
...
jmp A ; 循環體結束的最后,是一個無條件跳轉語句,調回直接
; 修改循環變量的代碼
2)do循環的匯編代碼模板
A: (循環體) ; 直接是循環體代碼
....
cmp <循環變量>, <限制變量> ; 判斷是否需要終止循環
jl <循環開始處> ; 如果不符合終止條件,直接調回循環體開始處繼續執行循環體
; 代碼(注意,這里的jl指令可以是其他的jge等,一具判斷條件而定)
3) while循環的匯編代碼模板
A: cmp <循環變量>, <限制變量> ; 先比較循環變量,是否需要進行循環
jge B ; 如果滿足停止循環,則直接跳到B,即循環體后的第一個指令
(循環體)
……
jmp A ; 循環體的最后一條指令,是無條件跳轉到循環控制判斷指令A處
B: (循環結束了)
4)if-else的匯編代碼模板(待續...)
理解調用棧最重要的兩點是:棧的結構,EBP寄存器的作用。
首先要認識到這樣兩個事實:
1、一個函數調用動作可分解為:零到多個PUSH指令(用于參數入棧),一個CALL指令。CALL指令內部其實還暗含了一個將返回地址(即CALL指令下一條指令的地址)壓棧的動作。
2、幾乎任何本地編譯器都會在每個函數體之前插入類似如下指令:PUSH EBP; MOV EBP ESP;
即,在程序執行到一個函數的真正函數體時,已有以下數據順序入棧:參數,返回地址,EBP。
由此得到類似如下的棧結構(參數入棧順序跟調用方式有關,這里以C語言默認的CDECL為例):
+| (棧底方向,高位地址) |
| ....................|
| ....................|
| 參數3 |
| 參數2 |
| 參數1 |
| 返回地址 |
-| 上一層[EBP] |
| 局部變量1 |
| 局部變量2 |
|.....................|
補充:棧一直隨著函數調用的深入,一直想棧頂方向壓下去。每次調用函數時候,先壓函數參數(從右往左順序壓),再壓入函數調用下條指令的地址(由call完成)。接著進入調用函數體中先執行PUSH EBP; MOV EBP ESP;(一般已經由編譯器加入到函數頭中了),接著就是吧函數體中的局部變量壓入棧中。再遇到函數的調用的嵌套則依此類推。(added by smsong)
“PUSH EBP”“MOV EBP ESP”這兩條指令實在大有深意:首先將EBP入棧,然后將棧頂指針ESP賦值給EBP。“MOV EBP ESP”這條指令表面上看是用ESP把EBP原來的值覆蓋了,其實不然——因為給EBP賦值之前,原EBP值已被壓棧(位于棧頂),而新的EBP又恰恰指向棧頂。
此時EBP寄存器就已處于一個很重要的地位,該寄存器中存儲著棧中的一個地址(原EBP入棧后的棧頂),從該地址為基準,向上(棧底方向)能獲取返回地址、參數值,向下(棧頂方向)能獲取函數局部變量值,而該地址處又存儲著上一層函數調用時的EBP值!
一般而言,ss:[ebp+4]處為返回地址,ss:[ebp+8]處為第一個參數值(最后一個入棧的參數值,此處假設其占用4字節內存),ss:[ebp-4]處為第一個局部變量,ss:[ebp]處為上一層EBP值。
由于EBP中的地址處總是“上一層函數調用時的EBP值”,而在每一層函數調用中,都能通過當時的EBP值“向上(棧底方向)能獲取返回地址、參數值,向下(棧頂方向)能獲取函數局部變量值”。
如此形成遞歸,直至到達棧底。這就是函數調用棧。
編譯器對EBP的使用實在太精妙了。
從當前EBP出發,逐層向上找到任何的EBP是很容易的:
unsigned int _ebp;
__asm _ebp, ebp;
while (not stack bottom)
{
//...
_ebp = *(unsigned int*)_ebp;
}
假如要寫一個簡單的調試器的話,注意需在被調試進程(而非當前進程——調試器進程)中讀取內存數據。
8個通用寄存器:
數據寄存器:AX,BX,CX,DX
指針寄存器:SP(堆棧指針),BP(基址指針)
變址寄存器:SI(原地址),DI(目的地址)
1、通用寄存器
數據寄存器,指針寄存器和變址寄存器統稱為通用寄存器。這些寄存器除了各自專門用途外,它們均可用于傳送和暫存數據,可以保存算術邏輯運算中的操作數和運算結果。
(1)數據寄存器
數據寄存器主要用來保存操作數或運算結果等信息,它們的存在節省了為存取操作數所需占用總線和訪問存儲器的時間。
(2)變址和指針寄存器
變址和指針寄存器主要用于存放某個存儲單元地址的偏移,或某組存儲單元地址的偏移,即作為存儲器(短)指針使用。作為通用寄存器,它們可以保存16位算術邏輯運算中的操作數和運算結果,有時運算結果就是需要的存儲單元地址的偏移。
2、控制寄存器(2個)
(1)指令指針寄存器
8086/8088CPU中的指令指針IP也是16位的。
指令指針IP給出接著要執行的指令在代碼段中的偏移。
(2)標志寄存器
8086/8088CPU中有一個16位的標志寄存器,包含了9個標志,主要用于反映處理器的狀態和運算結果的某些特征。6個條件標志+3個方向標志
3、段寄存器(4個)
8086/8088CPU依賴其內部的四個段寄存器實現尋址1M字節物理地址空間。
8086/8088把1M字節地址空間分成若干邏輯段,當前使用的段值存放在段寄存器中。
由于8086/8088有這四個段寄存器,所以有四個當前使用段可以直接存取,這四個當前段分別稱為代碼段,數據段,堆棧段和附加段。
(1)代碼段
(2)數據段
(3)堆棧段
(4)附加段
|
|