汇编实验

汇编实验

对应基础第6篇的部分

新建多模块程序

大型源文件难于管理且汇编速度慢,可以把单个文件拆分为多个子文件,但是对其中任意子文件的修改都需要对整个文件进行整体汇编。更好的方法是把一个程序按照 模块 (module)分割。每个模块可以单独汇编,因此,对一个模块源代码的修改就只需要重新汇编这个模块。链接器将所有汇编好的模块(obj文件)组合为一个可执行文件的速度相当快,链接大量目标模块比汇编同样数量的源代码花费的时间要少的多

调用外部过程

调用当前模块外的过程时使用EXTERN伪指令,它确定过程名和堆栈帧大小。

1
2
3
4
5
EXTERN sub1@0: PROC
.code
main PROC
call sub1@0
...

过程名的后缀@n确定了已声明参数占用的堆栈空间总量,若使用PROC伪指令,没有声明参数,则EXTERN中的每个过程名后缀都为@0。
或者,可以用PROTO伪指令来代替EXTERN,不过就需要写出参数了

1
2
3
AddTwo PROTO,
val1: DWORD,
val2: DWORD

使用Extern新建模块

ArraySum程序结构.jpg

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

.code
PromptForIntegers PROC
;--------------------
;提示用户输入整数填充数组
;参数:
; ptrPrompt: PTR BYTE ;提示信息字符串
; ptrArray: PTR DWORD ;数组指针
; arraySize: DWORD ;数组大小
;返回:无
;---------------------
ptrPrompt EQU [ebp+8]
ptrArray EQU [ebp+12]
arraySize EQU [ebp+16]

enter 0, 0
pushad ;保存全部寄存器
mov ecx, arraySize
cmp ecx, 0
jle L2 ;小于或等于跳转
mov esi, ptrArray
mov edx, ptrPrompt
L1:
call WriteString ;显示字符串
call ReadInt ;整数读入EAX
call Crlf
mov [esi], eax
add esi, 4
loop L1

L2:
popad
leave
ret 12 ;恢复堆栈(删除3个参数)
PromptForIntegers ENDP
END

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

.code
ArraySum PROC
;-------------------------
;计算32位整数数组之和
;参数:
; ptrArray ;数组指针
; arraySize ;数组长度
;返回: EAX=和
;--------------------------
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

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
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

