汇编语言学习(3)

更好的阅读体验,请点击 YinKai 's Blog 。

内存段

​ 上面讨论的汇编程序的三个部分,也代码各种内存段。

​ 有趣的是,如果将 section 关键字替换为 segment,将会得到相同的结果,这是因为对于汇编器而言,这两个关键字在某些上下文中是可以互相使用的,这两个关键字都是为了告诉汇编器下面的代码是代码段。

内存段

​ 在分段内存模型中,系统内存被划分为不同的独立段组,每个段组由位于段寄存器中的指针引用。

​ 每个段用于包含特定类其型的数据。其中一个段用于包含指令代码,另一个段用于存储数据元素,第三个段用于保存程序堆栈。

​ 这种划分使得程序可以更灵活地管理内存,有选择地引用不同类型的数据和指令,从而更有效地执行各种计算任务。

​ 因此,我们可以将各种内存段指定为:

  • **数据段:由 .data 部分和 .bss 部分表示。 .data 部分用于声明内存区域,其中为程序存储数据元素,该部分在数据元素声明后无法扩展,并且在整个程序中保持静态。.bss 部分也是一个静态内存部分,其中包含稍后在程序中声明的数据的缓冲区。该缓冲区被零填充。
  • **代码段:**它由 .text 部分表示。这定义了内存中存储指令代码的区域。这也是一个固定区域。
  • **堆栈:**该段包含传递给程序内的函数和过程的数据值。

寄存器

​ 处理器操作主要涉及对数据的处理,而数据通常存储在内存中。然而,内存访问可能会降低处理器速度,因为它需要通过控制总线发送请求并进行复杂的内存访问。

​ 为了提高速度,处理器包含一些内部存储位置,称为寄存器。

处理器寄存器

​ IA-32架构中包含 10 个 32 位和 6 个 16 位的处理器寄存器,主要分为三类:

  1. **通用寄存器:**通用寄存器用于存储临时数据,进行算术、逻辑运算等操作。
  2. **控制寄存器:**控制寄存器用于控制和反映处理器的状态。
  3. **段寄存器:**段寄存器用于存储各个段的起始地址,实现内存访问和管理。

​ 通用寄存器进一步可以分为:

  1. 数据寄存器
  2. 指针寄存器
  3. 索引寄存器
数据寄存器

​ 在IA-32架构中,有四个32位的数据寄存器,分别是EAX、EBX、ECX、EDX。这些寄存器可以按照不同的位数划分为更小的寄存器,具体如下:

  • 作为完整的32位数据寄存器:EAX、EBX、ECX、EDX。
  • 32 位寄存器的下半部分可用作四个 16 位数据寄存器:AX、BX、CX 和 DX。
  • 上述4个16位寄存器的下半部分和上半部分可以用作8个8位数据寄存器:AH、AL、BH、BL、CH、CL、DH和DL。

​ 一些数据寄存器在算术运算中具有特定用途:

  • AX: 主累加器,用于输入/输出和大多数算术指令。例如,在乘法运算中,根据操作数的大小将一个操作数存储在EAX或AX或AL寄存器中。
  • BX: 被称为基址寄存器,用于索引寻址。
  • CX: 被称为计数寄存器,与ECX一样,存储迭代操作中的循环计数。
  • DX: 数据寄存器,用于输入/输出操作,与AX寄存器和DX一起使用,用于涉及大值的乘法和除法运算。
指针寄存器

​ 指针寄存器是指 32 位的 EIP、ESP 和 EBP 寄存器以及相应的 16 位 右部分 IP、SP 和 BP。

