- 不同的汇编语言
- 准备阅读:基础tutorial
- Chap.1 简介
- Chap.2 运行NASM
- Chap.3 NASM Language
- Chap.4 The NASM Preprocessor
- Chap.5 Standart Macros
- Chap.6 Standart Macro Packages
- Chap.7 Assembler Directives
- Chap.8 Output Formats
不同的汇编语言
在汇编领域没有标准的语言规范,各个不同的汇编语言编译器都有自己的一套约定。
MASM
Microsoft Assembler,微软开发的汇编器,主要用于Windows平台上的开发。MASM是指用于16-bit/32-bit平台的汇编器,而ML64则是MASM在64-bit平台上的汇编器。
NASM/YASM
Netwide Assembler,Linux平台下流行的汇编器,主要用于Linux平台的开发,也可以用于Windows平台。
YASM完全兼容NASM,是在BSD协议下的完全重写。
NASM的文档:https://www.nasm.us/doc/,NASM的汇编目标是x86系列处理器。
FASM
Flat Assembler,另一个汇编器,在Windows和Linux下都可用,但并不是这两个平台的默认汇编器。
准备阅读:基础tutorial
在正式开始阅读NASM文档前,可以跟一下这篇Tutorial:https://cs.lmu.edu/~ray/notes/nasmtutorial/。
Chap.1 简介
NASM,全称Netwide Assembler,是80x86和x86-64汇编器。它支持多种目标文件格式,包括:
- ELF;
- Mach-O;
- COFF (包括Win32和Win64变种)
NASM语言格式被设计的简洁易懂,贴近Intel手册中的语法。NASM支持所有已知的x86架构扩展,并对宏有完善支持。
Chap.2 运行NASM
https://www.nasm.us/doc/nasmdoc2.html
NASM命令行语法
(略)
MASM用户Quick Start指南
这一小节的目标读者是熟悉MASM的开发者,如果你不熟悉MASM,或许可以跳过这一章。
(略)
Chap.3 NASM Language
Layout of a NASM Source Line
和大多数汇编器一样,NASM Source Line是四个字段的组合(除非这一行是macro、preprocessor指令、assembler指令,参考chap.4和chap.7):label: instruction operands ; comment
上述label, instruction, comment字段都是可选的,其中operands是否可选取决于instruction的要求。
当Source Line无法在一行完成书写时,可以使用反斜杠开启新行,如果反斜杠后紧随着换行符,则紧接着的下一行会被认为属于上一行的一部分。
NASM对空白符(空格、tab)没有严格的要求,label前面可以有任意多的空白符,instruction前面的空白符可以省略。
label后面的冒号也可以省略,但是非常不建议这么做,因为如果开发者的本意是写一个instruction without operands,但是instruction拼错了,就会导致NASM误把这一行当作是只有label的source line,如果NASM严格要求在每个label后面添加冒号就不会有这类问题了,NASM提供了 -w+orphan-labels
命令行选项来支持对label之后的冒号的严格检查,此时每个label后面必须要跟一个冒号。
NASM的合法label字符包括字母、数字、 _$#@~.?
,label必须以字母或 ._?
开始。在identifier的前面可以添加 $
前缀,用于表示该identifier不是一个reserved word。如果链接的其他模块中定义了一个名叫eax的符号,就必须使用$eax来引用该符号,直接使用eax会被NASM当作reserved word。
instruction字段可以包含任意的指令,包括Pentium和P6、FPU、MMX、甚至其他没有在文档中说明的指令。指令可以有前缀 LOCK、REP、REPE/REPZ、REPNE/REPNZ、XACQUIRE/XRELEASE或者BND/NOBND。
显式指定address-size和operand-size前缀也是支持的,这些前缀包括 A16, A32, A64, O16, O32, O64
,在Chap.11中给出了它们的使用场景。segment register指令前缀也得到支持: es mov [bx],ax
和 mov [es:bx],ax
是等价的,虽然一般建议将segment register写在操作数里,但对于某些没有操作数但又需要指定segment register的指令而言就必须将segment register写在指令前缀里,例如: es lodsb
。
使用指令前缀时,指令是可选的,诸如 CS, A32, LOCK, REPE
可以单独写在一行,NASM此时只会生成指令前缀对应的prefix bytes。
instruction除了可以写成指令集中规定的指令外,还可以写成NASM支持的一系列pseudo-instructions,这部分介绍参考下一小节。
instruction operands可以是多种格式之一:
- 由寄存器名称表示的寄存器,如ax, ebx, cr0,NASM没有采用gas风格的语法,因此寄存器名称不以%开头;
- 关于gas:gas是GNU提供的汇编器,其默认的汇编语法是AT&T风格的,而NASM默认的汇编语法是Intel风格的,如今大多数汇编器都同时支持两种风格的语法,但是要在运行时指定要使用的是哪种;
- address(参考下面的Effective Addresses小节)
- constant(参考下面的Constants小节)
- expression(参考下面的Expressions小节)
对于x87浮点数指令,NASM支持多种语法:
fadd st1 ; this sets st0 := st0 + st1
fadd st0,st1 ; so does this
fadd st1,st0 ; this sets st1 := st1 + st0
fadd to st1 ; so does this
Pseudo-Instructions
Pseudo-instruction,伪指令,它们虽然并不是x86指令集中规定的指令,但依然被规定应该写在instruction字段,因为这种写法是最方便的。当前的伪指令包括: DB, DW, DD, DQ, DT, DO, DY, DZ
,以及它们的不进行初始化的变种: RESB, RESW, RESD, RESQ, REST, RESO, RESY, RESZ
,以及 INCBIN
命令, EQU
命令,和 TIMES
前缀。
在本文档中,Dx和RESx用于代指所有的DB和RESB系列伪指令。
使用Dx来声明并初始化数据
DB, DW, DD, DQ, DO, DY, DZ被统称为Dx,用于声明已经初始化的数据。
初始化语法示例如下:
db 0x55 ; just the byte 0x55
db 0x55,0x56,0x57 ; three bytes in succession
db 'a',0x55 ; character constants are OK
db 'hello',13,10,'$' ; so are string constants
dw 0x1234 ; 0x34 0x12
dw 'a' ; 0x61 0x00 (it's just a number)
dw 'ab' ; 0x61 0x62 (character constant)
dw 'abc' ; 0x61 0x62 0x63 0x00 (string)
dd 0x12345678 ; 0x78 0x56 0x34 0x12
dd 1.234567e20 ; floating-point constant
dq 0x123456789abcdef0 ; eight byte constant
dq 1.234567e20 ; double-precision float
dt 1.234567e20 ; extended-precision float
从NASM 2.15开始,下面的eature就已经被支持了:
- ?用于声明未初始化的存储:
db ?
语法定义:
语法规范:
dx := DB | DW | DD | DQ | DT | DO | DY | DZ
type := BYTE | WORD | DWORD | QWORD | TWORD | OWORD | YWORD | ZWORD
atom := expression | string | float | '?'
parlist := '(' value [, value ...] ')'
duplist := expression DUP [type] ['%'] parlist
list := duplist | '%' parlist | type ['%'] parlist
value := atom | type value | list
stmt := dx value [, value...]
复杂语法示例:
db 33
db (44) ; Integer expression
; db (44,55) ; Invalid - error
db %(44,55)
db %('XX','YY')
db ('AA') ; Integer expression - outputs single byte
db %('BB') ; List, containing a string
db ?
db 6 dup (33)
db 6 dup (33, 34)
db 6 dup (33, 34), 35
db 7 dup (99)
db 7 dup dword (?, word ?, ?)
dw byte (?,44)
dw 3 dup (0xcc, 4 dup byte ('PQR'), ?), 0xabcd
dd 16 dup (0xaaaa, ?, 0xbbbbbb)
dd 64 dup (?)
在Dx语句中,$只可以用在两个地方表示当前地址:
- 用于statement的第一个expression中;
- 用于
value - $
这样的表达式中,这个表达式会被转换为self-relative relocation。
在Dx中,$$的使用没有限制。
使用RESx来声明未初始化数据
RESB, RESW, RESD, RESQ, REST, RESO, RESY, RESZ被统称为RESx,用于声明未初始化的存储空间。它们接受一个操作数,用于描述未初始化的存储单位个数。RESx的操作数是一个critical expression(参考本章的critical expression小节)。
虽然使用Dx伪指令也可以声明未初始化的存储空间,但是使用RESx更加方便,示例:
buffer: resb 64 ; reserve 64 bytes
wordvar: resw 1 ; reserve a word
realarray resq 10 ; array of ten reals
ymmval: resy 1 ; one YMM register
zmmvals: resz 32 ; 32 ZMM registers
;上面的RESx也可以使用Dx配合 DUP 和 ? 写成下面这样
buffer: db 64 dup (?) ; reserve 64 bytes
wordvar: dw ? ; reserve a word
realarray dq 10 dup (?) ; array of ten reals
ymmval: dy ? ; one YMM register
zmmvals: dz 32 dup (?) ; 32 ZMM registers
使用INCBIN引入外部二进制文件
incbin "file.dat" ; include the whole file
incbin "file.dat",1024 ; skip the first 1024 bytes
incbin "file.dat",1024,512 ; skip the first 1024, and
; actually include at most 512
使用EQU定义常量
EQU将一个符号定义为指定常量,必须配合label使用。EQU定义是绝对的,不可以在之后被修改。
示例:
message db 'hello, world'
msglen equ $-message
上面的代码将msglen定义为12,因为$表示当前地址,而前面的’hello, world’恰好占了12个字节,当前地址减去message地址,就是12。此外,$-message这个表达式会被立即计算,而不是每次使用时计算。
使用TIMES重复指令
TIMES这个伪指令前缀会将指令重复指定数量次,后面可以跟伪指令或者真实的指令。DUP和TIMES很像,但是DUP的操作数必须是一个立即数,而TIMES后面可以跟一个表达式,这使得TIMES使用上更加灵活,更具普适性。TIMES的操作数是一个critical expression。
示例:
;下面的两种写法一样
zerobuf: times 64 db 0
zerobuf2: db 64 dup (0)
;但是dup无法适用下面这种场景
;store exactly enough spaces to make the total length of buffer up to 64
buffer: db 'hello, world'
times 64-$+buffer db ' '
Effective Addresses
一个effective address就是一个指向内存的操作数。在NASM中,effective address的语法很简单:如果一个操作数是一对方括号包围着的表达式,那么这个操作数就是一个effective address。
示例:
wordvar dw 123
mov ax,[wordvar]
mov ax,[wordvar+1]
mov ax,[es:wordvar+bx]
NASM默认会对effective address进行优化,使用尽可能空间上紧凑的方式来表示该effective address。
可以使用byte, word, dword关键字,来让NASM生成一个指定宽度的offset。例如, [dword eax+3]
表示希望NASM使用宽度为dword的offset 3, [byte eax]
表示虽然effective address本来不需要offset,但还是希望NASM生成一个宽度为byte的offset 0并使用。
在使用mixed-size addressing(参考Chap.11)时可能会用到上面描述的技巧。
此外,NASM中还支持使用nosplit关键字来完全禁用effective address优化:
;原本[eax*2]会被优化为[eax+eax],但在nosplit的作用下会被禁用优化,相当于[eax*2+0]
[nosplit eax*2]
在64-bit模式下NASM默认生成绝对地址,REL关键字可用于让NASM生成RIP-relative address。因为相对地址是个很普遍的需求,NASM提供了DEFAULT指令允许开发者指定默认的地址生成模式(参考Chap.7)。
Constants
Numberic Constants
数字常量就是数字。NASM中可以使用一系列进制,其中:
- 表示十六进制:
0x前缀
,$前缀
,0h前缀
; - 十进制:
0d前缀
,0t前缀
; - 八进制:
0o前缀
,0q前缀
; - 二进制:
0b前缀
,0y前缀
;
上面虽然只列出了前缀,但是所有字母前缀同时也是合法的后缀,若在数字的结尾是上面的字母之一,则也可用于表示数字的进制。
Some examples (all producing exactly the same code):
mov ax,200 ; decimal
mov ax,0200 ; still decimal
mov ax,0200d ; explicitly decimal
mov ax,0d200 ; also decimal
mov ax,0c8h ; hex
mov ax,$0c8 ; hex again: the 0 is required
mov ax,0xc8 ; hex yet again
mov ax,0hc8 ; still hex
mov ax,310q ; octal
mov ax,310o ; octal again
mov ax,0o310 ; octal yet again
mov ax,0q310 ; octal yet again
mov ax,11001000b ; binary
mov ax,1100_1000b ; same binary constant
mov ax,1100_1000y ; same binary constant once more
mov ax,0b1100_1000 ; same binary constant yet again
mov ax,0y1100_1000 ; same binary constant yet again
Character Strings
Character Strings没有对应的中文表示,这里先用“字符字符串”代指。
一个字符字符串由最多八个字符组成,是用一个字符串表示的一个特定字符,字符字符串可以用单引号、双引号、反引号包围。使用反引号时,可以利用C-style的转义字符。
\u263a
、 \xe2\x98\xba
、 0E2h, 098h, 0BAh
都表示一个utf-8笑脸:☺︎。
Character Constants
一个character constant最多8个字节宽,被用在表达式上下文中,并被当作整数处理。
一个超过1字节的character constant会被以little endian的方式表示在内存中,这意味着一个’abcd’ character constant的内存中的数字表示不是0x61626364,而是0x64636261。little endian将most significant byte放在低地址。在写出像 mov eax,'abcd'
这样的代码时一定要注意字节序问题。
String Constants
String constants用在一些伪指令上下文中,例如Dx和INCBIN;也被用在某些preprocessor directive中。
string constants和character constants差不多,只不过更长。区分它们的唯一方式是检查上下文要求的是character constant还是string constant。
示例:
db 'hello' ; string constant
db 'h','e','l','l','o' ; equivalent character constants
;And the following are also equivalent:
dd 'ninechars' ; doubleword string constant
dd 'nine','char','s' ; becomes three doublewords
db 'ninechars',0,0,0 ; and really looks like this
当定义的是四字节的double word时,由于’nine’, ‘char’, ‘s’分别占4字节、4字节、1字节,最后有三个字节没有填充满,这剩余的字节会自动的被0填充。
Unicode Strings
当需要使用UTF编码的Unicode字符时,使用特殊操作符: __?utf16?__, __?utf16le?__, __?utf16be?__, __?utf32?__, __?utf32le?__, __?utf32be?__
可以将Unicode字符转换为对应UTF16或者UTF32编码。
Floating-Point Constants
Packed BCD Constants
(略)
Expressions
NASM中的表达式和C的语法差不多,表达式在NASM中都是先用64-bit进行计算,然后再转回原本的大小。
NASM支持两种特殊的token: $
和 $$
。其中,$表示当前行的位置,$$表示当前section的位置。表达式 $-$$
表示当前行距离当前section的位置。
NASM中非0值被计算为true,0被计算为false,一个返回bool值的函数应该返回1表示true,返回0表示false。
下面是NASM支持的算数运算符:
? ... : ...
- 三目运算符,和C的三目运算符语法一致;||
- 或&&
- 与^^
- 异或,xor|
比特或&
比特与^
比特异或<<
<<<
>>
>>>
比特挪动,当使用三个符号的版本时,向右侧挪动比特时会保留符号位- 比较运算符
=
==
- 相等!=
<>
- 不等<
>
<=
>=
- 大小比较
<==>
是特殊的比较运算符, 在左边的数较小时返回-1,相等时返回0,左边的数较大时返回1- 数字计算运算符
+
-
- 普通的加减法*
/
//
%
%%
- 单目运算符
-
+
~
!
SEG
其他由下划线作前缀或后缀的运算符,用于整数操作,参考Chap.6
SEG和WRT
(略)
STRICT:Inhibiting Optimization
(略)
Critical Expressions
Critical Expressions是必须能够one-pass计算完毕的表达式。
NASM的汇编过程two-pass的,在first pass中要先确定所有汇编代码的尺寸和每个变量引用的值,然后在second pass中再根据first pass得到的信息填充指令。如果在第一步中某个指令的表达式无法确定,NASM就无法正确完成汇编。
一种最常见的违反NASM汇编规则的情况就是:在汇编代码中,前面的代码引用了后面定义的label,此时NASM会直接报错退出:
;times这一行引用了未知的label符号,NASM会报错并退出
times (label-$) db 0
label: db 'Where am I?'
Local Labels
NASM对于名称以 .
开头的label给予特殊关照,这样的label被视作local label,local label和前面的上一个non local label关联,可以理解为每一个non local label都会建立一个新的local label作用域。
示例:
label1 ; some code
.loop
; some more code
jne .loop
ret
label2 ; some code
.loop
; some more code
jne .loop
ret
除了可以在当前local label作用域内访问同一作用域的local label外,还可以通过non local label访问其他local label作用域内定义的local label。
示例:
label3 ; some more code
; and some more
jmp label1.loop
如果想要定义一个不产生local label作用域的non local label,可以使用 ..@
前缀命名label。
示例:
label1: ; a non-local label
.local: ; this is really label1.local
..@foo: ; this is a special symbol
label2: ; another non-local label
.local: ; this is really label2.local
jmp ..@foo ; this will jump three lines up
Chap.4 The NASM Preprocessor
NASM中的所有预处理指令都以%开头。
Single-Line Macros
%define定义宏
使用%define定义case-sensitive的macro;
使用%idefine定义case-insensitive的macro。
macro是在调用时展开的,因此在macro中可以调用后面才定义的macro,因为在定义时不会执行展开,这也意味着如果调用的宏在后面忘记定义了,错误会直到调用时才被报告,如果不调用就没错误。在宏调用展开时,NASM会检查展开后的代码是否还会导致前面已经展开过的宏**被传入完全相同的参数**再次被展开,如果发生了这种情况NASM会停止宏调用展开,这种特性的使用场景参考Chap.10。
如果宏定义时定义了参数列表,则不同的参数个数的宏定义间可以overload,真正调用时会调用到和参数个数匹配的宏。但是,如果宏定义时没有定义参数列表,则这样的宏是不可overload的。
宏总是可以被override的,最后定义的宏会override前面的宏定义。
可以在定义宏的参数列表时,不写出参数名称,此时定义的依然是一个带参数的宏,虽然调用时还要传入参数,但参数不会被使用,示例如下:
;定义一个宏ereg,这个宏定义了两个参数
%define ereg(foo, ) e %+ foo
;定义一个宏myreg,这个宏定义了一个参数
%define myreg() eax
此外,宏定义时根据参数声明的前缀,NASM还提供以下特性:
- 如果前缀是 = ,NASM会将传入的内容视作表达式求值,该参数值为表达式求值结果;
- 如果前缀是 & ,NASM会将传入的内容转换为字符串,该参数值为内容字符串;
- 如果前缀是 + ,NASM会将参数视作变长模板参数,作贪婪匹配,这个参数最后会被展开为包含逗号、空白符的代码;
- 如果前缀是 ! ,NASM不会将括号和空白符去除。
上述部分特性的示例如下:
%define xyzzy(=expr,&val) expr, str
%define plugh(x) xyzzy(x,x)
db plugh(3+5), `\0` ; Expands to: db 8, "3+5", `\0`
在使用NASM命令行时,可以使用-d选项来定义macro。
%xdefine在定义宏时立即展开
%xdefine和%ixdefine分别用于定义大小写敏感和不敏感的宏定义。
%xdefine和%define基本一致,区别是%xdefine定义中如果出现了宏调用,会在定义时立即展开。
%[…]展开宏
使用%[…]可以在单词中间展开宏,在宏的周围没有分隔符时必须使用这种方式才能将宏展开,例如: abc%[mac]def
。 如果不使用%[…]展开mac宏,在连续的符号中间的mac不会被当作宏展开。
%+拼接宏
%+用于将宏内容拼接到代码后面。
mov ax,BDASTART + tBIOSDA.COM1addr
mov bx,BDASTART + tBIOSDA.COM2addr
;上面的代码可以写成下面这样,优化语意
%define BDA(x) BDASTART + tBIOSDA. %+ x
mov ax,BDA(COM1addr)
mov bx,BDA(COM2addr)
%?和%??取得当前宏名称
%?获取当前被调用宏的名称,%??获取当前被调用的宏在定义时的名称。
这两个宏可以用在multi-line macro定义中。
示例:
%imacro Foo 0
mov %?,%??
%endmacro
foo
FOO
;will expand to:
mov foo,Foo
mov FOO,Foo
%?和%??是专用于single-line macro的%?和%??
%?和%??在单行宏定义和多行宏定义中是通用的,而%?和%??仅用于单行宏定义,在多行宏定义上下文中不会被展开,有时候可能需要这样的特性。
示例如下,注意如果不是使用%?而是使用%?,那么在imacro宏定义中%?就会被立即展开为多行宏定义名称,使用单行宏定义中专用的%?可以确保只有在单行宏定义展开时,%*?才被展开为单行宏定义名称:
%imacro Foo 0
%idefine Bar _%*?
mov BAR,bAr
%endmacro
foo
mov eax,bar
;will expand to:
mov _BAR,_bAr
mov eax,_bar
%undef取消单行宏定义
示例:
%define foo bar
%undef foo
mov eax, foo
; expands to mov eax,foo
此外,使用-u命令行选项可以强制取消某个宏定义。
%assign, %defstr, %deftok
%assign用于定义宏为一个数字,如果传入的是表达式则表达式会在定义时被立即求值;
%defstr用于定义宏为一个字符串,传入的参数不必使用引号包围,该宏会自动将参数转为字符串;
%deftok用于将字符串转换为代码,可以理解为该宏会将字符串引号去除,作为代码展开。
示例和说明:
%assign i i+1
%defstr test TEST
;is equivalent to
%define test 'TEST'
%deftok test 'TEST'
;is equivalent to
%define test TEST
%defalias定义宏别名
%defalias起到的作用和unix中的symbolic link类似。因此,如果别名引用的原macro被undef了,这个别名也会被影响。别名存在的意义在于,保留原始宏名称的同时,为宏定义一个更加合适的名称。
使用%undefalias可以删除某个宏别名;
使用%clear defalias可以删除所有宏别名,包括NASM自己为了兼容性而定义的别名,因此别这么干。
使用%aliases off可以暂时停用宏别名,但是不删除宏别名。
使用%ifdefalias可以检查某个宏是否被定义为宏别名。
%defalias OLD NEW
; OLD and NEW both undefined
%define NEW 123
; OLD and NEW both 123
%undef OLD
; OLD and NEW both undefined
%define OLD 456
; OLD and NEW both 456
%undefalias OLD
; OLD undefined, NEW defined to 456
%, 条件展开为逗号
%,
在其后的表达式不是null expression时会展开为逗号 ,
,示例:
%define greedy(a,b,c+) a + 66 %, b * 3 %, c
db greedy(1,2) ; db 1 + 66, 2 * 3
db greedy(1,2,3) ; db 1 + 66, 2 * 3, 3
db greedy(1,2,3,4) ; db 1 + 66, 2 * 3, 3, 4
db greedy(1,2,3,4,5) ; db 1 + 66, 2 * 3, 3, 4, 5
NASM支持的字符串相关宏
NASM支持一系列用于操作字符串的宏定义,每个宏定义都用于产生一个新的单行宏,新的宏展开后就是我们想要得到的字符串。
%strcat拼接字符串
%strlen取字符串长度
%substr截取子字符串
(略)
%macro定义多行宏
使用%macro可以定义多行宏,语法为:
;基本的使用%macro定义多行宏的例子
%macro prologue 1
push ebp
mov ebp,esp
sub esp,%1
%endmacro
;定义并使用多行宏
%macro silly 2
%2: db %1
%endmacro
silly 'a', letter_a ; letter_a: db 'a'
silly 'ab', string_ab ; string_ab: db 'ab'
silly {13,10}, crlf ; crlf: db 13,10
其中,第一个例子prologue 1中的数字1表示宏参数的个数,在宏定义内部使用%1, %2引用参数。
需要注意几个multi-line macro和single-line macro的区别:
- multi-line macro调用不需要在参数周围加括号;
- multi-line macro不声明参数名称,而是仅声明参数个数,使用%1, %2这样的方式引用参数;
- multi-line macro在需要传递逗号作为参数内容时,可以选择让将参数用打括号包围,就像上面的{13, 10}那样,这样的参数会被当作一个参数,macro定义内接收到的参数是大括号内的内容:
13, 10
。
Overloading Macros
macro可以被overload,多个macro如果接受的参数格式不一样,后面的定义不会将前面的覆盖,它们会overload。
Macro-Local Labels
和local label能够将label定义在上一个non-local label上下文中的能力类似,macro-local label能够将label定义在当前macro上下文中。macro-local label的名称以 %%
开头。
示例:
%macro retz 0
jnz %%skip
ret
%%skip:
%endmacro
NASM是如何支持macro-local label的?每一次macro被展开,NASM都会将macro-local label的名称替换为诸如 ..@2345.skip
这样的特殊名称,其中数字部分确保每次生成的都不一样。因此在定义自己的local label时,一定不要将label定义成上面那样,否则有可能和NASM生成的macro-local label发生名字冲突。
贪婪macro参数匹配
NASM允许macro定义参数时,通过在参数数量后面加一个 +
,将最后一个参数定义为贪婪的,贪婪参数会匹配任意多个参数,所有超过参数定义数量的传入参数都会被收集到最后一个参数中。
在需要使用贪婪macro参数时,往往也可以选择让业务传入用大括号包围的参数。具体应该使用哪种,应该根据哪种语义更清晰来确定。
示例:
%macro writefile 2+
jmp %%endstr
%%str: db %2
%%endstr:
mov dx,%%str
mov cx,%%endstr-%%str
mov bx,%1
mov ah,0x40
int 0x21
%endmacro
;call writefile, %2 will expand to "hello, world",13,10
writefile [filehandle], "hello, world",13,10
Macro Parameter Range
NASM支持使用 ${x:y}
这种特殊的方式展开多个参数,其中x和y分别表示第一个和最后一个参数,正数和负数都是合法的,但是0不合法,在使用这种方式表示参数时,1表示第一个参数,-1表示最后一个参数。
示例(注意1-*定义参数数量为任意个,这种语法的说明参考下一小节):
; 正数x和y
%macro mpar 1-*
db %{3:5}
%endmacro
mpar 1,2,3,4,5,6
; 负数x和y
%macro mpar 1-*
db %{-1:-3}
%endmacro
mpar 1,2,3,4,5,6
默认Macro参数
NASM允许在定义multi-line macro时定义参数数量为一个范围,其中范围下限是调用时至少要提供的参数个数,上下限之间的参数是可选参数,在定义这种宏时,还可以为每个可选参数挨个定义默认值。
例如 %macro die 0-1 "Painful died"
定义一个参数数量为0~1个的宏,当调用时不传入参数时,在宏定义里%1默认为指定的默认值”Painful died”。
回头看看上面的 1-*
写法,其含义是宏调用时最少提供一个参数,但没有上限。由于没有提供default parameter,因此访问没有传入的参数时,例如%3,其展开后的结果是空的。
%0展开为macro参数个数
在将参数数量定义为一个范围时,很可能需要知道参数的个数,使用 %0
表达式就可以得到参数数量。
%00展开为macro调用那一行的label
如果macro被调用的那一行source line定义了label,使用%00就可以获取label名称。
%rotate旋转macro参数
%rotate和unix shell中的shift起到的作用类似,区别在于rotate不会将第一个参数丢弃,第一个参数会被“旋转”到参数列表的最后。
%rotate接受一个数字参数,数字表示要shift的参数个数,正数表示向左shift,负数表示向右shift。
使用%{…}展开macro参数
在macro参数展开的上下文中存在紧随其后的数字时,无法直接使用%n展开参数,例如下面的这个场景:city code is: %100
在上面的场景中,%1不会被作为macro参数展开,此时必须使用%{1}才能在上面的场景中将参数展开。
注意区分单行参数展开语法:%[…];
注意区分专用的宏参数拼接方式: %+ ;
Condition Code作为macro参数
如果macro要求参数必须是特殊的表示条件码的代码,如 ne
、 po
,可以使用 %+1 或 %-1 这种方式引用参数,其中+表示原条件,-表示相反的条件。
%macro retc 1
j%-1 %%skip ;j%-1 根据传入的参数会被展开成je或者jpe
ret
%%skip:
%endmacro
使用.nolist禁用Listing Expansion
(略)
使用%unmacro取消macro定义
使用%unmacro取消macro定义时需要像在定义macro时一样传入参数个数,取消的仅仅是匹配参数规范的macro定义。这一点和%undef不一样,%undef会直接将single-line macro的所有名称匹配的宏定义取消。
Conditional Assembly
NASM支持利用宏实现条件判断,决定哪些代码参与汇编。下面是条件判断代码示例:
%if<condition>
; some code which only appears if <condition> is met
%elif<condition2>
; only appears if <condition> is not met but <condition2> is
%else
; this appears if neither <condition> nor <condition2> was met
%endif
%if宏存在变种,如%ifdef,每个变种都有对应的%elif和%ifn,%elifn形式的宏,有时候这些宏的名字看起来有点奇怪。以%if和%ifdef为例:
- %if
- %ifn - if not
- %elif - else if
- %elifn - if not版本的else if
- %ifdef
- %ifndef - if not define
- %elifdef - if define版本的else if
- %elifndef - if not define版本的else if
下面介绍所有的%if变种。
%ifdef测试单行宏是否存在
%ifmacro测试多行宏定义是否存在
%ifctx测试context stack
%if测试数字表达式
%ifidn测试文本是否相同
%ifid, %ifnum, %ifstr 测试token类型
%iftoken测试是否是单个token
%ifempty测试表达式是否为空
%ifenv测试环境变量是否存在
(略)
源文件和依赖
NASM还提供了一系列宏,可以让我们将源码组织到多个文件中。
%include引入其他源文件
%pathsearch在search path中搜索指定文件名
%depend添加依赖文件
%use引入标准宏定义包
(略)
The Context Stack
NASM提供了Context Stack的概念,允许我们使用宏创建一个专门用于保存某些label的context。
context stack在某些场景下,是比local label更加合适的管理label的方式。
%push和%pop用于创建和移除context
%push foobar
可以创建一个名为foobar的context,并将该context置于context stack顶端。%pop
可以将context stack顶端的context移除,可以为%pop传入context名称作为参数,此时如果context stack顶端的context名称和传入的context名称不一致,就会被认为是发生了错误,NASM会报错。
%push的参数是可选的,在%push和%pop配合使用时一般不会为%push指定参数。
Context-Local Labels
名字以 %$
开头的label会被定义在当前context中。
示例:
%macro repeat 0
%push repeat
%$begin:
%endmacro
%macro until 1
j%-1 %$begin
%pop
%endmacro
如果想要在其他更下层的context中定义或访问label,也是可能的。此时,需要用 $
的个数表示要使用的context是从context stack顶端向下数第几层的context,例如%$$foo表示从顶端向下数第二个context中的名为foo的context-local label。
Context-Local Single-Line macro
和context-local label类似,Single-Line也可以被定义为context-local的,并且命名规则和context-label一致。
在当前顶部context中定义context-local single-line macro的示例:
%define %$localmac 3
%repl重命名当前context
(略)
用于获取stack信息的预处理指令
用%error, %warning, %fatal提示用户错误
用%pragma设置NASM选项
其他预处理指令
(略)
Chap.5 Standart Macros
Chap.6 Standart Macro Packages
Chap.7 Assembler Directives
虽然NASM努力避免变得向MASM和TASM那样臃肿,但是有些directive是必须由汇编器直接提供支持的。这些由NASM直接支持的directive在这一章中说明。
NASM的directive有两种类型:user-level directive和primitive directive。通常每个directive都具备两种形式的定义,其中user-level的directive是利用primitive版本的directive实现的,一般来说不会用到primitive directive,总是应该使用user-level directive。primitive directive都是用方括号包围的,而user-level primitive不是。
这一章中覆盖的directive并不是全部可用的directive,部分directive仅在特定object file format下才可用,这些format-specific directive在Chap.8中说明特定目标格式时才说明。