nasm使用
nasm是一种挥基于x86的汇编与反汇编软件。可以用于实现一些简单的功能或是简单操作系统
一、安装与使用
在Ubuntu环境下,可用使用sudo apt-get install nasm
指令来安装nasm。在Windows或其他地方应该也有自己的安装方法
在安装完毕后,我们需要自己编写一个.asm文件,里面存放的就是汇编代码。
想要编译这个文件(例如文件名为main.asm),可用使用指令:
1 | nasm –felf32 main.asm |
生成一个elf文件。
然后使用以下指令:
1 | ld main.o |
进行链接。
注意:如果这里提示ld命令未找到的话,可以使用以下命令安装:
1 | apt-get install libvcflib-tools |
注意:如果使用后提示incompatible with i386:x86-64 output的话,就是说你的虚拟机是64位的。解决方法是先把刚刚生成的.o文件删除,然后把上一步指令的-felf32改为-felf64,重新执行,就可以了
链接完毕后可用开始运行。输入以下命令:
1 | ./main.out |
就可以成功运行。
二、基本结构
1、section划分
以以下段代码为例:
1 | ;Section to store initialized variables |
其中,;后面的内容表示的是注释。
这部分内容中包含不同的section。每一个section表示的是一个板块,有不同的功能,例如:
- section .data表示用于声明并且初始化变量。
- section .bss表示用于声明变量但不初始化
- section .text表示存放可执行代码的部分,程序的入口。
2、.data部分操作
数据空间大小
不同的标识定义了一块不同的数据空间大小:
x | 含义 | 字节数量 |
---|---|---|
b | BYTE | 1 |
w | WORD | 2 |
d | DOUBLE WORD | 4 |
q | QUAD WORD | 8 |
t | TEN WORD | 20 |
以下是全部的数据类型,包括写法:
数据类型写法 | 含义 |
---|---|
BYTE | 8位无符号整数,B代表字节 |
SBYTE | 8位有符号整数,S代表有符号 |
WORD | 16位无符号整数 |
SWORD | 16位有符号整数 |
DWORD | 32位无符号整数,D代表双字 |
SDWORD | 32位有符号整数,SD代表有符号双字 |
FWORD | 48位整数(保护模式中的远指针) |
QWORD | 80位(10字节整数),T代表10字节 |
REAL4 | 32位(4字节)IEEE短实数 |
REAL8 | 64位(8字节)IEEE长实数 |
REAL10 | 80位(10字节)IEEE拓展实数s |
声明变量
在.data中可以声明不同的变量。例如:
1 | string: db 'Hello World', 0Ah |
表示声明一个字符串变量。其中,string是一个标签,只用于指示内存中的地址,可以替换为其他名词。而db才是用于定义变量类型,为变量分配内存并初始化。0Ah表示换行符。
注意这里的db的b代表的就是byte,1字节。通常字符串都使用1字节的空间组成
1 | length: equ $-string |
表示声明一个符号表达式,使用equ。其中$表示当前位置的地址,而string表示之前声明过的标记所在的内存地址。
可以同时声明多个变量,只需要用逗号隔开
3、.bss部分操作
在.bss中可以声明未初始化的全局或静态变量。
声明变量
1 | var: resb 2 |
resx表示每个单位大小,2表示数量。这里的resb表示的单位大小为1字节,声明两个大小1字节的空间,类似数组
4、.text部分操作
这一部分用于存放可执行的代码。
添加程序入口点
1 | global _start: |
global声明一个标签标识这个标签可以被其他源文件访问或引用。
5、寄存器
通用寄存器
注意寄存器有不同的长度,使用时需要注意
- EAX,EBX,ECX,EDX等等为4字节
- AX,BX,CX,DX为2字节
- AH,AL,BH,BL等为1字节
**注意:**如果在64位机器中还存在着rax,rbx等寄存器。它们的大小为8字节。
三、基础语法
使用指令
指令名+一些寄存器名/立即数
例如:
1 | MOV eax 10 |
表示把10存入eax中
访问变量:解引用
语法是变量大小[变量地址]
这个方法可以用于读取变量的值。因为通常标签存储的是地址。如果你想把标签当成变量名使用,访问变量的内容,就需要解引用。
例如:
1 | BYTE[label] |
表示访问的是label位置的大小为1字节的一个变量
1 | dword[ebx] |
表示将ebx中的内容视为一个地址,访问这个地址指向的一块4字节大小的内容。相当于地址的间接引用
系统调用
1 | int 80h |
通常int指令用于中断。它会转移到中断向量所指向的中断服务程序。这里的80h表示的是系统调用。
在系统调用前需要先设置eax和其他寄存器的值。例如
- sys_write:写入数据,置eax为4,并且ebx需要设置为输出目的地(0=标准输入,1=标准输出,2=标准错误输出)。如果想要打印到屏幕上可以置ebx为1。ecx表示写入的数据地址,edx表示写入数据大小
- sys_read:读入数据,置eax为3,ebx与sys_write相同。如果想要从控制台读入,那么你需要置ebx位0。之后,ecx表示读入的数据存放的地址,edx表示最多读取大小。
- sys_exit:退出程序,置eax为1,ebx为0
预处理指令
例如:
1 | %ifdef DEBUG |
当代码中使用%define定义了DEBUG的时候,这段代码就会被执行
函数
1 | call function |
使用call指令,function是函数名,它是一个标签。
函数中可以在最后添加一个ret,当执行到ret时就会返回到调用函数的位置
宏
宏类似一个函数,可以拥有参数、函数名。
例如:
1 | %macro print_msg 1 |
使用%macro来表示一个宏,然后print_msg是宏的名称,后面的数字代表参数的个数。如果想要访问这些参数,可以使用%数字的形式来访问。例如%1代表第一个参数,%1_len表示第一个参数的长度
四、常用指令
MOV指令
将后面的参数的内容赋值到前面的位置中
1 | MOV eax, ebx |
表示将ebx中的内容赋值到eax中
1 | mov word[var2], 200 |
表示将200赋值给var2位置的变量
MOVZX指令
当把一个较小的数据移动到较大的空间时,使用MOVZX指令,自动进行无符号扩展
1 | movzx eax,ah |
MOVSX指令
与上面相反,当大变量移到小空间时自动截断
ADD指令
1 | add eax, ebx |
表示把ebx加到eax上
SUB指令
1 | sub eax, ebx |
表示把eax减去ebx
MUL
两个数相乘之后位数会翻倍
例如:AX = AL * src
1 | MUL src |
AL中存放另一个乘数,并且最后的积存放在AX中
DIV
1 | DIV src |
被除数放在AX中,最后的商放在AL,余数放在AH
条件分支指令
JMP
1 | JMP label |
跳转到label的位置
CMP
1 | CMP op1, op2 |
能够影响到CPU的flag
基于CMP之后的flag的跳转:
位运算
- AND
- OR
- XOR
- NOT
- TEST
= SHL - SHR
- ROL
- ROR
- RCL
- RCR
AND
1 | AND op1, op2 |
op1和op2与运算的结果存放在op1中
or,xor类似
NOT
1 | NOT op1 |
TEST
1 | TEST op1, op2 |
相当于AND,但是结果存入CPU FLAG中
SHL/SHR
逻辑左移/右移(直接补0)
1 | SHL op1, op2 |
op1左移op2个位
ROL/ROR
循环左移/右移(移出去的位会去到另一边)
1 | ROL op1, op2 |
栈操作
- PUSH:入栈
- POP:出栈
- PUSHA:所有通用寄存器入栈
- POPA:所有通用寄存器出栈
push会减少ESP的值,pop会增加
注意:32位机器push和pop时需要操作的是eax、ebx等寄存器,而64位机器只能push和pop:rax、rbx等寄存器。
五、一些预设的函数/宏定义
- 从控制台读入数据,存入一个空间:
- 参数1:存入读取的数据的地址
- 参数2:读入数据的长度
1 | %macro input 2 |
- 将字符串打印到控制台上
- 参数1:要写入的数据的地址
- 参数2:写入数据的长度
1 | %macro print 2 |
-
用于退出程序
%macro exit 0
mov eax,1
mov ebx,0
int 80h
%endmacro -
用于通用寄存器出入栈
1 | %macro allPush 0 ;用于将全部寄存器入栈 |