15.2 用 NASM 宏将 Pcode 命令翻译成 x86 指令 ( print 命令)
上面简单介绍了 NASM 以及它的强大的宏命令,可以实现复杂多样的宏展开。从本节开始,将编写一系列的宏定义,将中间代码 Pcode 命令展开为 x86 汇编指令。建议读者先回顾一下第 3、4 章的内容,再来看本章接下来的内容。
在开始编写宏定义之前,首先说明一下两个约定: (1) 所有 Pcode 命令以及相应的宏名称都小写( FUNC / ENDFUNC 命令除外),而所有 x86 汇编指令都大写; (2) 本书中的 x86 汇编指令,均用 NASM 语法书写。
首先翻译 Pcode 命令中的 print 命令。上一节中定义的 print 宏已经和 Pcode 中的 print 命令在使用的格式上是一模一样的了,但是它还不能实现 %d 格式化输出。我们最终的 print 宏需要使下面这段代码( print.nasm )的输出为 “a = 1, b = 2, c = 3” :
- PUSH DWORD 1
- PUSH DWORD 2
- PUSH DWORD 3
- print "a = %d, b = %d, c = %d"
- exit 0
直接看代码吧。宏文件 macro.inc :
- %MACRO print 1
- [SECTION .DATA]
- %%STRING: DB %1, 10, 0
- [SECTION .TEXT]
- PUSH DWORD %%STRING
- CALL PRINT
- SHL EAX, 2
- ADD ESP, EAX
- %ENDMACRO
- %MACRO exit 1
- MOV EAX, 1
- MOV EBX, %1
- INT 0x80
- %ENDMACRO
- EXTERN PRINT
- GLOBAL _start
- [SECTION .TEXT]
- _start:
从宏文件可以看出, print 宏将会被展开为一个 PUSH 命令,一个函数调用命令(CALL PRINT),以及清栈的命令。具体的输出工作将由 PRINT 函数来处理,同时 PRINT 函数还需要返回字符串中含有的 %d 的个数,这样函数调用完毕后可以根据返回值(保存在 EAX 中)来进行清栈(这就是 “SHL EAX, 2” 和 “ADD ESP, EAX” 的作用)。
PRINT 函数可以用 C 语言来编写,然后编译成库文件,最后和目标文件一起链接成可执行文件。PRINT 函数源代码如下( tio.c ):
- void SYS_PRINT(char *string, int len);
- #define BUFLEN 1024
- int PRINT(char *fmt, ...)
- {
- int *args = (int*)&fmt;
- char buf[BUFLEN];
- char *p1 = fmt, *p2 = buf + BUFLEN;
- int len = -1, argc = 1;
- while (*p1++) ;
- do {
- p1--;
- if (*p1 == '%' && *(p1+1) == 'd') {
- p2++; len--; argc++;
- int num = *(++args), negative = 0;
- if (num < 0) {
- negative = 1;
- num = -num;
- }
- do {
- *(--p2) = num % 10 + '0'; len++;
- num /= 10;
- } while (num);
- if (negative) {
- *(--p2) = '-'; len++;
- }
- } else {
- *(--p2) = *p1; len++;
- }
- } while (p1 != fmt);
- SYS_PRINT(p2, len);
- return argc;
- }
- void SYS_PRINT(char *string, int len)
- {
- __asm__(
- ".intel_syntax noprefix\n\
- PUSH EAX\n\
- PUSH EBX\n\
- PUSH ECX\n\
- PUSH EDX\n\
- \n\
- MOV EAX, 4\n\
- MOV EBX, 1\n\
- MOV ECX, [EBP+4*2]\n\
- MOV EDX, [EBP+4*3]\n\
- INT 0X80\n\
- \n\
- POP EDX\n\
- POP ECX\n\
- POP EBX\n\
- POP EAX\n\
- .att_syntax"
- );
- }
用以下命令将 tio.c 编译成库文件 libtio.a 。
- gcc -m32 -c -o tio.o tio.c
- ar -crv libtio.a tio.o
再将 print.nasm 汇编成目标文件 print.o 。
- nasm -f elf32 -P"macro.inc" -o print.o print.nasm
最后将 print.o 链接为可执行文件 print ,链接时指定 tio 库(库文件为 libtio.a ),命令如下:
- ld -m elf_i386 -o print print.o -L. -ltio
运行 print 将输出 “a = 1, b = 2, c = 3” 。
以上文件中, print.nasm 中的 CALL PRINT (由 print 宏展开得到)将调用定义在 tio.c 中的 PRINT 函数。在 Linux(32位) 的汇编编程中,如果一个文件需要调用由外部文件定义的函数,那么需要遵循以下约定:
(1) 本文件中需有 EXTERN funcname ,表示需要引用外部函数(函数名为 funcname );
(2) 函数的参数通过栈传递,且按从右向左的顺序入栈,函数的第一个参数要最后一个入栈;
(3) 函数开头的汇编指令为 “PUSH EBP; MOV EBP, ESP” , 函数结尾的汇编指令为 “MOV ESP, EBP; POP EBP; RET” ,因此,在函数体内,第一个参数保存在 EBP+8 处,第二个参数保存在 EBP+12 ,第三个参数保存在 EBP+16 ,以此类推, … 。
(4) 入栈的参数由调用者负责出栈。
下面结合这四个约定来详细的说明一下 print 宏是如何模拟出 Pcode 中 print 命令的效果的:
(1) 首先,在 macro.inc 中定义了 print 宏,因此 print.nasm 中的代码:
- PUSH DWORD 1PUSH DWORD 2PUSH DWORD 3print "a = %d, b = %d, c = %d"
将会被展开为下面的形式:
- [SECTION .TEXT] PUSH DWORD 1 PUSH DWORD 2 PUSH DWORD 3 PUSH DWORD %%STRING CALL PRINT SHL EAX, 2 ADD ESP, EAX[SECTION .DATA] %%STRING: DB "a = %d, b = %d, c = %d", 10, 0
(2) 以上代码中的 “CALL PRINT” 将调用定义在 tio.c 中的 PRINT 函数,该函数原型为 int PRINT(char fmt, …) ,其中第一个参数 fmt 就是最后一个入栈的参数,也就是字符串 “a = %d, b = %d, c = %d\n\0” 的起始地址。
(3) PRINT 函数中的第一行 int args = (int)&fmt 得到 fmt 的地址(注意:不是 fmt 的值),因此, args+1 就是倒数第二个入栈的参数的地址, (args+1) 就是该参数的值(此处为 3 ), (args+2) 就是倒数第三个入栈的参数的值(此处为 2 ), … 。
(4) PRINT 函数首先找到字符串 fmt 的结尾,然后从结尾一直向前扫描该字符串,如果扫描到普通字符,则直接拷贝到 buf 数组中,如果扫描到一个 “%d” ,则执行 num = (++args) 得到相应的参数的数值,然后将此数值转换为字符串并拷贝到 buf 数组中,按此原则一直扫描到字符串的开头,最后将 buf 数组中的内容打印到终端。
(5) 打印完毕后,PRINT 函数返回 fmt 中含有的 “%d” 的个数(保存在 EAX 中),因此, “CALL PRINT” 后面的 “SHL EAX, 2” 和 “ADD ESP, EAX” 会将所有的入栈的参数都出栈。