80386汇编-高级过程
9unk Lv5

堆栈框架

堆栈框架也称为活动记录,它是为传递的参数、子程序的返回地址、局部变量和保存的寄存器保留的堆栈空间。堆栈框架是按以下的步骤创建的:

  • 如果有传递的参数,则压入堆栈
  • 子程序被调用,子程序的返回地址压入堆栈
  • 子程序开始执行时,EBP 被压入堆栈
  • EBP 设为 ESP 的值,从这时开始,EBP 就会被作为寻址所有子程序参数的基地址指针使用
  • 如果有局部变量,ESP减去一个数值,以便再堆栈上为局部变啊零保留空间
  • 如果任何寄存器需要保存,则压入堆栈

堆栈框架的结构受程序的内存模式以及参数传递约定影响。

堆栈参数

我们通常使用的传递参数的方式有3种:寄存器传参、使用约定存储单元传参(全局变量)、堆栈传参。另外还有一种 call 后区传参,这个不经常使用。
各个传参方式的优缺点:

  • 寄存器传参:传参方便且速度快;但寄存器较少,传参数量有限。
  • 约定的存储单元传参:传参数量没有限制;传参速度慢,且不能保证代码的可用性,在产生中断的时候,全局变量有可能被改动
  • 堆栈传参:传参数量没有限制,不会被改动;传参速度慢

在8086计算机中堆栈传参就是申请一个堆栈段,也可以是DOS系统默认分配的一个64KB的堆栈空间。但是在 80386 计算机中堆栈的分配就变得复杂一些,它主要涉及到计算机的内存管理。

我们写程序至少有一个代码段,除了代码段还有数据段和堆栈段。到了386计算机体系里面,计算机增加了如下几个区域:
1

我们在 8086 写的 .data 段,就是全局数据区。堆区是由程序员自定义的一块私有的内存区域,而栈区是一块公用的内存区域。上面划分的区块是人为划分的,这样划分就是为了让我们更容易理解。

2

在进行子程序调用时,通常在堆栈上压入两类参数:

  • 值参数(变量和常量值)
  • 引用参数(变量的地址)

传递值: 通过在堆栈上压入变量值的一份副本的方式传递参数,就称为传递值或简称传值。如下子程序传递两个 32 位整数:
4

下面是在 CALL 指令执行之前的堆栈示意图:
3

传递引用: 传递引用的参数是一个对象的地址,如下子程序 swap:
5

下面是调用 Swap 钱堆栈的示意图:
6

传递数组: 在传递数组时高级语言堆栈参数用的是传递引用,通过传递值的方式传递大量数据完全不切实际,同时还会降低程序的执行速度并减少宝贵的堆栈空间。传递数组的方式如下:
7

堆栈参数的访问(C/C++)

在调用函数时,c/c++ 程序使用标准的方法初始化访问参数:

  • 第一步:push ebp(保存 EBP 寄存器);mov ebp,esp(将 EBP 指向栈顶)
  • 第二步:push xxx;push xxx(……),保存多个寄存器值(这一步根据需求来,不是必要操作)
  • 第三步:写入程序代码
  • 第四步:pop xxx;pop xxx(与第二步操作相对应)
  • 第五步:pop ebp(还原 ebp)
  • 第六步:ret xxx(函数返回)

以上面的 AddTwo 函数为例,其堆栈框架如下图所示:
8

访问堆栈参数: C/C++ 函数使用相对基址寻址方式访问堆栈参数,EBP 用作基址寄存器,偏移部分是一个常量。函数一般通过 EAX返回一个 32 位的值
9

堆栈清理(堆栈平衡)

如下图所示,Example 函数破坏了堆栈:
10

如下图所示函数返回前使用 esp+(4*参数个数) 使堆栈平衡,恢复正常。
11

STDCALL 调用约定: 处理堆栈清理问题的另一种方法是使用 STDCALL 调用约定,可以在 AddTwo 过程中的 RET 指令后提供一个参数以修复 ESP 的值。
12

在8086汇编学习中写过代码,应该也熟悉另一种方法(在 ret 指令前使用 “mov esp,ebp” )恢复 ESP 的值。

通过堆栈传递 8 位和 16 位的参数

在保护模式下传递堆栈参数时,最好使用 32 位的操作数,虽然可以在堆栈压入 16 位的操作数,但这样会使得 ESP 无法对齐在双字边界上,由此可能会导致发生页错误故障,程序的性能也可能会降低。因此在传递 8 位或 16 位堆栈参数时,应把它扩展到 32 位再压栈。
13

