更好的阅读体验,请点击 YinKai 's Blog。
过程或子例程在汇编语言中非常重要,它们有助于组织和模块化代码,提高代码的可读性和可维护性。
过程通常以一系列的指令组成,用于完成特定的任务。这些过程可以有参数、局部变量,也可以返回一个值。
过程定义的语法如下:
proc_name:
procedure body
...
ret
使用 CALL 指令从另一个函数调用该过程,被调用过程的名称应作为 CALL 指令 的参数,如下:
CALL proc_name
示例:下面的程序将 ECX 和 EDX 寄存器中存储的变量相加,并将结果总和返回到 EAX 寄存器中,并显示
section .text
global _start ; 必须为了使用链接器 (gcc)
_start:
mov ecx, '4' ; 将字符 '4' 的 ASCII 值加载到 ECX 寄存器
sub ecx, '0' ; 将 '0' 的 ASCII 值从 ECX 中减去,以获得数字 4
mov edx, '5' ; 将字符 '5' 的 ASCII 值加载到 EDX 寄存器
sub edx, '0' ; 将 '0' 的 ASCII 值从 EDX 中减去,以获得数字 5
call sum ; 调用 sum 过程,将结果存储在 EAX 中
mov [res], eax ; 将结果存储在 res 变量中
; 输出 "The sum is:" 到标准输出
mov ecx, msg
mov edx, len
mov ebx, 1 ; 文件描述符 (stdout)
mov eax, 4 ; 系统调用号 (sys_write)
int 0x80 ; 调用内核
; 输出结果到标准输出
mov ecx, res
mov edx, 1
mov ebx, 1 ; 文件描述符 (stdout)
mov eax, 4 ; 系统调用号 (sys_write)
int 0x80 ; 调用内核
; 退出程序
mov eax, 1 ; 系统调用号 (sys_exit)
int 0x80 ; 调用内核
sum:
mov eax, ecx ; 将 ECX 中的值移动到 EAX
add eax, edx ; 将 EDX 中的值加到 EAX
add eax, '0' ; 将 '0' 的 ASCII 值加到 EAX,以将数字转换回字符
ret ; 返回
section .data
msg db "The sum is:", 0xA,0xD ; 输出消息
len equ $- msg
section .bss
res resb 1 ; 用于存储结果的变量,初始化为 1 个字节
编译运行后输出的结果如下:
The sum is:
9
堆栈是一种内存中的数据结构,类似于数组,用于存储和检索数据。数据可以通过"推入"到堆栈中进行存储,而通过"弹出"从堆栈中取出。堆栈采用后进先出(Last In First Out,LIFO)的原则,即最先存储的数据最后取出。
在汇编语言中,我们可以使用两种堆栈操作指令来进行操作:PUSH 和 POP。这些指令的语法如下:
PUSH operand
: 将操作数推入堆栈。POP address/register
: 从堆栈中弹出数据并存储到指定地址或寄存器中。堆栈的实现依赖于堆栈段中预留的内存空间。寄存器 SS 和 ESP(或 SP)用于管理堆栈。栈顶指针(ESP)指向最后插入到堆栈中的数据项,其中 SS 寄存器指向堆栈段的开头。堆栈的增长方向是向低内存地址增加,而栈顶指向最后插入的一项,指向插入的最后一个字的低字节。
堆栈的一些特点包括:
在使用寄存器的值之前,我们可以先将其存储到堆栈中,如下:
PUSH AX
PUSH BX
MOV AX, VALUE1
MOV BX, VALUE2
MOV VALUE1, AX
MOV VALUE2, BX
POP BX
POP AX
示例:下面程序利用循环输出整个 ascii 字符集
section .text
global _start ; 必须为了使用链接器 (gcc)
_start:
call display ; 调用 display 过程
mov eax, 1 ; 系统调用号 (sys_exit)
int 0x80 ; 调用内核
display:
mov ecx, 256 ; 设置循环计数器,控制输出字符的次数
next:
push ecx ; 保存循环计数器的值
mov eax, 4 ; 系统调用号 (sys_write)
mov ebx, 1 ; 文件描述符 (stdout)
mov ecx, achar ; 输出字符的地址
mov edx, 1 ; 输出字符的长度
int 80h ; 调用内核进行输出
pop ecx ; 恢复循环计数器的值
mov dx, [achar] ; 将当前字符的 ASCII 值加载到 DX 寄存器
cmp byte [achar], 0dh ; 比较当前字符是否为回车符 '\r'
inc byte [achar] ; 将字符 '0' 到 '9' 逐个增加
loop next ; 继续循环
ret ; 返回
section .data
achar db '0' ; 存储当前输出的字符
编译运行后的结果输出如下:
0123456789:;<=>?@ABCDEFGHIJKLMNOPQRSTUVWXYZ[\]^_`abcdefghijklmnopqrstuvwxyz{|} ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿AÃŅLJɉˋ͍ϏёӓՕחٙۛݝߟᢢ䥥稨ꫫ�ÿ
...
...
递归过程是一种调用自身的过程。递归又分为两种:直接递归和间接递归。直接递归是过程调用自身;间接递归是第一个过程调用第二个过程,第二个过程又调用第一个过程。
下面我们用汇编以递归的方式实现一个阶乘,计算阶乘 3:
section .text
global _start ; 必须为了使用链接器 (gcc)
_start:
mov bx, 3 ; 设置 bx 为 3,用于计算 3 的阶乘
call proc_fact ; 调用 proc_fact 过程计算阶乘
add ax, 30h ; 将结果转换为 ASCII 码
mov [fact], ax ; 将结果存储在 fact 变量中
; 输出 "Factorial 3 is:"
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 ; 比较 bl 是否为 1
jg do_calculation ; 如果 bl 大于 1,则进行计算
mov ax, 1 ; 如果 bl 等于 1,则结果为 1
ret
do_calculation:
dec bl ; 减少 bl 的值
call proc_fact ; 递归调用 proc_fact 过程
inc bl ; 恢复 bl 的值
mul bl ; 计算阶乘,ax = al * bl
ret
section .data
msg db 'Factorial 3 is:', 0xa ; 消息内容
len equ $ - msg ; 消息长度
section .bss
fact resb 1 ; 存储计算结果的变量
编译运行后的结果为:
Factorial 3 is:
6
编写宏是汇编语言实现模块化编程的另一种方式:
宏定义的语法 :
%macro macro_name number_of_params
%endmacro
其中,number_of_params指定参数数量,macro_name指定宏的名称。
通过使用宏名称以及必要的参数来调用宏。 当您需要在程序中多次使用某些指令序列时,可以将这些指令放入宏中并使用它
下面的示例演示了如何定义宏和使用宏:
%macro write_string 2
mov eax, 4
mov ebx, 1
mov ecx, %1
mov edx, %2
int 0x80
%endmacro
section .data
msg1 db 'Hello World'
len1 equ $ - msg1
section .text
global _start
_start:
write_string msg1, len1
mov eax, 1
int 0x80
上面的程序编译运行后的结果如下:
Hello World
系统将任何输入或输出数据视为字节流,标准的文件流有 3 种:
文件描述符作文文件 ID 分配给文件的 16 位整数。当创建文件或打开现有文件时,文件描述符用于访问文件
标准文件流的文件描述符 - stdin、stdout 和 stderr 分别为 0、1 和 2。
下表简要描述了与文件处理相关的系统调用 −
%eax | Name | %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 |
使用系统调用所需的步骤与我们之前讨论的相同 −
sys_creat()
编号 8 放入 EAX 寄存器。sys_open()
编号 5 放入 EAX 寄存器。sys_read()
编号 3 放入 EAX 寄存器。sys_write()
编号 4 放入 EAX 寄存器。sys_close()
编号 6 放入 EAX 寄存器。sys_lseek()
编号 19 放入 EAX 寄存器。 下面用一个复杂的例子演示一下如何使用系统调用:
section .text
global _start ; 必须声明以供使用gcc
_start: ; 告诉链接器入口点在这里
; 创建文件
mov eax, 8 ; 使用 sys_creat() 系统调用,编号为 8
mov ebx, file_name ; 文件名存储在 ebx 寄存器中
mov ecx, 0777o ; 文件权限,八进制表示,为所有用户设置读、写和执行权限
int 0x80 ; 调用内核
mov [fd_out], eax ; 存储文件描述符以供后续使用
; 写入文件
mov edx, len ; 要写入的字节数
mov ecx, msg ; 要写入的消息
mov ebx, [fd_out] ; 文件描述符
mov eax, 4 ; 使用 sys_write() 系统调用,编号为 4
int 0x80 ; 调用内核
; 关闭文件
mov eax, 6 ; 使用 sys_close() 系统调用,编号为 6
mov ebx, [fd_out] ; 文件描述符
int 0x80 ; 调用内核
; 写入表示文件写入结束的消息
mov eax, 4 ; 使用 sys_write() 系统调用,编号为 4
mov ebx, 1 ; 文件描述符为标准输出
mov ecx, msg_done ; 要写入的消息
mov edx, len_done ; 要写入的字节数
int 0x80 ; 调用内核
; 以只读方式打开文件
mov eax, 5 ; 使用 sys_open() 系统调用,编号为 5
mov ebx, file_name ; 文件名存储在 ebx 寄存器中
mov ecx, 0 ; 以只读方式打开
mov edx, 0777o ; 文件权限,八进制表示,为所有用户设置读、写和执行权限
int 0x80 ; 调用内核
mov [fd_in], eax ; 存储文件描述符以供后续使用
; 从文件中读取
mov eax, 3 ; 使用 sys_read() 系统调用,编号为 3
mov ebx, [fd_in] ; 文件描述符
mov ecx, info ; 存储读取的数据的缓冲区
mov edx, 26 ; 要读取的字节数
int 0x80 ; 调用内核
; 关闭文件
mov eax, 6 ; 使用 sys_close() 系统调用,编号为 6
mov ebx, [fd_in] ; 文件描述符
int 0x80 ; 调用内核
; 打印信息
mov eax, 4 ; 使用 sys_write() 系统调用,编号为 4
mov ebx, 1 ; 文件描述符为标准输出
mov ecx, info ; 要写入的消息
mov edx, 26 ; 要写入的字节数
int 0x80 ; 调用内核
mov eax, 1 ; 使用 sys_exit() 系统调用,编号为 1
int 0x80 ; 调用内核
section .data
file_name db 'myfile.txt' ; 文件名
msg db 'Welcome to Tutorials Point' ; 要写入文件的消息
len equ $-msg ; 计算消息的字节数
msg_done db 'Written to file', 0xa ; 文件写入结束的消息
len_done equ $-msg_done ; 计算消息的字节数
section .bss
fd_out resb 1 ; 存储文件描述符的变量(写入文件用)
fd_in resb 1 ; 存储文件描述符的变量(读取文件用)
info resb 26 ; 存储从文件读取的数据的缓冲区
上述程序创建并打开名为 myfile.txt 的文件,并在此文件中写入文本"Welcome to Tutorials Point"。 接下来,程序从文件中读取数据并将数据存储到名为 info 的缓冲区中。 最后,它显示存储在 info 中的文本。
sys_brk()
系统调用由内核提供,用于在应用程序映像的数据部分之后分配内存,而无需在稍后移动它。此调用允许设置数据部分的最高可用地址。系统调用的唯一参数是需要设置的最高内存地址,该值存储在EBX寄存器中。
这个程序使用 sys_brk()
系统调用分配了16 KB的内存:
assemblyCopy codesection .text
global _start ;必须为使用gcc而声明
_start: ;告知链接器入口点
mov eax, 45 ;sys_brk
xor ebx, ebx
int 80h
add eax, 16384 ;要保留的字节数
mov ebx, eax
mov eax, 45 ;sys_brk
int 80h
cmp eax, 0
jl exit ;如果出错则退出
mov edi, eax ;EDI = 最高可用地址
sub edi, 4 ;指向最后一个DWORD
mov ecx, 4096 ;已分配的DWORD数
xor eax, eax ;清空eax
std ;反向
rep stosd ;对整个分配区域重复
cld ;将DF标志设置回正常状态
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 "分配了16 KB的内存!", 10
len equ $ - msg