​ 指针寄存器可以分为三类:

  1. **指令指针(IP):**16 位 IP 寄存器存储下一条要执行的指令的偏移地址。 IP 与 CS 寄存器(代码段)(如CS : IP)关联,给出了代码段中当前指令的完整地址。
  2. 堆栈指针(SP): 16 位 SP 寄存器提供程序堆栈内的偏移值。 SP 与 SS 寄存器(堆栈段)(SS:SP)相关,指的是程序堆栈中数据或地址的当前位置。
  3. 基址指针(BP): 16 位 BP 寄存器主要帮助引用传递给子程序的参数变量。 SS 寄存器中的地址与 BP 中的偏移量相结合,得到参数的位置。 BP 还可以与 DI、SI(索引寄存器) 组合作为基址寄存器进行特殊寻址。
索引寄存器

​ 索引寄存器包括32位的 ESI 和 EDI 以及它们的 16 位最右边的部分。SI 和 DI 通常用于索引寻址,并有时用于执行加法和减法操作。这两个索引指针分别是:

  1. 来源索引 (SI): 用作字符串操作的源索引。在字符串处理中,SI通常用于指向源字符串的当前位置。
  2. 目的地索引 (DI): 用作字符串操作的目标索引。DI通常用于指向目标字符串的当前位置,特别是在字符串复制等操作中。
控制寄存器

​ 控制寄存器包括 32 位指令指针寄存器和 32 位标志寄存器,用于管理程序的执行流程和状态。其中的常见标志位有:

  • 溢出标志 (OF): 表示有符号算术运算后数据的高位(最左位)是否溢出。

  • 方向标志 (DF): 确定移动或比较字符串数据的左或右方向。DF为0时,字符串操作从左到右;DF为1时,字符串操作从右到左。

  • 中断标志 (IF): 决定是否忽略或处理外部中断,如键盘输入。IF为0时禁用外部中断,为1时启用中断。

  • 陷阱标志 (TF): 允许将处理器设置为单步模式,以便一次执行一条指令,常用于调试。

  • 符号标志 (SF): 显示算术运算结果的符号,由最左边位的高位表示。SF为0表示正结果,为1表示负结果。

  • 零标志 (ZF): 表示算术或比较运算的结果是否为零。ZF为1表示零结果,为0表示非零结果。

  • 辅助进位标志 (AF): 包含算术运算后从位 3 到位 4 的进位,用于特殊的算术操作。

  • 奇偶校验标志 (PF): 表示算术运算结果中1位的总数,用于奇偶校验。PF为1表示奇数个1位,为0表示偶数个1位。

  • 进位标志 (CF): 包含算术运算后从高位(最左边)的进位,也存储shift或rotate操作的最后一位内容。

段寄存器

​ 段在计算机内存中是为了组织和管理存储空间而引入的概念。在汇编编程中,处理器通过段寄存器来访问内存位置。以下是关于段的主要信息:

  1. 代码段(CS):
    • 包含要执行的指令的区域。
    • 由 16 位代码段寄存器(CS 寄存器)存储代码段的起始地址。
  2. 数据段(DS):
    • 包含数据、常量和工作区的区域。
    • 由 16 位数据段寄存器(DS 寄存器)存储数据段的起始地址。
  3. 堆栈段(SS):
    • 包含过程或子例程的数据和返回地址,实现为堆栈数据结构。
    • 由16位堆栈段寄存器(SS 寄存器)存储堆栈的起始地址。
  4. 其他段寄存器:
    • 额外段(ES): 提供额外的段来存储数据。
    • FS 和 GS: 提供额外的段用于特定目的。

​ 在汇编编程中,程序需要访问内存位置。段内的所有内存位置都相对于段的起始地址。段从可被 16 整除的地址开始,因此所有这类内存地址中最右边的十六进制数字通常是 0。为了引用段中的任何内存位置,处理器将段寄存器中的段地址与该位置的偏移值组合起来。

示例

​ 下面的程序会在代码中输出 9 个连续的星号。

section	.text
   global _start	 ;必须为链接器声明(gcc)
	
