10-子程序设计

一、子程序设计要点

  1. 两种传参方法

    1. 寄存器
    2. 堆栈
  2. 调用约定
    决定了到底怎么传参,在C语言写函数定义时,写以下关键词来显示指定调用约定,
    void _fastcall cf330(unsigned m, char *buffer)指定了约定方式为_fastcall

  3. 安排局部变量

    1. 子程序需要一些局部变量,限于子程序部分
    2. 寄存器可以作为局部变量提高效率,但是寄存器的数量过少,一般不把局部变量安排在寄存器中
    3. 使用堆栈来安排局部变量,较为复杂,但是可以安排足够多的局部变量
      1. 用堆栈要控制esp指针位置
      2. 如果局部变量数量少,可以push一个寄存器进去,如果数量多,可以直接修改esp的值,然后用堆栈操作赋值
  4. 保护寄存器的约定

    1. 子程序可能会破坏某些寄存器内容。为此必须对有关寄存器的内容进行保护与恢复。
    2. 事前压入堆栈,事后从堆栈弹出。在利用堆栈进行寄存器的保护和恢复时,一定要注意堆栈的先进后出特性,一定要注意堆栈平衡
    3. 可能会降低效率。
    4. 需要主程序和子程序之间的“默契”和“约定”。子程序只保护主程序关心的那些寄存器,通常保护ebx、esi、edi和ebp。
  5. 描述子程序的说明

    1. 在给出子程序代码时,应该给出子程序的说明信息。
    2. 子程序说明信息一般包括:
      1. 子程序名(或者入口标号);
      2. 子程序功能描述;
      3. 子程序的入口参数和出口参数;
      4. 所影响的寄存器等情况;
      5. 使用的算法和重要的性能指标;
      6. 其他调用注意事项和说明信息;
      7. 调用实例。

二、子程序举例说明

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
//子程序名(入口标号):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
  1. 段内间接
名称 call(段内间接调用指令)
格式 CALL OPDR
动作 把调用指令下一行指令地址压栈,然后OPDR内容送到EIP,转到OPDR给出偏移地址处执行
合法值 OPDR:保护方式下,32位通用寄存器双字存储单元
注意 除了保存返回地址,其他同无条件转JMP
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
#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;
}
  1. 函数指针
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
//源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

可以看到

  1. 指针的本质就是地址
  2. 这里把函数入口和函数参数都放在堆栈,用堆栈传参。
  3. 注意传参时从ESP开始向高地址找参数,push参数的时候按从右到左的顺序
  4. 采用的是段内间接调用的方法

(2)返回指令

  1. 分类

    1. 按段内段间分
      1. 段内返回指令(对应段内调用)
      2. 段间返回指令(对应段间调用)(不介绍)
    2. 按返回时是否平衡堆栈
      1. 不带立即数的返回指令
      2. 带立即数的返回指令
  2. 段内返回不带立即数

名称 RET(段内返回不带立即数指令)
格式 RET
动作 指令从堆栈弹出地址偏移,送到指令指针寄存器EIP,返回到call时压栈的返回地址处执行
  1. 段内返回带立即数
名称 RET(段内返回带立即数指令)
格式 RET count
动作 指令从堆栈弹出地址偏移(当然这也会影响esp),送到指令指针寄存器EIP,还额外把count 加到ESP
注意 用于平衡堆栈

四、示例

  • 以下是一个全汇编程序示例,它将十六进制数字符串转为数值(二进制),再转十进制输出查看,可以看一下函数调用的各种方法。
  • 此程序是按8086机资源写的,在64位机器上运行此程序,需要:
    1. 保存以下代码为.asm文件
    2. 用nasm编译成.com文件
    3. 用DOSbox模拟8086环境运行
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
;说明:将十六进制数字符串转为数值(二进制),再转十进制输出查看	
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", '$' ;在这里写要转换的十六进制数