实验5 子程序设计

实验5  子程序设计

汇编中的子程序等价于C语言的函数。在编写功能较复杂的程序时,需要将它分解为若干比较小的、易于实现的子程序来实现。在主程序运行过程中,需要执行某个功能时,就调用相应的子程序。子程序执行完毕后,返回到主程序。

5.1 子程序的定义和调用

1. 子程序的定义

伪指令PROCENDP用来定义子程序。其格式如下:

子程序名        proc

         

         

                ret

子程序名        endp

其中,PROC表示子程序定义开始,ENDP表示子程序定义结束。子程序名的命名规则和变量相同,不能使用数字开头。

子程序结束时,用RET指令返回主程序。

2. 子程序的调用

在主程序中,使用CALL指令来调用子程序。格式如下:

         

          call    子程序名

         

在下面的示例程序中,包含了两个子程序AddProc1AddProc2AddProc1使用ESIEDI作为加数,做完加法后把和放在EAX中,AddProc2使用XY作为加数,做完加法后把和放在Z中。主程序先后调用两个子程序,最后将结果显示出来。

AddProc2中用到了EAX,所以要先将EAX保存在堆栈中,返回时再恢复EAX的值。否则EAX中的值会被破坏。

;程序清单:callret.asm(子程序的调用与返回)

.386

.model flat,stdcall

option casemap:none

includelib      msvcrt.lib

printf          PROTO C :dword,:vararg

.data

szFmt           byte    '%d + %d = %d', 0ah, 0 ;输出结果格式字符串

X               dword   ?

Y               dword   ?

Z               dword   ?

.code

AddProc1        proc                            ; 使用寄存器作为参数

                mov     eax, esi                ; EAX = ESI + EDI

                add     eax, edi

                ret

AddProc1        endp

AddProc2        proc                            ; 使用变量作为参数

                push    eax                     ; 保存EAX的值

                mov     eax, X

                add     eax, Y

                mov     Z, eax                  ; Z = X + Y

                pop     eax                     ; 恢复EAX的值

                ret

AddProc2        endp

start:         

                mov     esi, 10                 ;

                mov     edi, 20                 ; 为子程序准备参数

                call    AddProc1                ; 调用子程序

                                                ; 结果在EAX

                mov     X, 50                   ;

                mov     Y, 60                   ; 为子程序准备参数

                call    AddProc2                ; 调用子程序

                                                ; 结果在Z

                invoke  printf, offset szFmt,

                        esi, edi, eax           ; 显示第1次加法结果

                invoke  printf, offset szFmt,

                        X, Y, Z                 ; 显示第2次加法结果

                ret

end             start

子程序AddProc1执行完毕后,RET指令要返回到主程序的“mov  X, 50 处继续执行。子程序AddProc2执行完毕后,RET指令要返回到主程序的“invoke”处继续执行。每个子程序的最后,执行的RET指令都返回到主程序。

3. 返回地址

从子程序返回后,主程序继续执行的指令的地址称为“返回地址”。或者说,返回地址就是主程序中CALL指令后面一条指令的地址。

CALL指令执行时,它首先把返回地址作为一个双字压栈,再进入子程序执行。子程序最后执行的RET指令从堆栈中取出返回地址,返回到主程序。所以,CALL指令和RET指令执行是必须依赖于堆栈的。

用以下命令编译callret.asm,生成callret.exe

ml /Zi /coff callret.asm /link /subsystem:console

VC 6.0中,执行菜单FileOpen,指定callret.exe,再按F11。按F10键,执行第1条跳转指令后,显示的内容如图5-1所示。

5-1  察看callret.asm的反汇编代码

从图5-1的反汇编代码中可以看出。程序从0040103CH处开始执行,第1条“call  AddProc 1 指令的返回地址为0040104BH;第2条“call  AddProc 2 指令的返回地址为00401064H

Memory窗口处Address后面的编辑框内,输入ESP-10和回车,再在内存显示部分点鼠标右键,选择“Long Hex Format”。如果屏幕上没有Memory窗口,可使用菜单ViewDebug WindowsMemory打开。

按三次F11键,执行5-1中黄色箭头指向的3条指令,进入AddProc1子程序。这时,返回地址0040104B被保存到堆栈中,ESP的值从0012FFC4变为0012FFC0

执行后面的RET指令时,CPU从堆栈中弹出返回地址,ESP=0012FFC0H。返回到主程序的0040104B处。

必须注意,在子程序中必须保持堆栈的平衡。在子程序中,压入堆栈的操作数必须在子程序返回前出栈,而不能留在主程序中再出栈。例如,下面的程序是错误的:

ErrorProc    PROC

             PUSH    EAX

RET

ErrorProc    ENDP

CALL    ErrorProc   

POP     EAX

5-2  返回地址被保存在堆栈中

5.2 参数传递规则

C/C++以及其他高级语言中,函数的参数是通过堆栈来传递的。C语言中的库函数,以及Windows API等也都使用堆栈方式来传递参数。例如,前面程序已经使用过的printfscanf属于C的库函数,而MessageBox属于Windows API