PUSH 指令自动把字符扩展到 32 位:

1
2
push 'x'
call Uppercase

14

PUSH 指令不允许 8 位的操作数:
15

可使用 MOVZX 指令把字符值扩展至 EAX:
16

16位参数的例子: 假设要向前面给出的 AddTwo 过程传递两个 16 位的整数,由于该过程期望接收 32 位参数,因此下面的代码将导致程序执行错误:

1
2
3
4
5
6
7
8
9
10
11
12
INCLUDE Irvine32.inc

.data
word1 WORD 1234h
word2 WORD 4111h

main PROC
push word1
push word2
call AddTwo
exit
main ENDP

17
18

在16位参数入栈之前使用 movzx 指令进行零扩展,存入正确的参数:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
INCLUDE Irvine32.inc

.data
word1 WORD 1234h
word2 WORD 4111h

.code
main PROC
movzx eax,word1
push eax
movzx eax,word2
push eax
call AddTwo
exit
main ENDP

过程的调用者必须确保传递的参数和过程期望的参数相一致。就堆栈参数而言,其大小和顺序都是非常重要的。

传递长整数参数

在使用堆栈过程传递长整数参数时,可以先把高位字压栈,然后再把低位字压栈,这样实际上是以小端存储把长整数压栈。下面的 WriteHex64 过程通过堆栈接收一个 64 位的整数并以十六进制显示:
19

下面的例子在调用 WriteHex64 之前,首先把 longVal 的高半部分压栈,然后再把低半部分压栈:

1
2
3
4
5
6
7
8
9
10
11
12
13
INCLUDE Irvine32.inc

.data
longVal DQ 1234567800ABCDEFh

.code
main PROC
push DWORD PTR longVal+4
push DWORD PTR longVal
call WriteHex64
exit
main ENDP
END main

下图展示了 WriteHex 过程在 EBP 压栈后的堆栈框架。
20

保存和恢复寄存器

子过程通常在修改寄存器之前保存其原来的值,以便在过程返回之前进行恢复。理想情况下,要保存的寄存器应在 EBP 设为 ESP 的值后、为局部变量保留空间之前压栈,这有助于避免改变堆栈参数的相对偏移。
21
22
23

书中的堆栈图是倒过来画的。

USES操作符对堆栈的影响

USES 操作符后写需要保存恢复的寄存器列表,对于列表中的每个寄存器,MASM 自动生成合适的 PUSH 和 POP 指令。
24

注:函数存储返回值的寄存器不要跟在 USES 操作符后,这样会导致返回值错误。

局部变量

在高级语言程序中,在单个过程中创建、使用和效汇的变量称为局部变量。局部变量与过程之外声明的全局变量相比有明显的优点:

  • 只有在局部变量所在过程之内的语句才能够看到和修改局部变量。这个特点有助于避免程序源码中多处修改一个变量导致的bug。
  • 局部变量使用的存储空间在过程结束后立即释放
  • 一个过程内的局部变量的名字可以和其他过程内的局部变量同名,不会发生名字冲突。这个特性对于大程序是非常有用的。在大程序中,变量名相同的可能性还是比较大的。
  • 对递归过程以及可能由多个线程同时执行的过程而言,局部变量是必须的。

局部变量在运行时栈上创建的。在内存中其位置通常在基址指针(EBP)之下,尽管在汇编时不能给定默认值,但可以在运行时初始化。在汇编语言中创建局部变量时,可以使用和 C/C++ 类似的技术。

例如:如下C++函数声明局部变量X和Y:

1
2
3
4
5
void MySub()
{
int X = 10;
int y = 20;
}

MySub 反汇编代码如下:
25

局部变量符号: 为使代码更加易读,可以给每个变量的引用地址都定义一个符号并在代码中使用这些符号:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
INCLUDE Irvine32.inc

.data
X_local EQU DWORD PTR [ebp-4]
Y_Local EQU DWORD PTR [ebp-8]

.code
main PROC
call MySub
exit
main ENDP

MySub PROC
push ebp
mov ebp,esp
sub esp,8
mov X_local,10
mov Y_Local,20
mov esp,ebp
pop ebp
ret
MySub ENDP
END main

26

访问引用参数

子过程通常使用相对基址寻址方式访问引用参数,这是由于每个引用参数都是一个指针,实际上引用参数是要被装入到寄存器中作为间接操作数使用的。

例子:下面的 ArrayFill 过程,使用 16 位随机整数序列填充一个数组。该过程接收两个参数:第一个参数是数组的偏移,第二个参数是指示数组长度的整数。第一个参数是通过传递引用方式来传递,第二个参数是通过传值方式传递。

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
INCLUDE Irvine32.inc

