Intel汇编语言程序设计学习-第五章 过程-下

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

运行结果:

 Intel汇编语言程序设计学习-第五章 过程-下_第1张图片

测试程序#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

运行结果:

Intel汇编语言程序设计学习-第五章 过程-下_第2张图片

测试程序#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


执行结果:

Intel汇编语言程序设计学习-第五章 过程-下_第3张图片

5.4  堆栈操作

堆栈的定义不解释了,后进先出。

5.4.1  运行时栈

运行时栈是由CPU直接管理的内存数组,它使用两个寄存器:SSESP。在保护模式下,SS寄存器存放的是段选择子,用户模式程序不应对其进行修改。ESP寄存器存放的是指向堆栈内特定位置的一个32位偏移值。我们很少需要直接操纵ESP的值,相反,ESP寄存器的值通常是由CALLRETPUSHPOP等指令间接修改的。

堆栈指令寄存器(ESP)指向最后压入(或添加)堆栈上的数据。

Intel汇编语言程序设计学习-第五章 过程-下_第4张图片

    这里讨论的运行时栈同程序设计课程中讲述的堆栈抽象数据类型(stack ADT)是不同的。运行时栈在系统层上(由硬件直接实现)处理子过程调用;堆栈抽象数据类型通常用于实现依赖于先进后出操作的算法,一般使用高级语言如C++Java等编写。

压栈操作

32位的压栈(PUSH)操作首先将堆栈指针减4,然后把要压栈的值赋值到堆栈指针所指向的位置处。

 Intel汇编语言程序设计学习-第五章 过程-下_第5张图片

出栈操作

    出栈与压栈相反

Intel汇编语言程序设计学习-第五章 过程-下_第6张图片

    堆栈中ESP之下的区域从逻辑上讲是空白的,在程序下次执行任何要压栈的指令时该区域将被覆盖重写。

堆栈的应用

寄存器在做多种用途的时候,堆栈可以方便的作为临时保存区域,在寄存器使用完毕之后,可通过堆栈恢复其原始值。

CALL指令执行的时候,CPU用堆栈保存当前被调用过程的返回地址。

调用过程的时候,可以通过压栈传递输入值(成为参数)。

过程内的局部变量在堆栈上创建,过程结束时,这些变量被丢弃。

5.4.2  PUSHPOP指令

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

PUSHFDPOPFD指令

PUSHFD指令在堆栈上压入32位的EFLAGS寄存器的值,POPFD指令将堆栈顶部弹出并送至EFLAGS寄存器:

pushfd

popfd

实地址则是16位的。

MOV指令不能复制标志寄存器的值至变量或寄存器中,因此使用PUSHFD指令可能就是保存寄存器的最佳方式了。有时保存标志的备份以便后面进行恢复是很有用的,这通常可以是用PUSHFDPOPFD指令把一块指令包围起来:

pushfd              ;保存标志

;

;......

;

popfd               ;恢复标志

在使用这种类型的标志压栈和标志出栈指令的时候,必须确保程序的执行路径不会跳过POPFD指令。随着时间的推移,在修改程序时很难激情所有的压栈指令放在哪里。因此,编写准确的文档是非常关键的!

可以完成同样功能但或许可减少犯错误的方法是将标志保存在变量中:

.data

saveFlags DWORD

.code

pushfd                 ;标志入栈

pop saveFlags           ;复制到变量

下列语句从同一变量中回复标志值:

push saveFlags           ;将保存的标志入栈

popfd                  ;回复标志

PUSHAD,PUSHA,POPADPOPA指令

PUSHAD指令在堆栈上按下列循序压入所有的32为通用寄存器:

EAX,ECX,EDX,EBX,RSP(执行PUSHAD指令之前的值),EBP,ESIEDIPOPAD指令以相反的循序从堆栈中弹出这些通用寄存器。于此类似,80286处理器引入的PUSHA指令以括号中列表的循序压入所有的16位寄存器(AX,CX,DX,BX,SP,BP,SIDI)。POPA指令则以相反顺序弹出这些寄存器。

如果在过程中修改了很多32位寄存器,那么可以在过程的开始和结束的位置分别用PUSHADPOPAD指令保存和恢复寄存器的值。

 