StartUp模块用于启动过程(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
INCLUDE Irvine32.inc

EXTERN PromptForIntegers@0:PROC
EXTERN ArraySum@0:PROC
EXTERN 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 "The 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
call WaitMsg
exit
main ENDP
END main

INVOKE + PROTO 创建新模块

与更加传统的CALL和EXTERN相比,优势在于:能够将INVOKE传递的参数列表与PROC的列表进行匹配
我们需要编写头文件

1
2
3
4
5
6
7
8
9
10
11
12
13
14
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: PTR DWORD

这样后面的子程序里只用INCLUDE sum.inc来获取过程原型,然后显示声明一下参数如下:

1
2
3
4
PromptForIntegers PROTO,
ptrPrompt: PTR BYTE,
ptrArray: PTR DWORD,
arraySize: DWORD

去掉enter和leave语句,因为MASM遇到PROC伪指令以及其声明的参数时会自动生成这两条语言,同时ret不需要带常数了,PROC会处理好万能的PROC
最后,将启动模块的call调用的过程去掉,换成INVOKE,同时由于include了sum,inc不需要再使用EXTERN,INVOKE指令写法如下:
INVOKE PromptForInteger, ADDR prompt1, ADDR array, Count

C语言调用外部变量函数的方法和该方法十分的相似(托腮(ˇˍˇ) )

课后题

由于篇幅受限,就放少量个课后题上来

近似相等元素计数器

编写过程接受两个有符号双字数组指针,表示两个数组长度的参数和表示两个匹配元素间的最大误差(称为diff)的参数。对第一个数组中的每个元素和第二个数组的对应元素,若误差小于等于diff,则计数器加1,用EAX返回相似个数。要求:使用INVOKE语句调用过程并传递堆栈参数,为过程创建PROTO说明,保存并恢复所有会被该过程修改的寄存器(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
41
42
43
44
45
46
47
48
49
50
51
52
53
.386
.model flat, stdcall
.stack 4096
ExitProcess PROTO, dwExitCode:DWORD

diff = +10
CountMatches PROTO,
ptr1: PTR DWORD,
ptr2: PTR DWORD,
arraySize:DWORD

.data
array1 DWORD 5, 7, 9, 11, 12, 13, 15, 14, 10, 19
array2 DWORD 1, 2, 3, 4, 5, 6, 7, 8, 9, 10
number DWORD LENGTHOF array1

.code
main PROC
INVOKE CountMatches, ADDR array1, ADDR array2, number
INVOKE ExitProcess, 0
main ENDP

CountMatches PROC,
ptr1: PTR DWORD,
ptr2: PTR DWORD,
arraySize:DWORD
;什么都不用做,PROC会给你弄好关于堆栈的一切
;只需要正常写就行了
push ecx
push esi
push edi
push ebx
mov eax, 0 ;初始化计数器
mov esi, ptr1
mov edi, ptr2
mov ecx, arraySize
L1:
mov edx, [esi]
sub edx, [edi]
cmp edx, diff
jg L2 ;大于跳转
inc eax
L2:
add esi, 4
add edi, 4
loop L1
pop ebx
pop edi
pop esi
pop ecx
ret
CountMatches ENDP
END main

在过程内无法使用变址操作来索引数据,必须使用间接索引,不知道为啥
同时如果声明指针参数要使用PTR,表示指向现有类型的指针

显示过程参数

编写过程ShowParams,显示被调用过程运行时堆栈中32位参数的地址和十六进制数值。参数按照从低地址到高地址。过程输入只有一个整数,用于表示显示参数的个数,如下:

INVOKE MySample, 1234h, 5000h, 6543h

然后在MySample中调用ShowParams,并向其传递希望显示参数的个数:

MySample PROC first:DWORD, second:DWORD,third:DWORD
paramCount = 3
call ShowParams, paramCount

ShowParams将按如下格式输出:

Stack Parameters:
Address 0012FF80 = 00001234
Address 0012FF84 = 00005000
Address 0012FF88 = 00006543

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
INCLUDE Irvine32.inc
paramCount = 4
MySample PROTO,
first:DWORD,
second:DWORD,
third:DWORD,
fourth:DWORD

.data
addrString BYTE "Address ", 0
equalSign BYTE " = ", 0

.code
main PROC
INVOKE MySample, 1234h, 5000h, 4321h, 5h
call Crlf
call WaitMsg
main ENDP

MySample PROC,
first:DWORD,
second:DWORD,
third:DWORD,
fourth:DWORD

push fourth
push third
push second
push first
push paramCount
call ShowParam
ret
MySample ENDP

ShowParam PROC
push ebp
mov ebp, esp
push eax
push esi
push edx
mov ecx, [ebp+8] ;参数个数
cmp ecx, 0
jle L2
mov esi, ebp
add esi, 8 ;起始地址
L1:
add esi, 4 ;向上走一个数
mov edx, OFFSET addrString
call WriteString
mov eax, esi ;写出地址
call WriteHex
mov edx, OFFSET equalSign
call WriteString
mov eax, [esi]
call WriteDec ;写出数值
call Crlf
loop L1
L2:
pop edx
pop esi
pop eax
mov esp, ebp
pop ebp ;清除局部参数
ret 8 + 4 * paramCount
ShowParam ENDP
END main

写在后面

本节还有一个重要的内容:参数的高级用法,主要讨论32位模式下向堆栈传参的不常见情况,但在查看C/C++编译器创建的代码时可能会用得到,这个将在后面抽空补上
下一讲会先跳过字符串数组的处理,先开宏和结构

Author

Ctwo

Posted on

2019-08-06

Updated on

2020-10-25

Licensed under

Comments