nasm汇编讲解
一、什么是nasm汇编
nasm使用在windows、linux等系统下的汇编。
二、语法介绍
2.1 nasm 是区分大小写
例如:符号 foo 与 FOO 是两个不同的标识符。
2.2 内存操作数表达式
2.2.1 在 nasm 语法里,对 memory 操作数需要加 [ ] 括号
下面的代码:
foo equ 1
bar dw 2
bits 32
mov eax, foo
mov ebx, bar
第 2 指令的意图是:将bar 内的值赋给ebx 寄存器。但这样是错误的,nasm 只会把 bar 当作是 immediate 赋给 ebx
00000000 0200 ; bar 变量
00000002 B801000000 mov eax,0x1
00000007 BB00000000 mov ebx,0x0 ; 将 bar 地址作为 immediate 赋给 ebx
因此,需要将 bar 用 [ ] 括起来
mov eax, foo
mov ebx, [bar]
nasm 就编译出正确的代码:
00000000 0200 ; bar
00000002 B801000000 mov eax,0x1
00000007 8B1D00000000 mov ebx,[dword 0x0] ; bar 的内容赋给 ebx
2.2.2 给 memory 操作数提供一个 displacement 值
下面代码展示了 [base + disp] 的寻址方式:
section .bss
buffer resb 10
section .text
bits 32
mov byte [buffer + 0x01] , 'a'
mov ebx, buffer
movzx eax, byte [ebx + 0x01]
2.2.3 指明 memory 操作数的 operand size
下面展示了为 memory 操作数提供一个 size 情况:
mov byte [buffer + 0x01] , 'a'
mov ebx, buffer
movzx eax, byte [ebx + 0x01]
代码中使用 byte 关键字对 memory 操作数进行了修饰,指明 memory 操作数的大小为 byte
在这方面,nasm 的语法与微软的 masm 的语法(Intel 语法)有些不同,masm 的语法是:
mov byte ptr [buffer + 0x01] , 'a'
mov ebx, buffer
movzx eax, byte ptr [ebx + 0x01]
在 masm 语法中需配合 ptr 指示字。
2.2.4 提供一个 segment
大多数 指令/内存操作数 缺省的 segment 是 DS ,x86/x64 允许为 memory 操作数提供另一个 segment 进行 segment override
在 nasm 语法中,如下:
mov byte [es: buffer + 0x01] , 'a'
mov ebx, buffer
movzx eax, byte [es: ebx + 0x01]
nasm 语法中,在 [ ] 括号内提供 segment ,不能在 [ ] 括号外提供 segment
而 masm 的语法中是在 [ ] 括号外提供 segment,如下:
mov byte ptr es: [buffer + 0x01] , 'a'
mov ebx, buffer
movzx eax, byte ptr es: [ebx + 0x01]
3.2.5 指明 memory 操作数的 address size
有些情况下必须指明 memory 操作数的 address size ,否则编译结果可能不是你想要的结果。下面的例子说明,如何为 memory 操作数指明 address size
例 1:
section .bss
buffer resb 10
section .text
bits 64
mov rax, [qword buffer] ; 指明 64 位的 address size(绝对地址)
mov rax, [buffer] ; 使用 32 位的 address size(绝对地址)
在 nasm 中,对于 绝对地址 形式,缺省是 32 位的,因此,需要明确使用 qword 来指明 64 位的 address size
这段代码编译后为:
00000000 48A11400000000000000 mov rax,[qword 0x14] ; 64 位地址
0000000A 488B042514000000 mov rax,[0x14] ; 32 位地址
它们的区别就是一个使用了 64 位地址,一个使用了 32 位地址。
例 2:
section .bss
buffer resb 10
section .text
bits 16
mov byte [es:dword buffer + 0x01], 'a' ; 指明为 32 位地址
mov ebx, buffer
movzx eax, byte [es:ebx+0x01]
上面代码是 16 位代码,使用了 dword 指明 memory 操作数是 32 位的地址。
它被编译为:
00000000 2667C6051D000000 mov byte [dword es:0x1d],0x61
-61
00000009 66BB1C000000 mov ebx,0x1c
0000000F 2667660FB6830100 movzx eax,byte [es:ebx+0x1]
-0000
16 位的 address size 被 override 为 32 位地址。
2.3 nasm 伪指令
伪指令不是 x86/x64 机器的真实指令,伪指令是用于给编译器指示如何进行编译。
2.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 型数据。
2.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
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
2.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
wordvar: resw 1 ; reserve a word
realarray resq 10 ; array of ten reals
ymmval: resy 1 ; one YMM register
3.3.4 包含 binary 文件
nasm 提供了一种包含 binary(二进制)文件的方法:使用 incbin 伪指令。incbin 伪指令包含的 binary 文件将直将写入输出文件中。此伪指令的作用是包含 graphics 以及 sound 这类数据文件。
···
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
···
2.3.5 使用 equ 定义常量
equ 用来为标识符定义一个 整型 常量,它的作用类似 C 语言中的 #define
a equ 0 ; OK
b equ 'abcd' ; OK! b = 0x64636261
c equ 'abcdefghi' ; warning! c = 0x6867666564636261
d equ 1.2 ; error!
section .data
string db 'hello,word',0
len equ $-string ; OK! len = 0x0b
section .text
textlen equ _end - entry ; OK! textlen = 0x05
_entry:
mov ecx, textlen
_end:
例子中: b 定义为常量 ‘abcd’ 它将是字符串的 ASCII 码序列,‘abcdefghi’ 常量将会被截断,整型常量最长为 quadword(8 bytes),而 d 企图被定义为一个 float 常量,这产生会错误。len 和 textlen 被定义为编译期确定的数值。
2.3.6 使用 times 重复写数据或指令
times 是一个比较实用伪指令,用来重复定义数据或指令。
下面是一个经典的使用例子:
times 510-($-$$) db 0
dw 0xaa55
这段代码经常出现在 boot 磁盘 MBR 引导代码中,目的是除了最后 2 个字节和 code 代码外的区域全部写 0 值。
times 还可以使用在重复写某一条指令,如下:
times 10 nop
这段代码结果是重复填入了 10 条 nop 指令:
00000000 90 nop
00000001 90 nop
00000002 90 nop
00000003 90 nop
00000004 90 nop
00000005 90 nop
00000006 90 nop
00000007 90 nop
00000008 90 nop
00000009 90 nop
2.4 常量值
nasm 下可以接受 4 种常量:整型常量 ,字符常量 ,字符串常量 以及浮点常量
2.4.1 整型常量
在 nasm 中,常用的整型进制有 4 种:
- decimal :十进制数
- hex :十六进制
- binary :二进制数
- octal :八进制数
每一种进制都有前缀 和后缀 表示法。当数值无前缀和后缀时,它是十进制数。因为缺省是十进制。
2.4.1.1 十进制数表示方法
看一看,下面的例子:
mov ax,200 ; decimal
mov ax,0200 ; still decimal
mov ax,0200d ; explicitly decimal
mov ax,0d200 ; also decimal
十进制的前缀是:0d , 后缀是:d
2.4.1.2 十六进制数表示方法
十六进制使用 0123456789ABCDEF 来表示 16 个数值。类似地,它的前缀是:0h 或 0x (c/c++ 风格)以及 $0 (pascal 风格),后缀是:h
mov ax, 0c8h ; hex
mov ax, 8h ; hex
上面例子中的 hex 数,表明:当以 h 后缀结尾时,如果含有字母 ,必须要以 0 开头。
mov ax,$0c8 ; hex again: the 0 is required
上面是 pascal 风格的十六进制表示法,使用前缀 $0 (0 是必须的)
mov ax,0xc8 ; hex yet again
mov ax,0hc8 ; still hex
上面是使用 0h 前缀和 C/C++ 风格的 0x 表示十六进制数。
2.4.1.3 八进制数表示方法
八进制的前缀可以是:0o 或 0q 后缀可以是:o 或 q
mov ax,310q ; octal
mov ax,310o ; octal again
mov ax,0o310 ; octal yet again
mov ax,0q310 ; octal yet again
2.4.1.4 二进制数表示方法
类似地,二进制的前缀是:0b 后缀是:b
mov ax,11001000b ; binary
mov ax,1100_1000b ; same binary constant
mov ax,0b1100_1000 ; same binary constant yet again
2.4.2 字符常量
在 nasm 中,可以使用 3 种引号来提供字符
- ’ …’ (单引号)
- ” …” (双引号)
- ` …` (反引号)
如下示例,它们的结果是一样的:
db 'abcd'
db "abcd"
db `abcd`
2.4.2.1 提供字符常量
下面看看如何提供字符量:
mov eax, 'a' ; eax = 0x61
mov eax, 'abcd' ; eax = 0x64636261
mov eax, 'abcdefghi' ; eax = 0x64636261
mov eax, `/x61/x62/x63/x64' ; eax = 0x64636261
第 3 条企图赋超过 4 bytes 的字符常量给 eax, 编译器会截断为 4bytes 再赋给 eax, 而第 4 条是另一种字符常量表示法,使用转义字符表示。
可见:字符常量是以 little-endian 排列
2.4.3 nasm 中的转义字符
在 nasm 中使用 c 风格的转义字符,在 / (反斜杠符)后面跟 转义码 :/ escape-code
转义码(escape-code)包括:字符转义码 , 八进制转义码 , 十六进制转义码
注意:nasm 中的转义符必须要用 (反引号)来引用
db `/x61` ; right! 'a'
db '/x61' ; wrong! '/' , 'x' , '6', '1'
第 1 个用反引号来包含转义符是正确的。而第 2 个用单引号来包含转义符,nasm 却视它为一般的字符串对待
2.4.3.1 字符转义码
下面是一些例子:
/' single quote (')
/" double quote (")
/` backquote (`)
// backslash (/)
/? question mark (?)
/a BEL (ASCII 7)
/b BS (ASCII 8)
/t TAB (ASCII 9)
/n LF (ASCII 10)
/v VT (ASCII 11)
/f FF (ASCII 12)
/r CR (ASCII 13)
/e ESC (ASCII 27)
2.4.3.2 八进制转义码
/ 后面最多可以跟着 3 位数字,构成八进制转义码,如下所示:
/377 Up to 3 octal digits - literal byte
/004 ; EOT
/006 ; ACK
/025 ; NAK
/0 ; NULL
2.4.3.3 十六进制转义码
十六进制转义码以 x 或 X 开头,如下所示:
db `/x61` ; 'a'
db `/x61/x62/x63/x64` ; 'abcd'
上面所示:十六进制转义码可以连串提供。
2.4.4 字符串常量
字符串是逐个逐个提供字符,看以下例子:
db 'hello' ; string constant
db 'h','e','l','l','o' ; equivalent character constants
提供字符串常量,是从左到右依次写入到内存。
这里要注意的是:字符常量 与字符串常量 的区别 ,看一看下面的例子
buffer db 'hello' ; 字符串常量
... ...
mov eax, 'hello' ; 字符常量
字符常量与字符串常量最大区别就是:字符常以 littlen-endian 存储,而字符串常量是从左到度
2.4.5 Unicode 字符串
nasm 定义了两个操作数符来定义 Unicode 字符串:
- utf16
- utf32
下面是 nasm 里的例子:
%define u(x) __utf16__(x)
%define w(x) __utf32__(x)
dw u('C:/WINDOWS'), 0 ; Pathname in UTF-16
dd w(`A + B = /u206a`), 0 ; String in UTF-32
2.4.6 浮点数常量
浮点数变量 可以使用 DB , DW , DD , DQ , DT 以及 DO ,浮点数常量 使用 float8 , float16 , float32 , float64 , float80m , float80e , float128l 以及 float128h 来定义。
下面是 nasm 提供的例子:
db -0.2 ; "Quarter precision"
dw -0.5 ; IEEE 754r/SSE5 half precision
dd 1.2 ; an easy one
dd 1.222_222_222 ; underscores are permitted
dd 0x1p+2 ; 1.0x2^2 = 4.0
dq 0x1p+32 ; 1.0x2^32 = 4 294 967 296.0
dq 1.e10 ; 10 000 000 000.0
dq 1.e+10 ; synonymous with 1.e10
dq 1.e-10 ; 0.000 000 000 1
dt 3.141592653589793238462 ; pi
do 1.e+4000 ; IEEE 754r quad precision
2.5 表达式
在 nasm 里的表达式很像 C 表达式,对于熟悉 C 表达式的人来说几乎可以马上上手。
2.5.1 与$ 标号
标号表示nasm编译后当前指令位置标号表示nasm编译后当前指令位置$ 标号表示当前 section 起始位置
看看下面的例子:
bits 64
section .rdata
dq 0
section .text
mov rax, 0
mov rax, $-$$
它的编译结果是:
00000000 48B8000000000000 mov rax,0x0
-0000
0000000A 48B80A0000000000 mov rax,0xa
-0000
00000014 0000 add [rax],al
00000016 0000 add [rax],al
00000018 0000 add [rax],al
0000001A 0000 add [rax],al
2.5.2 位运算符
与 C 一样,包括下面:
位运算符 | 描述 |
---|---|
& | 位与: AND |
| | 位或:OR |
~ | 位非: NOT |
^ | 位异或: XOR |
<< | 左移 |
>> | 右移 |
2.5.3 算术运算符
下面是 nasm 所支持的算术运算符
算术运算符 | 描述 |
---|---|
+ | 加 |
- | 减 |
* | 乘 |
/ | 除(无符号数) |
// | 除(符号数) |
% | 取模(无符号数) |
%% | 取模(符号数) |