.data
count = 100
array WORD count DUP(?)

.code
main PROC
push OFFSET array
push count
call ArrayFill
exit
main ENDP

ArrayFill PROC
push ebp
mov ebp,esp
pushad
mov esi,[ebp+12]
mov ecx,[ebp+8]
cmp ecx,0
je L2
L1:
mov ecx,1000h
call RandomRange
mov [esi],ax
add esi,TYPE WORD
loop L1
L2:
popad
pop ebp
ret 8
ArrayFill ENDP
END main

LEA 指令

27

LEA 指令之前经常用的,这里就不再重复写了。

ENTER 和 LEAVE 指令

ENTER 指令

ENTER 指令自动为被调用过程创建堆栈框架,它为局部变量保留堆栈空间并在堆栈上保存 EBP,该指令执行以下三个动作:

  • 在堆栈上压入EBP(push ebp)
  • 把 EBP 设为堆栈框架的基址针(mov ebp,esp)
  • 为局部变量保留空间(sub esp,numbytes)

ENTER 指令有两个操作数:第一个操作数是一个常量,用于指定要为局部变量保留出多少堆栈空间(numbytes);第二个操作数指定过程的嵌套层次(nestinglevel)。

1
ENTER numbytes,nestinglevel

两个操作数都是立即数。numbytes 总是向上取整为4的倍数,以使 ESP 按双子边界地址对齐。nestinglevel 决定了从调用过程复制到当前堆栈框架中的堆栈框架指针的数目。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
INCLUDE Irvine32.inc

.code
main PROC
call MySub
exit
main ENDP

MySub PROC
;ENTER 0,0
;ENTER 8,0
ENTER 8,1
mov DWORD PTR [ebp-4],10
mov DWORD PTR[ebp-8],20
ret
MySub ENDP

END main

经过多次尝试 ENTER 伪指令操作规则如下:

  • 第一个参数 numbytes:当值为0,执行操作(push ebp;mov ebp,esp);当值为X,执行操作(push ebp;mov ebp,esp;sub esp,X)
  • 第二个参数 nestinglevel:当值为0,不做任何操作;当值为X,执行操作(push ebp;mov ebp,esp;sub esp,8;sub esp,(X*指针宽度))

LEAVE 指令

LEAVE 指令释放一个过程的堆栈框架。LEAVE 指令执行与前面 ENTER 指令相反的操作。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
INCLUDE Irvine32.inc

.code
main PROC
call MySub
exit
main ENDP

MySub PROC
ENTER 8,1
mov DWORD PTR [ebp-4],10
mov DWORD PTR[ebp-8],20
LEAVE
ret
MySub ENDP

END main

LOCAL 伪指令

LOCAL 伪指令在过程内声明一个或多个命名局部变量,并同时赋予变量相应的尺寸属性。语句必须紧接在 PROC 伪指令所在行之后,格式如下:

1
LOCAL 变量列表

其中变量列表是一系列的变量定义,中间以逗号分隔,列表可能会占用多行。每个变量定义的格式如下:

1
标号:类型

标号可以是任何有效的标识符,类型既可以是标准类型(WORD、DWORD等),也可以是用户自定义的类型。
28

MASM 生成的代码

通过查看汇编代码,可以看在使用LOCAL伪指令时 MASM 生成相应的代码如下所示:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
INCLUDE Irvine32.inc

.code
main PROC
call Example1
exit
main ENDP

Example1 PROC
LOCAL temp:DWORD
mov eax,temp
ret
Example1 ENDP
END main

29

非双字局部变量

在声明不同尺寸大小的局部变量时,LOCAL 伪指令根据变量大小的不同,为其分配空间的方法也是不一样的:对于 8 位变量,在第一个可用的字节处为其分配空间;对于 16 位变量,在第一个可用的偶数地址处为其分配空间;对于 32 位变量,在第一个可用双字对齐边界地址处为其分配空间。

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
INCLUDE Irvine32.inc

.code
main PROC
call Example1
call Example2
call Example3
exit
main ENDP
Example1 PROC
LOCAL temp:BYTE,SwapFlag:BYTE
movzx eax,temp
movzx ebx,SwapFlag
ret
Example1 ENDP

Example2 PROC
LOCAL temp:WORD,SwapFlag:BYTE
movzx eax,temp
movzx ebx,SwapFlag
ret
Example2 ENDP

Example3 PROC
LOCAL temp:DWORD,SwapFlag:BYTE
mov eax,temp
movzx ebx,SwapFlag
ret
Example3 ENDP
END main