MySub PROC

    pushad

    .

    .

    mov  eax ,...

    mov  edx ,...

    mov  ecx ,...

    .

    .

    popad

    ret

MySub ENDP

对上面的例子,有一个例外情况必须指出:过程通过一个或多个寄存器返回结果时不应该使用PUSHAPUSHAD指令。假设下面的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

运行结果:

 Intel汇编语言程序设计学习-第五章 过程-下_第7张图片

    TIP:书中上面的代码有第一个地方有问题(L1: mov eax,aName[esi],如果这么写编译器会弹出编译错误,原因是aName[esi]8位,eax32位的,但是eax32位,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-DOS4c00h功能

int 21h          ;终止程序

例子:三个整数之和

我们创建一个名为SumOf的过程来计算332位整数之和,假设合适的整数在过程被调用以前已经存放在EAXEBXECX寄存器中了,过程在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  CALLRET指令

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的地址被装入EIPMySub内的指令开始执行,一直到RET指令位置。当RET指令被执行的时候,ESP所指的堆栈值被弹出并送至EIP。第二部,ESP的值将减少以指向堆栈上的前一个值。

 Intel汇编语言程序设计学习-第五章 过程-下_第8张图片

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调用了过程Sub1Sub1执行的时候又调用了过程Sub2,Sub2执行的时候又调用了Sub3,这个过程如下图:

Intel汇编语言程序设计学习-第五章 过程-下_第9张图片

最后,当Sub1返回时,堆栈中的[ESP]被弹出送指令指针寄存器,CPUmain中回复继续执行:


显然,堆栈已经被证明是存储信息(如嵌套过程调用的相关信息)的有效工具。通常堆栈适用于程序要以特定顺序回溯执行某些步骤的情况。

向过程传递参数

如果想要编写一个执行某些标准操作的过程,如计算整理数组之和的过程,那么在过程之内引用特定的变量并不是什么好主意。如果那么做的话,该过程就不可能用于其他数组了。一个较好的办法就是向过程传递参数。在汇编语言中,通过通用寄存器传递参数的做法是很普遍的。

上节中我们编写了一个把EAXEBXECX寄存器中整数相加的过程SumOf。在main中调用SumOf之前,首先为EAX,EBXECX寄存器赋值:

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  流程图

流程图是以图形化的方式描述程序逻辑的有效方法。流程图中的每个图形都表示一个逻辑步骤,把图形连接起来的带箭头的先显示了逻辑步骤之间的次序:

Intel汇编语言程序设计学习-第五章 过程-下_第10张图片

来一个ArraySum过程设计一个简单的流程图。

Intel汇编语言程序设计学习-第五章 过程-下_第11张图片

5.5.5  保存和恢复寄存器

读者已经可能注意到在ArraySum过程的开始处ECXESI被压入堆栈,过程结束的时候又被弹出,绝大多数修改寄存器值的过程都使用这种方式。修改寄存器值的过程应该总是保存和恢复寄存器值,以确保调用程序本身的寄存器值不会覆盖改写。这个规则的一种例外情况是用寄存器发回结果时,这时不要对这个寄存器进行保存和恢复工作。

USER操作符

PROC伪指令配套使用的USER操作符允许列出被过程修改的所有寄存器,它只是编译器做两件事:首先,在过程开始处生成PUSH指令在堆栈上保存寄存器;其次,在过程结束的处生成POP指令恢复这些寄存器的值。USER操作符应该紧跟PROC伪指令,其后跟由空格和制表符分割的寄存器列表。

5.5.3节中的ArraySum过程使用PUSHPOP指令保存和恢复被过程修改的寄存器ESIECX。使用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  整数求和程序(设计)

写一个程序,提示用户输入332位整数,将其保存在数组中,计算数组内的元素的和并在屏幕上显示。

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

运行结果:

 Intel汇编语言程序设计学习-第五章 过程-下_第12张图片

 5.7  本章小结

 Intel汇编语言程序设计学习-第五章 过程-下_第13张图片

 Intel汇编语言程序设计学习-第五章 过程-下_第14张图片

你可能感兴趣的:(Intel汇编语言程序设计)