常用语法
- 1、循环
- 1.1、使用条件跳转指令实现循环
- 1.2、使用LOOP指令实现循环
- 2、字符串
- 2.1、指定字符串的长度
- 2.2、字符串指令
- 2.3、重复前缀
- 3、数组
- 4、递归
- 5、宏
- 6、文件操作
- 7、内存管理
1、循环
1.1、使用条件跳转指令实现循环
汇编语言中实现循环通常需要使用跳转指令和条件判断指令。下面是一个示例代码,用于实现一个简单的循环结构:
MOV CX, 10 ; 将计数器初始值10存储到CX寄存器中
L1: ; 标记循环开始处
<LOOP-BODY> ; 循环体中的指令
DEC CX ; 将计数器值减1
JNZ L1 ; 如果计数器不为0,则跳转到循环开始处继续执行
这段代码的具体含义如下:
-
首先使用MOV指令将计数器的初值10(或者其它合适的值)存储到CX寄存器中。
MOV CX, 10
-
然后定义一个名为L1的标号,表示循环的开始位置。
L1:
-
接着执行循环体(LOOP-BODY)中的指令,这些指令可以根据具体需求进行编写。
<LOOP-BODY>
-
使用DEC指令将计数器CX寄存器中的值减1。
DEC CX
-
然后使用JNZ指令(Jump if Not Zero)判断计数器的值是否为0,如果不为0则跳转到L1标号处继续执行循环体内的指令;否则结束循环。
JNZ L1
在这个示例代码中,循环的次数由CX寄存器中的值控制,每次循环体执行结束后,计数器的值会减1。当计数器的值为0时,程序会跳转到循环结束处,结束循环。
1.2、使用LOOP指令实现循环
LOOP是x86汇编中的一个指令,用于循环执行某段代码块。它的语法格式如下:
LOOP destination
其中destination表示循环体的结束位置,可以是任何有效的内存地址或标号(label)。执行完这条指令后,CPU会将CX寄存器中的值减1,并判断CX寄存器中的值是否为0。如果不为0,则跳转到destination指向的内存地址处执行指令,否则继续往下执行。
LOOP指令的执行流程如下:
-
首先将CX寄存器中的值减1。
-
判断CX寄存器中的值是否为0,如果不为0,则跳转到destination指向的位置处执行指令。
-
如果CX寄存器中的值为0,则不跳转,直接继续往下执行。
需要注意的是,LOOP指令只能使用CX寄存器作为计数器,因此在使用LOOP指令前需要将CX寄存器初始化为循环次数。另外,由于LOOP指令只能处理8位或16位有符号整数的计数器,因此在循环次数较大时可能会出现计算错误的情况。
下面是一个简单的示例程序,演示了如何使用LOOP指令:
MOV CX, 10 ; 将计数器初值10存储到CX寄存器中
L1: ; 标记循环开始处
<LOOP-BODY> ; 循环体中的指令
LOOP L1 ; 继续执行循环,直到CX寄存器中的值为0
在这个示例中,首先使用MOV指令将计数器的初值10存储到CX寄存器中。然后定义一个名为L1的标号,表示循环的开始位置。接着执行循环体(LOOP-BODY)中的指令,这些指令可以根据具体需求进行编写。最后使用LOOP指令对CX先进行减1的操作,然后指示CPU在CX寄存器中的值不为0时跳转回L1标号处执行循环体,直到CX寄存器中的值为0时结束循环。
2、字符串
2.1、指定字符串的长度
我们可以使用表示位置计数器当前值的 $location 计数器符号来显式存储字符串长度。
在以下实例中:
msg db 'Hello, world!',0xa ;字符
len equ $ - msg ;字符长度
2.2、字符串指令
每个字符串指令可能需要一个源操作数,一个目标操作数或两者。对于 32 位段,字符串指令使用 ESI 和 EDI 寄存器分别指向源和目标操作数。但是,对于 16 位段,SI 和 DI 寄存器分别用于指向源和目标。
有 5 种用于处理字符串的基本指令
MOVS - 该指令将从ESI寄存器指向的源地址复制到EDI寄存器指向的目标地址
LODS - 该指令将数据从ESI寄存器指向的源地址读取到AL/AX/EAX寄存器中,并递增或递减ESI寄存器的值。
STOS - 该指令将AL/AX/EAX寄存器的值写入EDI寄存器指向的目标地址,并递增或递减EDI寄存器的值。
CMPS - 该指令将ESI寄存器指向的源地址处的一个字节与EDI寄存器指向的目标地址处的一个字节进行比较,并递增或递减ESI和EDI寄存器的值
SCAS -该指令将AL/AX/EAX寄存器的值与与EDI寄存器指向的目标地址处的一个字节进行比较,并递增或递减EDI寄存器的值。
下面我们分别看五个简短的示例:
; movs
section .data
src_buf db 1,2,3,4,5
dst_buf times 5 db 0 ; 初始化为5个0
section .text
global _start
_start:
mov esi, src_buf ; 设置源地址
mov edi, dst_buf ; 设置目标地址
mov ecx, 5 ; 设置要传送的字节数
rep movsb ; 在两个缓冲区之间传送所有字节
***
section .data
buf db 1,2,3,4,5
section .text
global _start
_start:
mov esi, buf ; 设置源地址
mov ecx, 5 ; 设置要读取的字节数
xor eax, eax ; 清零累加器eax
loop_start:
lodsb ; 读取一个字节到al寄存器中,并递增esi寄存器的值
add eax, al ; 将al寄存器中的值加到eax寄存器中
loop loop_start
mov ebx, eax ; 将结果保存到ebx寄存器中
mov eax, 1 ; 设置系统调用号为exit
xor ecx, ecx ; 设置退出码为0
int 0x80 ; 调用系统调用
section .data
buf db 1,2,3,4,5
section .text
global _start
_start:
mov edi, buf ; 设置目标地址
mov ecx, 5 ; 设置要写入的字节数
xor al, al ; 设置要写入的字节值为0
rep stosb ; 将5个字节设置为0
mov eax, 1 ; 设置系统调用号为exit
xor ebx, ebx ; 设置退出码为0
int 0x80 ; 调用系统调用
section .data
buf1 db 1,2,3,4,5
buf2 db 1,2,3,4,6
section .text
global _start
_start:
mov esi, buf1 ; 设置源地址1
mov edi, buf2 ; 设置源地址2
mov ecx, 5 ; 设置要比较的字节数
repe cmpsb ; 比较两个缓冲区中的5个字节是否相等
jz success ; 如果两个缓冲区中的字节都相等,则跳转到success标签
***
section .data
buf db 1,2,3,4,5
section .text
global _start
_start:
mov edi, buf ; 设置目标地址
mov ecx, 5 ; 设置要查找的字节数
mov al, 4 ; 设置要查找的字节值为4
repe scasb ; 在缓冲区中查找字节值为4的字节
jnz not_found ; 如果没找到,则跳转到not_found标签
***
2.3、重复前缀
-
REP是一个通用前缀,它可以与大多数指令一起使用。它的作用是根据指令类型重复执行指令,直到ECX计数器变为0。例如,REP MOVSB指令可以用于在两个存储器之间传输字符串,每执行一次MOVSB指令,ESI和EDI寄存器的值都会增加或减少,ECX计数器的值也会递减,直到ECX计数器为0。
-
REPE前缀表示“repeat while equal”,它通常与字符串比较指令(如CMPSB)一起使用,用于比较两个字符串,如果相等则继续执行,直到ECX计数器为0,或者找到不同处,即ZF标志位被清除。例如,REPE CMPSB指令可以用于比较两个字符串,每执行一次CMPSB指令,ESI和EDI寄存器的值都会增加或减少,ECX计数器的值也会递减,直到ECX计数器为0,或者字符串中出现不匹配的字符,即ZF标志位被清除。
-
REPNE前缀表示“repeat while not equal”,它与REPE类似,但是用于比较两个字符串时,如果不相等则继续执行,直到ECX计数器为0,或者找到相同处,即ZF标志位被设置。例如,REPNE CMPSB指令可以用于比较两个字符串,每执行一次CMPSB指令,ESI和EDI寄存器的值都会增加或减少,ECX计数器的值也会递减,直到ECX计数器为0,或者字符串中出现匹配的字符,即ZF标志位被设置。
-
REPF前缀表示“repeat while equal, forward”,它通常与字符串搜索指令(如SCASB)一起使用,用于搜索字符串中是否存在一个给定字符,如果找到了,则继续执行,直到ECX计数器为0,或者搜索完整个字符串,即ZF标志位被清除。例如,REPF SCASB指令可以用于搜索字符串中是否存在某个字符,每执行一次SCASB指令,EDI寄存器的值都会增加或减少,ECX计数器的值也会递减,直到ECX计数器为0,或者找到了给定字符,即ZF标志位被清除。
-
REPNF前缀表示“repeat while not equal, forward”,它与REPF类似,但是用于搜索字符串中是否存在一个给定字符时,如果没找到,则继续执行,直到ECX计数器为0,或者搜索完整个字符串,即ZF标志位被设置。例如,REPNF SCASB指令可以用于搜索字符串中是否存在某个字符,每执行一次SCASB指令,EDI寄存器的值都会增加或减少,ECX计数器的值也会递减,直到ECX计数器为0,或者没找到给定字符,即ZF标志位被设置。
3、数组
数组是啥不用我说了吧。
section .data
array db 1, 2, 3, 4, 5 ; 定义一个包含5个字节的数组
section .data
array: ; 数组名
times 5 db 0 ; 声明5个字节的数组,初始化为0;
4、递归
递归也不用介绍了,直接看个例子:
;程序展示了如何在汇编语言中实现阶乘 n。为了保持程序简单,我们将计算阶乘 3。
section .text
global _start ;必须声明才能使用 gcc
_start: ;告诉链接器入口点
mov bx, 3 ;用于计算阶乘 3
call proc_fact
add ax, 30h
mov [fact], ax
mov edx,len ;消息长度
mov ecx,msg ;消息
mov ebx,1 ;文件描述 (stdout)
mov eax,4 ;系统调用号 (sys_write)
int 0x80 ;调用内核
mov edx,1 ;消息长度
mov ecx,fact ;消息
mov ebx,1 ;文件描述 (stdout)
mov eax,4 ;系统调用号 (sys_write)
int 0x80 ;调用内核
mov eax,1 ;系统调用号 (sys_exit)
int 0x80 ;调用内核
proc_fact:
cmp bl, 1
jg do_calculation
mov ax, 1
ret
do_calculation:
dec bl
call proc_fact
inc bl
mul bl ;ax = al * bl
ret
section .data
msg db 'Factorial 3 is:',0xa
len equ $ - msg
section .bss
fact resb 1
5、宏
编写宏是确保用汇编语言进行模块化编程的另一种方法。宏是一系列指令,由名称指定,可以在程序中的任何位置使用。
在 NASM 中,宏是用 %macro 和 %endmacro 指令定义的。宏以 %macro 指令开始,以 %endmacro 指令结束。
宏的语法如下:
%macro macro_name number_of_params
<macro body>
%endmacro
其中,number_of_params 指定数字参数,macro_name 指定宏的名称。
通过使用宏名称和必要的参数来调用宏。当您需要在程序中多次使用某个指令序列时,可以将这些指令放在宏中并使用它,而不是一直编写指令。
例如,程序的一个常见需求是在屏幕上写入字符串。要显示字符串,您需要以下指令序列:
movedx,len ;message length
movecx,msg ;message to write
movebx,1 ;file descriptor (stdout)
moveax,4 ;system call number (sys_write)
int0x80 ;call kernel
在上面显示字符串的实例中,INT 80H 函数调用使用了寄存器 EAX、EBX、ECX 和 EDX。因此,每次需要在屏幕上显示时,都需要将这些寄存器保存在堆栈上,调用 INT 80H,然后从堆栈中恢复寄存器的原始值。因此,编写两个宏来保存和恢复数据可能很有用。
我们注意到,一些指令,如 IMUL、IDIV、INT 等,需要将一些信息存储在某些特定寄存器中,甚至在某些特定的寄存器中返回值。如果程序已经使用这些寄存器来保存重要数据,那么这些寄存器中的现有数据应该保存在堆栈中,并在指令执行后恢复。
6、文件操作
文件处理系统调用
下表简要介绍了与文件处理相关的系统调用:
%eax | 名称 | %ebx | %ecx | %edx | 备注 |
---|---|---|---|---|---|
2 | sys_fork | struct pt_regs | - | - | |
3 | sys_read | 文件描述符(unsigned int) | 缓冲区的地址(char) | 要读取的字节数(size_t) | |
4 | sys_write | 文件描述符(unsigned int) | (缓冲区的地址)const char | 要写入的字节数(size_t) | |
5 | sys_open | 文件名(const char) | 文件访问模式(int) | 文件权限(int) | |
6 | sys_close | 文件描述符(unsigned int) | - | - | |
8 | sys_creat | 文件名(const char) | 文件权限(int) | - | |
19 | sys_lseek | 文件描述符(unsigned int) | 偏移值(off_t) | 偏移量的参考位置(unsigned int) | 参考位置可以是:文件开头 :0;当前位置:1;文件结尾:2。 |
以上所有的系统调用如果出现错误,系统调用将返回 EAX 寄存器中的错误代码。
我们来看两个示例:
;sys_fork
***
_start:
; 调用sys_fork系统调用创建子进程
mov eax, 0x02 ; sys_fork的系统调用号
int 0x80 ; 触发系统调用
; 检查返回值
test eax, eax ; 如果eax为0,此条命令的执行结果为0,存储在标志寄存器中
jz child_process ; jz(jump if zero),当标志寄存器中的值为0时,则跳转到child_process继续执行。
***
child_process:
***
section .data
filename db 'example.txt', 0
mode rdwr ; 以读写方式打开文件
section .bss
fd resd 1 ; 用于存储文件描述符
section .text
global _start
_start:
; 调用sys_open函数打开文件
mov eax, 0x05 ; sys_open系统调用号
mov ebx, filename ; 文件名的地址
mov ecx, mode ; 打开文件的模式
int 0x80 ; 触发系统调用
mov [fd], eax ; 将返回值(文件描述符)存储在fd变量中
; 如果文件描述符小于0,则表示打开文件失败
cmp eax, 0 ; 比较eax与0的大小
jl open_failed ; 如果小于0,则跳转到open_failed
; 在此处对文件进行操作
; ...
; 关闭文件
mov eax, 0x06 ; sys_close系统调用号
mov ebx, [fd] ; 文件描述符
int 0x80 ; 触发系统调用
; 程序退出
mov eax, 0x01 ; sys_exit系统调用号
xor ebx, ebx ; 返回值为0
int 0x80 ; 触发系统调用
open_failed:
; 文件打开失败的处理
; ...
7、内存管理
说内存管理之前我们先来看一个系统调用,sys_brk。
sys_brk()是一个操作系统中的内核函数,用于改变进程的堆空间大小。堆是进程运行时动态分配内存空间的区域,通过sys_brk()函数可以在堆上分配或释放指定大小的内存空间。
在Linux系统中,使用malloc()、calloc()等函数来进行内存动态分配时,都会使用sys_brk()函数来实现。sys_brk系统调用详见博主的另一篇博文。
section .text
global _start ;必须声明才能使用 gcc
_start: ;告诉链接器入口点
mov eax, 45 ;sys_brk
xor ebx, ebx
int 80h ; 这一步执行的是sys_brk(0),即获取到当前的堆的尾地址,保存在eax中。
add eax, 16384 ; eax += 16384,即堆增长16KB。
mov ebx, eax
mov eax, 45 ;sys_brk
int 80h
cmp eax, 0
jl exit ;exit, if error
mov edi, eax ;EDI = highest available address
sub edi, 4 ;pointing to the last DWORD
mov ecx, 4096 ;number of DWORDs allocated
xor eax, eax ;clear eax
std ;backward
rep stosd ;repete for entire allocated area
cld ;put DF flag to normal state
mov eax, 4
mov ebx, 1
mov ecx, msg
mov edx, len
int 80h ;打印消息
exit:
mov eax, 1
xor ebx, ebx
int 80h
section .data
msg db "Allocated 16 kb of memory!", 10
len equ $ - msg
执行结果如下: