不同的汇编语言

在汇编领域没有标准的语言规范,各个不同的汇编语言编译器都有自己的一套约定。

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],axmov [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支持多种语法:

  1. fadd st1 ; this sets st0 := st0 + st1
  2. fadd st0,st1 ; so does this
  3. fadd st1,st0 ; this sets st1 := st1 + st0
  4. 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,用于声明已经初始化的数据。
初始化语法示例如下:

  1. db 0x55 ; just the byte 0x55
  2. db 0x55,0x56,0x57 ; three bytes in succession
  3. db 'a',0x55 ; character constants are OK
  4. db 'hello',13,10,'$' ; so are string constants
  5. dw 0x1234 ; 0x34 0x12
  6. dw 'a' ; 0x61 0x00 (it's just a number)
  7. dw 'ab' ; 0x61 0x62 (character constant)
  8. dw 'abc' ; 0x61 0x62 0x63 0x00 (string)
  9. dd 0x12345678 ; 0x78 0x56 0x34 0x12
  10. dd 1.234567e20 ; floating-point constant
  11. dq 0x123456789abcdef0 ; eight byte constant
  12. dq 1.234567e20 ; double-precision float
  13. dt 1.234567e20 ; extended-precision float

从NASM 2.15开始,下面的eature就已经被支持了:

  • ?用于声明未初始化的存储: db ?
  • 语法定义:

    1. 语法规范:
    2. dx := DB | DW | DD | DQ | DT | DO | DY | DZ
    3. type := BYTE | WORD | DWORD | QWORD | TWORD | OWORD | YWORD | ZWORD
    4. atom := expression | string | float | '?'
    5. parlist := '(' value [, value ...] ')'
    6. duplist := expression DUP [type] ['%'] parlist
    7. list := duplist | '%' parlist | type ['%'] parlist
    8. value := atom | type value | list
    9. stmt := dx value [, value...]
    10. 复杂语法示例:
    11. db 33
    12. db (44) ; Integer expression
    13. ; db (44,55) ; Invalid - error
    14. db %(44,55)
    15. db %('XX','YY')
    16. db ('AA') ; Integer expression - outputs single byte
    17. db %('BB') ; List, containing a string
    18. db ?
    19. db 6 dup (33)
    20. db 6 dup (33, 34)
    21. db 6 dup (33, 34), 35
    22. db 7 dup (99)
    23. db 7 dup dword (?, word ?, ?)
    24. dw byte (?,44)
    25. dw 3 dup (0xcc, 4 dup byte ('PQR'), ?), 0xabcd
    26. dd 16 dup (0xaaaa, ?, 0xbbbbbb)
    27. 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更加方便,示例:

  1. buffer: resb 64 ; reserve 64 bytes
  2. wordvar: resw 1 ; reserve a word
  3. realarray resq 10 ; array of ten reals
  4. ymmval: resy 1 ; one YMM register
  5. zmmvals: resz 32 ; 32 ZMM registers
  6. ;上面的RESx也可以使用Dx配合 DUP ? 写成下面这样
  7. buffer: db 64 dup (?) ; reserve 64 bytes
  8. wordvar: dw ? ; reserve a word
  9. realarray dq 10 dup (?) ; array of ten reals
  10. ymmval: dy ? ; one YMM register
  11. zmmvals: dz 32 dup (?) ; 32 ZMM registers

使用INCBIN引入外部二进制文件

  1. incbin "file.dat" ; include the whole file
  2. incbin "file.dat",1024 ; skip the first 1024 bytes
  3. incbin "file.dat",1024,512 ; skip the first 1024, and
  4. ; actually include at most 512

使用EQU定义常量

EQU将一个符号定义为指定常量,必须配合label使用。EQU定义是绝对的,不可以在之后被修改。
示例:

  1. message db 'hello, world'
  2. msglen equ $-message

上面的代码将msglen定义为12,因为$表示当前地址,而前面的’hello, world’恰好占了12个字节,当前地址减去message地址,就是12。此外,$-message这个表达式会被立即计算,而不是每次使用时计算。

使用TIMES重复指令

TIMES这个伪指令前缀会将指令重复指定数量次,后面可以跟伪指令或者真实的指令。DUP和TIMES很像,但是DUP的操作数必须是一个立即数,而TIMES后面可以跟一个表达式,这使得TIMES使用上更加灵活,更具普适性。TIMES的操作数是一个critical expression。
示例:

  1. ;下面的两种写法一样
  2. zerobuf: times 64 db 0
  3. zerobuf2: db 64 dup (0)
  4. ;但是dup无法适用下面这种场景
  5. ;store exactly enough spaces to make the total length of buffer up to 64
  6. buffer: db 'hello, world'
  7. times 64-$+buffer db ' '

Effective Addresses

一个effective address就是一个指向内存的操作数。在NASM中,effective address的语法很简单:如果一个操作数是一对方括号包围着的表达式,那么这个操作数就是一个effective address。
示例:

  1. wordvar dw 123
  2. mov ax,[wordvar]
  3. mov ax,[wordvar+1]
  4. 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优化:

  1. ;原本[eax*2]会被优化为[eax+eax],但在nosplit的作用下会被禁用优化,相当于[eax*2+0]
  2. [nosplit eax*2]

在64-bit模式下NASM默认生成绝对地址,REL关键字可用于让NASM生成RIP-relative address。因为相对地址是个很普遍的需求,NASM提供了DEFAULT指令允许开发者指定默认的地址生成模式(参考Chap.7)。

Constants

NASM理解四种常量:数字、字符、字符串和浮点数。

Numberic Constants

数字常量就是数字。NASM中可以使用一系列进制,其中:

  • 表示十六进制: 0x前缀$前缀0h前缀
  • 十进制: 0d前缀0t前缀
  • 八进制: 0o前缀0q前缀
  • 二进制: 0b前缀0y前缀

上面虽然只列出了前缀,但是所有字母前缀同时也是合法的后缀,若在数字的结尾是上面的字母之一,则也可用于表示数字的进制。

  1. Some examples (all producing exactly the same code):
  2. mov ax,200 ; decimal
  3. mov ax,0200 ; still decimal
  4. mov ax,0200d ; explicitly decimal
  5. mov ax,0d200 ; also decimal
  6. mov ax,0c8h ; hex
  7. mov ax,$0c8 ; hex again: the 0 is required
  8. mov ax,0xc8 ; hex yet again
  9. mov ax,0hc8 ; still hex
  10. mov ax,310q ; octal
  11. mov ax,310o ; octal again
  12. mov ax,0o310 ; octal yet again
  13. mov ax,0q310 ; octal yet again
  14. mov ax,11001000b ; binary
  15. mov ax,1100_1000b ; same binary constant
  16. mov ax,1100_1000y ; same binary constant once more
  17. mov ax,0b1100_1000 ; same binary constant yet again
  18. mov ax,0y1100_1000 ; same binary constant yet again

Character Strings

Character Strings没有对应的中文表示,这里先用“字符字符串”代指。
一个字符字符串由最多八个字符组成,是用一个字符串表示的一个特定字符,字符字符串可以用单引号、双引号、反引号包围。使用反引号时,可以利用C-style的转义字符。

\u263a\xe2\x98\xba0E2h, 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。
示例:

  1. db 'hello' ; string constant
  2. db 'h','e','l','l','o' ; equivalent character constants
  3. ;And the following are also equivalent:
  4. dd 'ninechars' ; doubleword string constant
  5. dd 'nine','char','s' ; becomes three doublewords
  6. 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会直接报错退出:

  1. ;times这一行引用了未知的label符号,NASM会报错并退出
  2. times (label-$) db 0
  3. label: db 'Where am I?'

Local Labels

NASM对于名称以 . 开头的label给予特殊关照,这样的label被视作local label,local label和前面的上一个non local label关联,可以理解为每一个non local label都会建立一个新的local label作用域。
示例:

  1. label1 ; some code
  2. .loop
  3. ; some more code
  4. jne .loop
  5. ret
  6. label2 ; some code
  7. .loop
  8. ; some more code
  9. jne .loop
  10. ret

除了可以在当前local label作用域内访问同一作用域的local label外,还可以通过non local label访问其他local label作用域内定义的local label。
示例:

  1. label3 ; some more code
  2. ; and some more
  3. jmp label1.loop

如果想要定义一个不产生local label作用域的non local label,可以使用 ..@ 前缀命名label。
示例:

  1. label1: ; a non-local label
  2. .local: ; this is really label1.local
  3. ..@foo: ; this is a special symbol
  4. label2: ; another non-local label
  5. .local: ; this is really label2.local
  6. 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前面的宏定义。

可以在定义宏的参数列表时,不写出参数名称,此时定义的依然是一个带参数的宏,虽然调用时还要传入参数,但参数不会被使用,示例如下:

  1. ;定义一个宏ereg,这个宏定义了两个参数
  2. %define ereg(foo, ) e %+ foo
  3. ;定义一个宏myreg,这个宏定义了一个参数
  4. %define myreg() eax

此外,宏定义时根据参数声明的前缀,NASM还提供以下特性:

  • 如果前缀是 = ,NASM会将传入的内容视作表达式求值,该参数值为表达式求值结果;
  • 如果前缀是 & ,NASM会将传入的内容转换为字符串,该参数值为内容字符串;
  • 如果前缀是 + ,NASM会将参数视作变长模板参数,作贪婪匹配,这个参数最后会被展开为包含逗号、空白符的代码;
  • 如果前缀是 ! ,NASM不会将括号和空白符去除。

上述部分特性的示例如下:

  1. %define xyzzy(=expr,&val) expr, str
  2. %define plugh(x) xyzzy(x,x)
  3. 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不会被当作宏展开。

%+拼接宏

%+用于将宏内容拼接到代码后面。

  1. mov ax,BDASTART + tBIOSDA.COM1addr
  2. mov bx,BDASTART + tBIOSDA.COM2addr
  3. ;上面的代码可以写成下面这样,优化语意
  4. %define BDA(x) BDASTART + tBIOSDA. %+ x
  5. mov ax,BDA(COM1addr)
  6. mov bx,BDA(COM2addr)

%?和%??取得当前宏名称

%?获取当前被调用宏的名称,%??获取当前被调用的宏在定义时的名称。
这两个宏可以用在multi-line macro定义中。
示例:

  1. %imacro Foo 0
  2. mov %?,%??
  3. %endmacro
  4. foo
  5. FOO
  6. ;will expand to:
  7. mov foo,Foo
  8. mov FOO,Foo

%?和%??是专用于single-line macro的%?和%??

%?和%??在单行宏定义和多行宏定义中是通用的,而%?和%??仅用于单行宏定义,在多行宏定义上下文中不会被展开,有时候可能需要这样的特性。
示例如下,注意如果不是使用%?而是使用%?,那么在imacro宏定义中%?就会被立即展开为多行宏定义名称,使用单行宏定义中专用的%?可以确保只有在单行宏定义展开时,%*?才被展开为单行宏定义名称:

  1. %imacro Foo 0
  2. %idefine Bar _%*?
  3. mov BAR,bAr
  4. %endmacro
  5. foo
  6. mov eax,bar
  7. ;will expand to:
  8. mov _BAR,_bAr
  9. mov eax,_bar

%undef取消单行宏定义

示例:

  1. %define foo bar
  2. %undef foo
  3. mov eax, foo
  4. ; expands to mov eax,foo

此外,使用-u命令行选项可以强制取消某个宏定义。

%assign, %defstr, %deftok

%assign用于定义宏为一个数字,如果传入的是表达式则表达式会在定义时被立即求值;
%defstr用于定义宏为一个字符串,传入的参数不必使用引号包围,该宏会自动将参数转为字符串;
%deftok用于将字符串转换为代码,可以理解为该宏会将字符串引号去除,作为代码展开。
示例和说明:

  1. %assign i i+1
  2. %defstr test TEST
  3. ;is equivalent to
  4. %define test 'TEST'
  5. %deftok test 'TEST'
  6. ;is equivalent to
  7. %define test TEST

%defalias定义宏别名

%defalias起到的作用和unix中的symbolic link类似。因此,如果别名引用的原macro被undef了,这个别名也会被影响。别名存在的意义在于,保留原始宏名称的同时,为宏定义一个更加合适的名称。

使用%undefalias可以删除某个宏别名;
使用%clear defalias可以删除所有宏别名,包括NASM自己为了兼容性而定义的别名,因此别这么干。

使用%aliases off可以暂时停用宏别名,但是不删除宏别名。

使用%ifdefalias可以检查某个宏是否被定义为宏别名。

  1. %defalias OLD NEW
  2. ; OLD and NEW both undefined
  3. %define NEW 123
  4. ; OLD and NEW both 123
  5. %undef OLD
  6. ; OLD and NEW both undefined
  7. %define OLD 456
  8. ; OLD and NEW both 456
  9. %undefalias OLD
  10. ; OLD undefined, NEW defined to 456

%, 条件展开为逗号

%, 在其后的表达式不是null expression时会展开为逗号 , ,示例:

  1. %define greedy(a,b,c+) a + 66 %, b * 3 %, c
  2. db greedy(1,2) ; db 1 + 66, 2 * 3
  3. db greedy(1,2,3) ; db 1 + 66, 2 * 3, 3
  4. db greedy(1,2,3,4) ; db 1 + 66, 2 * 3, 3, 4
  5. db greedy(1,2,3,4,5) ; db 1 + 66, 2 * 3, 3, 4, 5

NASM支持的字符串相关宏

NASM支持一系列用于操作字符串的宏定义,每个宏定义都用于产生一个新的单行宏,新的宏展开后就是我们想要得到的字符串。

%strcat拼接字符串

(略)

%strlen取字符串长度

(略)

%substr截取子字符串

(略)

%macro定义多行宏

使用%macro可以定义多行宏,语法为:

  1. ;基本的使用%macro定义多行宏的例子
  2. %macro prologue 1
  3. push ebp
  4. mov ebp,esp
  5. sub esp,%1
  6. %endmacro
  7. ;定义并使用多行宏
  8. %macro silly 2
  9. %2: db %1
  10. %endmacro
  11. silly 'a', letter_a ; letter_a: db 'a'
  12. silly 'ab', string_ab ; string_ab: db 'ab'
  13. 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的名称以 %% 开头。
示例:

  1. %macro retz 0
  2. jnz %%skip
  3. ret
  4. %%skip:
  5. %endmacro

NASM是如何支持macro-local label的?每一次macro被展开,NASM都会将macro-local label的名称替换为诸如 ..@2345.skip 这样的特殊名称,其中数字部分确保每次生成的都不一样。因此在定义自己的local label时,一定不要将label定义成上面那样,否则有可能和NASM生成的macro-local label发生名字冲突。

贪婪macro参数匹配

NASM允许macro定义参数时,通过在参数数量后面加一个 + ,将最后一个参数定义为贪婪的,贪婪参数会匹配任意多个参数,所有超过参数定义数量的传入参数都会被收集到最后一个参数中。
在需要使用贪婪macro参数时,往往也可以选择让业务传入用大括号包围的参数。具体应该使用哪种,应该根据哪种语义更清晰来确定。
示例:

  1. %macro writefile 2+
  2. jmp %%endstr
  3. %%str: db %2
  4. %%endstr:
  5. mov dx,%%str
  6. mov cx,%%endstr-%%str
  7. mov bx,%1
  8. mov ah,0x40
  9. int 0x21
  10. %endmacro
  11. ;call writefile, %2 will expand to "hello, world",13,10
  12. writefile [filehandle], "hello, world",13,10

Macro Parameter Range

NASM支持使用 ${x:y} 这种特殊的方式展开多个参数,其中x和y分别表示第一个和最后一个参数,正数和负数都是合法的,但是0不合法,在使用这种方式表示参数时,1表示第一个参数,-1表示最后一个参数。
示例(注意1-*定义参数数量为任意个,这种语法的说明参考下一小节)

  1. ; 正数xy
  2. %macro mpar 1-*
  3. db %{3:5}
  4. %endmacro
  5. mpar 1,2,3,4,5,6
  6. ; 负数xy
  7. %macro mpar 1-*
  8. db %{-1:-3}
  9. %endmacro
  10. 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要求参数必须是特殊的表示条件码的代码,如 nepo ,可以使用 %+1 或 %-1 这种方式引用参数,其中+表示原条件,-表示相反的条件。

  1. %macro retc 1
  2. j%-1 %%skip ;j%-1 根据传入的参数会被展开成je或者jpe
  3. ret
  4. %%skip:
  5. %endmacro

使用.nolist禁用Listing Expansion

(略)

使用%unmacro取消macro定义

使用%unmacro取消macro定义时需要像在定义macro时一样传入参数个数,取消的仅仅是匹配参数规范的macro定义。这一点和%undef不一样,%undef会直接将single-line macro的所有名称匹配的宏定义取消。

Conditional Assembly

NASM支持利用宏实现条件判断,决定哪些代码参与汇编。下面是条件判断代码示例:

  1. %if<condition>
  2. ; some code which only appears if <condition> is met
  3. %elif<condition2>
  4. ; only appears if <condition> is not met but <condition2> is
  5. %else
  6. ; this appears if neither <condition> nor <condition2> was met
  7. %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中。
示例:

  1. %macro repeat 0
  2. %push repeat
  3. %$begin:
  4. %endmacro
  5. %macro until 1
  6. j%-1 %$begin
  7. %pop
  8. %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的示例:

  1. %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中说明特定目标格式时才说明。

BITS指定目标处理器mode

DEFAULT改变汇编器默认行为

SECTION or SEGMENT改变和定义Sections

ABSOLUTE定义Absolute Labels

EXTERN从其他模块引入符号

REQUIRED无条件引入其他模块符号

GLOBAL导出符号供其他模块使用

COMMON定义Common Data Area

STATIC定义模块内Local Symbol

(G|L)PREFIX, (G|L)POSTFIX: Mangling Symbols

CPU定义处理器依赖

FLOAT定义浮点数常量处理方式

[WARNING]启用或禁用warning

Chap.8 Output Formats