小甲鱼汇编教程
# 8086CPU 为例
# 汇编语言
汇编语言是二进制指令的文本形式,是机器码的助记符。
- 不同的 CPU 型号所支持的汇编语言是不同的
# CPU 的型号
主要有:Intel 系列(8086 等)、AMD 系列和 Apple 系列(M1 等、基于 ARM 架构)
- CPU 由运算器、控制器和寄存器等组成,它们通过内部总线相连
- 区别:内部总线实现 CPU 内部各个器件之间的联系,外部总线实现 CPU 和主板上其他器件之间的联系
# CPU 寄存器
8086CPU 有 14 个寄存器:AX、BX、CX、DX、SI、DI、SP、BP、IP、CS、SS、DS、ES、PSW。
- 寄存器的大小通常与数据总线(CPU 的位数)的大小相匹配,8086CPU 寄存器的大小为 16 位。
- 寄存器目前常使用 16 位,为了兼容 8 位的,将单个寄存器分为了寄存器高位和寄存器低位,例如:AX 分为了 AH 和 AL
# 段寄存器
8086CPU 有 4 个段寄存器:CS(代码段)、DS(数据段)、SS(堆栈段)、ES(附加段),当要访问内存时,将会由这 4 个段寄存器提供内存单元的段地址。
# CS 和 IP
CS 和 IP 是 CPU 中最关键的寄存器,它们指示了 CPU 当前要读取指令的地址。CS 为代码段寄存器,IP 为指令段寄存器。
- 大部分寄存器都可以通过 mov 指令来改变寄存器的值,但是 CS、IP 不行,8086CPU 允许通过 jmp 指令来修改 CS、IP 的值
# DS 数据段寄存器
- CPU 要读取一个内存单元时,必须先给出这个内存单元的地址
- 段寄存器不同于通用寄存器,数据不能直接送入到段寄存器中,而是应该先送入到通用寄存器,然后再送入到段寄存器中。
# CPU 位数
- CPU 位数指的是 CPU 的字长(CPU 寄存器的大小),即 CPU 一次能够处理的数据的位数,8080CPU 的位数是 16(表示其寄存器的大小是 4 个字节),算数逻辑单元是 16,地址总线是 16,意味着拥有 65536 字节的寻址空间
# 字和字长
- 字是计算机存储和处理数据的基本单位,是一个固定大小的数据块,进行算数逻辑运算时,CPU 通常是以字为单位进行数据处理的
- 字长决定了 CPU 一次能够处理的数据量,通常与 CPU 的寄存器大小和数据总线(CPU 位数)相匹配
# 算数逻辑单元和 CPU 位数
- 算数逻辑单元是 CPU 的一个关键组件,负责执行所有的算数运算和逻辑运算,直接关系到处理器的计算能力。
- CPU 位数(数据总线的宽度)是指 CPU 在一个时钟周期内能够处理的数据的宽度,CPU 的位数决定了 CPU 一次可以处理多少位的数据。
# 寄存器、内存和磁盘
- 寄存器是 CPU 内部的高度存储设备,而内存和磁盘分别是 CPU 外部的高速和低速存储设备
- 磁盘用于长期存储数据,当操作磁盘时,CPU 首先将磁盘数据加载到内存中,再将内存读取到寄存器中进行处理,最后 CPU 处理完之后写回到内存,最终存回磁盘。
# CPU 执行
CPU 执行的是二进制代码,而不是汇编代码,不过二进制代码并不是每一种组合都有意义,因此使用汇编语言对有意义的关键二进制代码进行助记符的标识,以此方便使用
# CPU 的栈操作
以字为单位,对栈进行操作:
- push ax:将 ax 中的数据送入栈中
- pop ax:将栈顶取出数据送入 ax 中
- 8086CPU 只知道栈顶在哪(由 SS:SP 指示),不知道程序安排的栈空间有多大,需要编程的时候自己操心越界问题
# 数据和指令
- 一段相同的二进制可能被表示为数据也可能表示为指令,具体要看程序员如何使用,如果使用指令则会通过控制总线传输,此时它代表指令,如果使用数据则会通过数据总线传输,此时它代表数据
# 内存的物理地址
内存的物理地址 = 段地址 * 段长(地址总线) + 偏移地址(页地址)
# 分段和分页
- 内存并没有分页和分段,分页和分段的划分来自于 CPU
- 内存分段和分页的存在有利于对内存进行逻辑分区,提供内存保护和简化内存管理。
- 某些操作系统可能会定义一个段的大小为字长的倍数,以便与 CPU 的数据处理能力相匹配。
- 理论上寻址能力取决于地址总线的位数,但由于寄存器中往往存储不了相同地址总线位数的数据,此时就需要分页和分段的机制将大位数的数据分割为 CS(段 / 页地址)和 IP(偏移地址),通过地址加法器计算出实际地址,再通过地址总线进行寻址。
# 内存管理机制
- 内存管理机制分为分段和分页,分段机制需要更多的硬件支持,地址转换速度较慢,它提供了更加灵活的内存管理,而分页机制需要较少的硬件支持,地址转换的速度较快,适合于大规模的内存管理。
- 页的大小是固定的、段的大小是不固定的
- 现代操作系统通常采用分页嵌套分段的机制,分页提供了有效的大量内存管理,并且可以与虚拟内存技术结合使用,分段用于在特定的代码段、文本段中,作为分页机制的补充。
# 字和字节
- 字节是计算机中最小的存储单位,由 8 个 bit 组成
- 字是计算机中的一个较大的数据单位,由多个字节组成,字的大小取决于计算的架构,常见的有:2 字节、4 字节和 8 字节。
# 编译对照
例如 c 语言:
int add_a_and_b(int a, int b) { | |
return a + b; | |
} | |
int main() { | |
return add_a_and_b(2, 3); | |
} |
通过 gcc 或 llvm 编译之后生成的汇编:
_add_a_and_b: # 标签,代表CPU运行流程
push %ebx # 将寄存器ebx的值写入当前栈帧中
mov %eax, [%esp+8] # mov指令将esp寄存器地址加上8个字节,得到新的地址,取出数据写入eax寄存器
mov %ebx, [%esp+12]
add %eax, %ebx # add指令用于将两个运算子相加,将结果写入第一个运算子
pop %ebx # pop指令用于取出Stack最近一个写入的值,并将这个值写入运算子指定的位置,并将地址加4个字节,即回收4个字节
ret # ret指令终止当前函数的执行,将运算权交给上层函数,并回收当前函数的栈帧。
_main: # 程序从这里开始执行,会在stack上建立一个帧,并将stack指向的地址写入ESP寄存器,后续如果有数据要写入main这个帧,就会写在ESP寄存器所保存的地址
push 3 # 将运算子3放入stack,ESP寄存器减4(因为Stack从高位向低位发展,3的类型是int,占用4个字节)
push 2
call _add_a_and_b # 用于调用函数,程序会找对应的标签,并创建一个新的帧
add %esp, 8 # 将esp寄存器的地址手动加上8个字节,再写回到esp寄存器,并回收了8个字节。
ret # ret终止当前函数,返回运算权,回收当前函数栈帧
# 汇编文件执行
汇编程序也需要编译器进行编译,最终编译为机器码,在计算机上执行。
.model small ; 伪指令,指示程序的内存模型,samll意味着程序、数据和堆栈都位于一个段内
.stack 100h ; 伪指令,设置程序的堆栈大小,100h是十六进制数,等于256字节
.data ; 伪指令,定义数据段的开始,可以定义程序中的全局变量和静态变量
buffer db 1 ; 用于存储一个字符的缓冲区
.code ; 伪指令,指示代码段
main proc
mov ax, @data
mov ds, ax ; 初始化数据段
; 调用BIOS中断读取键盘输入
mov ah, 01h ; 功能码:读取字符,不回显
int 16h ; 调用中断
; 将输入的字符存储在buffer中
mov [buffer], al
; 调用BIOS中断回显字符
mov ah, 0Eh ; 功能码:显示字符
mov al, [buffer] ; 获取输入的字符
int 10h ; 调用中断
; 程序结束
mov ax, 4C00h
int 21h
main endp
end main