30

不管你定义的局部变量有多大,ESP=ESP-(4*局部变量数目),这个是为了保证字节对齐。

保留额外的堆栈空间: 如若准备创建大于几百字节的局部数组变量,一定要保留足骨的堆栈空间,这可以通过 .STACK 伪指令来完成。

过程 WriteStackFrame

在 Irvine 链接库中有一个过程 WriteStackFrame 能够显示当前过程的堆栈框架的内容:过程的堆栈参数、返回地址、局部变量以及保存的寄存器。下面是过程的原型声明:
31

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
INCLUDE Irvine32.inc

.code
aProc PROC USES eax ebx,
x:DWORD,y:DWORD
LOCAL a:DWORD,b:DWORD
PARAMS = 2
LOCALS = 2
SAVED_REGS = 2
mov a,0AAAAh
mov b,0BBBBBh
INVOKE WriteStackFrame,PARAMS,LOCALS,SAVED_REGS
LEAVE
ret
aProc ENDP

main PROC
mov eax,0EAEAEAEAh
mov ebx,0BEBEBEBEh
INVOKE aProc,1111h,2222h
exit
main ENDP
END main

32

INVOKE 伪指令调用的函数,必须写在 main 函数之前。call 调用的函数,可写在 main 函数之前或之后。

33

递归

递归子程序是直接或间接调用自身的子程序。

无尽循环递归:最显而易见的递归类型就是对自身进行调用。下面的程序 Endless 过程不断重复调用自己,永远不会停止。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
;---------------------------
;程序名:Endless.asm
;功能:递归函数 Endless,不断重复调用自身,永远不会停止。
;作者:9unk
;编写时间:2023-3-23
;----------------------------
INCLUDE Irvine32.inc
.data
endlessStr BYTE "This recursion never stops",0

.code
main PROC
call Endless
exit
main ENDP

Endless PROC
mov edx,OFFSET endlessStr
call WriteString
call Endless
ret
Endless ENDP
END main

递归求和

有实际意义的递归过程总会包含一个终止条件,只要终止条件为真,程序就会执行所有挂起的ret指令,堆栈就被展开了。下面创建一个 CalcSum 递归过程计算从1到 n 的和,n 是通过 ECX 传递的输入参数,过程 CalSum 在 EAX 中返回和。

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
;---------------------------
;程序名:CSum.asm
;功能:使用递归求和
;作者:9unk
;编写时间:2023-3-23
;----------------------------
INCLUDE Irvine32.inc

.code
main PROC
mov eax,0
mov ecx,5
call CalcSum
exit
main ENDP

CalcSum PROC
add eax,ecx
dec ecx
jz L2
call CalcSum
L2:
ret
CalcSum ENDP
END main

计算阶乘

递归通常通过堆栈参数存储临时数据,在递归展开时,在堆栈上保存的数据可能很有用。下面的例子计算整数n的阶乘。
第一次调用 factorial 函数时,参数n是起始数字,下面是 C/C++/Java的语法格式编写的代码:

1
2
3
4
5
6
7
int function factorial(int n)
{
if(n==0)
return 1;
else
return n * factorial(n-1);
}

例子程序:下面的汇编语言程序中包含了一个名为 Factorial 的过程,Factorial 使用递归计算阶乘。我们通过堆栈向 Factorial 过程传递整数 n,最后在 EAX 中返回阶乘值。因为使用的是 32 位寄存器存放阶乘,因此可容纳的最大阶乘值是 12!

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
;---------------------------
;程序名:Fat.asm
;功能:使用递归计算阶乘
;作者:9unk
;编写时间:2023-3-23
;----------------------------
INCLUDE Irvine32.inc

.code
main PROC
push 12 ;n-1
mov eax,[esp] ;初始化eax
call Factorial
call WriteDec
call Crlf
exit
main ENDP

Factorial PROC
ENTER 0,0
cmp DWORD PTR [ebp+8],1
jz L1
sub DWORD PTR [ebp+8],1
mul DWORD PTR [ebp+8]
push [ebp+8]
call Factorial
L1:
LEAVE
ret
Factorial ENDP
END main

书中的写法是加了一个函数 ReturnFact 来获取 n

.MODEL伪指令

34

之前8086汇编中学过,在80386中段的简化定义,多了一个调用约定。其他内容稍微了解一下就行。

语言选项关键字

35
36
37

这里有关调用约定的内容稍微看一下就行。

INVOKE 伪指令

38
39

