概述
实验5 子程序设计
汇编中的子程序等价于C语言的函数。在编写功能较复杂的程序时,需要将它分解为若干比较小的、易于实现的子程序来实现。在主程序运行过程中,需要执行某个功能时,就调用相应的子程序。子程序执行完毕后,返回到主程序。
5.1 子程序的定义和调用
1. 子程序的定义
伪指令PROC和ENDP用来定义子程序。其格式如下:
子程序名 proc
…
…
ret
子程序名 endp
其中,PROC表示子程序定义开始,ENDP表示子程序定义结束。子程序名的命名规则和变量相同,不能使用数字开头。
子程序结束时,用RET指令返回主程序。
2. 子程序的调用
在主程序中,使用CALL指令来调用子程序。格式如下:
…
call 子程序名
…
在下面的示例程序中,包含了两个子程序AddProc1和AddProc2。AddProc1使用ESI和EDI作为加数,做完加法后把和放在EAX中,AddProc2使用X和Y作为加数,做完加法后把和放在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中,执行菜单File→Open,指定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窗口,可使用菜单View→Debug Windows→Memory打开。
按三次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等也都使用堆栈方式来传递参数。例如,前面程序已经使用过的printf、scanf属于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]替换。在编程时采用a、b的参数形式更方便易懂。
(3)返回指令的区别
SubProc1采用c规则,用“ret”返回;返回后,在主程序中有“add esp, 8 ” 指令。SubProc2采用stdcall规则,用“ret 8 ” 返回。
(4)invoke语句转换为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为例来说明递归子程序的编写过程。首先将问题用递归的形式描述出来:
xn=x×xn-1 (若n>0)
xn=1 (若n=0)
因此,可以将x、n作为子程序的参数。子程序中首先判断n是否为0。如果n为0,返回1即可;如果n大于0,则调用它自己求出xn-1,将x、n-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);
}
在用汇编实现递归子程序时,以x、n作为子程序的参数,子程序计算的结果放在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 |
0012FF 5C | 00000003 | x | |
0012FF60 | 00000000 | n-1 | |
0012FF64 | 0012FF74 | 调用者的EBP | |
0012FF68 | 00401016 | power的返回地址 | invoke power, x, n-1
第4层, x=3, n=2 |
0012FF 6C | 00000003 | x | |
0012FF70 | 00000001 | n-1 | |
0012FF74 | 0012FF84 | 调用者的EBP | |
0012FF78 | 00401016 | power的返回地址 | invoke power, x, n-1
第3层, x=3, n=3 |
0012FF 7C | 00000003 | x | |
0012FF80 | 00000002 | n-1 | |
0012FF84 | 0012FF94 | 调用者的EBP | |
0012FF88 | 00401016 | power的返回地址 | invoke power, x, n-1
第2层, x=3, n=4 |
0012FF 8C | 00000003 | x | |
0012FF90 | 00000003 | n-1 | |
0012FF94 | 0012FFA4 | 调用者的EBP | |
0012FF98 | 00401016 | power的返回地址 | invoke power, x, n-1
第1层, x=3, n=5 |
0012FF 9C | 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=5、x=3压栈,调用power。
(2) 在第1层power中,x=3,n=5,x和n是堆栈中的入口参数。x位于0012FFAC单元、n位于0012FFB0单元。将n-1=4、x=3压栈,调用power。
(3) 在第2层power中,x=3,n=4。将n-1=3、x=3压栈,调用power。
(4) 在第3层power中,x=3,n=3。将n-1=2、x=3压栈,调用power。
(5) 在第4层power中,x=3,n=2。将n-1=1、x=3压栈,调用power。
(6) 在第5层power中,x=3,n=1。将n-1=0、x=3压栈,调用power。
(7) 在第6层power中,x=3,n=0。
由于n=0,不再递归调用。power逐层返回,堆栈从上到下释放。顺序为:
(8) 第6层中,令EAX=1,再返回到第5层;
(9) 第5层中,EAX和x相乘,结果放到EAX中,EAX=3,再返回到第4层;
(10) 第4层中,EAX和x相乘,结果放到EAX中,EAX=9,再返回到第3层;
(11) 第3层中,EAX和x相乘,结果放到EAX中,EAX=27,再返回到第2层;
(12) 第2层中,EAX和x相乘,结果放到EAX中,EAX=81,再返回到第1层;
(13) 第1层中,EAX和x相乘,结果放到EAX中,EAX=243,返回到start中。
程序执行到00401044H时,ESP=0012FFACH,EBP=0012FFC0H,EAX=243。
5.3 在C程序中直接嵌入汇编
可以在C程序中直接嵌入汇编语句,在汇编语句前用关键字_asm说明,其格式为:
_asm 汇编语句
这种方式称为内嵌汇编。在这种方式下,不能使用byte、word、dword等语句定义数据。
对于连续的多个汇编语句,可以采用下面的形式:
_asm {
汇编语句
汇编语句
…
}
在内嵌汇编中,还可以使用OFFSET、TYPE、SIZE、LENGTH等汇编语言操作符。
//;程序清单: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模块中变量的类型为char、short等,而在汇编模块中变量的类型为byte、word等。相互之间的转换关系如表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模块中的变量x、y、z。如:
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);
EBX、ESI、EDI这3个寄存器在C程序中具有特别的地位。C语言的编译程序总是假设这些寄存器的值在调用子程序后保持不变。
在汇编子程序中,如果用到了EBX、ESI、EDI这3个寄存器,在定义子程序时,可以在uses关键字后面跟上寄存器名字。这样,在编译这个子程序时,自动地在子程序的进入、退出代码中加入PUSH、POP指令来保护这些寄存器的值。例如:
Verify3 proc C uses esi edi /
x:sdword, y:sdword, z:sdword, /
pxxyy:ptr sdword, pzz:ptr sdword
这样,尽管在Verify3中改变了ESI、EDI的值,但返回到C语言后,ESI、EDI被恢复为主程序的值。
对于EBP,在子程序进入时,它自动设置为指向当前函数的堆栈帧。因此,在子程序中不应该改动EBP的值。
在以下示例程序中,united.c是C模块,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 { /* 结构_XYZ在C模块和汇编模块之间传递参数 */
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*y和z*z分别保存在pxxyy和pzz指向的整数中 */
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 ; 允许strFormula被C模块所使用
public _xval, _yval, _zval ; 允许xval,yval,zval被C模块所使用
; 在汇编中如果要共享C模块数据区的变量, 在变量名前前加下划线
; C模块中的x,y,z在汇编模块中写为_x, _y, _z
extrn _x:sdword, _y:sdword, _z:sdword ; _x, _y, _z在C模块中
.data
; 数据区的变量如果要和C模块共享, 需要在名字前加下划线.
; C模块使用这些变量时, 名字前不加下划线.
; 汇编模块中的_strFormula在C模块中写为strFormula
; 汇编模块中的_xval,_yval,_zval在C模块中写为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*y和z*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*y和z*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 选择菜单Project→Add To Project→Files…,再选择unite.asm,将unite.asm加入工程中。
l 在FileView的unite.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中使用菜单Build→Rebuild 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程序。快速排序的时间复杂度平均为O(n*(log n)),在最坏情况下,为O(n*n)。而冒泡排序的时间复杂度为O(n*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]);
}
最后
以上就是鳗鱼纸鹤为你收集整理的实验5 子程序设计实验5 子程序设计的全部内容,希望文章能够帮你解决实验5 子程序设计实验5 子程序设计所遇到的程序开发问题。
如果觉得靠谱客网站的内容还不错,欢迎将靠谱客网站推荐给程序员好友。
发表评论 取消回复