第 8章 汇编语言高级编程技术
8.1 宏
8.2 汇编高级语法
8.3 模块化程序设计
8.4 C和汇编的混合编程
8.5 程序优化
8.1 宏
8.1.1 宏指令的定义和使用宏指令的使用有 3个步骤:
宏定义
宏调用
宏扩展宏定义格式:
宏指令名 MACRO [形式参数表 ]
… ;宏指令体
ENDM
形式参数表给出形参,形参之间用逗号隔开 。
宏调用 和宏扩展宏调用的格式,
宏指令名 [实参数表 ]
实参应和形参顺序一致宏扩展,
用宏指令体的语句序列替换宏指令名
并用实参替换形参
2,LOCAL伪操作程序中多次调用宏指令,展开时其中的标号重复,导致编译出错 。
解决办法,将宏定义体中的标号说明为局部标号格式,local 标号 1[,标号 2,标号 3,… ]
3,PURGE伪指令
PURGE伪指令取消已定义的宏指令格式为:
PURGE 宏指令名 [,宏指令名,… ]
注意:
宏指令被取消的程序就不能再使用这个宏指令
指令取消之前的宏调用已经被展开,不受影响
8.1.2 宏指令中参数的使用
1,MOVE宏指令
MOV指令不允许源操作数和目标操作数同时为内存操作数,
我们如此定义:
dwVarA DWORD?
dwVarB DWORD 50
MOVE MACRO X,Y
PUSH Y
POP X
ENDM
调用 MOVE dwVarA,dwVarB,可将后者内容赋给前者
2,SUBX宏指令
SUB指令执行后,目标操作数中的被减数被,差,
替换掉。我们可以定义一个新的宏指令,它包括
3个参数:被减数( minuend),减数
( subtrahend),差( difference)。
SUBX MACRO minuend,subtrahend,difference
PUSH EAX
MOV EAX,minuend
SUB EAX,subtrahend
MOV difference,EAX
SUBX宏指令(续);接上页程序
POP EAX
ENDM
要计算 ECX=EDX–8,就调用 SUBX宏:
SUBX EDX,8,ECX
3,Idx宏指令对给定的数组下标 i,j,元素的地址为:
数组首地址 + (i * RowSize + j) * ElementSize
RowSize表示为数组每行的大小
ElementSize表示为每个元素的大小横线部分可以用宏指令表示:
Idx MACRO I,J,RowSize
IMUL EBX,I,RowSize
ADD EBX,J
Idx宏指令(续);接上页程序
ADD EBX,EBX
SHL EBX,2
ENDM
调用上述 Idx宏指令就可求得数组元素的位置
8.1.3 特殊的宏操作符
1.操作符 &
宏定义体中将 &放在形参前面
宏扩展时把 &后面的形参和前面的符号合并成一个符号通过 &把前后两个符号合并成一个变量名,
DefData MACRO Name,Value
byte&Name BYTE Value
ENDM
调用宏:
DefData Temp,0
DefData 1,-5
展开宏,形成以下语句:
1 byteTemp BYTE 0
1 byte1 BYTE -5
用 &连接的符号还可以形成一条指令
2.文本原样传递操作符 < >
把实参原封不动地传递到定义体中去替换形参
可以保证实参的完整性用 DefData定义一个 5字节的数组 Array5:
DefData Array5,<-2,-1,0,1,2>
3.表达式操作符 %
用 %可以取得符号常量的值宏调用时,%操作符后面的表达式立即求值后作为实参
4.字符原意操作符!
在宏的定义和调用时,,& < > ! %” 这些字符有其特殊的用途。 如果要使用原意,就在字符前加!。
! 字符后面跟的一个字符被原样传送。
!字符类似于 C语言字符串中的转义字符 \
宏调用 DefStr Book,<<Assembly Language>>
产生的语句为:
strBook BYTE "strBook,<Assembly Language>",0
而宏调用 DefStr Book,!<!<Assembly Language!>!>
产生的语句为:
strBook BYTE "strBook:<<Assembly Language>>",0
8.1.4 宏与子程序的区别
1,工作方式的区别
子程序在,空间,上占优势,使用子程序需要付出额外的开销,程序的体积较小
而宏在,时间,上占优势,但程序的体积较大宏与子程序的区别
2,参数传递方式不同
宏调用时实参直接替换形参;实参可以是任何形式或意义的字符组合 ;参数替换是在编译时完成的 。
而子程序参数的传递是通过寄存器,堆栈等实现的;参数则只能是以数值形式出现 ;
程序的参数传递是程序运行时完成的 。
8.1.5 重复汇编
1,重复伪指令 REPT
REPT的格式为:
REPT MACRO 数值表达式
… ; 重复块
ENDM
REPT也可以写做 repeat。
数值表达式决定重复次数
重复块中可以是任何有效的汇编语句重复块中是指令序列 的例子:
打印数字 9,99,999,9999的平方数
FmtSqureStr BYTE '%d * %d = %d',0ah,0
X =9
REPT 4
INVOKE printf,offset fmtSqureStr,X,X,X*X
X =X*10+9
ENDM
2.不定重复伪指令 IRP
IRP指令由参数来决定重复次数,格式为:
IRP MACRO 形参,<实参表>
… ; 重复块
ENDM
IRP也可以写做 for
形参只能有一个,实参表中可以有多个参数
实参表中参数个数就是重复次数,按顺序取代形参下面 IRP的实参表中包含 3个参数
IRP VALUE,<"USB","FLOPPY","CD-ROM">
BYTE VALUE,0
ENDM
其结果相当于:
BYTE "USB",0
BYTE "FLOPPY",0
BYTE "CD-ROM",0
3.不定重复伪指令 IRPC
参数表用一个字符串来表示,格式为
IRPC MACRO 形参,字符串
… ; 重复块
ENDM
IRPC也可以写做 FORC
形参只有一个,字符串可以包括字母,数字等,
字符个数就是重复次数 ;
字符串中的字符按顺序替代形参例如,使用字符串 123,每次用一个字符来代替 Z。
IRPC Z,123
BYTE Z
ENDM
展开后的结果为:
BYTE 1
BYTE 2
BYTE 3
8.1.6 条件汇编条件汇编伪指令的格式如下:
IFXX YY
… ; 语句块 1
ELSE
… ; 语句块 2
ENDIF
ELSE可以不写,对于 IFXX YY
条件为真时,将语句块 1包含在程序中;
条件为假时,将语句块 2包含在程序中 。
条件汇编伪指令条件汇编伪指令大体上分为:
1,IF和 IFE伪指令
2,IFB和 IFNB伪指令用在宏定义体中,用来确定形参是否为空
3,IFDEF和 IFNDEF伪指令用一个符号 DEBUG来控制是否包括打印语句
4,IFIDNI伪指令举例,IFIDNI
8.2 汇编高级语法汇编语言 语法问题:
分支和循环程序结构存在标号定义和程序流程复杂的问题
可读性、简洁性、可维护性不如高级语言
编写汇编程序时比较烦琐
MASM引入了一系列伪指令来实现条件测试、分支和循环语句等。
8.2.1 条件测试表达式条件测试表达式的形式,
1.寄存器或内存变量例如,x ; x不等 0时为真
EAX ; EAX不等于 0时为真
2,利用关系运算符
(数值表达式 1)关系运算符 (数值表达式 2)
关系运算符比较数值表达式 1和数值表达式 2的内容关系运算符列表条件测试表达式
3,利用逻辑运算符
(关系或数值表达式 1) 逻辑运算符 (关系或数值表达式 2)
逻辑运算符对表达式进行逻辑运算。
逻辑运算符和关系运算符的语法基本和 C语言基本类似。
逻辑运算符列表条件测试表达式
4,根据标志寄存器中的各种标志位符号
8.2.2 分支伪操作分支语句根据条件表达式的真假执行不同的代码模块,
与 C语言的 if/elseif/else/endif相似。语法如下
.IF 条件表达式 1
表达式 1为,真,时执行的指令
[.ELSEIF 条件表达式 2
表达式 2为,真,时执行的指令 ]
[.ELSE
上述条件均不满足时执行的指令 ]
.ENDIF
8.2.3 循环伪操作循环是重复执行的一组指令,分 3种:
1 WHILE-ENDW循环:
.WHILE 条件测试表达式循环体
.ENDW
首先判断条件测试表达式
如果结果是,真,,则执行循环体内的指令,结束后再判断表达式,直到表达式结果为,假,为止 。
如果第一次判断结果就为,假,,则直接退出循环循环伪操作
2 REPEAT-UNTIL循环:
.REPEAT
循环体
.UNTIL 条件测试表达式先执行一次循环体内的指令,然后判断条件测试表达式:
如果结果是,假,,则继续执行循环体内的指令,一直到表达式结果为,真,为止
循环体至少会执行一次循环伪操作
3 REPEAT-UNTILCXZ循环
.REPEAT
循环体
.UNTILCXZ [条件测试表达式 ]
如果,UNTILCXZ后面没有条件测试表达式,则循环的次数由 ECX指定;
如果,UNTILCXZ后面带有条件测试表达式,循环的次数由 ECX和表达式共同控制 。
当 ECX不等于 0且表达式的值为,假,时,ECX减 1,继续循环
当 ECX等于 0或者表达式的值为,真,时,终止循环 。
循环伪操作
使用,BREAK语句可以跳出循环;
,BREAK后面可以加,IF,满足条件才跳出循环。
格式,.BREAK [.IF 退出条件 ]
使用,CONTINUE语句可以跳到循环体的最后。
计算 0+1+2+… +8+9的几个例子例 1,.WHILE/.ENDW循环形式
XOR EAX,EAX
XOR EBX,EBX
.WHILE EBX < 10
ADD EAX,EBX
INC EBX
.ENDW
计算 0+1+2+… +8+9的几个例子例 2:用,BREAK语句来终止循环
XOR EAX,EAX
XOR EBX,EBX
.WHILE 1
ADD EAX,EBX
INC EBX
.BREAK,IF EBX >= 10
.ENDW
计算 0+1+2+… +8+9的几个例子例 3,.REPEAT/.UNTIL循环的形式
XOR EAX,EAX
XOR EBX,EBX
.REPEAT
ADD EAX,EBX
INC EBX
.UNTIL EBX >= 10
计算 0+1+2+… +8+9的几个例子例 4,.REPEAT/.UNTILCXZ循环形式
MOV ECX,10
XOR EAX,EAX
XOR EBX,EBX
.REPEAT
ADD EAX,EBX
INC EBX
.UNTILCXZ
8.3 模块化程序设计
8.3.1 模块化程序设计基本概念
可以将一个大的系统分解为小的模块,各个模块可并行完成,最后组合成为一个完整的系统。
8.3.2 模块间的通信
1,外部引用伪指令 EXTRN
格式,EXTRN 变量名,类型 [,...]
功能:说明在本模块中用到的变量是在另一个模块中定义的,同时指出变量的类型 。
说明,EXTRN伪指令应出现在程序引用该名字之前,一般放在程序开头 。
模块间的通信
2.全局符号说明伪指令 PUBLIC
格式,PUBLIC 名字 [,...]
功能:本模块中定义的名字可以被其他模块使用。
说明:名字可以是变量名,也可以是子程序名。
模块间的通信
3.子程序声明伪指令 PROTO
格式:子程序名 PROTO [C | stdcall],[第一个参数类型 ] [,:后续参数类型 ]
功能:说明子程序名字和参数类型,供主程序调用 。
程序示例,mod1.asm mod2.asm
结果,100 - 60 = 40
8.4 C和汇编的混合编程关键,两种语言的接口问题解决方法:
在 C程序中直接嵌入汇编代码
由 C语言主程序调用汇编子程序
8.4.1 直接嵌入
C语言程序中直接嵌入汇编语句格式为,_asm 汇编语句对于连续的多个汇编语句,格式为:
asm {
汇编语句汇编语句
…
}
内嵌汇编语句的操作码必须是有效的 80x86指令 。
不能使用 byte,word,dword等语句定义数据 。
内嵌汇编语句中的操作数可以是:
寄存器;
局部变量,全局变量和函数参数;
结构成员 。
程序清单,inline.c (嵌入汇编 )
8.4.2 C程序调用汇编子程序
C源程序中所有语句要符合 C的语法规则;
汇编源程序的所有语句要符合汇编的语法规则 ;
C模块可调用汇编模块中的子程序,还可以使用汇编模块中定义的全局变量;
汇编模块可调用 C模块中的函数,可以使用 C模块中定义的全局变量 。
1,C模块使用汇编模块中的变量
C和汇编有些变量类型是等价的,可以相互转换
C模块使用汇编模块中的变量
C源程序要使用汇编模块中的变量,则在汇编模块中的变量名必须以下划线开头。
例如:
_strFormula sbyte "Pythagorean theorem,x*x+y*y=z*z",0
_xval sdword 3
_yval sdword 4
_zval sdword 5
C模块中使用这些变量时,前面的下划线必须去掉
2.汇编模块使用 C模块中的变量
C模块中,应采用 extern来指明变量可以由外部模块所使用,
例如,extern int x,y,z;
在汇编模块中,要使用这个变量,应该用 EXTRN
加以说明,
例如,EXTRN _x:sdword,_y:sdword,_z:sdword
使用变量如,MOV EAX,_x
3,C模块调用汇编模块中的子程序关键功能用汇编语言来编写,再由 C语言来调用程序举例:
C/汇编联合编程的主模块 united.c
C/汇编联合编程的子模块 unite.asm
4.编译链接过程
对 C模块和汇编模块分别进行编译,生成各自的,obj文件。
将这些,obj文件链接成一个可执行文件如图:
5,C调用规则
上例的 Verify1,Verify2,Verify3,Verify4
都采用了 C调用规则,
主程序按照从右至左的顺序,将参数顺序压入堆栈;
子程序返回后,主程序通过,ADD ESP,n”指令调整
ESP的值 。
如果在说明子程序时不加调用规则,则应在子程序名前加下划线,参数也应自行定义。
6,stdcall调用规则定义子程序时使用 stdcall规则,不用 C规则举例:
Verify2 PROC stdcall x:dword,y:dword,z:dword
在 C模块中,在说明 Verify2时要加上 _stdcall:
extern int _stdcall Verify2(int x,int y,int z);
int ret = Verify2(x,y,z);
7.生成 map文件
链接过程生成 map文件
可以查看共享的变量、函数在目标文件中的真实名字例如:
link united.obj unite.obj /out:united.exe
/map:united.map /subsystem:console
8.4.3 汇编调用 C函数使用 PROTO说明 C函数的名称、调用方式、参数类型等,如:
input PROTO C px:ptr sdword,py:ptr sdword,
pz:ptr sdword
verify PROTO C x:dword,y:dword,z:dword
举例:
C/汇编联合编程中的子模块 mix.c
C/汇编联合编程的主模块 mixed.asm
8.4.4 C++与汇编
1,使用 C方式共享变量和函数在 C++一方,要将与汇编模块共享的变量,
函数等用 extern,C”的形式说明 。
举例:
C++/汇编联合编程 ArrSum.cpp
C++/汇编联合编程 ArraySum.asm
8.4.4 C++与汇编
2,C++类的实例与方法程序 demo.cpp中,有两个类 A,B。
A是 B的基类,类 A和类 B各有自己的 reset ( ) 方法和 output ( ) 方法 。 程序的输出结果为:
A,1
B,2,3
A,10
B,10,0
类 A的实例 a,类 B的实例 b所占用的内存单元的内容如图类的 vtable就像一张表格,存放它的虚函数的地址
8.5 程序优化评价一个程序优劣 的要素:
实现思想是否合理清晰;
书写风格是否符合规范 ;
……
程序的执行效率 (重要 )
程序在多长的时间内能够完成(时间)
程序需要多大的存储空间(空间)
8.5.1 运行时间的优化
1.选择执行速度快的指令
( 1) 寄存器清零将寄存器清零,有以下几种指令:
MOV EAX,0
SUB EAX,EAX
XOR EAX,EAX
其中 SUB,XOR指令执行速度比 MOV指令快,而且所需程序空间少,所以应选这 2种指令之一 ( XOR更常用 ) 。
选择执行速度快的指令(续)
( 2) 加减要使 EBX=EAX?30
可以用 LEA指令 LEA EBX,[EAX-30]
( 3) 乘除求 EAX=EAX/16,
可以用 SHR指令 SHR EAX,4
求 EAX=EAX*8,
可以用 SHL EAX,3
2.操作的转化
除法指令比乘法指令的速度慢,如果程序的除数为一个常数,可以将除法转换为乘法进行。
设被除数为 a,除数为 b,商为 c,余数为 d,均为 32位二进制数;
a÷ b=c余 d,即 a=bc+ d;
记 L=232=100000000H 求出 M=( L+ ( b–1))
÷ b,则 c = aM / L
操作的转化(续)
设,L÷ b = e mod f,L = be+ f
分两种情况:
( 1) f=0,即 L能被 b整除,M=( L+ ( b–1) ÷ b=L/b=e ;
aM = a( L/b) =(( bc+d) L/b) = cL+( dL/b)
a乘以 M后,结果是 64位数,高 32位数就是 c,即 EDX。
低 32位数为 dL / b 。
( 2) 0<f<b,L 不 能 被 b 整除,M=( L+ ( b–1)) ÷ b=
( L/b) +1=e+1 ;
同理可求,a乘以 M后,结果是 64位数,高 32位数就是 c,
即 EDX。 低 32位数为 de+( b-f) c+d。
3.求两个数中的较小的数
求两个数中的较小的数利用公式
min( x,y) =x+((( y–x) >>31) &( y-x))
求两个数中的较大的数利用公式
max(x,y)= x–( ( (x–y)>>(WORbyteITS–1) )&(x–y) )
尽管程序在长度上有所增加,但由于避免了分支判断,
CPU的效率得到了充分的利用,执行时间大大缩短 。
4.算法的优化举例:判断素数的算法一般思想:
guess为要判断的数,把从 2到 guess-1中每一个数作为除数,去除 guess。 如果商为 0,则不是素数。
优化:
偶数不是素数,因此循环时可每次加 2,且不取偶数
没有必要从 3到 guess取值,只需取到 guess开方值程序 prime.asm
5.提高 Cache命中率程序对内存的访问有两个局部性特点:
时间局部性
空间局部性解决:
最近被访问的内存及其相邻单元保留在 Cache中访问 Cache中的数据比访问内存要快因此,要提高 Cache命中率
6.查表法要将十六进制数字 0~15转换为 ‘ 0’ ~‘ 9’,
‘ A?~?F?
分析:
如果 al的初值为 0~9,jbe指令会发生跳转,得到
‘ 0’ ~‘ 9’ 。
如果 al的初值为 10~15,则 JBE指令不会跳转,得到
‘ A?~?F?。
指令:
HexChars BYTE '0123456789ABCDEF'
LEA EBX,HexChars
XLAT
8.5.2 占用空间的优化
1,选用长度短的指令
2,灵活利用堆栈
3,使用联合
4,压缩存储选用长度短的指令例如:
在函数中,如果局部变量占 4字节,使用下面的指令在堆栈中为局部变量分配空间:
SUB ESP,4
下面的指令,其效果也是 ESP减去 4,但占用程序空间更少:
PUSH ECX
灵活利用堆栈将 EAX中的数字输出 可行的办法是:
求出每一个十进制数位,保存起来,最后再逐个字符输出,直到商为 0时为止。
20040101÷ 10 = 2004010 余 1
2004010÷ 10 = 200401 余 0
200401÷ 10 = 20040 余 1
20040÷ 10 = 2004 余 0
2004÷ 10 = 200 余 4
200÷ 10 = 20 余 0
20÷ 10 = 2 余 0
2÷ 10 = 0 余 2
在除法的过程中,得到的余数可以保存在堆栈中输出时再逐个弹出。
怎么才能知道有多少个有效的十进制数位呢?
可以设置一个计数器,压入一个余数就进行计数,
最后的计数值就是十进制位数 ;或者首先压入一个数字 10,在弹出数字时,进行比较。
如果小于 10,就是一个有效的十进制数位;如果等于 10,就表示已经处理结束。
使用联合在某些情况下,程序中可能需要几个缓冲区,但同一时刻只会用到一个。
可以用这样的方式定义:
fileBuffer BYTE 4096 DUP (?)
outputBuffer BYTE 2000 DUP (?)
这样,在数据区需要 4096+2000?6096字节 。
可以将这两个缓冲区声明为一个联合,可以节省数据区占用空间 。
压缩存储年,月,日组成一个日期,声明为一个结构:
oneday STRUC
year DW 0 ;年
month BYTE 0 ;月
day BYTE 0 ;日
oneday ENDS
这里,年的表示范围是 0~ 65535,月、日的范围是 0~ 255。实际上根本不会用到这么大的范围,
数据的许多位都被浪费了。 但是节省数据空间的代价必然是程序代码变得复杂。
8.1 宏
8.2 汇编高级语法
8.3 模块化程序设计
8.4 C和汇编的混合编程
8.5 程序优化
8.1 宏
8.1.1 宏指令的定义和使用宏指令的使用有 3个步骤:
宏定义
宏调用
宏扩展宏定义格式:
宏指令名 MACRO [形式参数表 ]
… ;宏指令体
ENDM
形式参数表给出形参,形参之间用逗号隔开 。
宏调用 和宏扩展宏调用的格式,
宏指令名 [实参数表 ]
实参应和形参顺序一致宏扩展,
用宏指令体的语句序列替换宏指令名
并用实参替换形参
2,LOCAL伪操作程序中多次调用宏指令,展开时其中的标号重复,导致编译出错 。
解决办法,将宏定义体中的标号说明为局部标号格式,local 标号 1[,标号 2,标号 3,… ]
3,PURGE伪指令
PURGE伪指令取消已定义的宏指令格式为:
PURGE 宏指令名 [,宏指令名,… ]
注意:
宏指令被取消的程序就不能再使用这个宏指令
指令取消之前的宏调用已经被展开,不受影响
8.1.2 宏指令中参数的使用
1,MOVE宏指令
MOV指令不允许源操作数和目标操作数同时为内存操作数,
我们如此定义:
dwVarA DWORD?
dwVarB DWORD 50
MOVE MACRO X,Y
PUSH Y
POP X
ENDM
调用 MOVE dwVarA,dwVarB,可将后者内容赋给前者
2,SUBX宏指令
SUB指令执行后,目标操作数中的被减数被,差,
替换掉。我们可以定义一个新的宏指令,它包括
3个参数:被减数( minuend),减数
( subtrahend),差( difference)。
SUBX MACRO minuend,subtrahend,difference
PUSH EAX
MOV EAX,minuend
SUB EAX,subtrahend
MOV difference,EAX
SUBX宏指令(续);接上页程序
POP EAX
ENDM
要计算 ECX=EDX–8,就调用 SUBX宏:
SUBX EDX,8,ECX
3,Idx宏指令对给定的数组下标 i,j,元素的地址为:
数组首地址 + (i * RowSize + j) * ElementSize
RowSize表示为数组每行的大小
ElementSize表示为每个元素的大小横线部分可以用宏指令表示:
Idx MACRO I,J,RowSize
IMUL EBX,I,RowSize
ADD EBX,J
Idx宏指令(续);接上页程序
ADD EBX,EBX
SHL EBX,2
ENDM
调用上述 Idx宏指令就可求得数组元素的位置
8.1.3 特殊的宏操作符
1.操作符 &
宏定义体中将 &放在形参前面
宏扩展时把 &后面的形参和前面的符号合并成一个符号通过 &把前后两个符号合并成一个变量名,
DefData MACRO Name,Value
byte&Name BYTE Value
ENDM
调用宏:
DefData Temp,0
DefData 1,-5
展开宏,形成以下语句:
1 byteTemp BYTE 0
1 byte1 BYTE -5
用 &连接的符号还可以形成一条指令
2.文本原样传递操作符 < >
把实参原封不动地传递到定义体中去替换形参
可以保证实参的完整性用 DefData定义一个 5字节的数组 Array5:
DefData Array5,<-2,-1,0,1,2>
3.表达式操作符 %
用 %可以取得符号常量的值宏调用时,%操作符后面的表达式立即求值后作为实参
4.字符原意操作符!
在宏的定义和调用时,,& < > ! %” 这些字符有其特殊的用途。 如果要使用原意,就在字符前加!。
! 字符后面跟的一个字符被原样传送。
!字符类似于 C语言字符串中的转义字符 \
宏调用 DefStr Book,<<Assembly Language>>
产生的语句为:
strBook BYTE "strBook,<Assembly Language>",0
而宏调用 DefStr Book,!<!<Assembly Language!>!>
产生的语句为:
strBook BYTE "strBook:<<Assembly Language>>",0
8.1.4 宏与子程序的区别
1,工作方式的区别
子程序在,空间,上占优势,使用子程序需要付出额外的开销,程序的体积较小
而宏在,时间,上占优势,但程序的体积较大宏与子程序的区别
2,参数传递方式不同
宏调用时实参直接替换形参;实参可以是任何形式或意义的字符组合 ;参数替换是在编译时完成的 。
而子程序参数的传递是通过寄存器,堆栈等实现的;参数则只能是以数值形式出现 ;
程序的参数传递是程序运行时完成的 。
8.1.5 重复汇编
1,重复伪指令 REPT
REPT的格式为:
REPT MACRO 数值表达式
… ; 重复块
ENDM
REPT也可以写做 repeat。
数值表达式决定重复次数
重复块中可以是任何有效的汇编语句重复块中是指令序列 的例子:
打印数字 9,99,999,9999的平方数
FmtSqureStr BYTE '%d * %d = %d',0ah,0
X =9
REPT 4
INVOKE printf,offset fmtSqureStr,X,X,X*X
X =X*10+9
ENDM
2.不定重复伪指令 IRP
IRP指令由参数来决定重复次数,格式为:
IRP MACRO 形参,<实参表>
… ; 重复块
ENDM
IRP也可以写做 for
形参只能有一个,实参表中可以有多个参数
实参表中参数个数就是重复次数,按顺序取代形参下面 IRP的实参表中包含 3个参数
IRP VALUE,<"USB","FLOPPY","CD-ROM">
BYTE VALUE,0
ENDM
其结果相当于:
BYTE "USB",0
BYTE "FLOPPY",0
BYTE "CD-ROM",0
3.不定重复伪指令 IRPC
参数表用一个字符串来表示,格式为
IRPC MACRO 形参,字符串
… ; 重复块
ENDM
IRPC也可以写做 FORC
形参只有一个,字符串可以包括字母,数字等,
字符个数就是重复次数 ;
字符串中的字符按顺序替代形参例如,使用字符串 123,每次用一个字符来代替 Z。
IRPC Z,123
BYTE Z
ENDM
展开后的结果为:
BYTE 1
BYTE 2
BYTE 3
8.1.6 条件汇编条件汇编伪指令的格式如下:
IFXX YY
… ; 语句块 1
ELSE
… ; 语句块 2
ENDIF
ELSE可以不写,对于 IFXX YY
条件为真时,将语句块 1包含在程序中;
条件为假时,将语句块 2包含在程序中 。
条件汇编伪指令条件汇编伪指令大体上分为:
1,IF和 IFE伪指令
2,IFB和 IFNB伪指令用在宏定义体中,用来确定形参是否为空
3,IFDEF和 IFNDEF伪指令用一个符号 DEBUG来控制是否包括打印语句
4,IFIDNI伪指令举例,IFIDNI
8.2 汇编高级语法汇编语言 语法问题:
分支和循环程序结构存在标号定义和程序流程复杂的问题
可读性、简洁性、可维护性不如高级语言
编写汇编程序时比较烦琐
MASM引入了一系列伪指令来实现条件测试、分支和循环语句等。
8.2.1 条件测试表达式条件测试表达式的形式,
1.寄存器或内存变量例如,x ; x不等 0时为真
EAX ; EAX不等于 0时为真
2,利用关系运算符
(数值表达式 1)关系运算符 (数值表达式 2)
关系运算符比较数值表达式 1和数值表达式 2的内容关系运算符列表条件测试表达式
3,利用逻辑运算符
(关系或数值表达式 1) 逻辑运算符 (关系或数值表达式 2)
逻辑运算符对表达式进行逻辑运算。
逻辑运算符和关系运算符的语法基本和 C语言基本类似。
逻辑运算符列表条件测试表达式
4,根据标志寄存器中的各种标志位符号
8.2.2 分支伪操作分支语句根据条件表达式的真假执行不同的代码模块,
与 C语言的 if/elseif/else/endif相似。语法如下
.IF 条件表达式 1
表达式 1为,真,时执行的指令
[.ELSEIF 条件表达式 2
表达式 2为,真,时执行的指令 ]
[.ELSE
上述条件均不满足时执行的指令 ]
.ENDIF
8.2.3 循环伪操作循环是重复执行的一组指令,分 3种:
1 WHILE-ENDW循环:
.WHILE 条件测试表达式循环体
.ENDW
首先判断条件测试表达式
如果结果是,真,,则执行循环体内的指令,结束后再判断表达式,直到表达式结果为,假,为止 。
如果第一次判断结果就为,假,,则直接退出循环循环伪操作
2 REPEAT-UNTIL循环:
.REPEAT
循环体
.UNTIL 条件测试表达式先执行一次循环体内的指令,然后判断条件测试表达式:
如果结果是,假,,则继续执行循环体内的指令,一直到表达式结果为,真,为止
循环体至少会执行一次循环伪操作
3 REPEAT-UNTILCXZ循环
.REPEAT
循环体
.UNTILCXZ [条件测试表达式 ]
如果,UNTILCXZ后面没有条件测试表达式,则循环的次数由 ECX指定;
如果,UNTILCXZ后面带有条件测试表达式,循环的次数由 ECX和表达式共同控制 。
当 ECX不等于 0且表达式的值为,假,时,ECX减 1,继续循环
当 ECX等于 0或者表达式的值为,真,时,终止循环 。
循环伪操作
使用,BREAK语句可以跳出循环;
,BREAK后面可以加,IF,满足条件才跳出循环。
格式,.BREAK [.IF 退出条件 ]
使用,CONTINUE语句可以跳到循环体的最后。
计算 0+1+2+… +8+9的几个例子例 1,.WHILE/.ENDW循环形式
XOR EAX,EAX
XOR EBX,EBX
.WHILE EBX < 10
ADD EAX,EBX
INC EBX
.ENDW
计算 0+1+2+… +8+9的几个例子例 2:用,BREAK语句来终止循环
XOR EAX,EAX
XOR EBX,EBX
.WHILE 1
ADD EAX,EBX
INC EBX
.BREAK,IF EBX >= 10
.ENDW
计算 0+1+2+… +8+9的几个例子例 3,.REPEAT/.UNTIL循环的形式
XOR EAX,EAX
XOR EBX,EBX
.REPEAT
ADD EAX,EBX
INC EBX
.UNTIL EBX >= 10
计算 0+1+2+… +8+9的几个例子例 4,.REPEAT/.UNTILCXZ循环形式
MOV ECX,10
XOR EAX,EAX
XOR EBX,EBX
.REPEAT
ADD EAX,EBX
INC EBX
.UNTILCXZ
8.3 模块化程序设计
8.3.1 模块化程序设计基本概念
可以将一个大的系统分解为小的模块,各个模块可并行完成,最后组合成为一个完整的系统。
8.3.2 模块间的通信
1,外部引用伪指令 EXTRN
格式,EXTRN 变量名,类型 [,...]
功能:说明在本模块中用到的变量是在另一个模块中定义的,同时指出变量的类型 。
说明,EXTRN伪指令应出现在程序引用该名字之前,一般放在程序开头 。
模块间的通信
2.全局符号说明伪指令 PUBLIC
格式,PUBLIC 名字 [,...]
功能:本模块中定义的名字可以被其他模块使用。
说明:名字可以是变量名,也可以是子程序名。
模块间的通信
3.子程序声明伪指令 PROTO
格式:子程序名 PROTO [C | stdcall],[第一个参数类型 ] [,:后续参数类型 ]
功能:说明子程序名字和参数类型,供主程序调用 。
程序示例,mod1.asm mod2.asm
结果,100 - 60 = 40
8.4 C和汇编的混合编程关键,两种语言的接口问题解决方法:
在 C程序中直接嵌入汇编代码
由 C语言主程序调用汇编子程序
8.4.1 直接嵌入
C语言程序中直接嵌入汇编语句格式为,_asm 汇编语句对于连续的多个汇编语句,格式为:
asm {
汇编语句汇编语句
…
}
内嵌汇编语句的操作码必须是有效的 80x86指令 。
不能使用 byte,word,dword等语句定义数据 。
内嵌汇编语句中的操作数可以是:
寄存器;
局部变量,全局变量和函数参数;
结构成员 。
程序清单,inline.c (嵌入汇编 )
8.4.2 C程序调用汇编子程序
C源程序中所有语句要符合 C的语法规则;
汇编源程序的所有语句要符合汇编的语法规则 ;
C模块可调用汇编模块中的子程序,还可以使用汇编模块中定义的全局变量;
汇编模块可调用 C模块中的函数,可以使用 C模块中定义的全局变量 。
1,C模块使用汇编模块中的变量
C和汇编有些变量类型是等价的,可以相互转换
C模块使用汇编模块中的变量
C源程序要使用汇编模块中的变量,则在汇编模块中的变量名必须以下划线开头。
例如:
_strFormula sbyte "Pythagorean theorem,x*x+y*y=z*z",0
_xval sdword 3
_yval sdword 4
_zval sdword 5
C模块中使用这些变量时,前面的下划线必须去掉
2.汇编模块使用 C模块中的变量
C模块中,应采用 extern来指明变量可以由外部模块所使用,
例如,extern int x,y,z;
在汇编模块中,要使用这个变量,应该用 EXTRN
加以说明,
例如,EXTRN _x:sdword,_y:sdword,_z:sdword
使用变量如,MOV EAX,_x
3,C模块调用汇编模块中的子程序关键功能用汇编语言来编写,再由 C语言来调用程序举例:
C/汇编联合编程的主模块 united.c
C/汇编联合编程的子模块 unite.asm
4.编译链接过程
对 C模块和汇编模块分别进行编译,生成各自的,obj文件。
将这些,obj文件链接成一个可执行文件如图:
5,C调用规则
上例的 Verify1,Verify2,Verify3,Verify4
都采用了 C调用规则,
主程序按照从右至左的顺序,将参数顺序压入堆栈;
子程序返回后,主程序通过,ADD ESP,n”指令调整
ESP的值 。
如果在说明子程序时不加调用规则,则应在子程序名前加下划线,参数也应自行定义。
6,stdcall调用规则定义子程序时使用 stdcall规则,不用 C规则举例:
Verify2 PROC stdcall x:dword,y:dword,z:dword
在 C模块中,在说明 Verify2时要加上 _stdcall:
extern int _stdcall Verify2(int x,int y,int z);
int ret = Verify2(x,y,z);
7.生成 map文件
链接过程生成 map文件
可以查看共享的变量、函数在目标文件中的真实名字例如:
link united.obj unite.obj /out:united.exe
/map:united.map /subsystem:console
8.4.3 汇编调用 C函数使用 PROTO说明 C函数的名称、调用方式、参数类型等,如:
input PROTO C px:ptr sdword,py:ptr sdword,
pz:ptr sdword
verify PROTO C x:dword,y:dword,z:dword
举例:
C/汇编联合编程中的子模块 mix.c
C/汇编联合编程的主模块 mixed.asm
8.4.4 C++与汇编
1,使用 C方式共享变量和函数在 C++一方,要将与汇编模块共享的变量,
函数等用 extern,C”的形式说明 。
举例:
C++/汇编联合编程 ArrSum.cpp
C++/汇编联合编程 ArraySum.asm
8.4.4 C++与汇编
2,C++类的实例与方法程序 demo.cpp中,有两个类 A,B。
A是 B的基类,类 A和类 B各有自己的 reset ( ) 方法和 output ( ) 方法 。 程序的输出结果为:
A,1
B,2,3
A,10
B,10,0
类 A的实例 a,类 B的实例 b所占用的内存单元的内容如图类的 vtable就像一张表格,存放它的虚函数的地址
8.5 程序优化评价一个程序优劣 的要素:
实现思想是否合理清晰;
书写风格是否符合规范 ;
……
程序的执行效率 (重要 )
程序在多长的时间内能够完成(时间)
程序需要多大的存储空间(空间)
8.5.1 运行时间的优化
1.选择执行速度快的指令
( 1) 寄存器清零将寄存器清零,有以下几种指令:
MOV EAX,0
SUB EAX,EAX
XOR EAX,EAX
其中 SUB,XOR指令执行速度比 MOV指令快,而且所需程序空间少,所以应选这 2种指令之一 ( XOR更常用 ) 。
选择执行速度快的指令(续)
( 2) 加减要使 EBX=EAX?30
可以用 LEA指令 LEA EBX,[EAX-30]
( 3) 乘除求 EAX=EAX/16,
可以用 SHR指令 SHR EAX,4
求 EAX=EAX*8,
可以用 SHL EAX,3
2.操作的转化
除法指令比乘法指令的速度慢,如果程序的除数为一个常数,可以将除法转换为乘法进行。
设被除数为 a,除数为 b,商为 c,余数为 d,均为 32位二进制数;
a÷ b=c余 d,即 a=bc+ d;
记 L=232=100000000H 求出 M=( L+ ( b–1))
÷ b,则 c = aM / L
操作的转化(续)
设,L÷ b = e mod f,L = be+ f
分两种情况:
( 1) f=0,即 L能被 b整除,M=( L+ ( b–1) ÷ b=L/b=e ;
aM = a( L/b) =(( bc+d) L/b) = cL+( dL/b)
a乘以 M后,结果是 64位数,高 32位数就是 c,即 EDX。
低 32位数为 dL / b 。
( 2) 0<f<b,L 不 能 被 b 整除,M=( L+ ( b–1)) ÷ b=
( L/b) +1=e+1 ;
同理可求,a乘以 M后,结果是 64位数,高 32位数就是 c,
即 EDX。 低 32位数为 de+( b-f) c+d。
3.求两个数中的较小的数
求两个数中的较小的数利用公式
min( x,y) =x+((( y–x) >>31) &( y-x))
求两个数中的较大的数利用公式
max(x,y)= x–( ( (x–y)>>(WORbyteITS–1) )&(x–y) )
尽管程序在长度上有所增加,但由于避免了分支判断,
CPU的效率得到了充分的利用,执行时间大大缩短 。
4.算法的优化举例:判断素数的算法一般思想:
guess为要判断的数,把从 2到 guess-1中每一个数作为除数,去除 guess。 如果商为 0,则不是素数。
优化:
偶数不是素数,因此循环时可每次加 2,且不取偶数
没有必要从 3到 guess取值,只需取到 guess开方值程序 prime.asm
5.提高 Cache命中率程序对内存的访问有两个局部性特点:
时间局部性
空间局部性解决:
最近被访问的内存及其相邻单元保留在 Cache中访问 Cache中的数据比访问内存要快因此,要提高 Cache命中率
6.查表法要将十六进制数字 0~15转换为 ‘ 0’ ~‘ 9’,
‘ A?~?F?
分析:
如果 al的初值为 0~9,jbe指令会发生跳转,得到
‘ 0’ ~‘ 9’ 。
如果 al的初值为 10~15,则 JBE指令不会跳转,得到
‘ A?~?F?。
指令:
HexChars BYTE '0123456789ABCDEF'
LEA EBX,HexChars
XLAT
8.5.2 占用空间的优化
1,选用长度短的指令
2,灵活利用堆栈
3,使用联合
4,压缩存储选用长度短的指令例如:
在函数中,如果局部变量占 4字节,使用下面的指令在堆栈中为局部变量分配空间:
SUB ESP,4
下面的指令,其效果也是 ESP减去 4,但占用程序空间更少:
PUSH ECX
灵活利用堆栈将 EAX中的数字输出 可行的办法是:
求出每一个十进制数位,保存起来,最后再逐个字符输出,直到商为 0时为止。
20040101÷ 10 = 2004010 余 1
2004010÷ 10 = 200401 余 0
200401÷ 10 = 20040 余 1
20040÷ 10 = 2004 余 0
2004÷ 10 = 200 余 4
200÷ 10 = 20 余 0
20÷ 10 = 2 余 0
2÷ 10 = 0 余 2
在除法的过程中,得到的余数可以保存在堆栈中输出时再逐个弹出。
怎么才能知道有多少个有效的十进制数位呢?
可以设置一个计数器,压入一个余数就进行计数,
最后的计数值就是十进制位数 ;或者首先压入一个数字 10,在弹出数字时,进行比较。
如果小于 10,就是一个有效的十进制数位;如果等于 10,就表示已经处理结束。
使用联合在某些情况下,程序中可能需要几个缓冲区,但同一时刻只会用到一个。
可以用这样的方式定义:
fileBuffer BYTE 4096 DUP (?)
outputBuffer BYTE 2000 DUP (?)
这样,在数据区需要 4096+2000?6096字节 。
可以将这两个缓冲区声明为一个联合,可以节省数据区占用空间 。
压缩存储年,月,日组成一个日期,声明为一个结构:
oneday STRUC
year DW 0 ;年
month BYTE 0 ;月
day BYTE 0 ;日
oneday ENDS
这里,年的表示范围是 0~ 65535,月、日的范围是 0~ 255。实际上根本不会用到这么大的范围,
数据的许多位都被浪费了。 但是节省数据空间的代价必然是程序代码变得复杂。