INVOKE 伪指令可以理解为 call 指令的高级语言。INVOKE伪指令使用的是 STDCALL 调用约定。

ADDR操作符

40

PROC伪指令

41
42
43
44
45
46

PROTO 伪指令

PROTO伪指令为一个已存在的过程创建一个原型。原型声明了过程的名字和参数列表,它允许在定义过程之前就调用该过程并验证调用时传递的参数的类型和数目是否相匹配。
MASM 要求 INVOKE 调用的过程要有合适的原型声明:

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
INCLUDE Irvine32.inc

.code

aProc PROTO,
x:DWORD,y:DWORD

main PROC
mov eax,0EAEAEAEAh
mov ebx,0BEBEBEBEh
INVOKE aProc,1111h,2222h
exit
main ENDP

aProc PROC USES eax ebx,
x:DWORD,y:DWORD
LOCAL a:DWORD,b:DWORD
PARAMS = 2
LOCALS = 2
SAVED_REGS = 2
mov a,0AAAAh
mov b,0BBBBBh
INVOKE WriteStackFrame,PARAMS,LOCALS,SAVED_REGS
LEAVE
ret
aProc ENDP

END main

47

汇编时的参数检查

48
49

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
;---------------------------
;程序名:ArraySum.asm
;功能:使用 PROC 声明堆栈参数、INVOKE 伪指令修改array.asm
;作者:9unk
;编写时间:2023-3-24
;----------------------------
INCLUDE Irvine32.inc

.data
array DWORD 10000h,20000h,30000h,40000h,50000h
theSum DWORD ?

.code
ArraySum PROTO,
ptrArray:PTR DWORD,
szArray:DWORD

main PROC
INVOKE ArraySum,ADDR array,LENGTHOF array
MOV theSum,eax
CALL DumpRegs
exit
main ENDP
;------------------------------
ArraySum PROC USES esi ecx,
ptrArray:PTR DWORD,
szArray:DWORD
;Calculates the sum of an array of 32-bit integers
;Recevies: ESI = the array offset
; ECX = number of elements in the array
;Returns: EAX = sum of the array elements
;------------------------------
mov esi,ptrArray
mov ecx,szArray
MOV eax,0
L1:
ADD eax,[esi]
ADD esi,TYPE DWORD
LOOP L1
RET
ArraySum ENDP
END main

参数分类

过程的参数通常依据调用程序和被调用过程之间的数据传输方向进行分类。

  • 输入参数:输入参数是调用程序向被调用过程传递的数据,这时并不期望被调用过程修改对应的变量。即使这样做了,修改也仅局限于过程自身之内。
  • 输出参数:输出参数是通过向过程传递一个变量的指针而创建的。过程使用变量的地址定位变量并为变量赋值。
  • 输入输出参数:输入输出参数与输出参数基本等同,只有一点不同:被调用的过程期望参数引用的变量包含某些数据,过程也同时期望通过变量的指针修改变量。

例子:交换两个整数

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
;---------------------------
;程序名:Swap.asm
;功能:Swap函数交换两个整数
;作者:9unk
;编写时间:2023-2-23
;----------------------------
INCLUDE Irvine32.inc

Swap PROTO,
PValX:PTR DWORD,
pValY:PTR DWORD

.data
Array DWORD 10000h,20000h

.code
main PROC
;显示数组交换数据前的值
mov esi,OFFSET Array
mov ecx,2
mov ebx,TYPE Array
call DumpMem

INVOKE Swap,ADDR Array,ADDR [Array+4]

;显示交换数据后的结果
call DumpMem
exit
main ENDP

;--------------------------------
Swap PROC USES eax esi edi,
PValX:PTR DWORD,
pValY:PTR DWORD
;功能:交换两个32位整数的值
;返回值:无
;--------------------------------
mov esi,PValX
mov edi,pValY
mov eax,[esi]
xchg eax,[edi]
mov [esi],eax
Swap ENDP
END main

调用问题疑难提示

50

创建多模块程序

非常大的源码文件难于管理并且汇编起来也很慢。我们可以把一个文件分成多个包含文件,但是对任何源文件的修改仍然要汇编所有文件。另一种方法是吧程序拆分成多个模块。每个模块单独汇编,对一个模块源码的修改直选哦重新汇编那个模块即可,连接器可以相当迅速地把所有汇编模块(OBJ文件)链接成一个可执行文件。链接多个目标代码模块要比汇编同样数量的源码文件快得多。

