小甲鱼汇编教程

# 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
更新于 阅读次数

请我喝[茶]~( ̄▽ ̄)~*

dmq 微信支付

微信支付

dmq 支付宝

支付宝