- 記錄彙編語言課筆記,可能有不正確的地方,歡迎指出
- 教材《新概念彙編語言》—— 楊季文
- 這篇文章對應書第二章 IA32處理器基本功能 3.5部分
文章目錄
一、子程序設計要點
-
兩種傳參方法
- 寄存器
- 堆棧
-
調用約定
決定了到底怎麼傳參,在C語言寫函數定義時,寫以下關鍵詞來顯示指定調用約定,
如void _fastcall cf330(unsigned m, char *buffer)
指定了約定方式爲_fastcall
- 注意都是從右向左入棧
- 注意不同調用約定對於清理堆棧的情況不同,如果函數自己不清,一定要手動清,維持堆棧平衡
-
安排局部變量的方法
- 子程序往往需要定義一些局部變量。所謂的局部,也就是限於子程序,或者限於代碼片段。
- 寄存器作爲局部變量可以提高效率。但寄存器數量較少,一般不把局部變量安排在寄存器中。
- 利用堆棧來安排局部變量。這個方法雖然較複雜,但可以安排足夠多的局部變量。
- 用堆棧安排,關鍵在於移動esp指針位置
- 如果局部變量數量少,可以push一個寄存器進去;如果數量多,可以直接修改esp的值,然後用堆棧操作賦值
-
保護寄存器的約定
- 子程序可能會破壞某些寄存器內容。爲此必須對有關寄存器的內容進行保護與恢復。
- 事前壓入堆棧,事後從堆棧彈出。在利用堆棧進行寄存器的保護和恢復時,一定要注意堆棧的先進後出特性,一定要注意堆棧平衡
- 可能會降低效率。
- 需要主程序和子程序之間的“默契”和“約定”。子程序只保護主程序關心的那些寄存器,通常保護ebx、esi、edi和ebp。
-
描述子程序的說明
- 在給出子程序代碼時,應該給出子程序的說明信息。
- 子程序說明信息一般包括:
- 子程序名(或者入口標號);
- 子程序功能描述;
- 子程序的入口參數和出口參數;
- 所影響的寄存器等情況;
- 使用的算法和重要的性能指標;
- 其他調用注意事項和說明信息;
- 調用實例。
二、子程序設計舉例
//子程序名(入口標號):BTOHS
//功 能: 把32位二進制數轉換爲8位十六進制數的ASCII碼串
//入口參數:(1)存放ASCII碼串緩衝區的首地址(先壓入堆棧)
// (2)二進制數據(後壓入堆棧)
//出口參數: 無
//其他說明:(1)緩衝區應該足夠大(至少9個字節)
// (2)ASCII串以字節0爲結束標記
// (3)影響寄存器EAX、ECX、EDX的值
_asm
{
BTOHS: ;子程序入口標號
PUSH EBP
MOV EBP, ESP
PUSH EDI //保護EDI
MOV EDI, [EBP+12]
MOV EDX, [EBP+8]
MOV ECX, 8
NEXT:
ROL EDX, 4
MOV AL, DL
AND AL, 0FH
ADD AL, '0'
CMP AL, '9'
JBE LAB580
ADD AL, 7
LAB580:
MOV [EDI], AL
INC EDI
LOOP NEXT
MOV BYTE PTR [EDI], 0
POP EDI
POP EBP
RET
}
//子程序名(入口標號):ISDIGIT
//功 能:判斷字符是否爲十進制數字符
//入口參數:AL=字符
//出口參數:如果爲非數字符,AL=0;否則AL保持不變
_asm
{
ISDIGIT:
CMP AL, '0' ;與字符'0'比較
JL ISDIG1 ;有效字符是'0'-'9'
CMP AL,'9'
JA ISDIG1
RET
ISDIG1: ;非數字符
XOR AL,AL ; AL= 0
RET
}
//演示調用上述子程序as334和子程序as335
#inclue <stdio.h>
int main( )
{
char buff1[16] = "328";
char buff2[16] = "1234024";
unsigned x1, x2;
unsigned sum;
_asm
{
LEA ESI, buff1 ;轉換一個字符串
CALL DSTOB
MOV x1, EAX
LEA ESI, buff2 ;轉換另一個字符串
CALL DSTOB
MOV x2, EAX
;
MOV EDX, x1 ;求和
ADD EDX, x2
MOV sum, EDX
; ;如這些代碼位於前面,
JMP OK ;需要通過該指令來跳過隨後的子程序部分!
}
//
//在這裏安排子程序DSTOB和ISDIGIT的代碼
//
OK:
printf("%d\n", sum);
return 0;
}
三、子程序調用方法
(1)調用指令
1. 分類
- 段內直接調用
- 段內間接調用
- 段間直接調用(不介紹)
- 段間間接調用(不介紹)
2. 段內直接
名稱 | call(段內直接調用指令) |
---|---|
格式 | CALL LABEL |
動作 | 把調用指令下一行指令地址壓棧,然後轉到LABEL處執行 |
注意 | 除了保存返回地址,其他同無條件轉JMP |
3. 段內間接
名稱 | call(段內間接調用指令) |
---|---|
格式 | CALL OPDR |
動作 | 把調用指令下一行指令地址壓棧,然後OPDR內容送到EIP,轉到OPDR給出偏移地址處執行 |
合法值 | OPDR:保護方式下,32位通用寄存器、雙字存儲單元 |
注意 | 除了保存返回地址,其他同無條件轉JMP |
#include <stdio.h>
int subr_addr; //存放子程序入口地址
int valu; //保存結果
int main( )
{
_asm
{
LEA EDX, SUBR2 //取得子程序二的入口地址
MOV subr_addr, EDX //保存到存儲單元
LEA EDX, SUBR1 //取得子程序一的入口地址
XOR EAX, EAX //入口參數EAX=0
CALL EDX //調用子程序一(段內間接,32位Reg)
CALL subr_addr //調用子程序二(段內間接,雙字存儲單元)
MOV valu, EAX
}
printf("valu=%d\n",valu); //顯示爲valu=28
return 0;
}
4、函數指針
//源C程序
#include <stdio.h>
int max(int x, int y); //聲明函數原型
int min(int x, int y); //
int main()
{
int (*pf)(int,int); //定義指向函數的指針變量
int val1, val2; //存放結果的變量
pf = max; //使得pf指向函數max
val1 = (*pf)(13,15); //調用由pf指向的函數
pf = min; //使得pf指向函數min
val2 = (*pf)(23,25); //調用由pf指向的函數
printf("%d,%d\n",val1,val2); //顯示爲15,23
return 0;
}
//反編譯(不優化)
//標號max_YAHHH、min_YAHHH分別是兩個函數入口地址
push ebp
mov ebp, esp ;建立堆棧框架
sub esp, 12 ;安排3個局部變量pf、val1和val2
; pf = max;
mov DWORD PTR [ebp-4], OFFSET max_YAHHH
; val1 = (*pf)(13,15);
push 15
push 13
call DWORD PTR [ebp-4] ;間接調用指針所指的函數max
add esp, 8 ;平衡堆棧
; val1= 返回結果
mov DWORD PTR [ebp-12], eax
; pf = min;
mov DWORD PTR [ebp-4], OFFSET min_YAHHH
; val2 = (*pf)(23,25);
push 25
push 23
call DWORD PTR [ebp-4] ;間接調用指針所指的函數min
add esp, 8
; val2= 返回結果
mov DWORD PTR [ebp-8], eax
mov eax, DWORD PTR [ebp-8] ; eax= val2
push eax
mov ecx, DWORD PTR [ebp-12] ; ecx= val1
push ecx
push OFFSET FORMTS ;格式字符串
call _printf ;段內直接調用
add esp, 12 ;平衡堆棧
;
xor eax, eax ;準備返回值
mov esp, ebp ;撤銷局部變量
pop ebp ;撤銷堆棧框架
ret
可以看到
- 指針的本質就是地址
- 這裏把函數入口和函數參數都放在堆棧,用堆棧傳參。
- 注意傳參時從ESP開始向高地址找參數,push參數的時候按從右到左的順序
- 採用的是段內間接調用的方法
(2)返回指令
1、分類
-
按段內段間分
- 段內返回指令(對應段內調用)
- 段間返回指令(對應段間調用)(不介紹)
-
按返回時是否平衡堆棧
- 不帶立即數的返回指令
- 帶立即數的返回指令
2、 段內返回不帶立即數
名稱 | RET(段內返回不帶立即數指令) |
---|---|
格式 | RET |
動作 | 指令從堆棧彈出地址偏移,送到指令指針寄存器EIP,返回到call時壓棧的返回地址處執行 |
3、 段內返回帶立即數
名稱 | RET(段內返回帶立即數指令) |
---|---|
格式 | RET count |
動作 | 指令從堆棧彈出地址偏移(當然這也會影響esp),送到指令指針寄存器EIP,還額外把count 加到ESP |
注意 | 用於平衡堆棧 |
四、示例
- 以下是一個全彙編程序示例,它將十六進制數字符串轉爲數值(二進制),再轉十進制輸出查看,可以看一下函數調用的各種方法。
- 此程序是按8086機資源寫的,在64位機器上運行此程序,需要:
- 保存以下代碼爲
.asm
文件 - 用nasm編譯成
.com
文件 - 用DOSbox模擬8086環境運行
- 保存以下代碼爲
;說明:將十六進制數字符串轉爲數值(二進制),再轉十進制輸出查看
segment code ;不分段,所有段共用一片內存空間
org 100H ;從100H開始
MOV AX, CS ;使得數據段與代碼段相同
MOV DS, AX ;DS = CS
MOV AX, string ;取到要轉換的字符串首地址
PUSH AX
CALL Hex2Bin ;轉換
CALL PutWordDec ;顯示轉換結果
MOV AH, 4CH
INT 21H
;子程序名:Hex2Bin
;功 能:把十六進制字符串轉數值
;入口參數:堆棧存字符串起始
;出口參數:ax
Hex2Bin:
PUSH BP
MOV BP, SP ;建立堆棧框架
MOV SI, [BP+4] ;字長16位
MOV CX, -1 ;避免提前結束
XOR AX,AX ;存結果
DEC SI ;方便循環
TOBIN:
INC SI
MOV DL, '$' ;字符串結尾用$標記,DX在MUL的時候會被刷掉,這裏要重新賦值
CMP [SI],DL
JE DONE
MOV BX,16 ;乘數16,BX在下面Hex2Bin_WORD的時候會被刷掉,要重新賦值
MUL BX ;AX是被乘數,積的低16位仍在AX
PUSH WORD [SI] ;取一個16進制字符,轉值存到BX(這裏入棧後面要手動平衡)
CALL Hex2Bin_WORD
ADD SP,2 ;平衡堆棧
ADD AX,BX
;CALL PutWordDec
;CALL PutSpace
LOOP TOBIN
DONE:
POP BP ;撤銷堆棧框架
RET
;子程序名:Hex2Bin_WORD
;功 能:把一個十六進制字符轉成二進制值
;入口參數:堆棧
;出口參數:BX
Hex2Bin_WORD:
PUSH BP
MOV BP, SP ;建立堆棧框架
MOV BX,[BP+4]
MOV BH,0
CMP BL,'A'
JB NUM
SUB BL,'A'-10
JMP OK
NUM:
SUB BL,'0'
OK:
POP BP ;撤銷堆棧框架
RET
;子程序名:PutWordDec
;功 能:把一個字的值轉十進制輸出
;入口參數:AX
;出口參數:無
PutWordDec:
PUSH BP
MOV BP, SP ;建立堆棧框架
PUSHA ;保護所有reg(關鍵是AX/BX/CX/DX)
MOV CX, -1
MOV BX,10
LoopPWD1:
XOR DX, DX
DIV BX
PUSH DX
CMP AX, 0
LOOPNE LoopPWD1
NOT CX
LoopPWD2:
POP DX
ADD DL, '0'
CALL PutChar
LOOP LoopPWD2
POPA ;恢復所有reg
POP BP ;撤銷堆棧框架
RET
;子程序名:PutChar
;功 能:顯示輸出一個字符
;入口參數:DL = 顯示輸出字符ASCII碼
;出口參數:無
PutChar:
PUSH AX ;簡單的函數,可以不建立堆棧框架
MOV AH,2
INT 21H ;調用2號系統功能顯示輸出
POP AX
RET
;子程序名:PutSpace
;功 能:顯示輸出一個空格
;入口參數:無
;出口參數:無
PutSpace:
PUSH AX ;簡單的函數,可以不建立堆棧框架
PUSH DX
MOV DL,20H
MOV AH,2
INT 21H ;調用2號系統功能顯示輸出
POP DX
POP AX
RET
;---------------------------------------------
string db "1234", '$' ;在這裏寫要轉換的十六進制數