创建多个模块程序时有两种常用的方法:第一种方法是似乎用传统的EXTERN 伪指令,在不同的 80x86 汇编器之间移植可能会有些问题。第二种方法是使用 MASM 的高级 INVOKE 和 PROTO 伪指令,这些伪指令简化了调用并隐藏了一些底层细节。

过程名的隐藏和导出

51
52

调用外部过程

53

跨模块的边界使用变量和符号

变量和符号默认对于其所在的模块而言是私有的。。可以使用 PUBLIC 伪指令导出特地给的名字,案例如下:

1
2
3
4
PUBLIC count,SYM1
SYM1 = 10
.data
count DWORD 0

访问外部变量和符号
可以使用 EXTERN 伪指令访问外部模块定义的符号和变量:
EXTERN name:type

对于符号而言(以 EQU 和 “=” 定义的),type 应该是 ABS;对于变量而言。type 应该是数据定义的属性,如 BYTE、WORD、DWORD、SDWORD 以及 PTR。下面是一些例子:

1
EXTERN one:WORD,two:SDWORD,three:PTR BYTE,four:ABS

使用包含 EXTERNDEF 伪指令包含文件
MASM 伪指令 EXTERNDEF 可代替 PUBLIC 和 EXTERN,该伪指令可放在一个文本文件中,使用 INCLUDE 伪指令包含进每个程序模块块中。例如定义 vars.inc 文件:

1
2
;vars.inc
EXTERNDEF count:DWORD,SYM1:ABS

之后再创建文件 sub1.asm,该文件包含count 和 SYM1 的定义。

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
;---------------------------
;程序名:sub1.asm
;功能:演示包含 EXTERNDEF 伪指令的文件
;作者:9unk
;编写时间:2023-2-25
;----------------------------
;INCLUDE Irvine32.inc

.386
.model flat,STDCALL

INCLUDE kernel32.inc
INCLUDELIB kernel32.lib
INCLUDELIB masm32.lib
INCLUDE vars.inc

SYM1=10

.data
count DWORD 0

.code
main PROC
mov eax,count
mov count,2000h
mov ebx,count
mov ecx,SYM1
invoke ExitProcess,0
main ENDP
END main

编译时 MAKEFILE 文件 “LINK_FLAG” 变量改为 “LINK_FLAG = /subsystem:console kernel32.lib user32.lib”

例子 ArraySum 程序

54
55

使用 EXTERN 创建模块

这一节使用 EXTERN 伪指令引用单独模块改写第 5 章的 Array.asm。
PromptForIntegers:模块 _Prompt.asm 包含了 PromptForIntegers 过程中的源码。该过程提示用户输入三个整数,调用 ReadInt 读取输入,然后插入到数组中:

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
;---------------------------
;程序名:_prompt.asm
;功能:提示用户输入三个整数,调用 ReadInt 读取输入,然后插入到数组中。
;作者:9unk
;编写时间:2023-2-25
;----------------------------
INCLUDE Irvine32.inc

.code
;--------------------------------------------------------
PromptForIntegers PROC
;功能:提示用户输入三个整数,调用 ReadInt 读取输入,然后插入到数组中。
;入口参数:
; ptrPrompt:PTR byte ;输入的字符串
; ptrArray:PTR DWORD ;数组指针
; arraySize:DWORD ;数组大小
;返回值:无
;--------------------------------------------------------
arraySize EQU [ebp+16]
ptrArray EQU [ebp+12]
ptrPrompt EQU [ebp+8]

ENTER 0,0
pushad

mov ecx,arraySize
cmp ecx,0
jle L2
mov edx,ptrPrompt
mov esi,ptrArray
L1:
call WriteString
call ReadInt
call Crlf
mov [esi],eax
add esi,4
loop L1
L2:
popad
leave
ret 12
PromptForIntegers ENDP
END

ArraySum:模块 _arraysum.asm 包含了 ArraySum 过程的,该过程计算数组元素的和并在 EAX 中返回:

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
;---------------------------
;程序名:_arraysum.asm
;功能:计算数组元素的和并在 EAX 中返回。
;作者:9unk
;编写时间:2023-2-25
;----------------------------
INCLUDE Irvine32.inc

.code
;--------------------------------------------------------
ArraySum PROC
;功能:计算数组元素的和并在 EAX 中返回。
;入口参数:
; ptrArray ;数组指针
; araySize ;数组大小
;返回值:EAX = sum
;--------------------------------------------------------
ptrArray EQU [ebp+8]
arraySize EQU [ebp+12]

ENTER 0,0
push ecx
push esi

mov eax,0
mov esi,ptrArray
mov ecx,arraySize
cmp ecx,0
jle L2
L1:
add eax,[esi]
add esi,4
loop L1
L2:
pop esi
pop ecx
leave
ret 8
ArraySum ENDP
END

