上一节中我们编译运行了一段NASM汇编代码,接下来我们将针对上一节的代码进行解析,了解一些具体的汇编知识。
程序结构
我们先将上一节中的代码通过调整变成如下形式:
global _start
section .text
_start: mov rax, 1
mov rdi, 1
mov rsi, message
mov rdx, 13
syscall
mov rax, 60
xor rdi, rdi
syscall
section .data
message: db "Hello, World", 10
然后再整理成表格形式:
主程序中需要指定代码从哪里开始运行,做法是设置_start
标签并使用global
将其暴露给汇编器;程序由指令和段组成,常见的段有代码段.text
、数据段.data
;指令语句主要由指令或指令+操作数组成,例如:mov rax, 1
。
通常我们会在代码段声明语句section .text
后接上入口标签_start:
并开始编写一系列的指令操作;对于数据段声明语句section .data
,我们会在其后接上声明的数据以及对应的标签。
操作指令
NASM的操作指令非常多,但是我们并不需要全部掌握,以下列举最常见的指令:
mov x, y
将y的值给到x;and x, y
将x、y进行与运算,把结果给到x;or x, y
将x、y进行或运算,把结果给到x;xor x, y
将x、y进行亦或运算,把结果给到x;add x, y
将y累加到x;sub x, y
从x中减去y;inc x
让x自增1;dec x
让x自减1;
寄存器
寄存器主要用于存取数值,可以被当作高级程序语言中的数值变量来使用。
当我们想进行64位数值操作的时候,我们常会使用英文字母r开头的寄存器,可以初步理解为64位的整型变量:
- RAX
- RCX
- RDX
- RBX
- RSP
- RBP
- RSI
- RDI
当我们想进行32位数值操作的时候,我们常使用英文字母e开头的寄存器,可以初步理解为32位的整型变量:
- EAX
- ECX
- EDX
- EBX
- ESP
- EBP
- ESI
- EDI
把64位寄存器的首字母r去掉,或者把32位寄存器的首字母e去掉则变为16位的寄存器,可以理解为16位的整型变量:
- AX
- CX
- DX
- BX
- SP
- BP
- SI
- DI
可以将16位的寄存器拆分为两个8位的寄存器,高位的部分称作高8位寄存器,低位的部分称作低8位寄存器。
常见低8位寄存器:
- AL
- CL
- DL
- BL
- SPL
- BPL
- SIL
- DIL
常见高8位寄存器:
- AH
- CH
- DH
- BH
下面,我们类比C语言,使用NASM代码来实现对应的操作。
有C语言如下:
char c;
c = 'a';
变换成NASM:
mov al, 'a'
有C语言如下:
c += 1;
变换成NASM:
add al, 1
内存寻址
由于寄存器数量有限,在处理较为复杂的数据结构时,我们需要利用内存来完成数据存取。
在汇编中,我们可以指定一段内存地址,然后写入或者读取一段数据。通过汇编语言的寻址操作,我们可以得到目标地址:
[数字]
[寄存器]
[寄存器 + 数字]
[寄存器 + 寄存器*scale]
scale 可以是 1, 2, 4, 8[寄存器 + 寄存器*scale + 数字]
数字指的是偏移量,普通的寄存器被称作基址寄存器,寄存器*scale被称作索引。
示例:
[123]
偏移[rbp]
基址[rbx - 8]
基址 + 偏移量 -8「反向偏移8个单位」[rcx + rsi*4]
基址 + 索引*scale[rbp + rdx]
scale为1可以省略[rax + rdi*8 + 500]
完整的写法[rbx + message]
可以使用标签作为偏移量,这使得标签具备变量名的作用
立即数操作
在代码中我们可以直接使用数值,例如: 10进位的100;16进位的0xff;2进位的0b11……,有个好消息是我们仍旧可以使用字符「编译器会转换为ascii对应的数字,本质上也是数字的一种写法」。
下面我们修改之前的代码,使用寻址操作将输出字符串Hello, World
变为小写的hello, world
,我们仅需要通过寻址修改两个字符:
global _start
section .text
_start:
; 将偏移量交给rax
mov rax, message
; 给rax所在的地址赋值'h',需要声明是在字节范围进行的操作
mov byte [rax], 'h'
; 给rax+7所在的地址赋值'w',需要声明是在字节范围进行的操作
mov byte [rax + 7], 'w'
mov rax, 1
mov rdi, 1
mov rsi, message
mov rdx, 13
syscall
mov rax, 60
xor rdi, rdi
syscall
section .data
message:
db "Hello, World", 10
系统调用
在C语言中我们常使用printf
函数打印字符串,然而这个函数并不是由我们自己完成,一般来说系统库会为我们提供他。在汇编当中我们也可以使用类似的由系统提供的工具,在64位的NASM中称其为系统调用syscall
。
在我们的示例代码中有两次使用了系统调用。
第一次是打印输出字符串:
mov rax, 1
mov rdi, 1
mov rsi, message
mov rdx, 13
syscall
syscall
前的寄存器操作是在设定我们以何种方式进行系统调用。rax的值决定使用哪一个系统调用,1对应的就是写入操作;此时rdi设置为1表示标准输出;rsi则存放字符串地址;rdx记录字符串长度。
第二次是结束程序,相当于exit(0)
:
mov rax, 60
xor rdi, rdi
syscall
当rax的值为60时表示调用退出操作;此时rdi为0表示以0号状态进行退出,就像C语言中exit(0);
。
定义或声明数据
我们将数据的定义或声明放在数据段中,它的形式是这样: 标签: 类型 值
。
db
用于定义字节byte,例如:
message:
db 'H', 'e', 'l', 'l', 'o', ' ', "World", 10 ;
dw
用于定义字word,例如:
number_dw:
dw 1, 2, 3
dd
用于定义双字double word,例如:
number_dd:
dd 1, 2, 3