本文主要是介绍汇编语言学习(7)完结篇,希望对大家解决编程问题提供一定的参考价值,需要的开发者们随着小编来一起学习吧!
更好的阅读体验,请点击 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 中减去,以获得数字 4mov edx, '5' ; 将字符 '5' 的 ASCII 值加载到 EDX 寄存器sub edx, '0' ; 将 '0' 的 ASCII 值从 EDX 中减去,以获得数字 5call sum ; 调用 sum 过程,将结果存储在 EAX 中mov [res], eax ; 将结果存储在 res 变量中; 输出 "The sum is:" 到标准输出mov ecx, msgmov edx, lenmov ebx, 1 ; 文件描述符 (stdout)mov eax, 4 ; 系统调用号 (sys_write)int 0x80 ; 调用内核; 输出结果到标准输出mov ecx, resmov edx, 1mov ebx, 1 ; 文件描述符 (stdout)mov eax, 4 ; 系统调用号 (sys_write)int 0x80 ; 调用内核; 退出程序mov eax, 1 ; 系统调用号 (sys_exit)int 0x80 ; 调用内核sum:mov eax, ecx ; 将 ECX 中的值移动到 EAXadd eax, edx ; 将 EDX 中的值加到 EAXadd 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 寄存器指向堆栈段的开头。堆栈的增长方向是向低内存地址增加,而栈顶指向最后插入的一项,指向插入的最后一个字的低字节。
堆栈的一些特点包括:
- 只有字(words)或双字(doublewords)可以保存到堆栈中,而不是字节。
- 堆栈向相反方向增长,即向低内存地址增加。
- 栈顶指针指向栈中最后插入的一项,它指向插入的最后一个字的低字节。
在使用寄存器的值之前,我们可以先将其存储到堆栈中,如下:
PUSH AX
PUSH BXMOV AX, VALUE1
MOV BX, VALUE2MOV VALUE1, AX
MOV VALUE2, BXPOP 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 是否为 1jg do_calculation ; 如果 bl 大于 1,则进行计算mov ax, 1 ; 如果 bl 等于 1,则结果为 1retdo_calculation:dec bl ; 减少 bl 的值call proc_fact ; 递归调用 proc_fact 过程inc bl ; 恢复 bl 的值mul bl ; 计算阶乘,ax = al * blretsection .data
msg db 'Factorial 3 is:', 0xa ; 消息内容
len equ $ - msg ; 消息长度section .bss
fact resb 1 ; 存储计算结果的变量
编译运行后的结果为:
Factorial 3 is:
6
宏
编写宏是汇编语言实现模块化编程的另一种方式:
- 宏是一系列指令,由名词指定,可以在程序中的任意位置使用
- 在 NASM 中,宏使用 %macro 和 %endmarro 指令定义,以前者开头,后者结尾
宏定义的语法 :
%macro macro_name number_of_params
<macro body>
%endmacro
其中,number_of_params指定参数数量,macro_name指定宏的名称。
通过使用宏名称以及必要的参数来调用宏。 当您需要在程序中多次使用某些指令序列时,可以将这些指令放入宏中并使用它
示例
下面的示例演示了如何定义宏和使用宏:
%macro write_string 2mov eax, 4mov ebx, 1mov ecx, %1mov edx, %2int 0x80
%endmacrosection .datamsg1 db 'Hello World'len1 equ $ - msg1section .textglobal _start_start:write_string msg1, len1mov eax, 1int 0x80
上面的程序编译运行后的结果如下:
Hello World
文件管理
系统将任何输入或输出数据视为字节流,标准的文件流有 3 种:
- 标准输入(stdin)
- 标准输出(stdout)
- 标准错误(stderr)
文件描述符作文文件 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 |
使用系统调用所需的步骤与我们之前讨论的相同 −
- 将系统调用号放入EAX寄存器中。
- 将系统调用的参数存储在寄存器 EBX、ECX 等中。
- 调用相关中断(80h)。
- 结果通常返回到 EAX 寄存器中。
创建并打开文件
- 将系统调用
sys_creat()
编号 8 放入 EAX 寄存器。 - 将文件名放入 EBX 寄存器。
- 将文件权限放入 ECX 寄存器。
- 系统调用返回 EAX 寄存器中创建的文件的文件描述符,错误代码存储在 EAX 寄存器中。
打开现有文件
- 将系统调用
sys_open()
编号 5 放入 EAX 寄存器。 - 将文件名放入 EBX 寄存器。
- 将文件访问模式放入 ECX 寄存器。
- 将文件权限放入 EDX 寄存器。
- 系统调用返回 EAX 寄存器中打开的文件的文件描述符,错误代码存储在 EAX 寄存器中。
- 常用的文件访问模式包括:只读(0)、只写(1)和读写(2)。
从文件中读取
- 将系统调用
sys_read()
编号 3 放入 EAX 寄存器。 - 将文件描述符放入 EBX 寄存器。
- 将指向输入缓冲区的指针放入 ECX 寄存器。
- 将缓冲区大小(即要读取的字节数)放入 EDX 寄存器。
- 系统调用返回在 EAX 寄存器中读取的字节数,错误代码存储在 EAX 寄存器中。
写入文件
- 将系统调用
sys_write()
编号 4 放入 EAX 寄存器。 - 将文件描述符放入 EBX 寄存器。
- 将指向输出缓冲区的指针放入 ECX 寄存器。
- 将缓冲区大小(即要写入的字节数)放入 EDX 寄存器。
- 系统调用返回 EAX 寄存器中实际写入的字节数,错误代码存储在 EAX 寄存器中。
关闭文:
- 将系统调用
sys_close()
编号 6 放入 EAX 寄存器。 - 将文件描述符放入 EBX 寄存器。
- 如果出现错误,系统调用将返回 EAX 寄存器中的错误代码。
更新文件
- 将系统调用
sys_lseek()
编号 19 放入 EAX 寄存器。 - 将文件描述符放入 EBX 寄存器。
- 将偏移值放入 ECX 寄存器。
- 将偏移的参考位置放入 EDX 寄存器。
- 参考位置可以是文件开头(值 0)、当前位置(值 1)或文件结尾(值 2)。
- 如果出现错误,系统调用将返回 EAX 寄存器中的错误代码。
示例
下面用一个复杂的例子演示一下如何使用系统调用:
section .textglobal _start ; 必须声明以供使用gcc_start: ; 告诉链接器入口点在这里; 创建文件mov eax, 8 ; 使用 sys_creat() 系统调用,编号为 8mov 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() 系统调用,编号为 4int 0x80 ; 调用内核; 关闭文件mov eax, 6 ; 使用 sys_close() 系统调用,编号为 6mov ebx, [fd_out] ; 文件描述符int 0x80 ; 调用内核; 写入表示文件写入结束的消息mov eax, 4 ; 使用 sys_write() 系统调用,编号为 4mov ebx, 1 ; 文件描述符为标准输出mov ecx, msg_done ; 要写入的消息mov edx, len_done ; 要写入的字节数int 0x80 ; 调用内核; 以只读方式打开文件mov eax, 5 ; 使用 sys_open() 系统调用,编号为 5mov ebx, file_name ; 文件名存储在 ebx 寄存器中mov ecx, 0 ; 以只读方式打开mov edx, 0777o ; 文件权限,八进制表示,为所有用户设置读、写和执行权限int 0x80 ; 调用内核mov [fd_in], eax ; 存储文件描述符以供后续使用; 从文件中读取mov eax, 3 ; 使用 sys_read() 系统调用,编号为 3mov ebx, [fd_in] ; 文件描述符mov ecx, info ; 存储读取的数据的缓冲区mov edx, 26 ; 要读取的字节数int 0x80 ; 调用内核; 关闭文件mov eax, 6 ; 使用 sys_close() 系统调用,编号为 6mov ebx, [fd_in] ; 文件描述符int 0x80 ; 调用内核; 打印信息mov eax, 4 ; 使用 sys_write() 系统调用,编号为 4mov ebx, 1 ; 文件描述符为标准输出mov ecx, info ; 要写入的消息mov edx, 26 ; 要写入的字节数int 0x80 ; 调用内核mov eax, 1 ; 使用 sys_exit() 系统调用,编号为 1int 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 .textglobal _start ;必须为使用gcc而声明_start: ;告知链接器入口点mov eax, 45 ;sys_brkxor ebx, ebxint 80hadd eax, 16384 ;要保留的字节数mov ebx, eaxmov eax, 45 ;sys_brkint 80hcmp eax, 0jl exit ;如果出错则退出 mov edi, eax ;EDI = 最高可用地址sub edi, 4 ;指向最后一个DWORD mov ecx, 4096 ;已分配的DWORD数xor eax, eax ;清空eaxstd ;反向rep stosd ;对整个分配区域重复cld ;将DF标志设置回正常状态mov eax, 4mov ebx, 1mov ecx, msgmov edx, lenint 80h ;打印一条消息exit:mov eax, 1xor ebx, ebxint 80hsection .data
msg db "分配了16 KB的内存!", 10
len equ $ - msg
这篇关于汇编语言学习(7)完结篇的文章就介绍到这儿,希望我们推荐的文章对编程师们有所帮助!