5.3.3 库测试程序
测试程序#1:整数I/O
该测试程序把输出文本的颜色改为蓝底黄字,然后以十六进制数显示七个数组的内容,最后提示用户输入一个有符号整数,再分别以十进制、十六进制和二进制格式重复显示该整数:
TITLE Library Test #1: Integer I/O (TestLib1.asm)
;Test the Clrscr,Crlf,DumpMem,ReadInt,
;SetTextColor,WaiMsg,WriteBin,WriteHex,
;and WriteString procedures.
INCLUDE Irvine32.inc
.data
arrayD DWORD 1000h,2000h,3000h
prompt1 BYTE "Enter a 32-bit stgned integer:",0
dwordVal DWORD ?
.code
start: call main
main PROC
mov eax ,yellow + (blue * 16)
call SetTextColor
call Clrscr
mov esi ,OFFSET arrayD
mov ecx ,LENGTHOF arrayD
mov ebx ,TYPE arrayD
call DumpMem
call Crlf
mov edx ,OFFSET prompt1
call WriteString
call ReadInt
mov dwordVal ,eax
call Crlf
call WriteInt
call Crlf
call WriteHex
call Crlf
call WriteBin
call Crlf
call WaitMsg
mov eax ,lightGray + (black * 16)
call SetTextColor
call Clrscr
exit
main ENDP
end start
运行结果:
测试程序#2:随机整数
第2个库测试程演示随机数使用过程。首先,随机产生10个在0~4294967294内的无符号整数,接着随机再生成10个在范围-50~+49内的有符号整数:
TITLE Link Library Test #2 (TestLib2.asm)
INCLUDE Irvine32.inc
TAB = 9
.code
main PROC
call Randomize
call Rand1
call Rand2
exit
main ENDP
Rand1 PROC
mov ecx ,10
L1: call Random32
call WriteDec
mov al ,TAB
call WriteChar
loop L1
call Crlf
ret
Rand1 ENDP
Rand2 PROC
mov ecx ,10
L1 :mov eax ,100
call RandomRange
sub eax ,50
call WriteInt
mov al ,TAB
call WriteChar
loop L1
call Crlf
ret
Rand2 ENDP
END main
运行结果:
测试程序#3:性能度量
汇编语言常用于优化对程序性能而言至关重要的代码。GetMseconds过程返回自午夜以来逝去的毫秒数,在循环之前调用了GetMseconds过程,然后执行嵌套循环约170亿次,在循环结束后再次调用GetMsgconds过程并报告用掉的时间:
TITLE Link Library Test #3
INCLUDE Irvine32.inc
OUTER_LOOP_COUNT = 3
.data
startTime DWORD ?
msg1 BYTE "Please wait..." ,0dh ,0ah ,0;
msg2 BYTE "Elapsed milliseconds:" ,0
.code
main PROC
mov edx ,OFFSET msg1
call WriteString
call GetMSeconds
mov startTime ,eax
mov ecx ,OUTER_LOOP_COUNT
L1: call innerLoop
loop L1
call GetMSeconds
sub eax ,startTime
mov edx ,OFFSET msg2
call WriteString
call WriteDec
call Crlf
exit
main ENDP
innerLoop PROC
push ecx
mov ecx ,0FFFFFFFFh
L1: mov eax ,eax
loop L1
pop ecx
ret
innerLoop ENDP
END main
执行结果:
5.4 堆栈操作
堆栈的定义不解释了,后进先出。
5.4.1 运行时栈
运行时栈是由CPU直接管理的内存数组,它使用两个寄存器:SS和ESP。在保护模式下,SS寄存器存放的是段选择子,用户模式程序不应对其进行修改。ESP寄存器存放的是指向堆栈内特定位置的一个32位偏移值。我们很少需要直接操纵ESP的值,相反,ESP寄存器的值通常是由CALL,RET,PUSH和POP等指令间接修改的。
堆栈指令寄存器(ESP)指向最后压入(或添加)堆栈上的数据。
这里讨论的运行时栈同程序设计课程中讲述的堆栈抽象数据类型(stack ADT)是不同的。运行时栈在系统层上(由硬件直接实现)处理子过程调用;堆栈抽象数据类型通常用于实现依赖于先进后出操作的算法,一般使用高级语言如C++、Java等编写。
压栈操作
32位的压栈(PUSH)操作首先将堆栈指针减4,然后把要压栈的值赋值到堆栈指针所指向的位置处。
出栈操作
出栈与压栈相反
堆栈中ESP之下的区域从逻辑上讲是空白的,在程序下次执行任何要压栈的指令时该区域将被覆盖重写。
堆栈的应用
寄存器在做多种用途的时候,堆栈可以方便的作为临时保存区域,在寄存器使用完毕之后,可通过堆栈恢复其原始值。
CALL指令执行的时候,CPU用堆栈保存当前被调用过程的返回地址。
调用过程的时候,可以通过压栈传递输入值(成为参数)。
过程内的局部变量在堆栈上创建,过程结束时,这些变量被丢弃。
5.4.2 PUSH和POP指令
PUSH指令
PUSH指令首先减少ESP的值,然后再把一个16位或32位的源操作数复制到堆栈上。对于16位的操作数,ESP值将减2;对于32位操作数,ESP值将减4.PUSH指令有一下三种格式:
PUSH r/m16
PUSH r/m32
PUSH imm32
如果程序调用Irvine32中的过程,应总是压入32位值,否则库中使用的Win32控制数将不能正常运行。如果程序调用Irvine16中的库过程(实地址模式下)则可压入16位或32位的值。
在保护模式下立即数总是32位。在实地址模式下,如果未使用.386(或更高的)处理器伪指令,立即数默认是16位的。
POP指令
POP指令首先将ESP所指的堆栈元素复制到16位或32位的目的操作数中,然后增加ESP的值。如果操作数是16位的,ESP值将加2;如果操作数是32位的,ESP值将加4.其格式如下:
POP r/m16
POP r/m32
PUSHFD和POPFD指令
PUSHFD指令在堆栈上压入32位的EFLAGS寄存器的值,POPFD指令将堆栈顶部弹出并送至EFLAGS寄存器:
pushfd
popfd
实地址则是16位的。
MOV指令不能复制标志寄存器的值至变量或寄存器中,因此使用PUSHFD指令可能就是保存寄存器的最佳方式了。有时保存标志的备份以便后面进行恢复是很有用的,这通常可以是用PUSHFD和POPFD指令把一块指令包围起来:
pushfd ;保存标志
;
;......
;
popfd ;恢复标志
在使用这种类型的标志压栈和标志出栈指令的时候,必须确保程序的执行路径不会跳过POPFD指令。随着时间的推移,在修改程序时很难激情所有的压栈指令放在哪里。因此,编写准确的文档是非常关键的!
可以完成同样功能但或许可减少犯错误的方法是将标志保存在变量中:
.data
saveFlags DWORD ?
.code
pushfd ;标志入栈
pop saveFlags ;复制到变量
下列语句从同一变量中回复标志值:
push saveFlags ;将保存的标志入栈
popfd ;回复标志
PUSHAD,PUSHA,POPAD和POPA指令
PUSHAD指令在堆栈上按下列循序压入所有的32为通用寄存器:
EAX,ECX,EDX,EBX,RSP(执行PUSHAD指令之前的值),EBP,ESI和EDI;POPAD指令以相反的循序从堆栈中弹出这些通用寄存器。于此类似,80286处理器引入的PUSHA指令以括号中列表的循序压入所有的16位寄存器(AX,CX,DX,BX,SP,BP,SI和DI)。POPA指令则以相反顺序弹出这些寄存器。
如果在过程中修改了很多32位寄存器,那么可以在过程的开始和结束的位置分别用PUSHAD和POPAD指令保存和恢复寄存器的值。
MySub PROC
pushad
.
.
mov eax ,...
mov edx ,...
mov ecx ,...
.
.
popad
ret
MySub ENDP
对上面的例子,有一个例外情况必须指出:过程通过一个或多个寄存器返回结果时不应该使用PUSHA或PUSHAD指令。假设下面的RcadValue过程想要通过EAX返回一个整数但对POPAD的调用将覆盖EAX中的返回值:
ReadValue PROC
pushad ;保存通用寄存器
.
.
mov eax ,return_value
.
.
popad
ret
ReadValue ENDP
例子:反转字符串
RevStr.asm程序循环遍历字符串并把每个字符串都压入堆栈,然后取出来。
TITLE Reversing a String (RevStr.asm)
INCLUDE Irvine32.inc
.data
aName BYTE "Abraham Lincoln",0
nameSize = ($ - aName) - 1
.code
main PROC
mov ecx ,nameSize
mov esi ,0
mov eax ,0
L1: mov al,aName[esi]
push eax
inc esi
loop L1
mov ecx ,nameSize
mov esi ,0
L2: pop eax
mov aName[esi] ,al
inc esi
loop L2
mov edx ,OFFSET aName
call WriteString
call Crlf
exit
main ENDP
END main
运行结果:
TIP:书中上面的代码有第一个地方有问题(L1: mov eax,aName[esi]),如果这么写编译器会弹出编译错误,原因是aName[esi]是8位,eax是32位的,但是eax是32位,ax是第16位,al是第八位,所以直接eax换成al就行了,但是记住之前要把eax清零,因为堆栈是接收32位的,我们处理al之后把eax压入堆栈可能把前面的高位压进去(但是上面这个程序压进去结果也看不出来,因为我们始终只操作al),但是压入eax进栈本身就是错误的。所以需要eax清零。在使用低位。
5.5 过程的定义和使用
可以理解成是C++或是其它语言里的函数或者方法等。
5.5.1 PROC伪指令
过程的定义
可以把过程非正式地定义为以返回语句结束的命令语句块。过程使用PROC伪指令和ENDP伪指令来声明,另外还必须给过程定义一个名字。到现在写的所有程序都包含一个名为main的过程,例如:
main PROC
.
.
.
main ENDP
程序启动过程之外的其他过程以RET指令结束,以强制CPU返回到过程被调用的地方:
sample PROC
.
.
ret
sample ENDP
启动过程(main)是个特例,它以exit语句结束。如果程序中使用了INCLUD Irvine32.inc语句的话,exit语句实际上就是对ExitProcess函数的调用,ExitProcess是用来终止程序的系统函数
INVOKE ExitProcess ,0
如果在程序中使用了INCLUDE Irvine16.inc语句,那么exit被翻译成.EXIT伪指令。汇编器为.EXIT生成下面两条语句:
mov ah,4C00h ;调用MS-DOS的4c00h功能
int 21h ;终止程序
例子:三个整数之和
我们创建一个名为SumOf的过程来计算3个32位整数之和,假设合适的整数在过程被调用以前已经存放在EAX、EBX和ECX寄存器中了,过程在EAX中发回和:
SumOf PROC
add eax ,ebx
add eax ,ecx
ret
SumOf ENDP
为过程添加文档
应该养成的良好编程习惯之一就是为程序添加清晰易读的文档。下面是对放在每个过程开始处的文档信息的几点建议:
过程完成的所有任务的描述。
输入参数的清单使用方法。
过程返回值的描述。
列出特殊要求。
;------------------------------------------------------------
SumOf PROC
;
;Calculates and returns the sum of three 32-bit integers.
;Receines:EAX,EBX,ECX,the three integers,May be signed or unsigned.
;Retuens: EAX = sum
;--------------------------------------------------------------
add eax ,ebx
add eax ,ecx
ret
SumOf ENDP
用C/C++之类的高级语言编写的函数,典型情况下在AL中返回8位值,在AX中返回16位置,在EAX中返回32位值。
5.5.2 CALL和RET指令
CALL指令只是处理器在新的内存地址执行指令,以实现过程的调用。过程使用RET(从过程返回)指令使处理器返回到程序过程被调用的地方继续执行。从底层细节角度来讲,CALL指令把返回地址压入堆栈并把被调用过程的地址复制到指令寄存器中。当程序返回时,RET指令从堆栈中弹出返回地址并送到指令寄存器中。在32位模式下,CPU总是执行EIP(指令指针寄存器)所指向的内存出的指令;在16位模式下,CPU总是执行IP寄存器指向的指令。
调用和返回的例子
假设在main中,CALL语句位于偏移00000020处。通常CALL指令的机器码需要5字节,因此下一条指令位于偏移00000025处:
main PROC
00000020 call MySub
00000025 mov eax ,ebx
接下来,假设MySub中的第一条指令位于偏移00000040处:
MySub PROC
00000040 mov eax ,edx
.
.
ret
MySub ENDP
当CALL指令执行的时候,金针CALL指令的地址(00000025)被压入堆栈,而MySub的地址被装入EIP。MySub内的指令开始执行,一直到RET指令位置。当RET指令被执行的时候,ESP所指的堆栈值被弹出并送至EIP。第二部,ESP的值将减少以指向堆栈上的前一个值。
Sub3过程结束的时候执行RET指令,从堆栈中弹出[ESP]处的值送至指令寄存器,这将使得CPU从紧跟调用Sub3之后的指令处恢复执行,下图显示了在从Sub3过程返回之前的堆栈状况:
返回之后,ESP指向相邻的堆栈表项,在Sub2末尾RET指令准备执行时,堆栈如下表示:
TIP:看到这我就一直在想一个问题,如果我自己写了一个函数,然后我在里面直接更改了堆栈,但是我并没有还原相关,也就是我更改堆栈会不会导致这个函数return不回去(因为我不确定我用的堆栈和CPU调度用的堆栈是不是同一个堆栈,也就是作用域的问题),于是我做了这个尝试:
TITLE TEST STACK (teststack.asm)
INCLUDE Irvine32.inc
.data
strStart BYTE "Start!" ,0dh ,0ah,0
strEnd BYTE "End!" ,0dh ,0ah ,0
strTest BYTE "RunTestFun!",0dh ,0ah ,0
.code
main PROC
mov edx ,OFFSET strStart
call WriteString
call TestFun
mov edx ,OFFSET strEnd
call WriteString
exit
main ENDP
TESTFun PROC
mov edx ,OFFSET strTest
call WriteString
pop edx
ret
TESTFun ENDP
END main
我在函数里直接POP了栈里的东西,如果用的是同一个栈,那么这样应该是跳转不回去的。结果也应该是不可预知的。然后操作的结果却是是这样。直接没有return成功,我还用vs反汇编看了下地址,在函数里面POP出来的那个值就是call
TestFun接下来那个call writestring的地址。so...
过程的嵌套调用
被调用的过程在返回之前又调用了其他过程时,就发生了过程嵌套调用。假设main调用了过程Sub1,Sub1执行的时候又调用了过程Sub2,Sub2执行的时候又调用了Sub3,这个过程如下图:
最后,当Sub1返回时,堆栈中的[ESP]被弹出送指令指针寄存器,CPU在main中回复继续执行:
显然,堆栈已经被证明是存储信息(如嵌套过程调用的相关信息)的有效工具。通常堆栈适用于程序要以特定顺序回溯执行某些步骤的情况。
向过程传递参数
如果想要编写一个执行某些标准操作的过程,如计算整理数组之和的过程,那么在过程之内引用特定的变量并不是什么好主意。如果那么做的话,该过程就不可能用于其他数组了。一个较好的办法就是向过程传递参数。在汇编语言中,通过通用寄存器传递参数的做法是很普遍的。
上节中我们编写了一个把EAX,EBX和ECX寄存器中整数相加的过程SumOf。在main中调用SumOf之前,首先为EAX,EBX和ECX寄存器赋值:
data
theSum DWORD ?
.code
main PROC
mov eax ,10000h
mov ebx ,20000h
mov ecx ,30000h
call SumOf
mov theSum ,eax
在CALL语句之后,可以把EAX中的和复制到一个变量中保存。
5.5.3 例子:对整数数组求和
一种非常常见的类型的循环是计算整数数组之和,或许读者用C++或Java编写过,在汇编语言中是非常易于实现的,经过精心编写,循环可以以尽可能快的速度运行。比如我们可以在循环中使用寄存器而不是变量。
下面创建一个名为AraySum的过程,它从调用程序那里接受连个参数:一个指向32位整数数组的指针和一个包含数组元素数目的技术,ArraySum计算数组之和并通过EAX寄存器返回:
;----------------------------------
ArraySum PROC
;
;calculates the sum of array of 32-bit integers.
;Receives: ESI = the array offset
; ECX = number of elements in the array
;Returns : EAX = sum of the array elements
;-----------------------------------
push esi
push ecx
mov eax ,0
L1:
add eax ,[esi]
add esi ,TYPE DWORD
loop L1
pop ecx
pop esi
ret
ArraySum ENDP
注意该过程中没有任何东西与特定数组的名字或大小相关,所以它可用于任何需要计算32位整数数组和的程序。无论何时只要有可能的话,读者应尽量编写灵活和易于修改的过程。
调用ArraySum:下面是一个调用ArraySum的过程的例子,通过ESI传递array的地址,并通过ECX传递数组元素数目。在调用之后,把EAX中的和复制到一个变量中。
INCLUDE Irvine32.inc
.data
array DWORD 10000h ,20000h ,30000h ,40000h ,50000h
theSum DWORD ?
.code
main PROC
mov esi ,OFFSET array
mov ecx ,LENGTHOF array
call ArraySum
mov theSum ,eax
main ENDP
5.5.4 流程图
流程图是以图形化的方式描述程序逻辑的有效方法。流程图中的每个图形都表示一个逻辑步骤,把图形连接起来的带箭头的先显示了逻辑步骤之间的次序:
来一个ArraySum过程设计一个简单的流程图。
5.5.5 保存和恢复寄存器
读者已经可能注意到在ArraySum过程的开始处ECX和ESI被压入堆栈,过程结束的时候又被弹出,绝大多数修改寄存器值的过程都使用这种方式。修改寄存器值的过程应该总是保存和恢复寄存器值,以确保调用程序本身的寄存器值不会覆盖改写。这个规则的一种例外情况是用寄存器发回结果时,这时不要对这个寄存器进行保存和恢复工作。
USER操作符
与PROC伪指令配套使用的USER操作符允许列出被过程修改的所有寄存器,它只是编译器做两件事:首先,在过程开始处生成PUSH指令在堆栈上保存寄存器;其次,在过程结束的处生成POP指令恢复这些寄存器的值。USER操作符应该紧跟PROC伪指令,其后跟由空格和制表符分割的寄存器列表。
5.5.3节中的ArraySum过程使用PUSH和POP指令保存和恢复被过程修改的寄存器ESI和ECX。使用USER操作符做相同的事情更简单一些:
ArraySum PROC USER esi ,ecx
mov eax ,0
L1:
add eax ,[esi]
add esi ,4
loop L1
ret
ArraySum ENDP
汇编生成的相应代码显示了使用USER操作符的效果:
Array PROC
push esi
push ecx
mov eax ,0
L1:
add eax ,[esi]
add esi ,4
loop L1
pop ecx
pop esi
ret
Array ENDP
5.6.1 整数求和程序(设计)
写一个程序,提示用户输入3个32位整数,将其保存在数组中,计算数组内的元素的和并在屏幕上显示。
TITLE Integer Summation Program (Sum2.asm)
; This program prompts the user for three integers,
; stores them in an array, calculates the sum of the
; array ,and displays the sum.
INCLUDE Irvine32.inc
INTEGER_COUNT = 3
.data
str1 BYTE "Enter a signed integer:" ,0
str2 BYTE "The sum of the integers is:" ,0
array DWORD INTEGER_COUNT DUP(?)
.code
main PROC
call Clrscr
mov esi ,OFFSET array
mov ecx ,INTEGER_COUNT
call PromptForIntegers
call ArraySum
call DisplaySum
exit
main ENDP
;-----------------------------------------
PromptForIntegers PROC USES ecx edx esi
;
; Prompts the user for an arbitrary number of integers
; and inserts the integers into an array.
; Receives: ESI points to the array ,ECX = array size
; Return: nothing
;-----------------------------------------
mov edx ,OFFSET str1
L1: call WriteString
call ReadInt
call Crlf
mov [esi] ,eax
add esi ,TYPE DWORD
loop L1
ret
PromptForIntegers ENDP
;----------------------------------------
ArraySum PROC USES esi ecx
;
; Calculates the sum of an array of 32-bit integers.
; Receives : ESI points to the array, ECX = number
; of array elements
; Returns: EAX = sum of the array elements
;------------------------------------------
mov eax ,0
L1: add eax ,[esi]
add esi ,TYPE DWORD
loop L1
ret
ArraySum ENDP
;------------------------------------------
DisplaySum PROC USES edx
;
; Displays the sum on the screen
; Receives :EAX = the sum
; Returns nothing
;-------------------------------------------
mov edx ,OFFSET str2
call WriteString
call WriteInt
call Crlf
ret
DisplaySum ENDP
END main
运行结果:
5.7 本章小结