实验5 子程序设计

来源:互联网 发布:西安火神网络怎么样 编辑:程序博客网 时间:2024/05/20 00:14

实验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  AddProc1指令的返回地址为0040104BH;第2条“call  AddProc2指令的返回地址为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        0C8h

0040103D   call        0040100B

00401042   push        eax

00401043   push        5

00401045   push        0C8h

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(局部变量)

.386

.model flat, stdcall

includelib      msvcrt.lib

printf          PROTO C :dword,:vararg

.data

r               dword   10

s               dword   20

szMsgOut1       byte    'r=%d s=%d', 0ah, 0

szMsgOut2       byte    'u=%d v=%d', 0ah, 0

.code          

swap            proc    C  a:ptr dword, b:ptr dword  ; 使用堆栈传递参数

                local   temp1,temp2:dword

                mov     eax, a                  

                mov     ecx, [eax]

                mov     temp1, ecx              ; temp1 = *a

                mov     ebx, b                 

                mov     edx, [ebx]

                mov     temp2, edx              ; temp2 = *b

                mov     ecx, temp2

                mov     eax, a                  

                mov     [eax], ecx              ; *a = temp2

                mov     ebx, b                 

                mov     edx, temp1

                mov     [ebx], edx              ; *b = temp1

                ret

swap            endp

start           proc

                local   u,v:dword

                invoke  swap, offset r, offset s 

                invoke  printf, offset szMsgOut1, r, s

                mov     u, 70

                mov     v, 80

                invoke  swap, addr u, addr v

                invoke  printf, offset szMsgOut2, u, v

                ret

start           endp

end             start

5.4  幂的计算

子程序自己调用自己的情况称做递归。对某些问题,递归算法是最简洁的解决方法,比如著名的“汉诺塔”(hanoi tower)问题。

这里以计算xn为例来说明递归子程序的编写过程。首先将问题用递归的形式描述出来:

