Skip to content

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 运行模式:安全与权限的进化

  1. 实模式 (Real Mode):

    • 环境: 8086 CPU 的唯一工作模式。现代所有 x86/x64 电脑在开机加电的瞬间,依然处于实模式。

    • 特点: 没有安全保护。任何程序都可以直接访问 1MB 地址空间内的任何物理内存,甚至是修改底层硬件端口。如果一个程序崩溃,整个系统就会死机。

  2. 保护模式 (Protected Mode):

    • 环境: 从 80286 开始引入,在 80386 走向成熟。

    • 特点: 现代操作系统(Windows, Linux)的基石。引入了虚拟内存、分页机制和权限特权级 (Ring 0 - Ring 3)。

    • Ring 权限: 操作系统内核运行在 Ring 0(最高权限),普通用户程序(如浏览器、游戏)运行在 Ring 3(最低权限)。用户程序无法直接操作硬件,必须向操作系统发送请求。

  3. 虚拟 8086 模式 (Virtual 8086 Mode):

    • 特点: 在保护模式下模拟出的一个安全“沙箱”,让老旧的 16位 DOS 程序可以在 32位 Windows 下运行,而不会威胁系统安全。
  4. 长模式 (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 位命令行解释器)

具体运作机制:

  1. Windows XP 是一个纯粹的 32 位保护模式操作系统。它不能直接运行 16 位的实模式代码。为了兼容旧软件,Windows NT 内核引入了一个名为 ntvdm.exe 的子系统。

  2. 当 XP 探测到运行 16 位程序时,会启动 NTVDM。NTVDM 会利用 x86 CPU 硬件自带的虚拟 8086 模式。

  3. 在 V86 模式下,16 位代码在 CPU 上原生执行,速度极快。但是,一旦程序试图执行特权指令(比如直接读写硬件端口,或者触发 INT 21H 这样的 DOS 中断),CPU 就会触发一个异常(Exception),把控制权交还给 Windows XP 的内核(Ring 0)。

  4. XP 的内核捕获到这个中断后,会通过 NTVDM 模拟出一个假的结果返回给程序。

2. Bochs 环境

把编译好的 16 位程序放进一个虚拟软盘或硬盘,然后用 Bochs 启动,此时程序运行在模拟出来的纯“实模式”(Real Mode)下。

具体运作机制:

  1. Bochs 是一个全系统模拟器。它不依赖宿主机的 CPU 特性。Bochs 内部用 C++ 代码来模拟 CPU 的每一个寄存器、算术逻辑单元(ALU)甚至内存条。

  2. 当 Bochs 启动时,它模拟了一台物理电脑刚刚通电的状态。根据 x86 架构的规定,所有 x86 CPU 开机时都处于实模式(Real Mode)。

  3. 在 Bochs 中跑纯 DOS 和 16 位汇编时,这个模拟出来的 CPU 就在实模式下工作。如果汇编代码写错了,导致内存越界或系统崩溃,Bochs 里的虚拟系统就会死机。


八、 代码框架

.386
code segment use16
assume cs:code
main:


code ends
end main

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 寄存器:

    MOV AX, data  ; 将 data 段运行时的真实基址放入 AX (通用寄存器做中转)
    MOV DS, AX    ; 将基址填入 DS 数据段寄存器
    

4. code ends

  • 作用: 标记名为 code 的逻辑段到此结束。它与前面的 code segment 是一对括号,将内部的代码包裹起来。

5. end main

  • 作用: 具有双重功能的伪指令。

    1. 文件结束标志: 告诉汇编器,源代码文件到此结束,后面的任何文本都不再进行编译。

    2. 指定程序入口点: 这里的 main 呼应了前面的标号 main:。它告诉链接器(Linker),当这个程序被操作系统加载到内存准备运行时,CPU 的指令指针(IP/EIP)应该直接指向 main 标号所在的地址,从这里开始执行第一条指令。