DisplayNum:模块 _display.asm 包含了 DisplaySum 过程,该过程显示一个标号,后跟数组之和:

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
;---------------------------
;程序名:_display.asm
;功能:显示一个标号,后跟数组之和。
;作者:9unk
;编写时间:2023-2-25
;----------------------------
INCLUDE Irvine32.inc

.code
;--------------------------------------------------------
DisplaySum PROC
;功能:显示一个标号,后跟数组之和。
;入口参数:
; ptrPrompt ;字符串偏移
; thesum ;数组总和(DWORD)
;返回值:无
;--------------------------------------------------------
thesum EQU [ebp+12]
ptrPrompt EQU [ebp+8]

ENTER 0,0
push eax
push edx

mov edx,ptrPrompt
call WriteString
mov eax,thesum
call WriteInt
call Crlf

pop edx
pop eax
leave
ret 8
DisplaySum ENDP
END

启动模块:模块 Sum_main.asm 包含了 main 过程,该模块包含了三个外部过程的 EXTERN 声明。为使源码更加刻度,使用 EQU 伪指令重新定义了过程名字:

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
;---------------------------
;程序名:Sum_main.asm
;功能:多模块调用示例,用户输入多个整数到数组中,之后再计算数组总和并显示出来。
;作者:9unk
;编写时间:2023-2-25
;----------------------------
INCLUDE Irvine32.inc

EXTERN PromptForIntegers@0:PROC
EXTERN ArraySum@0:PROC,DisplaySum@0:PROC

;将调用的扩展模块用标号替换
ArraySum EQU ArraySum@0
PromptForIntegers EQU PromptForIntegers@0
DisplaySum EQU DisplaySum@0

;设置数组的大小
Count = 3

.data
prompt1 BYTE "Enter a signed integer: ",0
prompt2 BYTE "This sum of the integers is: ",0
array DWORD Count DUP(?)
sum DWORD ?

.code
main PROC
call Clrscr

;PromptForIntegers(addr prompt1,addr array,Count)
push Count
push OFFSET array
push OFFSET prompt1
call PromptForIntegers

;sum = ArraySum(Addr array,Count)
push Count
push OFFSET array
call ArraySum
mov sum,eax

;DisplaySum(addr prompt2,sum)
push sum
push OFFSET prompt2
call DisplaySum
call Crlf
Exit
main ENDP
END main

编译

  1. 编译 asm 文件
    ml /c /coff _prompt.asm _arraysum.asm _display.asm

  2. 修改 MakeFile 文件如下

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    EXE = Sum_main.exe		#指定输出文件
    OBJS = Sum_main.obj _arrysum.obj _display.obj _prompt.obj #需要的目标文件

    LINK_FLAG = /subsystem:console irvine32.lib kernel32.lib user32.lib #连接选项
    ML_FLAG = /c /coff #编译选项

    $(EXE): $(OBJS) $(RES)
    Link $(LINK_FLAG) $(OBJS)

    .asm.obj:
    ml $(ML_FLAG) $<
  3. 执行文件
    56

使用 INVOKE 和 PROTO 创建模块

多模块程序可以使用 MASM 的高级伪指令 INVOKE,PROTO华人扩展 PROC 伪指令创建。与使用 CALL 和 EXTERN 的传统方法相比,其主要有点在于能够匹配 INVOKE 伪指令中传递的参数列表和PROC伪指令声明的参数列表并检查是否一致。

创建 ArrraySum程序的第一步是创建一个包含了每个外部过程的 PROTO 伪指令声明的包含文件,每个模块都可以包含到该文件,这还不会影响代码尺寸和程序运行时间。如果某个模块没有调用特定的过程,那么对应的 PROTO 伪指令就会被汇编器忽略。
下面是 sum.inc 包含文件的内容:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
;(sum.inc)
INCLUDE Irvine32.inc

PromptForIntegers PROTO,
ptrPrompt:PTR BYTE,
ptrArray:PTR DWORD,
arraySize:DWORD

ArraySum PROTO,
ptrArray:PTR DWORD,
arraySize:DWORD

DisplaySum PROTO,
ptrPrompt:PTR BYTE,
thesum:DWORD

_prompt 模块:文件 _prompt.asm 使用 PROC 伪指令声明 PromptForIntegers 所需的参数,该文件还用 INCLUDE 把 sum.inc 复制到该文件中来:

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
;---------------------------
;程序名:_prompt.asm
;功能:提示用户输入三个整数,调用 ReadInt 读取输入,然后插入到数组中。
;作者:9unk
;编写时间:2023-2-25
;----------------------------
INCLUDE sum.inc

