写这个系列文章的主要目的是记录书中重要的知识点,并和大家分享一些个人理解与实践。由于笔记中的知识点比较零散,而书中系统的介绍了一个 x86-16 处理器在实模式下的工作原理以及如何使用汇编语言与其进行“沟通”,所以推荐想要系统学习的朋友们去学习这本书。当我们掌握了实模式的工作原理之后,就可以进一步研究后来出现的其他运行模式(如保护模式)。除此之外,熟悉汇编语言有助于我们掌握上层语言(如 C)的执行原理,因为它们都要对汇编(机器码)进行抽象,而汇编程序就是基于 CPU 的执行机理写出来的。

前言

学习汇编的两个最根本目的

  • 充分获得底层编程的体验(在一个没有操作系统的环境中面向硬件编程。所以本书中不会涉及汇编器
  • 深刻理解机器运行程序的机理
一门课程是由相互关联的知识构成的,这些知识在一本书中如何组织则是一种信息组织和加工的艺术。学习是一个循序渐进的过程(物有本末,事有终始,之所先后,则近道矣!),但并不是所有的教学都是以这种方式完成的,这并不是我们所希望的事情,因为任何不以循序渐进的方式进行的学习,都将出现盲目探索和不成系统的情况,最终学习到的也大都是相对零散的知识,并不能建立起一个系统的知识结构。非循序渐进的学习,也达不到循序渐进学习所能达到的深度,因为后者是步步深入的,每一步都以前一步为基础。

第一章

汇编语言的组成

  • 汇编指令:机器码的助记符(与 ISA 中的机器码一一对应)
  • 伪指令:没有对应的机器码,由编译器执行,CPU 并不执行(主要作用是引导汇编器进行编译
  • 标号(Label):一个标号代表了一个地址
  • 其他符号:如 + - * / 等,由编译器识别,没有对应的机器码

系统总线

电子计算机能处理、传输的信息都是电信号,电信号要用导线传送。所以总线从物理上来讲就是一根根导线的集合。

  • 地址总线的宽度决定了寻址能力(8086 地址总线宽度为 20 位)
  • 数据总线的宽度决定了 CPU 和外界的数据传送速度(传送数据需要的次数
  • 控制总线是一些不同控制线的集合。有多少根控制线,就意味着 CPU 提供了对外部器件的多少种控制。所以,它的宽度决定了 CPU 对外部器件的控制能力

存储器

存储器可以理解为由 N 个大小相同的存储单元组成,所以说存储单元是存储器最小的存储单位
存储单元的大小取决于存储器的编址方式。在现代计算机中:每个存储单元的大小通常是 1 字节。

一个存储单元有地址内容两个属性:

  • 地址:每个存储单元的唯一编号(用于供 CPU 访存时来定位存储单元)
  • 内容:存储单元存放的内容即 指令 或 数据

输入/输出设备

I/O 设备通常被划分为两个部分:外设I/O 接口

  • 外设就是连接在计算机外部的设备,如:鼠标、键盘、打印机、显示器、网络等。由于外设的种类繁多,不同外设之间的差异很大(比如:接口、信号、数据传输率等差异),而 CPU 与外部器件的交互方式比较单一(CPU 的想法是:我给出一个地址,来让我操作对应的数据即可),导致它无法与外设直接连接。这时,I/O 接口就出现了。
  • I/O 接口就是连接在 CPU 和外设之间的“中转站”。它的出现让 CPU 和外设之间实现了“解耦”:CPU 只需像往常一样,给出一个地址与之交互;它来帮助 CPU 完成与不同外设之间的交互。

    • I/O 接口主要负责信号转换(如:数字信号<->模拟信号、串行信号<->并行信号)、协调、数据缓冲(解决 CPU 和外设的速度差异)等工作。
    • I/O 接口中包含许多寄存器,如:数据输入寄存器用来保存来自外设输入的数据、数据输出寄存器用来保存 CPU 向外设输出的数据、控制寄存器、状态寄存器等等,这些寄存器被称为 I/O 端口。与存储器的存储单元一样,每个 I/O 端口都会对应一个地址。

存储器和 I/O 接口的编址方式

存储器和 I/O 接口的编址方式通用有两种:

  1. Memory Mapped I/O——存储器和 I/O 接口统一编址(使用同一个地址空间)
  2. I/O Mapped I/O——存储器和 I/O 接口分开编址(使用两个独立的地址空间。常用于 RISC 中,因为 RISC 指令的特征能避免这种编址方式存在的一些弊端)

本书中介绍的是 Memory Mapped I/O 方式:

  • 8086 主存的地址空间为:0~9FFFF (640KB)
  • 8086 显存的地址空间为:A0000~BFFFF (128KB)
  • 8086 只读存储器的地址空间为:C0000~FFFFF (256KB)
    地址空间总大小:1024KB (1MB) = 2^20 bytes, 所以 8086 对外提供了 20 根地址总线。
    FFFF0H 单元中的指令是 8086 开机后执行的第一条指令

第二章

N 位结构(N 位机、字长为 N 位)特性

  • 运算器每次最多能支持 N 位的算数运算或逻辑运算
  • 寄存器的最大位宽通常是 N 位
  • 寄存器和运算器之间的内部通路支持 N 位的数据传输
  • 外部数据总线的宽度通常是 N 位(8088 比较特殊,对外提供的数据总线是 8 位)

8086的段

注意:物理内存并没有分段,段的逻辑划分来自于 CPU8086 物理地址的生成方式:基地址(段基址*16) + 偏移地址 = 物理地址”),使得可以用分段的方式来管理内存。(这种分段方式的弊端是:不同段对应的内存可能是:完全独立、部分重合、完全重合的,如 2000:01001:FFF0 物理地址完全重合)

一个段的起始地址(基地址)一定是 16 的倍数;偏移地址为 16 位,16 位地址的寻址能力为 64KB, 所以在 8086 处理器中一个段的长度最大为 64KB.

8086 中 CS:IP 表示的内存区域代表着指令。8086 中没有提供 mov 类指令来修改 IP 寄存器,只能通过特殊指令(如转移指令)改变。转移指令详见第九章内容。

实验一:查看 CPU 和内存,用机器指令和汇编指令编程

MacOS 中搭建 DOS 环境

  1. 下载 dosbox: https://www.dosbox.com/download.php?main=1
  2. 创建目录,作为挂载点

    mkdir ~/DOS
  3. 将下载的 DOSBox-0.74-3-3.dmg 包里面的内容拷贝至 ~/DOS 目录中
  4. 下载 debug.exe, 并将其拷贝至 ~/DOS/bin 目录中
  5. 修改 dosbox 的配置文件,将挂载等操作加入到 [autoexec] 中(这样每次启动 dosbox 会自动执行这些命令)

    # vi ~/Library/Preferences/DOSBox\ 0.74-3-3\ Preferences
    # 在 [autoexec] 中加入以下命令:
    mount c ~/DOS
    C:
    set PATH=%PATH%;C:\bin\
  6. 启动 DOSBox.app
  7. 执行 debug, 可以进行调试了!

    debug

debug 的常用参数选项(可以和 GDB 进行类比)

# 寄存器
-r         # 查看所有寄存器的内容
-r ax      # 修改寄存器内容

# 内存
-d 1000:0    # 从指定内存单元开始,显示 128 个内存单元的内容(128字节)
-d           # 继续显示“接下来的” 128 个内存单元的内容
-d 1000:0 9  # 格式————“d 段地址:偏移地址 结尾的偏移地址”
-e 1000:0 1 2 3 'a' 'b' 'c' "str"  # 修改指定内存单元开始的连续字节内容
-e 1000:0    # 修改指定内存单元开始的连续字节内容(通过“回车、空格、空格……回车”进行交互)

# 指令(使用 -e 直接输入机器码的16进制进行编辑)
-u 1000:0  # 从指定内存单元开始,显示其对应的指令(机器码+汇编)
-u         # 继续显示“接下来的”指令
-t         # 执行一条指令
-a 1000:0  # 以汇编的形式在指定内存中写入指令
-a         # 以汇编的形式在“接下来的”内存中写入指令

# 退出 debug
-quit
-q

# 补充
-d|e|u|a 段寄存器名:偏移地址(逻辑地址)  # 如:-a ds:0, -e cs:0
-g 偏移地址  # 如:-g 0012 执行 [CS:IP~CS:0012) 之间的指令,相当于打【断点】
-p  # 执行 `int 21H` 指令时,需要用 -p; 在循环中,使用 -p 可以完成所有循环的执行

第三章

起始地址为 N 的字单元简称为:N 地址字单元

8086 指令操作数规则(了解即可,必要时查阅《Intel 开发手册(第二卷)》来确定具体的格式):

  • 不支持将【立即数】直接送入【段寄存器】的操作
  • 不支持将【立即数】直接送入【内存单元】的操作
  • 不支持操作数不能同时是两个【内存单元】
  • 支持的形式这里不一一列举,参见书中 3.4(第53页)

处理器通过 ss(栈基址) 和 sp(栈指针) 来确定一个栈。

入栈指令 pushsp 的影响,比如:push ax

  • sp=sp-2
  • ax 内容写入栈顶

出栈指令 pop 对 sp 的影响,比如:pop ax

  • 栈顶内容写入 ax
  • sp=sp+2(注:刚刚栈顶的内容不会被擦除,下次使用时覆盖即可)

栈顶越界问题

书中说 8086 硬件层面没有做这种控制,在 debug 中测试了确实是。但在保护模式中,提供了“段界限检查”机制。


zandy
1 声望0 粉丝