1. cdecl方式和stdcall方式

常见的有2种参数传递方式是cdecl方式和stdcall方式。

(1)    cdecl方式

cdecl方式是C函数的默认方式,在C/C++程序中,函数缺省使用cdecl调用规则。

主程序按从右向左的顺序将参数逐个压栈。最后一个参数先入栈。每一个参数压栈一次,在堆栈中占4字节。

在子程序中,使用[EBP+x]的方式来访问参数。x=8代表第1个参数;x=12代表第2个参数,依次类推。子程序用RET指令返回,子程序的返回值放在EAX中。

由主程序执行“ADD ESP, n”指令调整ESP,达到堆栈平衡。n等于参数个数乘以4

(2)    stdcall方式

stdcall方式也是使用堆栈传递参数,使用从右向左的顺序将参数入栈。与cdecl方式的区别,堆栈的平衡是由子程序来完成的。子程序使用“RET n”指令,在返回主程序的同时调整ESP的值。子程序的返回值也保存在EAX中。

Windows API采用的stdcall调用规则。例如,lstrcmpA )的函数原型为:

WINBASEAPI int WINAPI lstrcmpA(LPCSTR lpStr1, LPCSTR lpStr2);

其中的WINAPI定义为:

#define WINAPI      __stdcall

2.  invoke伪指令

前面的程序中多次用到invoke伪指令,它对函数、子程序的调用方式与C程序类似。

在汇编语言中,定义子程序时,可以在后面说明,是使用cdecl规则还是stdcall规则,以及各参数的名称。在调用子程序时,使用invoke伪指令,后面跟子程序名和各个参数的取值即可。

;程序清单:invoke.asm(invoke伪指令)

.386

.model flat,stdcall

includelib      msvcrt.lib

printf          PROTO C :dword,:vararg

.data

szFmt           byte    '%d - %d = %d', 0ah, 0  ;输出结果格式字符串

.code

SubProc1        proc    c  a:dword, b:dword     ; 使用堆栈传递参数, C规则

                mov     eax, a                  ; 取出第1个参数

                sub     eax, b                  ; 取出第2个参数

                ret                             ;

SubProc1        endp

SubProc2        proc    stdcall a:dword, b:dword; 使用堆栈传递参数, stdcall规则

                mov     eax, a                  ; 取出第1个参数

                sub     eax, b                  ; 取出第2个参数

                ret                             ;

SubProc2        endp

start:

                invoke  SubProc1, 100, 40       ; 调用SubProc1

                invoke  printf, offset szFmt,

                        100, 40, eax            ; 显示第1次减法结果

                invoke  SubProc2, 200, 5        ; 调用SubProc2

                invoke  printf, offset szFmt,

                        200, 5, eax             ; 显示第2次减法结果

                ret

end             startt

程序invoke.asm对应的机器指令如下所示。可以看到,在编译invoke.asm时,MASM根据子程序定义的参数个数和调用规则,自动地处理了参数转换和堆栈平衡等烦琐问题。

00401000   push        ebp

00401001   mov         ebp,esp

00401003   mov         eax,dword ptr [ebp+8]

00401006   sub         eax,dword ptr [ebp+0Ch]

00401009   leave

0040100A    ret

0040100B   push        ebp

0040100C    mov         ebp,esp

0040100E   mov         eax,dword ptr [ebp+8]

00401011   sub         eax,dword ptr [ebp+0Ch]

00401014   leave

00401015   ret         8

00401018   push        28h

0040101A    push        64h

0040101C    call        00401000

00401021   add         esp,8

00401024   push        eax

00401025   push        28h

00401027   push        64h

00401029   push        403000h

0040102E   call        00401058

00401033   add         esp,10h

00401036   push        5

00401038   push        0C 8h

0040103D   call        0040100B

00401042   push        eax

00401043   push        5

00401045   push        0C 8h

0040104A    push        403000h

0040104F    call        00401058

00401054   add         esp,10h

00401057   ret

对照invoke.asm和机器指令列表,可以观察到以下几点:

1 自动加入的指令

子程序的进入、退出代码,如“push  ebp”、“mov  ebp, esp”指令,以及“leave”指令。

2)参数的替换

参数a[ebp+8]替换,参数b[ebp+12]替换。在编程时采用ab的参数形式更方便易懂。

3)返回指令的区别

SubProc1采用c规则,用“ret”返回;返回后,在主程序中有“add  esp, 8 指令。SubProc2采用stdcall规则,用“ret  8 返回。

4invoke语句转换为CALL指令

invoke后面跟的参数被逐一压入堆栈,再跟上一条CALL指令。

5.3  局部变量

局部变量仅仅在子程序内部使用,使用局部变量能提高程序的模块化程度。局部变量也被称为自动变量。

MASM提供了local伪指令,可以在子程序中方便地定义局部变量。在swap子程序中,使用这两条伪指令在堆栈中保留了8字节的局部空间。如:

local   temp1,temp2:dword

局部变量的地址不能使用offset操作符,而必须用addr操作符。

;程序清单:local.asm(局部变量)

  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值