前面的章节讲的都是指令和逻辑,本章咱们介绍一个新的概念。在C、C++、Java等高级高阶语言中结构体和宏是很重要的编程模块,汇编语言也有结构体和宏的部分,接下来本章讲讲结构和宏。
10.2 宏
10.2.1 概述
宏过程(macroprocedure)是一个命名的汇编语句块。一旦定义好了,它就可以在程序中多次被调用。在调用宏过程时,其代码的副本将被直接插入到程序中该宏被调用的位置。这种自动插入代码也被称为内联展开(inline expansion)。尽管从技术上来说没有CALL 指令,但是按照惯例仍然说调用(calling)宏过程。
提示 Microsof 汇编程序手册中的术语宏过程是指无返回值的宏。还有一种宏函数(macro function)则有返回值。在程序员中,单词宏(macro)通常被理解为宏过程。从现在开始,本书将使用宏这个简短的称呼。
位置 宏定义一般出现在程序源代码开始的位置,或者是放在独立文件中,再用INCLUDE伪指令复制到程序里。宏在汇编器预处理(preprocessing)阶段进行扩展。在这个阶段中,预处理程序读取宏定义并扫描程序剩余的源代码。每到宏被调用的位置,汇编器就将宏的源代码复制插人到程序中。汇编器在调用宏之前,必须先找到宏定义。如果程序定义了宏但却没有调用它,那么在编译好的程序中不会出现宏代码。
在下例中,宏 PrintX 调用了Irvine32链接库的 WriteChar 过程。这个定义通常会被放置在数据段之前:
PrintX MACRO
mov al, 'X'
call WriteChar
ENDM
接着,在代码段中调用这个宏:
.code
PrintX
当预处理程序扫描这个程序并发现对 PrintX 的调用后,它就用如下语句替换宏调用:
mov al, 'X'
call WriteChar
这里发生的是文本替换。虽然宏有点不灵活,但后面很快就会展示如何向宏传递实参,使它们变得更有用。
完整代码测试笔记
;10.2.1.asm 10.2.1 宏的概述
;宏过程(macroprocedure)是一个命名的汇编语句块。一旦定义好了,它就可以在程序中多次被调用。
;在调用宏过程时,其代码的副本将被直接插入到程序中该宏被调用的位置。
;在下例中,宏 PrintX 调用了Irvine32链接库的 WriteChar 过程。
INCLUDE Irvine32.inc
;这个定义通常会被放置在数据段之前:
PrintX MACRO
mov al, 'X'
call WriteChar
ENDM
.data
array BYTE "hello world",0
.code
main PROC
mov eax, OFFSET array
PrintX
call Crlf
PrintX
call Crlf
INVOKE ExitProcess, 0
main ENDP
END main
在调试的过程中,查看反汇编代码,遇到PrintX会把宏代码扩展开来,如下:
运行调试:
10.2.2 定义宏
定义一个宏使用的是MACRO和ENDM伪指令,其语法如下所示;
macroname MACRO parameter-1, parameter-2...
statement-list
ENDM
关于缩进没有硬性规定,但是还是建议对macroname和ENDM之间的语句进行缩进。同时,还希望在宏名上使用前缀m,形成易识别的名称,如 mPutChar,mWriteString 和mGotoxy。除非宏被调用,否则 MACRO 和 ENDM 伪指令之间的语句不会被汇编。宏定义中还可以有多个形参,参数之间用逗号隔开。
参数 宏形参(macroparameter)是需传递给调用者的文本实参的命名占位符。实参实际上可能是整数、变量名或其他值,但是预处理程序把它们都当做文本。形参不包含类型信息,因此,预处理程序不会检查实参类型来看它们是否正确。如果发生类型不匹配,它将会在宏展开之后,被汇编器捕获。
mPutChar 示例 下面宏mPutChar 接收一个名为char 的输入形参,通过调用本书链接库的WriteChar将其显示在控制台:
;10.2.2.asm 10.2.2 定义宏
;mPutChar 示例 下面宏mPutChar 接收一个名为char 的输入形参,
;通过调用本书链接库的WriteChar将其显示在控制台:
INCLUDE Irvine32.inc
;这个定义通常会被放置在数据段之前:
mPutchar MACRO char
push eax
mov al, char
call WriteChar
pop eax
ENDM
.data
array BYTE "hello world",0
.code
main PROC
mov ebx, OFFSET array
mPutchar 'A'
call Crlf
mPutchar 'B'
call Crlf
INVOKE ExitProcess, 0
main ENDP
END main
运行调试:
10.2.3 调用宏
调用宏的方法是把宏名插人到程序中,后面可能跟有宏的实参。宏调用语法如下:
macroname argument-1, argument-2, . . .
Macroname 必须是源代码中在此之前被定义宏的名称。每个实参都是文本值,用以替换宏的一个形参。实参的顺序要与形参一致,但是两者的数量不须相同。如果传递的实参数太多,则汇编器会发出警告。如果传递给宏的实参数太少,则未填充的形参保持为空。
调用mPutChar 上一节定义了宏mPutChar。调用mPutChar 时,可以传递任何字符或ASCII 码。下面的语句调用了mPutChar,并向其传递了字母“A”:
mPutchar 'A'
汇编器的预处理程序将这条语句展开为下述代码,以列表文件的形式展开如下:
1 push eax
1 mov al, 'A'
1 call WriteChar
1 pop eax
左侧的1表示宏展开的层次,如果在宏的内部又调用了其他的宏,那么该值将会增加。下面的循环显示了字母表中前 20个字母:
mov al, 'A'
mov ecx, 20
L1: mPutchar al ;宏调用
inc al
loop L1
该循环由预处理程序在下面的代码中展开(源列表文件中可见),其中,宏调用在其展开的前面:
mov al, 'A'
mov ecx, 20
L1: mPutchar al ;宏调用
1 push eax
1 mov al, al
1 call WriteChar
1 pop eax
inc al
loop L1
提示 通常,与过程相比,宏执行起来更快,其原因是过程的 CALL 和RET 指令需要额外的开销。但是,使用宏也有缺点:重复使用大型宏会增加程序的大小,因为,每次调用宏都会在程序中插入宏代码的一个新副本。
调试宏
调试使用了宏的程序相当具有挑战性。程序汇编之后,检查其列表文件(扩展名为LST)以确保每个宏都按照程序员的要求展开。然后,在 Visual Studio 调试器中启动该程序,在调试窗口点击右键,从弹出菜单中选择 Go to Disassembly。每个宏调用的后面都紧跟其生成代码。示例如下:
mWriteAt 15, 10, "Hi there"
push edx
mov dh, 0Ah
mov dl, 0Fh
call _Gotoxy@0(401551h)
pop edx
push edx
mov edx, offset ??0000(405004h)
call _WriteString@0(401D64h)
pop edx
由于Irvine32 链接库使用的是STDCALL 调用规范,因此函数名用下划线(_)开始。
完整代码测试笔记
;10.2.3.asm 10.2.3 调用宏
INCLUDE Irvine32.inc
;这个定义通常会被放置在数据段之前:
mPutchar MACRO char
push eax
mov al, char
call WriteChar
pop eax
ENDM
.data
array BYTE "hello world",0
.code
main PROC
mov ebx, OFFSET array
mPutchar 'A'
INVOKE ExitProcess, 0
main ENDP
END main
运行调试反汇编代码:
lst文件代码:
00000000 .code
00000000 main PROC
00000000 BB 00000000 R mov ebx, OFFSET array
mPutchar 'A'
00000005 50 1 push eax
00000006 B0 41 1 mov al, 'A'
00000008 E8 00000000 E 1 call WriteChar
0000000D 58 1 pop eax
INVOKE ExitProcess, 0
0000000E 6A 00 * push +000000000h
00000010 E8 00000000 E * call ExitProcess
00000015 main ENDP
END main
10.2.4其他宏特性
1.规定形参
利用 REQ 限定符,可以指定必需的宏形参。如果被调用的宏没有实参与规定形参相匹配,那么汇编器将显示出错消息。如果一个宏有多个规定形参,则每个形参都要使用REQ限定符。下面是宏mPutChar,形参 char 是必需的:
mPutchar MACRO char:REQ
push eax
mov al,char
call WriteChar
pop eax
ENDM
2.宏注释
宏定义中的注释行一般都出现在每次宏展开的时候。如果希望忽略宏展开时的注释,就在它们的前面添加双分号(;;)。示例如下:
mPutchar MACRO char:REQ
push eax ;;提示:char必须包括8个比特
mov al,char
call WriteChar
pop eax
ENDM
3.ECHO 伪指令
在程序汇编时,ECHO 伪指令写一个字符串到标准输出。下面的 mPutChar 在汇编时会显示消息“Expanding the mPutChar macro”:
mPutchar MACRO char:REQ
ECHO Expanding the mPutchar macro
push eax
mov al,char
call WriteChar
pop eax
ENDM
提示 Visual Studio 2012的控制台窗口不会捕捉ECHO伪指令的输出,除非在编写程序时将其设置为生成详细输出。设置方法如下:从Tool菜单选择Options,选择Projects and Solutions,选择Build and Run,再从MSBuild project build output verbosity下拉列表中选择 Detailed。或者打开一个命令提示符并汇编程序。首先,执行如下命令调整Visual Studio 当前版本的路径:
"C:\Program Files\Microsoft Visual studio 11.0\Vc\bin\vcvars32"
然后,键入如下指令,其中filename.asm是程序的源代码文件名:
ml.exe /e/"c:\Irvine" filename.asm
4.LOCAL 伪指令
宏定义中常常包含了标号,并会在其代码中对这些标号进行自引用。例如,下面的宏makeString声明了一个变量string,且将其初始化为字符数组;
makeString MACRO text
.data
string BYTE text, 0
ENDM
假设两次调用宏:
makeString “Hello"
makeString "Goodbye"
由于汇编器不允许两个标号有相同的名字,因此结果出现错误:
makeString "Hello"
.data
string BYTE "Hello",0
.data
string BYTE "Goodbyte", 0 ;错误!
使用LOCAL 为了避免标号重命名带来的问题,可以对一个宏定义内的标号使用LOCAL 伪指令。若标号被标记为LOCAL,那么每次进行宏展开时,预处理程序就把标号名转换为唯一的标识符。下面是使用了LOCAL的宏makeString:
makeString MACRO text
LOCAL string
.data
string BYTE text,0
ENDM
假设和前面一样,也是两次调用宏,预处理程序生成的代码会将每个string替换成唯一的标识符:
makeString "Hello"
.data
??0000 BYTE "Hello",0
makeString "Goodbye"
.data
??0001 BYTE "Goodbye",0
汇编器生成的标号名使用了??nnnn的形式,其中nnnn是具有唯一性的整数。LOCAL伪指令还可以用于宏内的代码标号。
5.包含代码和数据的宏
宏通常既包含代码又包含数据。例如,下面的宏mWrite 在控制台显示文本字符串:
mWrite MACRO text
LOCAL string ;;local标号
.data ;;定义字符串
string BYTE text, 0
.code
push edx
mov edx, OFFSET string
call WriteString
pop edx
ENDM
下面的语句两次调用宏,并向其传递不同的字符串文本:
mWrite "Please enter your first name"
mWrite "please enter your last name"
汇编器对这两条语句进行展开时,每个字符串都被赋予了唯一的标号,且 MOV指令也作了相应的调整:
mWrite "Please enter your first name"
.data
??0000 BYTE "Please enter your first name", 0
.code
push edx
mov edx, OFFSET ??0000
call WriteString
pop edx
mWrite "Please enter your last name",0
.code
push edx
mov edx, OFFSET ??0001
call WriteString
pop edx
6.宏嵌套
被其他宏调用的宏称为被嵌套的宏(nestedmacro)。当汇编器的预处理程序遇到对被嵌套宏的调用时,它会就地展开该宏。传递给主调宏的形参也将直接传递给它的被嵌套宏。
提示 使用模块方法创建宏。保持它们的简短性,以便将它们组合到更复杂的宏内。这样有助于减少程序中的复制代码量。
mWriteln示例 下面的宏mWriteIn写一个字符串文本到控制台,并添加换行符。它调用宏mWrite 和 Crlf过程:
mWriteln MACRO text
mWrite text
call Crlf
ENDM
形参text被直接传递给mWrite。假设用下述语句调用mWriteLn:
mWriteln "My Sample Macro Program"
在结果代码展开,语句旁边的嵌套级数(2)表示被调用的是一个嵌套宏:
mWriteln "My Sample Macro Program"
2 .data
2 ??0002 BYTE "My Sample Macro Program", 0
2 .code
2 mov edx, OFFSET ??0002
2 call WriteString
2 pop edx
1 call Crlf
完整代码测试笔记
;10.2.4.asm 10.2.4 其他宏特性
;1.规定形参 2.宏注释 3.ECHO伪指令 4.LOCAL伪指令
;5.包含代码和数据宏 6.宏嵌套
INCLUDE Irvine32.inc
;这个定义通常会被放置在数据段之前:
mPutchar MACRO char:REQ ;;提示:char必须包括8个比特
ECHO Expanding the mPutchar macro ;;ECHO伪指令
push eax
mov al,char
call WriteChar
pop eax
ENDM
;LOCAL伪指令
makeString MACRO text
LOCAL string
.data
string BYTE text,0
ENDM
;包含代码和数据宏
mWrite MACRO text
LOCAL string ;;local标号
.data ;;定义字符串
string BYTE text, 0
.code
push edx
mov edx, OFFSET string
call WriteString
pop edx
ENDM
;宏嵌套
mWriteln MACRO text
mWrite text
call Crlf
ENDM
.data
array BYTE "hello world",0
.code
main PROC
mov ebx, OFFSET array
mPutchar 'A'
mWrite "Please enter your first name"
call Crlf
mWrite "please enter your last name"
call Crlf
mWriteln "My Sample Macro Program"
INVOKE ExitProcess, 0
main ENDP
END main
反汇编扩展代码:
运行调试:
10.2.5 使用本书的宏库(仅32位模式)
本书提供的示例程序包含了一个小而实用的32位链接库,只需要在程序的INCLUDE后面添加如下代码行就可以使用该链接库:
INCLUDE Macros.inc
有些宏封装在了Irvine32链接库的过程中,这样传递参数就更加容易。其他宏则提供新的功能。表10-2 详细介绍了每个宏,示例代码在 MacroTest.asm 中。
表10-2 Macro.inc 库中的宏
宏名 |
形式参数 |
说明 |
mDump |
varName, useLabel |
用变量名和默认属性显示一个变量 |
mDumpMem |
abbress,itemCount,componentSize |
显示内存区域 |
mGotoxy |
X, Y |
将光标位置设置在控制台窗口缓冲区 |
mReadString |
varName |
从键盘读取一个字符串 |
mShow |
itsName, format |
用各种格式显示一个变量或寄存器 |
mShowRegister |
itsName, regValue |
显示 32 位寄存器名,并用十六进制显示其内容 |
mWrite |
text |
向控制台窗口输出一个字符串文本 |
mWriteSpace |
count |
向控制台窗口输出一个或多个空格 |
mWriteSpace |
bufer |
向控制台窗口输出一个字符串变量的内容 |
1.mDumpMem
宏mDumpMem在控制台窗口显示一个内存区域。向其传递的第一个实参为包含待显示内存偏移量的常数、寄存器或者变量,第二个实参应为待显示内存中存储对象的数量,第三个实参为每个存储对象的大小。(宏在调用mDumpMem库过程时,分别将这三个实参分配给ESI、ECX和EBX。)现假设有一数据定义如下:
.data
array DWORD 1000h, 2000h, 3000h, 4000h
下面的语句按照默认属性显示数组:
mDumpMem OffSET array, LENGTHOF array, TYPE array
输出为:
下面的语句则将同一个数组显示为字节序列:
mDumpMem OFFSET array, SIZEOF array, TYPE BYTE
输出为:
下面的代码把三个数值压入堆栈,并设置好 EBX、ECX和ESI,然后调用mDumpMem显示堆栈:
mov eax, 0AAAAAAAAh
push eax
mov eax, 0BBBBBBBBh
push eax
mov eax, 0CCCCCCCCh
push eax
mov ebx, 1
mov ecx, 2
mov esi, 3
mDumpMem esp, 8, TYPE DWORD
显示出来的结果堆栈区域表明,宏已经先把 EBX、ECX和ESI压入了堆栈。这些数值之后是在调用mDumpMem之前入栈的3个整数:
实现 宏代码清单如下:
;10.2.5_1.asm 10.2.5 使用本书的宏库(仅32位模式)
;1.mDumpMem宏代码实现及测试
INCLUDE Irvine32.inc
;-----------------------------------------------
;用 DumpMem过程显示一个内存区域。
;接收:内存偏移量、显示对象的数量,以及每个存储对象的大小。
;避免用EBX、ECX和ESI传递实参。
;-----------------------------------------------
mDumpMem MACRO address:REQ, itemCount:REQ, componentSize:REQ
push ebx
push ecx
push esi
mov esi, address
mov ecx, itemCount
mov ebx, componentSize
call DumpMem
pop esi
pop ecx
pop ebx
ENDM
.data
array DWORD 1000h, 2000h, 3000h, 4000h
.code
main PROC
mov ebx, OFFSET array
mDumpMem OFFSET array, LENGTHOF array, TYPE array
INVOKE ExitProcess, 0
main ENDP
END main
运行调试:
2.mDump
宏mDump用十六进制显示一个变量的地址和内容。传递给它的参数有:变量名和(可选的)一个字符以表明在该变量之后应显示的标号。显示格式自动与变量的大小属性(BYTE、WORD或DWORD)匹配。下面的例子展示了对mDump的两次调用:
.data
diskSize DWORD 12345h
.code
mDump diskSize ;no label
mDump diskSize, Y ;show label
代码执行后,产生的输出如下所示:
实现 下面是宏mDump的代码清单,它反过来又调用了mDumpMem。代码用一个新的伪指令IFNB(若不为空)来发现主调者是否向第二个形参传递了实参(参见10.3 节);
;-----------------------------------------------
;用其已知属性显示一个变量。
;接收:varName为变量名。
;如果useLabel不为空,则显示变量名。
;-----------------------------------------------
mDump MACRO varName:REQ, useLabel
call Crlf
IFNB <useLabel>
mWrite "Variable name: &varName"
ELSE
mWrite " "
ENDIF
mDumpMem OFFSET varName, LENGTHOF varName, TYPE varName
ENDM
&varName 中的符号& 是替换操作符,它允许将 varName 形参的值插入到字符串文本中。详细内容参见10.3.7 节。
完整代码测试笔记:
;10.2.5_2.asm 10.2.5 使用本书的宏库(仅32位模式)
;2.mDump宏代码实现及测试
INCLUDE Irvine32.inc
;-----------------------------------------------
;用 DumpMem过程显示一个内存区域。
;接收:内存偏移量、显示对象的数量,以及每个存储对象的大小。
;避免用EBX、ECX和ESI传递实参。
;-----------------------------------------------
mDumpMem MACRO address:REQ, itemCount:REQ, componentSize:REQ
push ebx
push ecx
push esi
mov esi, address
mov ecx, itemCount
mov ebx, componentSize
call DumpMem
pop esi
pop ecx
pop ebx
ENDM
;包含代码和数据宏
mWrite MACRO text
LOCAL string ;;local标号
.data ;;定义字符串
string BYTE text, 0
.code
push edx
mov edx, OFFSET string
call WriteString
pop edx
ENDM
;-----------------------------------------------
;用其已知属性显示一个变量。
;接收:varName为变量名。
;如果useLabel不为空,则显示变量名。
;-----------------------------------------------
mDump MACRO varName:REQ, useLabel
call Crlf
IFNB <useLabel>
mWrite "Variable name: &varName"
ELSE
mWrite " "
ENDIF
mDumpMem OFFSET varName, LENGTHOF varName, TYPE varName
ENDM
.data
array DWORD 1000h, 2000h, 3000h, 4000h
diskSize DWORD 12345h
.code
main PROC
mov ebx, OFFSET array
mDump diskSize ;no label
mDump diskSize, Y ;show label
INVOKE ExitProcess, 0
main ENDP
END main
运行调试:
3. mGotoxy
宏mGotoxy把光标定位在控制台窗口缓冲区内指定的行列上。可以向其传递8位立即数、内存操作数和寄存器值:
mGotoxy 10, 20 ;立即数
mGotoxy row,col ;内存操作数
mGotoxy ch, cl ;寄存器值
实现 下面是宏的源代码清单:
;-----------------------------------------------
;设置光标在控制台窗口的位置。
;接收:X和Y坐标(类型为BYTE)。避免用DH和DL传递实参。
;-----------------------------------------------
mGotoxy MACRO X:REQ, Y:REQ
push edx
mov dh, Y
mov dl, X
call Gotoxy
pop edx
ENDM
避免寄存器冲突 若宏的实参是寄存器,它们有时可能会与宏内使用的寄存器发生冲突。比如,调用 mGotoxy 时用了 DH 和DL,那么就不会生成正确的代码。为了说明原因,现在来查看上述参数被替换后展开的代码:
push edx
mov dh, al ;;行
mov dl, dh ;;列
call Gotoxy
pop edx
假设 DL 传递的是Y 值,DH 传递的是 X 值,代码行 2 会在代码行 3 有机会把列值复制到DL之前就替换了DH的原值。
提示 只要有可能,宏定义应该用注释说明哪些寄存器不能用作实参。
完整代码测试笔记
;10.2.5_3.asm 10.2.5 使用本书的宏库(仅32位模式)
;3.mGotoxy宏代码实现及测试
INCLUDE Irvine32.inc
;-----------------------------------------------
;设置光标在控制台窗口的位置。
;接收:X和Y坐标(类型为BYTE)。避免用DH和DL传递实参。
;-----------------------------------------------
mGotoxy MACRO X:REQ, Y:REQ
push edx
mov dh, Y
mov dl, X
call Gotoxy
pop edx
ENDM
.data
row BYTE 3
col BYTE 5
.code
main PROC
mGotoxy 5, 5 ;立即数
mGotoxy row,col ;内存操作数
mGotoxy ch, cl ;寄存器值
INVOKE ExitProcess, 0
main ENDP
END main
运行结果:
4. mReadString
宏mReadSrting从键盘读取一个字符串,并将其存储在缓冲区。在这个宏的内部封装了一个对 ReadString库过程的调用。需向其传递缓冲区名:
完整代码测试笔记
;10.2.5_4.asm 10.2.5 使用本书的宏库(仅32位模式)
;4.mReadString宏代码实现及测试
INCLUDE Irvine32.inc
;-----------------------------------------------
;用 DumpMem过程显示一个内存区域。
;接收:内存偏移量、显示对象的数量,以及每个存储对象的大小。
;避免用EBX、ECX和ESI传递实参。
;-----------------------------------------------
mReadString MACRO varName:REQ
push ecx
push edx
mov edx, OFFSET varName
mov ecx, SIZEOF varName
call ReadString
pop edx
pop ecx
ENDM
.data
firstName BYTE 30 DUP(?)
.code
main PROC
mReadString firstName
mov edx, OFFSET firstName
call WriteString
call Crlf
INVOKE ExitProcess, 0
main ENDP
END main
运行调试:
5.mShow
宏mShow按照主调者选择的格式显示任何寄存器或变量的名字和内容。传递给它的是寄存器名,其后可选择性地加上一个字母序列,以表明期望的格式。字母选择如下:H=十六进制,D-无符号十进制,I-有符号十进制,B-二进制,N-换行。可以组合多种输出格式,还可以指定多个换行。默认格式为“HIN”。mShow 是一种有用的辅助调试工具,经常被 DumpRegs 库过程使用。可以把mShow 当作调试工具,显示重要寄存器或变量的值。
示例 下面的语句将AX寄存器的值显示为十六进制、有符号十进制、无符号十进制和二进制:
mov ax, 4096
mShow AX ;默认选项:HIN
mShow AX, DBN ;无符号十进制,二进制,换行
输出如下:
示例 下面的语句在同一行上,用无符号十进制格式显示AX,BX,CX和DX:
;插入测试数值,显示4个寄存器
mov ax, 1
mov bx, 2
mov cx, 3
mShow AX, D
mShow BX, D
mShow CX, D
mShow DX, DN
相应输出如下:
AX = 1d BX = 2d CX = 3d DX = 4d
示例 下面的代码调用mShow,用无符号十进制格式显示mydword 的内容,并换行:
.data
mydword DWORD ?
.code
mShow mydword, DN
实现 mShow的实现代码太长不便在这里给出,不过可以在本书安装文件夹(C:\Irvine)内的Macros.inc文件中找到完整代码。在编写mShow 时,需要注意在寄存器被宏自身的内部语句修改之前显示其当前值。
完整代码测试笔记:
;10.2.5_5.asm 10.2.5 使用本书的宏库(仅32位模式)
;5.mShow宏代码实现及测试
INCLUDE Irvine32.inc
;包含代码和数据宏
mWrite MACRO text
LOCAL string ;;local标号
.data ;;定义字符串
string BYTE text, 0
.code
push edx
mov edx, OFFSET string
call WriteString
pop edx
ENDM
;-----------------------------------------------
;显示寄存器或变量的名称和内容。
;接收:itsName是寄存器或变量的名称。
; format是由格式代码组成的有序字符串
;H: 十六进制, D: 无符号十进制, I: 符号十进制
;B: 二进制, N-添加换行符(CR/LF)(可能出现多次)
;默认格式为“HIN”
;-----------------------------------------------
mShow MACRO itsName:REQ, format:=<HIN>
LOCAL tempStr
.data
tempStr BYTE " &itsName = ",0
.code
pushad
;;If itsName is a register name, define its type
IF (OPATTR (itsName)) AND 00010000b ;;true if register name
MSHOWITSNAMETYPE = 0 ;; initialize TYPE to not found
FOR reg8,<al,ah,bl,bh,cl,ch,dl,dh>
IFIDNI <itsName>,<reg8>
MSHOWITSNAMETYPE = 1
movzx ecx,itsName ;; get unsigned value
movsx edx,itsName ;; get signed value
ENDIF
ENDM
FOR reg16,<ax,bx,cx,dx,si,di,bp,sp>
IFIDNI <itsName>,<reg16>
MSHOWITSNAMETYPE = 2
movzx ecx,itsName ;; get unsigned value
movsx edx,itsName ;; get signed value
ENDIF
ENDM
FOR regseg,<cs,ds,es,fs,gs,ss>
IFIDNI <itsName>,<regseg>
MSHOWITSNAMETYPE = 2
mov ax,itsName ;; get value into general purpose reg.
movsx edx,ax ;; get signed value (who would want it?)
movzx ecx,ax ;; get unsigned value
ENDIF
ENDM
FOR reg32,<eax,ebx,ecx,edx,esi,edi,ebp,esp>
IFIDNI <itsName>,<reg32>
MSHOWITSNAMETYPE = 4
mov ecx,itsName ;; get unsigned value
mov edx,itsName ;; get signed value
ENDIF
ENDM
ELSE ;; itsName is not a register name, assume variable name
MSHOWITSNAMETYPE = TYPE itsName
IF MSHOWITSNAMETYPE EQ 4
mov ecx,itsName ;; get unsigned value
mov edx,ecx ;; get signed value
ELSE
movzx ecx,itsName ;; get unsigned value
movsx edx,itsName ;; get signed value
ENDIF
ENDIF ;;OPATTR
;; Display the register or variable's name
push edx
mov edx,OFFSET tempStr
call WriteString
pop edx
;; Display the register or variable's contents
FORC fmt,<format>
IFIDNI <fmt>,<H> ;; H - write unsigned hex
mov eax,ecx ;; get unsigned
mov ebx,MSHOWITSNAMETYPE
call WriteHexB ;; write in hexadecimal
mWrite "h "
ENDIF
IFIDNI <fmt>,<D> ;; D - write unsigned dec
mov eax,ecx ;; get unsigned
call WriteDec
mWrite "d "
ENDIF
IFIDNI <fmt>,<I> ;; I - write signed Integer
mov eax,edx ;; get signed
call WriteInt
mWrite "d "
ENDIF
IFIDNI <fmt>,<B> ;; B - write unsigned binary
mov eax,ecx ;; get unsigned
mov ebx,MSHOWITSNAMETYPE
call WriteBinB ;; display binary
mWrite "b "
ENDIF
IFIDNI <fmt>,<N> ;; N - write new line
call Crlf
ENDIF
ENDM ;end FORC
popad
ENDM ;; end mShow macro
.data
mydword DWORD ?
.code
main PROC
mShow mydword,DN
mov ax, 4096
mShow AX ;默认选项:HIN
mShow AX, DBN ;无符号十进制,二进制,换行
;插入测试数值,显示4个寄存器
mov ax, 1
mov bx, 2
mov cx, 3
mShow AX, D
mShow BX, D
mShow CX, D
mShow DX, DN
INVOKE ExitProcess, 0
main ENDP
END main
运行调试:
6.mShowRegister
宏mShowRegister 显示单个32 位寄存器的名称,并用十六进制格式显示其内容。传递给它的是希望被显示的寄存器名,其后紧跟寄存器本身。下面的宏调用指定了被显示的名称为EBX:
mShowRegister EBx, ebx
产生的输出如下:
EBX=7FFD9000
下面的调用使用尖括号把标号括起来,其原因是标号内有一个空格;
mShowRegister <Stack Pointer>, esp
产生输出如下:
Stack Pointer-0012FFC0
实现 宏的源代码如下:
完整代码测试笔记
;10.2.5_6.asm 10.2.5 使用本书的宏库(仅32位模式)
;6.mDumpMem宏代码实现及测试
INCLUDE Irvine32.inc
;-----------------------------------------------
;显示寄存器名和内容。
;接收:寄存器名,寄存器值
;-----------------------------------------------
mShowRegister MACRO regName, regValue
LOCAL tempStr
.data
push esi
tempStr BYTE "®Name=", 0
.code
push eax
;显示寄存器名
push edx
mov edx, OFFSET tempStr
call WriteString
pop edx
;显示寄存器内容
mov eax, regValue
call WriteHex
pop eax
ENDM
.data
array DWORD 1000h, 2000h, 3000h, 4000h
.code
main PROC
mov esi, OFFSET array
mShowRegister EBX, ebx
call Crlf
mShowRegister <Stack Pointer>, esp
call Crlf
INVOKE ExitProcess, 0
main ENDP
END main
运行调试:
7.mWriteSpace
宏mWriteSpace向控制台窗口输出一个或多个空格。可以选择性地向其传递一个整数形参,以指定空格数(默认为一个)。例如,下面的语句写了 5个空格:
mWriteSpace 5
实现 mWriteSpace的源代码如下:
完整代码测试笔记
;10.2.5_7.asm 10.2.5 使用本书的宏库(仅32位模式)
;7.mWriteSpace宏代码实现及测试
INCLUDE Irvine32.inc
;-----------------------------------------------
;向控制台窗口输出一个或多个空格。
;接收:一个整数以指定空格数。
;默认个数为 1。
;-----------------------------------------------
mWriteSpace MACRO count:=<1>
LOCAL spaces
.data
spaces BYTE count DUP(' '), 0
.code
push edx
mov edx, OFFSET spaces
call WriteString
pop edx
ENDM
.data
msg BYTE "output space test", 0
.code
main PROC
mov edx, OFFSET msg
call WriteString
mWriteSpace 5
call WriteString
call Crlf
INVOKE ExitProcess, 0
main ENDP
END main
运行调试:
10.3.2节说明了如何使用宏形参的默认初始值。
8.mWriteString
宏mWriteSrting向控制台窗口输出一个字符串变量的内容。从宏的内部来看,它通过在同一语句行上传递字符串变量名简化了对WriteString的调用。例如:
.data
str1 BYTE "Please enter your name: ", 0
.code
mWriteString str1
实现 mWriteString的实现如下,它将EDX保存到堆栈,然后把字符串偏移量赋给EDX,在过程调用后,再从堆栈恢复 EDX 的值:
完整代码测试笔记
;10.2.5_8.asm 10.2.5 使用本书的宏库(仅32位模式)
;8.mWriteString宏代码实现及测试
INCLUDE Irvine32.inc
;-----------------------------------------------
;向标准输出写一个字符串变量。
;接收:字符串变量名。
;-----------------------------------------------
mWriteString MACRO buffer:REQ
push edx
mov edx, OFFSET buffer
call WriteString
pop edx
ENDM
.data
str1 BYTE "Please enter your name: ", 0
.code
main PROC
mov ebx, OFFSET str1
mWriteString str1
call Crlf
INVOKE ExitProcess, 0
main ENDP
END main
运行调试:
10.2.6 示例程序:封装器
现在创建一个简短的程序 Wraps.asm 来展示之前已介绍的作为过程封装器的宏。由于每个宏都隐含了大量繁琐的参数传递,因此程序出奇得紧凑。假设这里所有的宏当前都在Macros.inc 文件内:
;Wraps.asm 10.2.6 示例程序:封装器
;本程序演示宏作为库过程的封装器。
;内容:mGotoxy、mWrite、mwriteString、mReadString和mDumpMem。
INCLUDE Irvine32.inc
INCLUDE Macros.inc
.data
array DWORD 1,2,3,4,5,6,7,8
firstName BYTE 31 DUP(?)
lastName BYTE 31 DUP(?)
.code
main PROC
mGotoxy 0, 0
mWrite <"Sample Macro Program", 0dh, 0ah>
;输入用户名。
mGotoxy 0, 5
mWrite "Please enter your first name: "
mReadString firstName
call Crlf
mWrite "Please enter your last name: "
mReadString lastName
call Crlf
;显示用户名。
mWrite "Your name is "
mWriteString firstName
mWriteSpace
mWriteString lastName
call Crlf
;显示整数数组。
mDumpMem OFFSET array,LENGTHOF array, TYPE array
INVOKE ExitProcess, 0
main ENDP
END main
程序输出 程序输出的示例如下:
10.2.7 本节回顾
1.(真/假):当一个宏被调用时,CALL 和RET 指令将自动插人到汇编程序中。
答:假
2.(真/假):宏展开由汇编器的预处理程序控制。
答:真
3.与不使用参数的宏相比,使用参数的宏有哪些主要优势?
答:带参数的宏更容易重用。
4.(真/假):只要宏定义在代码段中,它就既能出现在宏调用语句之前,也能出现在宏调用语句之后。
答:假
5.(真/假):对一个长过程而言,若用包含这个过程代码的宏来代替它,则多次调用该宏通常就会增加程序的编译代码量。
答:真
6.(真/假):宏不能包含数据定义。
答:假