_start:	         ;告诉链接器入口点
   mov	edx,len  ;消息长度
   mov	ecx,msg  ;要写入的消息
   mov	ebx,1    ;文件描述符(stdout)
   mov	eax,4    ;系统调用号(sys_write)
   int	0x80     ;调用内核
	
   mov	edx,9    ;消息长度
   mov	ecx,s2   ;要写入的消息
   mov	ebx,1    ;文件描述符(stdout)
   mov	eax,4    ;系统调用号(sys_write)
   int	0x80     ;调用内核
	
   mov	eax,1    ;系统调用号(sys_exit)
   int	0x80     ;调用内核
	
section	.data
msg db 'Displaying 9 stars',0xa ;一条消息
len equ $ - msg  ;消息的长度
s2 times 9 db '*' ;9个星号

​ 我们使用以下命令进行编译和执行:

nasm -f elf nine_stars.asm 
ld -m elf_i386 -s -o nine_stars nine_stars.o

​ 输出结果如下:

Displaying 9 stars
*********

系统调用

​ 系统调用是用户空间和内核空间之间接口的 API。我们之前已经使用过了 sys_write 和 sys_exit 这两个系统调用,分别用于写入屏幕和退出程序。

Linux 系统调用

​ 我们在汇编程序中使用系统调用,需要按照如下步骤:

  1. 将系统调用号放入 EAX 寄存器中;
  2. 将系统调用的参数存储在 EBX、ECX 等寄存器中
  3. 调用相关中断(0x80),然后执行 EAX 中的系统调用号对应的程序
  4. 结果通常返回 EAX 寄存器中

​ 可以存储系统调用参数的存储器有 基址寄存器 EBX、计数寄存器 ECX、数据寄存器 EDX、源索引寄存器 ESI、目标索引寄存器 EDI、基址指针寄存器 EBP。

​ 下面给大家演示一下几个示例:

​ (1)使用 sys_exit:

mov eax, 1 ; 系统调用号 sys_exit
int 0x80   ; 调用内核

​ (2)使用 sys_write:

mov edx, 4		; 消息长度
mov ecx, msg	; 要写入的消息
mov ebx, 1		; 文件描述符
mov eax, 4		; 系统调用号
int 0x80		; 调用内核
常见系统调用
%eax Name %ebx %ecx %edx %esx %edi
1 sys_exit int - - - -
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 - - - -
示例

​ 下面举一个复杂一点的例子,包含了之前我们讲过的 data、bss、text 三个部分,也希望通过这个例子,加深一下大家对 data 部分和 bss 部分的区别

section .data                           ; 数据段
   userMsg db '请输入一个数字: '        ; 提示用户输入数字的消息
   lenUserMsg equ $-userMsg             ; 消息的长度
   dispMsg db '您输入的是: '
   lenDispMsg equ $-dispMsg                 

section .bss           ; 未初始化的数据
   num resb 5            ; 用于存储用户输入的变量,5字节

section .text          ; 代码段
   global _start        ; 声明程序入口点

_start:                ; 程序入口
   ; 输出提示消息 '请输入一个数字: '
   mov eax, 4
   mov ebx, 1
   mov ecx, userMsg
   mov edx, lenUserMsg
   int 80h
   ; 读取并存储用户输入
   mov eax, 3
   mov ebx, 2
   mov ecx, num  
   mov edx, 5          ; 读取5字节的信息(数字和符号,1字节)
   int 80h	
   ; 输出消息 '您输入的是: '
   mov eax, 4
   mov ebx, 1
   mov ecx, dispMsg
   mov edx, lenDispMsg
   int 80h  
   ; 输出用户输入的数字
   mov eax, 4
   mov ebx, 1
   mov ecx, num
   mov edx, 5
   int 80h  
   ; 退出程序
   mov eax, 1
   mov ebx, 0
   int 80h

​ 同样,我们需要通过下述命令来编译运行:

nasm -f elf get_num.asm 				# 将汇编程序编译成机器码
ld -m elf_i386 -s -o get_num get_num.o	# 将目标文件和其他必要的文件组合成可执行文件
./get_num 								# 运行可执行文件

​ 输出结果如下:

请输入一个数字: 
123
您输入的是: 123

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