更好的阅读体验,请点击 YinKai 's Blog 。
上面讨论的汇编程序的三个部分,也代码各种内存段。
有趣的是,如果将 section 关键字替换为 segment,将会得到相同的结果,这是因为对于汇编器而言,这两个关键字在某些上下文中是可以互相使用的,这两个关键字都是为了告诉汇编器下面的代码是代码段。
在分段内存模型中,系统内存被划分为不同的独立段组,每个段组由位于段寄存器中的指针引用。
每个段用于包含特定类其型的数据。其中一个段用于包含指令代码,另一个段用于存储数据元素,第三个段用于保存程序堆栈。
这种划分使得程序可以更灵活地管理内存,有选择地引用不同类型的数据和指令,从而更有效地执行各种计算任务。
因此,我们可以将各种内存段指定为:
处理器操作主要涉及对数据的处理,而数据通常存储在内存中。然而,内存访问可能会降低处理器速度,因为它需要通过控制总线发送请求并进行复杂的内存访问。
为了提高速度,处理器包含一些内部存储位置,称为寄存器。
IA-32架构中包含 10 个 32 位和 6 个 16 位的处理器寄存器,主要分为三类:
通用寄存器进一步可以分为:
在IA-32架构中,有四个32位的数据寄存器,分别是EAX、EBX、ECX、EDX。这些寄存器可以按照不同的位数划分为更小的寄存器,具体如下:
一些数据寄存器在算术运算中具有特定用途:
指针寄存器是指 32 位的 EIP、ESP 和 EBP 寄存器以及相应的 16 位 右部分 IP、SP 和 BP。
指针寄存器可以分为三类:
索引寄存器包括32位的 ESI 和 EDI 以及它们的 16 位最右边的部分。SI 和 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操作的最后一位内容。
段在计算机内存中是为了组织和管理存储空间而引入的概念。在汇编编程中,处理器通过段寄存器来访问内存位置。以下是关于段的主要信息:
在汇编编程中,程序需要访问内存位置。段内的所有内存位置都相对于段的起始地址。段从可被 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 这两个系统调用,分别用于写入屏幕和退出程序。
我们在汇编程序中使用系统调用,需要按照如下步骤:
可以存储系统调用参数的存储器有 基址寄存器 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