NASM (Netwide Assembler),是一个 8086 和 x86-64 平台的汇编器,支持多种文件格式( a.out、ELF、Mach-O 和 COFF)。还可以输出二进制文件(bin)和 Intel 汇编十六进等格式,这一篇用来记录 NASM 的基本知识。
常用选项
NASM 使用如下格式来进行汇编
nasm -f <format> <filename> [-o <output>]
比如
nasm -f bin myfile.asm -o myfile.com
会将源文件 myfile.com 汇编为二进制文件 myfile.com。
-o
默认下 NASM 会根据 -f 选项选择合适的文件名,如 微软文件格式 (obj, win32 和 win64)会删除 .asm 后缀并添加 .ojb 后缀;对于 Unix 目标格式( aout、elf32 和 elf64 等)会使用 .o 进行替换;对于 bin 格式会直接删除 .asm 后缀名。
若工作目录下有同名文件 NASM 会直接覆盖,可以使用 -o 选项改变此行为。
-f
NASM 默认 bin 为输出文件格式。并且类似于 -o 选项,可以忽略选项和值之间的空格
nasm -fbin demo.asm -oprogram
-l
如果使用了 -l 选项,并且跟着一个文件名,NASM 会输出一个源文件十六进制格式的列表文件,其中地址和代码在左,源码(包括宏)在右。
nasm -f elf myfile.asm -l myfile.lst
可以在源代码中使用 [list -] 来去掉不需要显示的段。
-M
该选项可以向标准输出产生 makefile 依赖关系,可以重定位到一个文件中做进一步处理。
-F
类似于 -f 选项,但是产生的是附带指定调试格式的输出文件。使用
nasm -F <format> -y
来得到具体格式的可调试文件。
-i
NASM 允许源文件中有 %include
操作符,制定后不仅会在当前工作目录下搜索文件想要导入的文件,还会使用 -i 列出的路径。注意,NASM 只会将 -i 后面的路径加上 %include
构成完整文件名,所以在最后添加斜杠或者反斜杠是必要的(新版本会在 -i 选项后自动加上 /)。
-d, -u
-d 预定义一个宏,等价于 %define
操作符,可以方便 debug。相反,-u 取消一个宏。
-E
该选项命令 NASM 仅预处理,并将输出打印在标准输出上,-o 选项指定时保存为文件。但是,该选项在源文件需要计算表达式时会出错。
assign tablesize ($-tablestart)
会在 -e 选项下出错。
-On
指定多遍优化。如果有些复杂源文件需要多于两边的汇编,需要使用该选项。
-w
使汇编警告信息有效或者无效。
NASMENV 环境变量
系统中如果定义了环境变量 NASMENV,汇编器会将其视为对应值的命令行参数。通过在值开头写一个非减号字符,NASM 会将其视为选项间的分隔符,如
!-s !-ic:\nasmlib
\ 等价于 -s -ic:\nasmlib\
NASM 指令
NASM 指令格式如下
label: instruction operands ;comment
- NASM 使用 / 来作为续行符。
- NASM 中 label 前后可以添加空格,冒号也是可选的
- label 中的字符为字母、数字、-、$、#、@、~、.、和 ?,只有字母、.、下划线和问号可以开头。使用 $ 开头则会将其作为标识符而不是保留字处理。( $eax 便是合法的标识符)。
- 对于指令,可以使用指令前缀 LOCK、REP、REPE/REPZ、REPNE/REPNZ。也可以添加段寄存器作为前缀,如
es mov [bx], ax ; ==> mov [es:bx] ax
- 操作数可以使用寄存器(eax,非 gas 格式)、有效地址、常数或者表达式。
- 对于 x87 浮点指令集,NASM 支持多种语法
fadd st1 ; st0 := st0 + st1 fadd st0, st1 ; 同上 fadd to st1 ; st1 := st0 + st1
伪指令
NASM 支持了一部分伪指令
- 初始化数据: DB、DW、DD、DQ、DT
db 0x55 ; byte 0x55 db 'hello', 13, 10, '$' ; char constants dw 0x1234 ; 0x34, 0x12( in order) dd 1.234567e20 ; single float dq 1.234567e20 ; double float dt 1.234567e20 ; extended-presion float
注意 dq 和 dt 不支持数值常数或者字符串常数为操作数。
为了方便存放连续的重复数据,NASM 还提供一种 DUP 修饰,如
db %('AA') db 6 dup dword ('a', word 'b', 'c'); 'a','c'以 四字节存放,'b' 以两字节存放
- 存放未初始化数据:RESB、RESW、RESD、RESQ 和 REST
buffer : resb 64 ; reserve 64 bytes word : resbw 1 ; reserve 1 word
- 包含二进制文件:INCBIN
将一个二进制文件逐字包含到输出文件中,如
incbin "file.data" ; import whole file incbin "file.data" 1024, 512 ; skip first 1024 bytes and import at most 512 btyes
- 定义常数:EQU
equ 指令用来定义一个符号,该指令要求源文件该行必须包含一个 label,如
message db 'hello world' msglen equ $-message
上面两句指令使得 msglen 定义为常量 12,并且不能再被重定义。
-
重复:TIMES
TIMES 前缀可以使得某一句指令被汇编多次,如
zerobuf: times 64 db 0
将连续占用 64 字节。类似与 equ、resb,times 的操作数也是关键表达式(critical expressions)。类似的功能还有预处理指令
%rep
TIMES 不可被用在宏上,原因是 times 在预处理后才被解析。
有效地址
NASM 中地址使用比较灵活,可以在中括号内使用一些代数表达式,比如
wordvar dw 123 ; reserve a word
mov ax, [wordwar] ; as --> [wordwar]
mov ax, [es:wordwav + bx] ;
mov ax, [ax * 5] ; suppoesed to be ax * 4 + ax
mov ax, [ label1 * 2 - label2] ; label1 + (label1 - label2)
许多有效地址实际上等价,NASM 会自动产生最小化形式,如
[eax * 2 + 0] ; == [eax+eax], NASM 会输出后一种
;可以使用 nosplit 关键词来强制 NASM 不做拆分
[nosplit eax*2]; != [eax+eax]
如果向强制输出定长的地址偏移,可以使用 BTYE、WORD、DWOR。如
mov ax, [dword eax + 3]
[eax] 和 [byte eax] 输出并不同,原因是后者会产生一个一个字节偏移 0。如果在 16 位模式下使用了一个 16 位地址,如果不是用长短关键字,则会丢失高位部分。
在 64 位模式中,NASM 默认产生的是绝对地址,可以使用 REL 关键字来产生 IP 相对地址。
常数
NASM 有四种不同类型常数:数字、字符、字符串和浮点数。
- 数字。可以使用 H、Q、B 来指定十六进制、八进制和二进制数。或者使用 0x 表示十六进制,或者使用 前缀表示十六进制数(使用 前缀需要紧跟数字),可以使用下划线来分隔过长的数字常量。
mov ax, 0c8h
- 字符常数。使用单引号(双)最多包含八个字符,允许中间出现双引号。多个字符同时存在时,会按照小端法存取。
mov eax, 'abcd'; --> 0x64636261
C 风格的转义字符也是支持的
- 字符串常量。字符串常量是出现在伪指令中的字符串,如 dx 指令。此外,NASM 还支持 Unicode 字符串。
-
浮点数常量
浮点数还有一些其它需要解释的语法。上面使用了 dd 和 dq 来分别存储单精度和双精度的浮点数,然而还可以使用 db(1:4:3) 和 dw( IEEE 754r half precision)来存储,代价是精度较小,可以使用的格式如下
dw -0.5
dd 1.222_222_222
dq 0x1p+32 ; 1^32
dq 1.e-10 ; 1^(-10)
可以使用一些特殊的运算符来编码特定长度的浮点数,如
mov rax, __?float64?__(3.141592653589793238462)
上面可以生成 64 位长度的浮点数,还有 __?float128h?__ 和 __?float128l?__ 分别生成 128 位浮点数的高 64 位和低 64 位。此外,还有符号 __?Infinity?__ 、__?QNan?__、__?SNan?__ 来分别生成无穷大、QNAN(未定义算数结果)和SNAN(未初始化),这种标记经常用于宏定义。
需要说明,NASM 由于可移植性不支持对浮点数进行运算。
表达式
NASM 支持和 C 类似的表达式记法,汇编时会计算为 64 bit 的整数,然后存储时会被截断为合适的长度,位运算和逻辑运算同样是支持的。
SEG 和 WRT
编写 16 位程序时,能够得到不同符号的段基址和段偏移是很有用的,NASM 提供了 SEG 和 WRT 运算符来获取上述地址。
mov ax, seg symbol
mov es, ax
mov bx, symbol
上述代码会使得 ES:BX 指向符号 symbol。如果想要该符号在另一个段下的偏移,可以使用 WRT(with reference to)
mov ax, symbol2
mov es, ax
mov bx, symbol wrt symbol2
这样段基址不同,但是最终还是指向同一个符号。
STRICT
当开启多遍优化时(O2 或者更高级优化),NASM 会使用最小的大小而不是指定的关键字来对数据进行编码,可以使用 STRICT 来强制生成指定的大小。
push strict dowrd 33; 带有优化选项时,不使用 strict 会编码为 3 个字节,使用 strict 会编码为 6 个字节
关键表达式
NASM 拥有一个可以进行多遍的优化器,可以是第一遍计算各个标签的地址,第二遍开始编址,等等。然而,对于某些指令来说,需要在第一遍时就需要计算出结果,这里使用的表达式我们便称为关键表达式。
times (label-$+1) db 0
label: db 'Where ?'
上面 times 伪指令操作数引用了一个对于该行还暂时不知道的地址 label,因此 NASM 会直接拒绝这样的写法。
本地标记
NASM 使用一种以句点 . 为前缀的本地标签声明的方法,比如
label1 ;
...
.label
...
jne .label
ret
label2 ;
...
.label
...
jne .label
ret
上面的 jne 指令都只会跳转到对应的 label 标签。