.code
;--------------------------------------------------------
PromptForIntegers PROC,
ptrPrompt:PTR BYTE,
ptrArray:PTR DWORD,
arraySize:DWORD

;功能:提示用户输入三个整数,调用 ReadInt 读取输入,然后插入到数组中。
;入口参数:
; ptrPrompt:PTR byte ;输入的字符串
; ptrArray:PTR DWORD ;数组指针
; arraySize:DWORD ;数组大小
;返回值:无
;--------------------------------------------------------


pushad

mov ecx,arraySize
cmp ecx,0
jle L2
mov edx,ptrPrompt
mov esi,ptrArray
L1:
call WriteString
call ReadInt
call Crlf
mov [esi],eax
add esi,4
loop L1
L2:
popad
ret
PromptForIntegers ENDP
END

和之前的版本相比这里少了 enter 0,0 和leave 伪指令,ret 指令后也没有常量。这是因为 MASM 遇到声明了参数的 PROC 伪指令时会生成这些指令。

_arraysum.asm 模块:

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
;---------------------------
;程序名:_arraysum.asm
;功能:计算数组元素的和并在 EAX 中返回。
;作者:9unk
;编写时间:2023-2-25
;----------------------------
INCLUDE sum.inc

.code
;--------------------------------------------------------
ArraySum PROC,
ptrArray:PTR DWORD,
arraySize:DWORD
;功能:计算数组元素的和并在 EAX 中返回。
;入口参数:
; ptrArray ;数组指针
; araySize ;数组大小
;返回值:EAX = sum
;--------------------------------------------------------

push ecx
push esi

mov eax,0
mov esi,ptrArray
mov ecx,arraySize
cmp ecx,0
jle L2
L1:
add eax,[esi]
add esi,4
loop L1
L2:
pop esi
pop ecx
ret
ArraySum ENDP
END

Display模块:

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
;---------------------------
;程序名:_display.asm
;功能:显示一个标号,后跟数组之和。
;作者:9unk
;编写时间:2023-2-25
;----------------------------
INCLUDE sum.inc

.code
;--------------------------------------------------------
DisplaySum PROC,
ptrPrompt:PTR BYTE,
thesum:DWORD
;功能:显示一个标号,后跟数组之和。
;入口参数:
; ptrPrompt ;字符串偏移
; thesum ;数组总和(DWORD)
;返回值:无
;--------------------------------------------------------

push eax
push edx

mov edx,ptrPrompt
call WriteString
mov eax,thesum
call WriteInt
call Crlf

pop edx
pop eax
ret
DisplaySum ENDP
END

Sum_main.asm模块:

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
;---------------------------
;程序名:Sum_main.asm
;功能:多模块调用示例,用户输入多个整数到数组中,之后再计算数组总和并显示出来。
;作者:9unk
;编写时间:2023-2-25
;----------------------------
;INCLUDE Irvine32.inc
INCLUDE sum.inc

;设置数组的大小
Count = 3

.data
prompt1 BYTE "Enter a signed integer: ",0
prompt2 BYTE "This sum of the integers is: ",0
array DWORD Count DUP(?)
sum DWORD ?

.code
main PROC
call Clrscr

;PromptForIntegers(addr prompt1,addr array,Count)
INVOKE PromptForIntegers,ADDR prompt1,ADDR array,Count

;sum = ArraySum(Addr array,Count)
INVOKE ArraySum,ADDR array,Count
mov sum,eax

;DisplaySum(addr prompt2,sum)
INVOKE DisplaySum,ADDR prompt2,sum
call Crlf
Exit
main ENDP
END main

PROTO 和 PROC 定义的局部变量顺序也要一样的,同时需要注意 sum_main.asm 中函数参数的调用顺序,要与 PROTO 声明的局部变量顺序是对应的。

小结:前面第一种方法使用传统的 EXTERN 伪指令的方式;第二种方法是使用高级伪指令 INVOKE,PROTO和PROC。这些伪指令简化了很多细节,还专门针对 Windows API函数调用进行了优化。

  • 本文标题:80386汇编-高级过程
  • 本文作者:9unk
  • 创建时间:2023-02-25 23:25:00
  • 本文链接:https://9unkk.github.io/2023/02/25/80386-hui-bian-gao-ji-guo-cheng/
  • 版权声明:本博客所有文章除特别声明外,均采用 BY-NC-SA 许可协议。转载请注明出处!