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” :

  1. PUSH DWORD 1
  2. PUSH DWORD 2
  3. PUSH DWORD 3
  4. print "a = %d, b = %d, c = %d"
  5.  
  6. exit 0

直接看代码吧。宏文件 macro.inc

  1. %MACRO print 1
  2. [SECTION .DATA]
  3. %%STRING: DB %1, 10, 0
  4. [SECTION .TEXT]
  5. PUSH DWORD %%STRING
  6. CALL PRINT
  7. SHL EAX, 2
  8. ADD ESP, EAX
  9. %ENDMACRO
  10.  
  11. %MACRO exit 1
  12. MOV EAX, 1
  13. MOV EBX, %1
  14. INT 0x80
  15. %ENDMACRO
  16.  
  17. EXTERN PRINT
  18. GLOBAL _start
  19.  
  20. [SECTION .TEXT]
  21. _start:

从宏文件可以看出, print 宏将会被展开为一个 PUSH 命令,一个函数调用命令(CALL PRINT),以及清栈的命令。具体的输出工作将由 PRINT 函数来处理,同时 PRINT 函数还需要返回字符串中含有的 %d 的个数,这样函数调用完毕后可以根据返回值(保存在 EAX 中)来进行清栈(这就是 “SHL EAX, 2” 和 “ADD ESP, EAX” 的作用)。

PRINT 函数可以用 C 语言来编写,然后编译成库文件,最后和目标文件一起链接成可执行文件。PRINT 函数源代码如下( tio.c ):

  1. void SYS_PRINT(char *string, int len);
  2.  
  3. #define BUFLEN 1024
  4.  
  5. int PRINT(char *fmt, ...)
  6. {
  7. int *args = (int*)&fmt;
  8. char buf[BUFLEN];
  9. char *p1 = fmt, *p2 = buf + BUFLEN;
  10. int len = -1, argc = 1;
  11.  
  12. while (*p1++) ;
  13.  
  14. do {
  15. p1--;
  16. if (*p1 == '%' && *(p1+1) == 'd') {
  17. p2++; len--; argc++;
  18. int num = *(++args), negative = 0;
  19.  
  20. if (num < 0) {
  21. negative = 1;
  22. num = -num;
  23. }
  24.  
  25. do {
  26. *(--p2) = num % 10 + '0'; len++;
  27. num /= 10;
  28. } while (num);
  29.  
  30. if (negative) {
  31. *(--p2) = '-'; len++;
  32. }
  33. } else {
  34. *(--p2) = *p1; len++;
  35. }
  36. } while (p1 != fmt);
  37.  
  38. SYS_PRINT(p2, len);
  39.  
  40. return argc;
  41. }
  42.  
  43. void SYS_PRINT(char *string, int len)
  44. {
  45. __asm__(
  46. ".intel_syntax noprefix\n\
  47. PUSH EAX\n\
  48. PUSH EBX\n\
  49. PUSH ECX\n\
  50. PUSH EDX\n\
  51. \n\
  52. MOV EAX, 4\n\
  53. MOV EBX, 1\n\
  54. MOV ECX, [EBP+4*2]\n\
  55. MOV EDX, [EBP+4*3]\n\
  56. INT 0X80\n\
  57. \n\
  58. POP EDX\n\
  59. POP ECX\n\
  60. POP EBX\n\
  61. POP EAX\n\
  62. .att_syntax"
  63. );
  64. }

用以下命令将 tio.c 编译成库文件 libtio.a 。

  1. gcc -m32 -c -o tio.o tio.c
  2. ar -crv libtio.a tio.o

再将 print.nasm 汇编成目标文件 print.o 。

  1. nasm -f elf32 -P"macro.inc" -o print.o print.nasm

最后将 print.o 链接为可执行文件 print ,链接时指定 tio 库(库文件为 libtio.a ),命令如下:

  1. 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 中的代码:

  1. PUSH DWORD 1PUSH DWORD 2PUSH DWORD 3print "a = %d, b = %d, c = %d"

将会被展开为下面的形式:

  1. [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” 会将所有的入栈的参数都出栈。