对应基础第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新建模块
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++编译器创建的代码时可能会用得到,这个将在后面抽空补上
下一讲会先跳过字符串数组的处理,先开宏和结构