xnx×xn-1       (若n>0

xn1            (若n=0

因此,可以将xn作为子程序的参数。子程序中首先判断n是否为0。如果n0,返回1即可;如果n大于0,则调用它自己求出xn-1,将xn-1作为子程序的参数,求出xn-1后,再将它乘以x作为子程序的返回值。

为简化编程,这里不考虑xn溢出的情况,假设xn能用32位无符号整数来表示。

下面是用C实现的递归程序。

;程序清单:recurse.c(计算xn的递归程序)

int power(int x, int n)

{

        if (n > 0)

                return power(x, n-1) * x;

        else

                return 1;

}

int main( )

{

        int x = 3;

        int n = 5;

        int p = power(3, 5);

        printf("x=%d n=%d x(n)=%d/n", x, n, p);

}

在用汇编实现递归子程序时,以xn作为子程序的参数,子程序计算的结果放在EAX中作为返回值。

;程序清单:recurse.asm(计算xn的递归程序)

.386

.model flat,stdcall

includelib      msvcrt.lib

printf          PROTO C :dword,:vararg

.data

szOut           byte    'x=%d n=%d x(n)=%d', 0ah, 0

.code          

power           proc    C  x:dword, n:dword

                cmp     n, 0

                jle     exitrecurse

                mov     ebx, n                  ; EBX = n

                dec     ebx                     ; EBX = n-1

                invoke  power, x, EBX           ; EAX = x(n-1)

                imul    x                       ; EAX = EAX * x

                ret                             ;     = x(n-1) * x = x(n)

exitrecurse:

                mov     eax, 1                  ; n = 0, x(n) = 1

                ret

power           endp

start           proc

                local   x,n,p:dword

                mov     x, 3

                mov     n, 5

                invoke  power, x, n             ; EAX = x(n)

                mov     p, eax

                ; printf ("x=%d n=%d x(n)=%d/n" , x, n, p)

                invoke  printf, offset szOut, x, n, p  

                ret

start           endp

end             start

用以下命令编译生成recurse.exe

ml /coff recurse.asm /link /subsystem:console

VC中执行recurse.exe,在exitrecurse处(地址为0040101EH)设置断点。按F5键执行到断点处,VC的显示如图5-3所示。

5-3  递归结束时堆栈的状态

相关寄存器的值为:

EIP = 0040101E ESP = 0012FF54 EBP = 0012FF54

分析堆栈的使用状况,如图5-4所示。


ESP=EBP=0012FF54

0012FF64

调用者的EBP

6, x=3, n=0

0012FF58

00401016

power的返回地址

invoke power, x, n-1

 

5, x=3, n=1

0012FF5C

00000003

x

0012FF60

00000000

n-1

0012FF64

0012FF74

调用者的EBP

0012FF68

00401016

power的返回地址

invoke power, x, n-1

 

4, x=3, n=2

0012FF6C

00000003

x

0012FF70

00000001

n-1

0012FF74

0012FF84

调用者的EBP

0012FF78

00401016

power的返回地址

invoke power, x, n-1

 

3, x=3, n=3

0012FF7C

00000003

x

0012FF80

00000002

n-1

0012FF84

0012FF94

调用者的EBP

0012FF88

00401016

power的返回地址

invoke power, x, n-1

 

2, x=3, n=4

0012FF8C

00000003

x

0012FF90

00000003

n-1

0012FF94

0012FFA4

调用者的EBP

0012FF98

00401016

power的返回地址

invoke power, x, n-1

 

1, x=3, n=5

0012FF9C

00000003

x

0012FFA0

00000004

n-1

0012FFA4

0012FFC0

调用者的EBP

0012FFA8

00401044

power的返回地址

invoke power, x, n

0012FFAC

00000003

x

0012FFB0

00000005

n

0012FFB4

77E5EB66

local  p:dword

 

0012FFB8

00000005

local  n:dword

 

0012FFBC

00000003

local  x:dword

 

0012FFC0

0012FFF0

调用者的EBP

 

0012FFC4

77E5EB69

start的返回地址

 

5-4  递归过程中堆栈空间的使用状况

程序从start处开始执行,逐层调用power,堆栈的生长从下向上(从高地址到低地址),直到n=0。执行顺序为:

(1)      主程序将n=5x=3压栈,调用power

(2)      在第1power中,x=3n=5xn是堆栈中的入口参数。x位于0012FFAC单元、n位于0012FFB0单元。将n-1=4x=3压栈,调用power

(3)      在第2power中,x=3n=4。将n-1=3x=3压栈,调用power

(4)      在第3power中,x=3n=3。将n-1=2x=3压栈,调用power

(5)      在第4power中,x=3n=2。将n-1=1x=3压栈,调用power

(6)      在第5power中,x=3n=1。将n-1=0x=3压栈,调用power

(7)      在第6power中,x=3n=0

由于n=0,不再递归调用。power逐层返回,堆栈从上到下释放。顺序为:

(8)      6层中,令EAX=1,再返回到第5层;

(9)      5层中,EAXx相乘,结果放到EAX中,EAX=3,再返回到第4层;

(10)  4层中,EAXx相乘,结果放到EAX中,EAX=9,再返回到第3层;

(11)  3层中,EAXx相乘,结果放到EAX中,EAX=27,再返回到第2层;

(12)  2层中,EAXx相乘,结果放到EAX中,EAX=81,再返回到第1层;

(13)  1层中,EAXx相乘,结果放到EAX中,EAX=243,返回到start中。

程序执行到00401044H时,ESP=0012FFACHEBP=0012FFC0HEAX=243

5.3  C程序中直接嵌入汇编

可以在C程序中直接嵌入汇编语句,在汇编语句前用关键字_asm说明,其格式为:

_asm    汇编语句

这种方式称为内嵌汇编。在这种方式下,不能使用byteworddword等语句定义数据。

对于连续的多个汇编语句,可以采用下面的形式:

_asm {

        汇编语句

        汇编语句

       

}

在内嵌汇编中,还可以使用OFFSETTYPESIZELENGTH等汇编语言操作符。

//;程序清单:inline.c(嵌入汇编)

#include "stdio.h"

#pragma warning (disable:4101)

// disable warning about unreferenced local variables

struct Misc {

  char  misc1;   // 1 bytes

  short misc2;   // 2 bytes

  int   misc3;   // 4 bytes

  long  misc4;   // 4 bytes

};

char    myChar;

short   myShort=-5;

int main()

{

        int     myInt=20;

        long    myLong;

        __int64 myLongLong;

        int     myLongArray[10];

        struct  Misc myMisc;

 

        _asm    mov     myChar, '9'

        _asm {

                mov     eax, LENGTH myInt;       // 1

                mov     eax, TYPE myInt;         // 4

                mov     eax, SIZE myInt;         // 4

                mov     eax, LENGTH myLongArray; // 10

                mov     eax, TYPE myLongArray;   // 4

                mov     eax, SIZE myLongArray;   // 40

                mov     eax, LENGTH myMisc;      // 1

                mov     eax, TYPE myMisc;        // 12

                mov     eax, SIZE myMisc;        // 12

                mov     eax, TYPE myChar;        // 1

                mov     eax, TYPE myShort;       // 2

                mov     eax, TYPE myLong;        // 4

                mov     eax, TYPE myLongLong;    // 8

                add     myInt, 30

                mov     ax, myShort

                mov     myMisc.misc2, ax

                movsx   eax, myMisc.misc2

                lea     ebx, myMisc

                mov     [ebx].misc3, eax

                mov     myLongArray[2*4], 200

        }

 

        printf("myChar=%c myInt=%d myMisc.misc3=%d myLongArray[2]=%d/n",

                myChar, myInt, myMisc.misc3, myLongArray[2]);

        return 0;

}

程序执行的结果为:

myChar=9 myInt=50 myMisc.misc3=-5 myLongArray[2]=200

5.4  C/C++程序与汇编的混合编程

C模块可以调用汇编模块中的子程序,还可以使用汇编模块中定义的全局变量。反过来,汇编模块可以调用C模块中的函数,也可以使用C模块中定义的全局变量。

1. C程序调用汇编子程序

为叙述方便,这里将C源程序称为C模块,汇编源程序称为汇编模块。

C模块中变量的类型为charshort等,而在汇编模块中变量的类型为byteword等。相互之间的转换关系如表5-1所示。

5-1  变量类型的互换

C变量类型

汇编变量类型

大小

char

sbyte

1字节

short

sword

2字节

int

sdword

4字节

long

sdword

4字节

unsigned char

byte

1字节

unsigned short

word

2字节

unsigned int

dword

4字节

unsigned long

dword

4字节

指针

dword

4字节

1 C模块使用汇编模块中的变量

C程序中使用汇编模块中的变量时,汇编模块中的变量名必须以下划线开头。例如:

_strFormula     sbyte   "Pythagorean theorem: x*x+y*y=z*z", 0

_xval           sdword  3

_yval           sdword  4

_zval           sdword  5

同时,用public允许外部模块来访问这些变量。如:

public  _strFormula

public  _xval, _yval, _zval

C模块中,用extern语句表明这些变量是来自于外部模块,同时说明这些变量的类型,例如:

extern int xval, yval, zval;

extern char strFormula[];

C模块中使用这些变量时,前面的下划线必须去掉。因为在编译C模块时,这些变量的名称前会被自动加上一个下划线。例如:

printf("%s/n", strFormula);

printf("Ex: x=%d y=%d z=%d/n", xval, yval, zval);

2)汇编模块使用C模块中的变量

C模块中,采用extern来指明这些变量可以由汇编模块所使用。例如:

extern int x, y, z;

在汇编模块中,要使用这个变量,应该用EXTRN加以说明,即:

extrn   _x:sdword, _y:sdword, _z:sdword

在汇编模块中,用名字_x_y_z来访问C模块中的变量xyz。如:

mov     eax, _x

3 C模块调用汇编模块中的子程序

汇编模块中的语句以子程序的形式编写,相当于C语言的一个函数,例如:

Verify1         proc    C

               

                mov     eax, 1

                ret  

Verify1         endp

C模块中,使用extern表明这个函数来自于外部模块,同时说明它的参数类型及返回值类型,例如:

extern int Verify1(void);

之后,就可以在C模块中调用汇编模块中的子程序:

int ret = Verify1( );

这里,Verify1没有使用参数,返回值类型为int。在汇编模块中,Verify1把返回值存入eax中,C模块将返回值从eax复制到ret中。

对带参数的函数,在汇编模块中,子程序定义时将使用的参数予以说明:

Verify2         proc    C       x:sdword, y:sdword, z:sdword

C模块中的用法为:

extern int Verify2(int x, int y, int z);

int ret = Verify2(x, y, z);

EBXESIEDI3个寄存器在C程序中具有特别的地位。C语言的编译程序总是假设这些寄存器的值在调用子程序后保持不变。

在汇编子程序中,如果用到了EBXESIEDI3个寄存器,在定义子程序时,可以在uses关键字后面跟上寄存器名字。这样,在编译这个子程序时,自动地在子程序的进入、退出代码中加入PUSHPOP指令来保护这些寄存器的值。例如:

Verify3         proc    C       uses esi edi /

                                x:sdword, y:sdword, z:sdword, /

                                pxxyy:ptr sdword, pzz:ptr sdword

这样,尽管在Verify3中改变了ESIEDI的值,但返回到C语言后,ESIEDI被恢复为主程序的值。

对于EBP,在子程序进入时,它自动设置为指向当前函数的堆栈帧。因此,在子程序中不应该改动EBP的值。

在以下示例程序中,united.cC模块,unite.asm是汇编模块。可以在VC中打开工程文件united.dsw,调试观察。

//程序清单:united.c(C/汇编联合编程的主模块)

#include "stdio.h"

extern int xval, yval, zval;    /* 说明xval,yval,zval的类型 */

extern char strFormula[];       /* 说明strFormula的类型 */

extern int x, y, z;             /* 允许x,y,z被汇编模块所使用 */

struct _XYZ {           /* 结构_XYZC模块和汇编模块之间传递参数 */

        int x, y, z;    /* x,y,z: C模块传递到汇编模块 */

        int xxyy;       /* xxyy:  从汇编模块传递到C模块, xxyy = x*x+y*y */

        int zz;         /* zz:    从汇编模块传递到C模块*/

};

/* 以下4个函数/子程序位于汇编模块中, extern说明 */

/* x,y,z作为全局变量, 返回1表示x*x+y*y=z*z */

extern int Verify1(void);

/* x,y,z作为函数参数, 返回1表示x*x+y*y=z*z */

extern int Verify2(int x, int y, int z);

/* x,y,z作为函数参数, x*x+y*yz*z分别保存在pxxyypzz指向的整数中 */

extern void Verify3(int x, int y, int z, int *pxxyy, int *pzz);

/* pxyz作为函数参数, 传递一个指向_XYZ结构的指针到汇编模块 */

extern void Verify4(struct _XYZ *pxyz);

/*

 * strFormula, xval, yval, zval 定义在汇编模块的数据区中,

 * 在前面已通过extern说明它们的类型

 */

void test0()

{

        printf("%s/n", strFormula);

        printf("Ex: x=%d y=%d z=%d/n", xval, yval, zval);

}

/*

 * Verify1子程序访问C模块的全局变量x,y,z来验证公式x*x+y*y=z*z

 * 在前面已通过extern允许汇编模块访问全局变量x,y,z

 */

void test1()

{

        int ret = Verify1();

        printf("Verify1() = %d/n/n", ret);

}

/*

 * x,y,z通过函数参数的形式传递给Verify2子程序

 */

void test2()

{

        int ret = Verify2(x, y, z);

        printf("Verify2(%d, %d, %d) = %d/n/n", x, y, z, ret);

}

/*

 * x,y,z,xxyy的地址,zz的地址一共5个参数传递给Verify3子程序

 * Verify3子程序求出x*x+y*y放入xxyy, 求出z*z放入zz

 */

void test3()

{

        int xxyy;

        int zz;

        Verify3(x, y, z, &xxyy, &zz);

        printf("Verify3(%d, %d, %d, 0x%p, 0x%p)/n", x, y, z, &xxyy, &zz);

        printf("xxyy=%d, zz=%d/n/n", xxyy, zz);

}

/*

 * 结构xyz的地址作为函数参数传递给Verify4子程序

 * Verify4子程序从结构xyz中取出x, y, z

 * 求出x*x+y*y放入xyz.xxyy, 求出z*z放入xyz.zz

 */

void test4()

{

        struct _XYZ xyz;

        xyz.x = x;

        xyz.y = y;

        xyz.z = z;

        Verify4(&xyz);

        printf("Verify4(0x%p)/n", &xyz);

        printf("xyz.xxyy=%d, xyz.zz=%d/n/n", xyz.xxyy, xyz.zz);

}

int x, y, z;

int main()

{

        test0();

        printf("input x y z: ");

        scanf("%d %d %d", &x, &y, &z);

        printf("x=%d, y=%d, z=%d/n/n", x, y, z);

        test1();

        test2();

        test3();

        test4();

        return 0;

}

;程序清单:unite.asm(C/汇编联合编程的子模块)

.386

.model flat

public  _strFormula                     ; 允许strFormulaC模块所使用

public  _xval, _yval, _zval             ; 允许xval,yval,zvalC模块所使用

; 在汇编中如果要共享C模块数据区的变量, 在变量名前前加下划线

; C模块中的x,y,z在汇编模块中写为_x, _y, _z

extrn   _x:sdword, _y:sdword, _z:sdword    ; _x, _y, _zC模块中

.data

; 数据区的变量如果要和C模块共享, 需要在名字前加下划线.

; C模块使用这些变量时, 名字前不加下划线.

; 汇编模块中的_strFormulaC模块中写为strFormula

; 汇编模块中的_xval,_yval,_zvalC模块中写为xval,yval,zval

_strFormula     byte    "Pythagorean theorem: x*x+y*y=z*z", 0

_xval           sdword   3

_yval           sdword   4

_zval           sdword   5

.code

Verify1         proc    C

                mov     eax, _x         ; _x是全局变量x

                mul     eax             ; x*x -> eax

                mov     ecx, eax        ; x*x -> ecx

                mov     eax, _y         ; _y是全局变量y

                mul     eax             ; y*y -> eax

                add     ecx, eax        ; x*x+y*y -> ecx

                mov     eax, _z         ; _z是全局变量z

                mul     eax             ; z*z -> eax

                cmp     eax, ecx        ; 比较x*x+y*yz*z

                jz      IsEqual

                mov     eax, 0          ; 不等, 返回0

                ret    

IsEqual:               

                mov     eax, 1          ; 相等, 返回1

                ret  

Verify1         endp

; x,y,z作为函数参数从C传递到汇编模块

Verify2         proc    C       x:sdword, y:sdword, z:sdword

                mov     eax, x          ; x在堆栈中            

                mul     eax             ; x*x -> eax

                mov     ecx, eax        ; x*x -> ecx      

                mov     eax, y          ; y在堆栈中

                mul     eax             ; y*y -> eax

                add     ecx, eax        ; x*x+y*y -> ecx

                mov     eax, z          ; z在堆栈中

                mul     eax             ; z*z -> eax

                cmp     eax, ecx        ; 比较x*x+y*yz*z

                jz      IsEqual2       

                mov     eax, 0          ; 不等, 返回0

                ret

IsEqual2:              

                mov     eax, 1          ; 相等, 返回1

                ret

                       

Verify2         endp

; x,y,z,pxxyy,pzz作为函数参数从C传递到汇编模块

Verify3         proc    C       x:sdword, y:sdword, z:sdword, /

                                pxxyy:ptr sdword, pzz:ptr sdword

                push    esi             ; 子程序中用到ebx, esi, edi

                push    edi             ; 必须保存在堆栈中

                mov     eax, x          ; x在堆栈中            

                mul     eax             ; x*x -> eax

                mov     ecx, eax        ; x*x -> ecx      

                mov     eax, y          ; y在堆栈中

                mul     eax             ; y*y -> eax

                add     ecx, eax        ; x*x+y*y -> ecx

                mov     eax, z          ; z在堆栈中

                mul     eax             ; z*z -> eax

                mov     esi, pxxyy      ; pxxyy在堆栈中, 指向C模块中的xxyy

                mov     [esi], ecx      ; x*x+y*y -> xxyy

                mov     edi, pzz        ; pzz在堆栈中, 指向C模块中的zz

                mov     [edi], eax      ; z*z -> eax

                pop     edi             ; 恢复edi

                pop     esi             ; 恢复esi     

                ret  

Verify3         endp

; 此处定义的汇编格式的_XYZ结构与C模块中的_XYZ结构一致

_XYZ            struc

                x       sdword ?

                y       sdword ?

                z       sdword ?

                xxyy    sdword ?

                zz      sdword ?

_XYZ            ends

; pxyz作为函数参数从C传递到汇编模块

Verify4         proc    C       pxyz:ptr _XYZ

                push    ebx                        ; 子程序中用到

                push    esi                        ; ebx, esi, edi

                push    edi                        ; 必须保存在堆栈中

                mov     ebx, pxyz                  ; ebx指向结构xyz

                mov     eax, (_XYZ PTR [ebx]).x    ; 从结构中取出x

                mul     eax                        ; x*x -> eax

                mov     ecx, eax                   ; x*x -> ecx

                mov     eax, (_XYZ PTR [ebx]).y    ; 从结构中取出y

                mul     eax                        ; y*y -> eax

                add     ecx, eax                   ; x*x+y*y -> ecx

                mov     eax, (_XYZ PTR [ebx]).z    ; 从结构中取出y

                mul     eax                        ; z*z -> eax

                lea     esi, (_XYZ PTR [ebx]).xxyy ; esi指向xyz.xxyy

                mov     [esi], ecx                 ; x*x+y*y -> xyz.xxyy

                lea     edi, (_XYZ PTR [ebx]).zz   ; edi指向xyz.zz

                mov     [edi], eax                 ; z*z -> xyz.zz

                pop     edi                        ; 恢复edi

                pop     esi                        ; 恢复esi

                pop     ebx                        ; 恢复ebx

                ret  

Verify4         endp

end

2. 编译调试环境的建立

C模块和汇编模块分别进行编译,生成各自的obj文件。最后,再将这些obj文件链接成一个可执行文件。其过程如图5-5所示。

united.c

united.obj

 

 

 

编译

 

链接

united.exe

unite.asm

 

unite.obj

 

 

5-5  多个源程序文件编译链接的过程

其具体步骤为:

cl /c united.c

ml /c /coff unite.asm

link united.obj unite.obj /out:united.exe /subsystem:console

也可以在VC中建立上述编译环境,其方法为:

l         首先创建一个普通的united工程,只包括united.c

l         选择菜单ProjectAdd To ProjectFiles…,再选择unite.asm,将unite.asm加入工程中。

l         FileViewunite.asm点击鼠标右键,选择Settings…,如图5-6所示;

5-6  进入汇编源程序的设置对话框

l         在设置对话框中,选择Custom Build,在Commands中键入“ml /Zi /c /coff unite.asm”,在Outputs中键入“$(ProjDir)/unite.obj”,如图5-7所示;

 

5-7  为汇编程序源文件设定编译过程

之后,就可以在VC中使用菜单BuildRebuild All来编译、连接了。还可以在源程序级别上调试C程序和汇编程序。

4 MAP信息

连接时,可以生成映射文件。在Project Settings中的Link选项中,选中“Generate mapfile”,如图5-8所示。

5-8  映射文件生成的选项

映射文件包括了变量名、所属的模块、地址等信息,便于调试。

3. C++程序调用汇编子程序

C++程序与汇编程序的联合编程,其方法基本相同。将united.c改名为united.cpp后,编译时就采用了C++语法、规则。

这时,要将extern说明的变量、函数等替换为extern "C"的形式,例如:

extern "C" int xval, yval, zval;    /* 说明xval,yval,zval的类型 */

extern "C" char strFormula[];       /* 说明strFormula的类型 */

extern "C" int x, y, z;             /* 允许x,y,z被汇编模块所使用 */

extern "C" int Verify1(void);

extern "C" int Verify2(int x, int y, int z);

extern "C" void Verify3(int x, int y, int z, int *pxxyy, int *pzz);

extern "C" void Verify4(struct _XYZ *pxyz);

5.5 实验题:快速排序

用递归子程序实现快速排序(quick sort),其算法描述可参照附后的C程序。快速排序的时间复杂度平均为On*log n)),在最坏情况下,为On*n)。而冒泡排序的时间复杂度为On*n)。其算法较复杂,但时间复杂度比冒泡排序要低。

要求:

1.    子程序quicksort使用递归方式;

2.    对于数组排序,quicksort是否优于冒泡排序?

3.    数组元素个数设为1000,随机生成1000个数组元素,运行quicksort对数组进行排序。

4.    统计对上述数组排序时,quicksort需要比较的次数。

 

//程序清单:quicksort.c(快速排序的递归程序)

#include <stdio.h>

 

int a[] = {6, 1, 2, 5, 7, 3, 10, 4, 9, 8};

int entries = sizeof(a)/sizeof(int);

 

void swap_int(int x, int y) {

  int t;

  t = a[y];

  a[y] = a[x];

  a[x] = t;

}

 

void quicksort(int lo, int hi) {

  int i,p; 

 

  if (lo >= hi) return;

 

  for (p=(i=lo)-1; i<hi; i++) if (a[i]<a[hi]) swap_int(i,++p);

  quicksort(lo, p);

 

  for (i=p+1; i<=hi; i++) if (a[i]<=a[hi]) swap_int(i,++p);

  quicksort(p+1, hi);

}

 

int main()

{

  int i;

 

  quicksort(0, entries-1);

 

  for (i = 0; i < entries; i++)

    printf("%d ", a[i]);

}

 
原创粉丝点击