汇编语言学习(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 中减去,以获得数字 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 寄存器指向堆栈段的开头。堆栈的增长方向是向低内存地址增加,而栈顶指向最后插入的一项,指向插入的最后一个字的低字节。

​ 堆栈的一些特点包括:

  • 只有字(words)或双字(doublewords)可以保存到堆栈中,而不是字节。
  • 堆栈向相反方向增长,即向低内存地址增加。
  • 栈顶指针指向栈中最后插入的一项,它指向插入的最后一个字的低字节。

​ 在使用寄存器的值之前,我们可以先将其存储到堆栈中,如下:

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

​ 编写宏是汇编语言实现模块化编程的另一种方式:

  • 宏是一系列指令,由名词指定,可以在程序中的任意位置使用
  • 在 NASM 中,宏使用 %macro%endmarro 指令定义,以前者开头,后者结尾

​ 宏定义的语法 :

%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 种:

  • 标准输入(stdin)
  • 标准输出(stdout)
  • 标准错误(stderr)

​ 文件描述符作文文件 ID 分配给文件的 16 位整数。当创建文件或打开现有文件时,文件描述符用于访问文件

​ 标准文件流的文件描述符 - stdin、stdoutstderr 分别为 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 .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

你可能感兴趣的:(汇编,学习,汇编)