00_前置知识
一、 指令集架构 ISA
-
定义: 计算机架构,特指指令集架构(Instruction Set Architecture, ISA)。它是软件与底层硬件(CPU)之间的“契约”或接口。它定义了 CPU 能理解的所有机器指令集合、寄存器的数量与种类、内存寻址模式、中断处理机制等。
-
本质: 汇编语言仅仅是 ISA 的人类可读表示形式。一种架构对应一种汇编语言(例如 x86 架构对应 x86 汇编,ARM 架构对应 ARM 汇编)。
-
发展:
-
早期大型机(如 IBM 巨型机)没有统一标准,软件完全无法跨硬件移植。
-
70-80年代,以 Intel x86 为代表的复杂指令集 (CISC) 成为 PC 市场主流,指令长度不固定,功能复杂,旨在节省极其昂贵的内存空间。
-
80年代后期,精简指令集 (RISC)(如 ARM、MIPS、PowerPC)兴起,主张指令等长、简单、极速执行,成为移动设备和嵌入式设备的主宰。
-
现代 x86 CPU 在底层其实会把复杂的 CISC 指令实时“解码”成类似 RISC 的微操作(Micro-ops)来执行,实现了软件兼容与硬件高效的统一。
-
二、 x86 体系结构与硬件演进
汇编语言的发展紧随 Intel x86 系列处理器的硬件更迭,核心逻辑是从 16位 向 32位 进而向 64位 兼容扩展。
1. 16位时代 (8086)
-
特征: x86 架构的鼻祖。采用“段寄存器 (16位) : 偏移地址 (16位)”的二维寻址方式,物理寻址空间仅 1MB。
-
寄存器: 使用 16位寄存器。
2. 32位时代 (80386 / IA-32)
-
特征: 架构史上的里程碑,引入了平坦的 32 位地址空间,支持 4GB 物理内存寻址。
-
寄存器扩展: 寄存器扩展到 32位。
3. 64位时代 (x86-64 / x64 / AMD64)
-
特征: 由 AMD 首先提出并由 Intel 跟进。在硬件层面保留了对 32/16 位的向下兼容。
-
寄存器翻倍: 寄存器扩展至 64位,并新增 R8-R15 通用寄存器。
三、 CPU 运行模式:安全与权限的进化
-
实模式 (Real Mode):
-
环境: 8086 CPU 的唯一工作模式。现代所有 x86/x64 电脑在开机加电的瞬间,依然处于实模式。
-
特点: 没有安全保护。任何程序都可以直接访问 1MB 地址空间内的任何物理内存,甚至是修改底层硬件端口。如果一个程序崩溃,整个系统就会死机。
-
-
保护模式 (Protected Mode):
-
环境: 从 80286 开始引入,在 80386 走向成熟。
-
特点: 现代操作系统(Windows, Linux)的基石。引入了虚拟内存、分页机制和权限特权级 (Ring 0 - Ring 3)。
-
Ring 权限: 操作系统内核运行在 Ring 0(最高权限),普通用户程序(如浏览器、游戏)运行在 Ring 3(最低权限)。用户程序无法直接操作硬件,必须向操作系统发送请求。
-
-
虚拟 8086 模式 (Virtual 8086 Mode):
- 特点: 在保护模式下模拟出的一个安全“沙箱”,让老旧的 16位 DOS 程序可以在 32位 Windows 下运行,而不会威胁系统安全。
-
长模式 (Long Mode):
- 特点: x64 CPU 的原生 64位模式,支持海量内存寻址,摒弃了传统的内存分段机制。
四、 汇编语言与操作系统的演进
1. DOS 时代 (实模式汇编)
-
内存模型: 程序员必须处理复杂的分段机制(代码段、数据段、附加段)。
-
系统调用: 程序拥有对硬件的绝对控制权。通过触发软件中断(如
INT 21H)来调用 DOS 系统服务(如读写文件、打印字符)。
2. 现代 Windows / 类 UNIX 时代 (保护模式/长模式汇编)
-
内存模型: 操作系统向应用程序提供了平坦内存模型 (Flat Memory Model)。汇编程序员不再需要关心段寄存器(CS, DS 默认指向相同的庞大空间),内存被抽象为一个连续的字节数组。
-
系统调用: 中断被淘汰,取而代之的是高效的
SYSCALL/SYSENTER指令。应用程序必须遵循操作系统的 API(如 Win32 API 或 Linux Syscalls)来进行底层操作。
五、 主流汇编器生态 Assemblers
不同的汇编器有着不同的语法流派。
| 汇编器名称 | 缩写 | 历史与特点 | 语法流派 | 适用场景 |
|---|---|---|---|---|
| Microsoft Macro Assembler | MASM | 微软官方出品,闭源。历史悠久,宏功能极其强大,深度绑定 Windows 平台。 | Intel 语法(操作数:目的, 源) | Windows 底层开发、驱动编写、传统大学教材。 |
| Turbo Assembler | TASM | Borland 公司出品(90年代)。编译速度极快,高度兼容 MASM,但随着 Borland 衰落已基本淘汰。 | Intel 语法 | 复古 DOS 编程、遗留老代码维护。 |
| Netwide Assembler | NASM | 现代开源界的绝对主力。跨平台(Linux, Windows, macOS),去除了 MASM 中许多繁琐的伪指令。 | Intel 语法(更简洁纯粹) | 现代操作系统开发(如自制 OS 内核)、逆向工程、跨平台底层优化。 |
| GNU Assembler | GAS | GNU 编译器套件(GCC)的默认后端。 | AT&T 语法(操作数:源, 目的,寄存器前加 %) |
Linux 内核开发、C/C++ 内联汇编。 |
六、 汇编伪指令:控制编译行为 (以 MASM 为例)
伪指令(Pseudo-instructions)不会生成机器码,它们是写给 汇编器(如 MASM) 的指示,用于告知硬件目标和运行模式。
1. 指令集选择
-
.8086 / .286: 限制汇编器只接受 16位 或早期的保护模式指令。
-
.386: 允许使用 32位通用寄存器(如 EAX)和 32位指令。
-
.486 / .586: 进一步支持 FPU 浮点指令和其他特定指令。
2. 段定义与模式控制
-
USE16(16位段): 告诉汇编器,该段代码将来会运行在实模式下。默认的寄存器操作数和地址偏移都是 16 位的。多用于引导扇区 (Bootloader) 开发。 -
USE32(32位段): 告诉汇编器,该段代码将运行在保护模式下。默认偏移量为 32 位。在定义 32位 Windows 程序的数据段或代码段时使用。
(注:在现代 NASM 汇编器中,与之对应的是 BITS 16,BITS 32 和 BITS 64 伪指令。)
七、 课程使用环境
1. Windows XP 环境
在一台 32 位的 Windows XP 中,运行 16 位 DOS 汇编程序时,是在虚拟 8086 模式(Virtual 8086 Mode, V86)下运行的。(利用 command.exe -- MS-DOS 时代的 16 位命令行解释器)
具体运作机制:
-
Windows XP 是一个纯粹的 32 位保护模式操作系统。它不能直接运行 16 位的实模式代码。为了兼容旧软件,Windows NT 内核引入了一个名为
ntvdm.exe的子系统。 -
当 XP 探测到运行 16 位程序时,会启动 NTVDM。NTVDM 会利用 x86 CPU 硬件自带的虚拟 8086 模式。
-
在 V86 模式下,16 位代码在 CPU 上原生执行,速度极快。但是,一旦程序试图执行特权指令(比如直接读写硬件端口,或者触发
INT 21H这样的 DOS 中断),CPU 就会触发一个异常(Exception),把控制权交还给 Windows XP 的内核(Ring 0)。 -
XP 的内核捕获到这个中断后,会通过 NTVDM 模拟出一个假的结果返回给程序。
2. Bochs 环境
把编译好的 16 位程序放进一个虚拟软盘或硬盘,然后用 Bochs 启动,此时程序运行在模拟出来的纯“实模式”(Real Mode)下。
具体运作机制:
-
Bochs 是一个全系统模拟器。它不依赖宿主机的 CPU 特性。Bochs 内部用 C++ 代码来模拟 CPU 的每一个寄存器、算术逻辑单元(ALU)甚至内存条。
-
当 Bochs 启动时,它模拟了一台物理电脑刚刚通电的状态。根据 x86 架构的规定,所有 x86 CPU 开机时都处于实模式(Real Mode)。
-
在 Bochs 中跑纯 DOS 和 16 位汇编时,这个模拟出来的 CPU 就在实模式下工作。如果汇编代码写错了,导致内存越界或系统崩溃,Bochs 里的虚拟系统就会死机。
八、 代码框架
1. .386
-
指令类型: 处理器选择伪指令(Processor Directive)。
-
作用: 开放 80386 处理器的指令集。
-
允许在代码中使用 32 位通用寄存器,以及 386 引入的高级指令。
2. code segment use16
-
指令类型: 段定义伪指令。
-
code segment: 声明一个名为code的逻辑段(Segment)。 -
use16: 这是一个段属性,告诉汇编器:“这段代码将来是在 16 位环境 下运行的”。- 这意味着汇编器在把汇编代码翻译成机器码时,默认生成的地址偏移量是 16 位的,默认的操作数大小也是 16 位的。
3. assume cs:code
-
指令类型: 段寄存器关联伪指令。
-
作用:
assume告诉汇编器:“在编译时,假设 CPU 的代码段寄存器(CS,Code Segment)指向了刚刚定义的code这个段”。
汇编器的相对地址计算与操作系统加载器
-
汇编器在编译代码段时,只关注偏移量
-
EXE 文件头: 链接器在生成可执行文件(如
.exe)时,会在文件头部记录下各个段的大小以及程序的“入口点”(由代码末尾的end main指定)。 -
加载机制: 当系统启动该程序时,操作系统的 加载器(Loader) 会将程序完整地搬入内存的空闲区域。
-
加载器会读取 EXE 文件头,主动将系统分配给该代码段的真实物理段基址硬性填入 CPU 的
CS寄存器,同时将入口标号的偏移量填入IP(或EIP) 寄存器。此时,编译期的“假设”转化为运行期的“现实”。
数据段(DS)必须手动赋值
-
区别: 对于代码段(CS),操作系统会自动设置初始值;但对于数据段(DS)或附加段(ES),操作系统加载器是不负责初始化的。
-
代码中
assume ds:data仅仅能让汇编器算变量的偏移量。如果在程序运行时不进行干预,CPU 的DS寄存器里大概率残留着上一个程序的垃圾值,导致内存访问越界或崩溃。 -
必须在代码执行的最开始,手动将真实的段基址赋给
DS寄存器:
4. code ends
- 作用: 标记名为
code的逻辑段到此结束。它与前面的code segment是一对括号,将内部的代码包裹起来。
5. end main
-
作用: 具有双重功能的伪指令。
-
文件结束标志: 告诉汇编器,源代码文件到此结束,后面的任何文本都不再进行编译。
-
指定程序入口点: 这里的
main呼应了前面的标号main:。它告诉链接器(Linker),当这个程序被操作系统加载到内存准备运行时,CPU 的指令指针(IP/EIP)应该直接指向main标号所在的地址,从这里开始执行第一条指令。
-