3.1 nasm 是区分大小写
例如:符号 foo 与 FOO 是两个不同的标识符。
3.2 内存操作数表达式
3.2.1 在 nasm 语法里,对 memory 操作数需要加 [ ] 括号
下面的代码:
foo equ 1 bits 32 mov eax, foo |
第 2 指令的意图是:将 bar 内 的值赋给 ebx 寄存器。但这样是错误的,nasm 只会把 bar 当作是 immediate 赋给 ebx
00000000 0200 ; bar 变量 |
因此,需要将 bar 用 [ ] 括起来
mov eax, foo |
nasm 就编译出正确的代码:
00000000 0200 ; bar |
3.2.2 给 memory 操作数提供一个 displacement 值
下面代码展示了 [base + disp] 的寻址方式:
section .bss buffer resb 10
bits 32 mov byte [buffer + 0x01], 'a' |
3.2.3 指明 memory 操作数的 operand size
下面展示了为 memory 操作数提供一个 size 情况:
mov byte [buffer + 0x01], 'a' |
代码中使用 byte 关键字对 memory 操作数进行了修饰,指明 memory 操作数的大小为 byte
在这方面,nasm 的语法与微软的 masm 的语法(Intel 语法)有些不同,masm 的语法是:
mov byte ptr [buffer + 0x01], 'a' |
在 masm 语法中需配合 ptr 指示字。
3.2.4 提供一个 segment
大多数 指令/内存操作数 缺省的 segment 是 DS,x86/x64 允许为 memory 操作数提供另一个 segment 进行 segment override
在 nasm 语法中,如下:
mov byte [es:buffer + 0x01], 'a' |
nasm 语法中,在 [ ] 括号内提供 segment,不能在 [ ] 括号外提供 segment
而 masm 的语法中是在 [ ] 括号外提供 segment,如下:
mov byte ptr es:[buffer + 0x01], 'a' |
3.2.5 指明 memory 操作数的 address size
有些情况下必须指明 memory 操作数的 address size,否则编译结果可能不是你想要的结果。下面的例子说明,如何为 memory 操作数指明 address size
例 1:
section .bss buffer resb 10
bits 64 mov rax, [qword buffer] ; 指明 64 位的 address size(绝对地址) |
在 nasm 中,对于 绝对地址 形式,缺省是 32 位的,因此,需要明确使用 qword 来指明 64 位的 address size
这段代码编译后为:
00000000 48A11400000000000000 mov rax,[qword 0x14] ; 64 位地址 |
它们的区别就是一个使用了 64 位地址,一个使用了 32 位地址。
例 2:
section .bss buffer resb 10
bits 16 mov byte [es:dword buffer + 0x01], 'a' ; 指明为 32 位地址 |
上面代码是 16 位代码,使用了 dword 指明 memory 操作数是 32 位的地址。
它被编译为:
00000000 2667C6051D000000 mov byte [dword es:0x1d],0x61 |
16 位的 address size 被 override 为 32 位地址。
3.3 nasm 伪指令
伪指令不是 x86/x64 机器的真实指令,伪指令是用于给编译器指示如何进行编译。
3.3.1 nasm 定义的 7 种数据 size
- byte : 8 位
- word : 16 位
- dword : 32 位
- qword : 64 位
- tword : 80 位
- oword : 128 位
- yword : 256 位
oword 可以对应 Microsoft MASM 的 xmmword 类型,yword 对应 Microsoft MASM 的 ymmword 类型。
tword, oword 以及 yword 使用在 非整型 数据,使用在 float 和 SSE 型数据。
3.3.2 定义初始化数据:db 家族
nasm 定义了用于初始化上面 7 种 size 的 db 家族,它们用于定义初化常量值。
- db : define byte
- dw :define word
- dd :define doubleword
- dq :define quadword
- dt :define tword
- do :define oword
- dy :define yword
正如前面所说的:dt, do, dy 不接受整型数值常量,它们被使用在定义 float 或 SSE 数据常量。dt 可以定义 extended-precision float 数据,do 可以定义 quad-precision float,dy 可定义 ymm 数据。而 dq 可以定义 double-precision float 数据,dd 可以定义 single-precision float 数据。
下面是 NASM Manual 上的例子:
db 0x55 ; just the byte 0x55 |
3.3.3 定义非初始化数据:resb 家族
程序中使用到的非初始化数据通常放在 bss section 里,bss 代表 uninitialized storage space
nasm 使用了 resb(reserve byte) 家族来定义非初始化数据。
- resb :reserve byte
- resw :reserve word
- resd :reserve doubword
- resq :reserve quadword
- rest :reserve tword
- reso :reserve oword
- resy :reserve yword
resb 相当于 Microsoft MASM 语法中的 db ?
下面是 NASM Manual 的例子:
buffer: resb 64 ; reserve 64 bytes |
3.3.4 包含 binary 文件
nasm 提供了一种包含 binary(二进制)文件的方法:使用 incbin 伪指令。incbin 伪指令包含的 binary 文件将直将写入输出文件中。此伪指令的作用是包含 graphics 以及 sound 这类数据文件。
incbin "file.dat" ; include the whole file |
3.3.5 使用 equ 定义常量
equ 用来为标识符定义一个 整型 常量,它的作用类似 C 语言中的 #define
a equ 0 ; OK
string db 'hello,word',0 section .text _entry: |
例子中: b 定义为常量 'abcd' 它将是字符串的 ASCII 码序列,‘abcdefghi' 常量将会被截断,整型常量最长为 quadword(8 bytes),而 d 企图被定义为一个 float 常量,这产生会错误。len 和 textlen 被定义为编译期确定的数值。
3.3.6 使用 times 重复写数据或指令
times 是一个比较实用伪指令,用来重复定义数据或指令。
下面是一个经典的使用例子:
times 510-($-$$) db 0 |
这段代码经常出现在 boot 磁盘 MBR 引导代码中,目的是除了最后 2 个字节和 code 代码外的区域全部写 0 值。
times 还可以使用在重复写某一条指令,如下:
times 10 nop |
这段代码结果是重复填入了 10 条 nop 指令:
00000000 90 nop |
3.4 常量值
nasm 下可以接受 4 种常量:整型常量,字符常量,字符串常量以及浮点常量
3.4.1 整型常量
在 nasm 中,常用的整型进制有 4 种:
- decimal :十进制数
- hex :十六进制
- binary :二进制数
- octal :八进制数
每一种进制都有前缀和后缀表示法。当数值无前缀和后缀时,它是十进制数。因为缺省是十进制。
3.4.1.1 十进制数表示方法
看一看,下面的例子:
mov ax,200 ; decimal |
十进制的前缀是:0d, 后缀是:d
3.4.1.2 十六进制数表示方法
十六进制使用 0123456789ABCDEF 来表示 16 个数值。类似地,它的前缀是:0h 或 0x (c/c++ 风格)以及 $0(pascal 风格),后缀是:h
mov ax, 0c8h ; hex |
上面例子中的 hex 数,表明:当以 h 后缀结尾时,如果含有字母,必须要以 0 开头。
mov ax,$0c8 ; hex again: the 0 is required |
上面是 pascal 风格的十六进制表示法,使用前缀 $0 (0 是必须的)
mov ax,0xc8 ; hex yet again |
上面是使用 0h 前缀和 C/C++ 风格的 0x 表示十六进制数。
3.4.1.3 八进制数表示方法
八进制的前缀可以是:0o 或 0q 后缀可以是:o 或 q
mov ax,310q ; octal |
3.4.1.4 二进制数表示方法
类似地,二进制的前缀是:0b 后缀是:b
mov ax,11001000b ; binary |
3.4.2 字符常量
在 nasm 中,可以使用 3 种引号来提供字符
- '...'(单引号)
- "..."(双引号)
- `...`(反引号)
如下示例,它们的结果是一样的:
db 'abcd' |
3.4.2.1 提供字符常量
下面看看如何提供字符量:
mov eax, 'a' ; eax = 0x61 |
第 3 条企图赋超过 4 bytes 的字符常量给 eax, 编译器会截断为 4bytes 再赋给 eax, 而第 4 条是另一种字符常量表示法,使用转义字符表示。
可见:字符常量是以 little-endian 排列
3.4.3 nasm 中的转义字符
在 nasm 中使用 c 风格的转义字符,在 \ (反斜杠符)后面跟 转义码:\ escape-code
转义码(escape-code)包括:字符转义码, 八进制转义码, 十六进制转义码
注意:nasm 中的转义符必须要用 ` `(反引号)来引用
db `\x61` ; right! 'a' |
第 1 个用反引号来包含转义符是正确的。而第 2 个用单引号来包含转义符,nasm 却视它为一般的字符串对待
3.4.3.1 字符转义码
下面是一些例子:
\' single quote (') |
3.4.3.2 八进制转义码
\ 后面最多可以跟着 3 位数字,构成八进制转义码,如下所示:
\377 Up to 3 octal digits - literal byte |
3.4.3.3 十六进制转义码
十六进制转义码以 x 或 X 开头,如下所示:
db `\x61` ; 'a' |
上面所示:十六进制转义码可以连串提供。
3.4.4 字符串常量
字符串是逐个逐个提供字符,看以下例子:
db 'hello' ; string constant |
提供字符串常量,是从左到右依次写入到内存。
这里要注意的是:字符常量与字符串常量的区别,看一看下面的例子
buffer db 'hello' ; 字符串常量 mov eax, 'hello' ; 字符常量 |
字符常量与字符串常量最大区别就是:字符常以 littlen-endian 存储,而字符串常量是从左到度
3.4.5 Unicode 字符串
nasm 定义了两个操作数符来定义 Unicode 字符串:
- __utf16__
- __utf32__
下面是 nasm 里的例子:
%define u(x) __utf16__(x) |
3.4.6 浮点数常量
浮点数变量可以使用 DB, DW, DD, DQ, DT 以及 DO,浮点数常量使用 __float8__, __float16__, __float32__, __float64__, __float80m__, __float80e__, __float128l__ 以及__float128h__ 来定义。
下面是 nasm 提供的例子:
db -0.2 ; "Quarter precision" |
3.5 表达式
在 nasm 里的表达式很像 C 表达式,对于熟悉 C 表达式的人来说几乎可以马上上手。
3.5.1 $ 与 $$ 标号
$ 标号表示 nasm 编译后当前指令位置
$$ 标号表示当前 section 起始位置
看看下面的例子:
bits 64 section .rdata |
它的编译结果是:
00000000 48B8000000000000 mov rax,0x0 |
3.5.2 位运算符
与 C 一样,包括下面:
位运算符
|
描述
|
&
|
位与: AND
|
|
|
位或:OR
|
~
|
位非: NOT
|
^
|
位异或: XOR
|
<<
|
左移
|
>>
|
右移
|
3.5.3 算术运算符
下面是 nasm 所支持的算术运算符
算术运算符
|
描述
|
+
|
加
|
-
|
减
|
*
|
乘
|
/
|
除(无符号数)
|
//
|
除(符号数)
|
%
|
取模(无符号数)
|
%%
|
取模(符号数)
|