我的汇编学习之路(5)

这是关于x86_64汇编的第五章,这一章主要说宏。

NASM支持两种形式的宏:

  • 单行
  • 多行

所有单行宏必须从%define指令开始。格式如下:

1
%define macro_name(parameter) value

Nasm宏的行为和外观与C类似。例如,我们可以创建以下单行宏:

1
2
%define argc rsp + 8
%define cliArg1 rsp + 24

在代码中使用它:

1
2
3
4
5
6
;;
;; argc will be expanded to rsp + 8
;;
mov rax, [argc]
cmp rax, 3
jne .mustBe3args

多行宏以%macro指令开始,并以%endmacro结尾。一般形式如下:

1
2
3
4
5
%macro number_of_parameters
instruction
instruction
instruction
%endmacro

例如:

1
2
3
4
%macro bootstrap 1
push ebp
mov ebp,esp
%endmacro

我们可以用它:

1
2
_start:
bootstrap

例如我们来看看PRINT宏:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
%macro PRINT 1
pusha
pushf
jmp %%astr
%%str db %1, 0
%%strln equ $-%%str
%%astr: _syscall_write %%str, %%strln
popf
popa
%endmacro

%macro _syscall_write 2
mov rax, 1
mov rdi, 1
mov rsi, %%str
mov rdx, %%strln
syscall
%endmacro

我们来试试看宏,了解它的工作原理:在第一行我们用一个参数定义了PRINT宏。比推送所有通用寄存器(用pusha指令)和标志寄存器(用pushf指令)。之后,我们跳到%% astr标签。注意宏中定义的所有标签必须以%%开头。现在我们转到__syscall_write具有2参数的宏。我们来看__syscall_write的实现。其实就是之前博客用到的将字符串写入系统调用打印到stdout的代码。看起来像这样:

1
2
3
4
5
6
7
8
9
10
;; write syscall number
mov rax, 1
;; file descriptor, standard output
mov rdi, 1
;; message address
mov rsi, msg
;; length of message
mov rdx, 14
;; call write syscall
syscall

在我们的__syscall_write宏中,我们定义了前两个指令,用于将1置为rax(写系统调用号)和rdi(stdout文件描述符)。然后我们把%%str放到rsi寄存器(指向字符串的指针),其中%%str是PRINT宏的第一个参数(用$parameter_number的宏参数访问)并以0结尾的本地标签(每个字符串必须以零结束)。而%%strln则计算字符串长度。之后,我们使用syscall指令调用系统调用,这就是所有。
现在我们可以用它:

1
label: PRINT "Hello World!"

有用的标准宏

NASM支持以下标准宏:

STRUC

我们可以使用STRUCENDSTRUC定义数据结构。例如:

1
2
3
4
struc person
name: resb 10
age: resb 1
endstruc

接下来我们就能创建一个数据结构实例:

1
2
3
4
5
6
7
8
9
section .data
p: istruc person
at name db "name"
at age db 25
iend

section .text
_start:
mov rax, [p + person.name]

%include

我们可以包括其他汇编文件,并跳转到带有%include指令的标签或调用函数。

翻译 https://0xax.github.io/asm_5/
文中的PRINT宏是有问题的,所以参考看看就好
修正的代码见我的repo https://github.com/ejunjsh/asm-code