前言

第 3 章是汇编语言学习的「分水岭」—— 它标志着我们从「单纯操作寄存器」正式迈向「内存访问」,是理解 x86 实模式内存模型、CPU 寻址机制的核心基石。本章的核心难点的在于「段地址与偏移地址的配合」「栈的底层实现」,而这些知识点恰恰是后续学习函数调用、中断机制、操作系统底层的关键前提。

3.1 内存中字的存储

核心知识点(必掌握)

  • 字的定义:x86 汇编中,「字(Word)」是基础数据单位,1 个字 = 2 个字节(16 位),必须占用连续的两个内存单元。与之对应,1 个字节(Byte)= 8 位,可单独占用 1 个内存单元。
  • 小端存储规则(重点):这是 x86 架构的核心存储规则,也是本章第一个易错点 —— 低字节(低位 8 位)存放在低地址内存单元,高字节(高位 8 位)存放在高地址内存单元。
  • 字地址的定义:一个字数据的地址,由其「低字节所在的内存地址」决定。例如:字数据1234H(二进制0001 0010 0011 0100),低字节34H存放在0000H单元,高字节12H存放在0001H单元,那么这个字的地址就是0000H
  • 边界对齐(优化点):字数据建议存放在「偶地址」(地址值为偶数,如0000H0002H)开始的内存单元。原因是 CPU 读取内存时,一次总线操作可读取 2 个字节(1 个字),若字数据存放在奇地址,CPU 需进行 2 次总线操作,效率会降低(实模式下影响不明显,但养成对齐习惯很重要)。

图文示例(直观理解)

假设内存单元分布如下(地址→数据):0000H → 34H0001H → 12H0002H → 56H0003H → 78H

  • 地址0000H处的字数据:低字节34H + 高字节12H = 1234H
  • 地址0002H处的字数据:低字节56H + 高字节78H = 7856H
  • 若读取地址0001H处的字数据,则是12H(低字节)+ 56H(高字节)= 5612H(非预期数据,这也是边界对齐的意义之一)。

易错点警示

❌ 误区:字数据的高字节存放在低地址,低字节存放在高地址(混淆大小端);

✅ 牢记:小端存储 = 低字节→低地址,高字节→高地址(可联想「小端 = 低位在前」)。

3.2 DS 和 [address]

核心知识点(核心难点)

CPU 访问内存时,不会直接使用内存单元的物理地址(如10000H),而是通过「段地址 + 偏移地址」的方式计算物理地址,这是 x86 实模式的核心寻址方式 —— 而DS寄存器,就是负责提供「数据段的段地址」的关键寄存器。

  • DS 段寄存器:全称「数据段寄存器(Data Segment)」,专门存放当前程序使用的数据段的「段基地址」。CPU 访问内存中的数据时,默认会使用DS作为段寄存器。
  • 内存寻址格式[address] 表示「偏移地址」(也叫有效地址),CPU 会自动计算物理地址:物理地址 = 段地址 × 16 + 偏移地址(段地址 ×16 等价于 段地址左移 4 位)。
  • 关键限制(必记):CPU 不允许直接向DS寄存器传送立即数(如mov ds, 1000H是非法指令),必须通过通用寄存器(如axbx)中转。
  • 指令解析mov al, [0] 的含义是:将「DS段地址 × 16 + 0」这个物理地址处的「字节数据」,送入al寄存器(8 位);若用mov ax, [0],则读取该物理地址开始的「字数据」(2 字节),送入ax(16 位)。

示例代码(可直接运行)

; 功能:设置DS段寄存器,读取指定内存地址的数据
mov ax, 1000H   ; 将段基地址1000H送入ax寄存器(中转)
mov ds, ax      ; 设置DS=1000H,此时数据段的基地址为1000H×16=10000H
mov al, [0]     ; 读取物理地址10000H(1000H×16 + 0)的字节数据到al
mov bx, [2]     ; 读取物理地址10002H的字数据到bx(10000H+2)

Debug 调试实操(关键步骤)

在 DOSBox 中使用 Debug 工具验证上述代码,步骤如下:

  1. 输入debug进入调试模式;
  2. 输入a 100(从地址 100H 开始编写汇编指令);
  3. 依次输入上述 4 条指令,输入q退出编写;
  4. 输入r查看寄存器状态,手动修改ds=1000
  5. 输入d 1000:0查看 1000H 段、偏移 0 处的内存数据;
  6. 输入g 108执行到第 4 条指令后暂停,输入r查看albx的值,验证是否与内存数据一致。

易错点警示

❌ 误区 1:直接向 DS 传送立即数(如mov ds, 1000H),编译报错;

❌ 误区 2:混淆「偏移地址」和「物理地址」,认为[0]就是物理地址 0;

✅ 牢记:[address]是偏移地址,物理地址必须结合 DS 段地址计算。

3.3 字的传送

核心知识点

字的传送本质是「16 位数据的内存与寄存器之间的交互」,核心是利用 16 位通用寄存器(axbxcxdx),一次操作完成 2 个字节的读写,无需手动处理高、低字节。

  • 字操作指令特点:使用 16 位寄存器时,CPU 会自动读取 / 写入连续的两个内存单元,低地址字节对应寄存器的低 8 位(如al),高地址字节对应寄存器的高 8 位(如ah)。
  • 核心指令解析
    • mov ax, [0]:读取DS:0(物理地址DS×16+0)和DS:1两个单元的字数据,DS:0的低字节送入alDS:1的高字节送入ah
    • mov [2], ax:将ax中的字数据,写入DS:2DS:3两个单元,al(低字节)写入DS:2ah(高字节)写入DS:3
  • 注意事项:字传送时,偏移地址只需指定「低字节地址」,CPU 会自动处理高字节地址,无需手动指定[3]等地址。

示例代码(带注释)

mov ax, 1000H   ; 中转段基地址1000H
mov ds, ax      ; 设置DS=1000H,数据段物理基地址=10000H
mov ax, [0]     ; 读取10000H(低字节)和10001H(高字节)的字数据到ax
mov [2], ax     ; 将ax中的字数据,写入10002H(低字节)和10003H(高字节)
mov bx, [0]     ; 再次读取10000H~10001H的字数据到bx,验证传送正确性

实战验证

使用 Debug 工具,先通过e 1000:0 34 12(将 10000H 设为 34H,10001H 设为 12H),执行上述代码后,查看ax的值应为1234H,再查看1000:21000:3,数据应为34H12H,与ax一致。

3.4 mov、add、sub 指令

核心知识点(高频考点)

这三条指令是汇编语言中最基础、最常用的指令,核心掌握「操作数组合规则」和「指令对标志位的影响」,这是后续学习算术运算、逻辑判断的基础。

1. 指令格式
  • mov 目标操作数, 源操作数:数据传送指令,将源操作数的值送入目标操作数,不改变源操作数的值;
  • add 目标操作数, 源操作数:加法指令,将目标操作数与源操作数相加,结果存入目标操作数;
  • sub 目标操作数, 源操作数:减法指令,将目标操作数减去源操作数,结果存入目标操作数。
2. 操作数组合规则(必背)

CPU 对操作数的组合有严格限制,以下表格清晰列出合法与非法组合(重点记忆非法组合):

表格

目标操作数 源操作数 合法性 示例
寄存器(8 位 / 16 位) 寄存器(8 位 / 16 位) ✅ 合法 mov ax, bxadd al, bl
寄存器(8 位 / 16 位) 立即数 ✅ 合法 mov ax, 10Hsub bx, 20H
寄存器(8 位 / 16 位) 内存单元(8 位 / 16 位) ✅ 合法 mov ax, [0]add al, [1]
内存单元(8 位 / 16 位) 寄存器(8 位 / 16 位) ✅ 合法 mov [0], axsub [2], bl
内存单元(8 位 / 16 位) 立即数 ✅ 合法 mov [0], 30Hadd [1], 5
内存单元 内存单元 ❌ 非法 mov [0], [1](报错)
段寄存器(DS/SS/ES) 立即数 ❌ 非法 mov ds, 1000H(报错,需中转)
3. 指令对标志位的影响

标志位是 CPU 用于记录运算结果状态的特殊寄存器(如进位、零值、符号等),后续条件判断(如 if-else)需依赖标志位,重点记忆:

  • mov指令:仅做数据传送,不影响任何标志位;
  • add/sub指令:会影响 4 个核心标志位:
    • CF(进位标志):无符号数运算产生进位 / 借位时,CF=1,否则 = 0;
    • ZF(零标志):运算结果为 0 时,ZF=1,否则 = 0;
    • SF(符号标志):运算结果为负数(最高位为 1)时,SF=1,否则 = 0;
    • OF(溢出标志):有符号数运算超出范围时,OF=1,否则 = 0。

示例代码(实战练习)

; 功能:练习mov/add/sub指令,观察标志位变化
mov ax, 10H     ; ax = 10H(十六进制),不影响标志位
add ax, 20H     ; ax = 30H,影响CF=0、ZF=0、SF=0、OF=0
sub ax, 5H      ; ax = 2BH,影响标志位(仍为0)
mov bx, ax      ; bx = 2BH,不影响标志位
mov [0], bx     ; 将bx的值写入DS:0开始的字单元
add [0], 15H    ; 内存单元的值 += 15H,影响标志位

Debug 调试技巧

执行上述代码后,输入r f(查看标志位),可直观看到 CF、ZF、SF、OF 的状态,结合运算结果理解标志位的变化规律。

易错点警示

❌ 误区 1:使用mov [0], [1]进行内存到内存的传送,非法;

❌ 误区 2:认为add指令只改变目标操作数,不影响其他寄存器 / 标志位;

✅ 牢记:内存到内存的传送需通过寄存器中转(如mov ax, [1] → mov [0], ax)。

3.5 数据段

核心知识点(编程规范)

数据段是汇编程序中「专门用于存储数据」的内存区域,通过DS寄存器关联,其核心作用是「集中管理数据」,避免内存地址混乱,提升程序的可读性和可维护性 —— 这是编写规范汇编程序的基础。

  • 数据段的定义:使用汇编伪指令segment(定义段)和ends(结束段),配合db(定义字节)、dw(定义字)等伪指令,定义数据段中的数据。
  • 段地址与偏移地址的配合:数据段的段地址由DS寄存器提供,偏移地址由[address]提供,物理地址 = DS × 16 + 偏移地址(与 3.2 节寻址规则一致)。
  • 段大小限制:x86 实模式下,一个段的最大大小为 64KB,因为偏移地址的范围是 0~FFFFH(16 位),64KB = 65536 字节 = 0FFFFH + 1。
  • 编程规范(必遵循)
    • 将所有程序需要使用的数据(常量、变量等)集中存放在数据段中;
    • 程序开头必须设置DS寄存器,使其指向数据段的段基地址;
    • 使用assume伪指令,告知编译器DS与数据段的关联(仅编译时生效,不生成机器码)。

标准示例代码(规范写法)

assume ds:data   ; 伪指令:告知编译器DS寄存器关联data数据段
data segment     ; 定义数据段,段名是data
    db 10H, 20H  ; 定义2个字节数据:10H、20H(偏移地址0、1)
    dw 3040H     ; 定义1个字数据:3040H(偏移地址2、3)
    db 50H       ; 定义1个字节数据:50H(偏移地址4)
data ends        ; 数据段结束

code segment     ; 定义代码段,存放程序指令
start:           ; 程序入口(必须有,用于指定程序开始执行的位置)
    mov ax, data ; 将data段的段基地址送入ax(中转)
    mov ds, ax   ; 设置DS指向data数据段,此时DS = data段基地址
    
    ; 读取数据段中的数据
    mov al, [0]  ; 读取偏移地址0的字节数据(10H)到al
    mov bx, [2]  ; 读取偏移地址2的字数据(3040H)到bx
    mov [4], al  ; 将al中的数据(10H)写入偏移地址4的单元(覆盖原有50H)

    mov ah, 4CH  ; 程序退出功能号
    int 21H      ; 调用DOS中断,退出程序
code ends        ; 代码段结束
end start        ; 伪指令:指定程序入口为start,结束汇编

关键解析

  • assume ds:data:仅用于编译器识别,告诉编译器「后续 DS 寄存器将指向 data 段」,不生成机器码,若不写,编译器会给出警告,但程序仍可运行;
  • data segmentdata ends:定义数据段的范围,其中dbdw是数据定义伪指令,用于分配内存单元并初始化数据;
  • start:程序入口标签,end start指定程序从start处开始执行,避免程序执行混乱。

实战编译运行

使用 MASM 编译器编译上述代码(步骤):

  1. 将代码保存为data_seg.asm
  2. 在 DOSBox 中输入masm data_seg.asm,生成data_seg.obj目标文件;
  3. 输入link data_seg.obj,生成data_seg.exe可执行文件;
  4. 输入data_seg运行程序,再用 Debug 加载查看内存数据,验证程序执行结果。

3.6 栈

核心知识点(基础概念)

栈是一种「后进先出(LIFO,Last In First Out)」的数据结构,类比生活中的「堆叠盘子」—— 最后放上去的盘子,最先拿下来。在汇编语言中,栈的核心作用是「临时存储数据」「保存寄存器现场」「实现函数调用与返回」,是程序运行不可或缺的机制。

  • 栈的核心特性:后进先出,仅能在「栈顶」进行操作(入栈、出栈),栈底固定,栈顶动态变化。
  • 栈的操作
    • 入栈(push):将数据从寄存器 / 内存送入栈顶,栈顶向「低地址」方向移动;
    • 出栈(pop):将栈顶的数据取出,送入寄存器 / 内存,栈顶向「高地址」方向移动。
  • 栈的生长方向(重点):x86 实模式下,栈向「低地址」方向生长,这是与普通数据段最大的区别 —— 栈底地址 > 栈顶地址,入栈时栈顶地址减小,出栈时栈顶地址增大。
  • 栈的作用(实际应用)
    • 临时存储数据:程序执行过程中,暂时不用的数据可存入栈,需要时再取出;
    • 保存寄存器现场:调用函数前,将当前寄存器的值入栈,函数执行完后出栈恢复,避免数据丢失;
    • 函数调用与返回:保存函数返回地址,实现函数执行完后回到原调用位置。

栈结构示意图(直观理解)

高地址 → 栈底(初始位置,固定不变)
          ↓
          已使用栈空间(存放入栈数据)
          ↓
低地址 → 栈顶(当前操作位置,动态变化)
          ↓
          未使用栈空间

说明:假设栈底地址为20010H,初始栈顶地址也为20010H(栈为空);入栈 1 个字后,栈顶地址变为2000EH(减少 2 字节);再入栈 1 个字,栈顶地址变为2000CH,以此类推。

易错点警示

❌ 误区:栈向高地址生长(与数据段生长方向混淆);

✅ 牢记:x86 实模式下,栈向低地址生长,栈顶地址随入栈减小、出栈增大。

3.7 CPU 提供的栈机制

核心知识点(底层实现)

CPU 为栈的操作提供了专门的寄存器和硬件支持,无需程序员手动计算栈顶地址 —— 核心依赖SS(栈段寄存器)和SP(栈指针寄存器),这是理解栈操作底层逻辑的关键。

  • 栈相关寄存器
    • SS(栈段寄存器):存放栈段的「段基地址」,与DS作用类似,专门用于栈操作;
    • SP(栈指针寄存器):存放栈顶的「偏移地址」,始终指向当前栈顶的位置,栈操作时自动更新。
  • 栈顶物理地址计算:与数据段寻址规则一致,栈顶物理地址 = SS × 16 + SP
  • 入栈操作(push)的底层步骤(必记)
    1. SP = SP - 2:栈顶向低地址移动 2 字节(因为栈操作都是 16 位,一次操作 2 字节);
    2. 将 16 位数据(寄存器 / 内存中的字数据)写入SS:SP指向的内存单元(此时 SP 已更新,指向新的栈顶)。
  • 出栈操作(pop)的底层步骤(必记)
    1. SS:SP指向的内存单元(当前栈顶)读取 16 位数据,送入目标寄存器 / 内存;
    2. SP = SP + 2:栈顶向高地址移动 2 字节,指向新的栈顶。

示例代码(拆解栈操作)

; 功能:拆解push/pop操作,观察SP和栈顶地址变化
mov ax, 2000H   ; 栈段基地址2000H
mov ss, ax      ; 设置SS=2000H,栈段物理基地址=20000H
mov sp, 0010H   ; 初始栈顶偏移地址=0010H,栈顶物理地址=20000H+10H=20010H(栈为空)

push ax         ; 入栈操作:1. SP=0010H-2=000EH;2. 将ax(2000H)写入2000:000EH
; 此时栈顶物理地址=20000H+000EH=2000EH,栈顶数据=2000H

pop bx          ; 出栈操作:1. 从2000:000EH读取数据(2000H)送入bx;2. SP=000EH+2=0010H
; 此时栈顶恢复到初始位置20010H,bx=2000H

Debug 调试拆解(关键步骤)

通过 Debug 工具,逐步执行上述代码,观察SP和栈顶内存的变化:

  1. 执行mov ss, axmov sp, 0010H后,输入r sp,查看SP=0010H
  2. 执行push ax后,再次查看SP=000EH,输入d 2000:000E,可看到内存数据为00 20(即 2000H,小端存储);
  3. 执行pop bx后,查看SP=0010H,输入r bx,可看到bx=2000H,验证出栈正确性。

易错点警示

❌ 误区 1:入栈时先写入数据,再移动 SP(顺序颠倒);

❌ 误区 2:栈操作可以处理 8 位数据(如push al);

✅ 牢记:push/pop 操作均为 16 位,只能处理字数据,不能单独操作 8 位寄存器(如 al、bl)。

3.8 栈顶超界的问题

核心知识点(风险防范)

栈顶超界是汇编编程中常见的严重错误,会导致程序崩溃 —— 因为 CPU 硬件本身无法检测栈顶是否超界,若入栈操作过多,SP 会不断减小,最终超出栈段的范围,覆盖其他内存区域(如代码段、数据段)的数据,导致程序执行混乱、崩溃。

  • 超界的原因
    • 栈空间分配不足,入栈次数过多(如栈大小为 16 字节,却入栈 10 个字,共 20 字节);
    • 程序逻辑错误,导致 push 操作多于 pop 操作(如循环中只 push 不 pop);
    • 初始化 SP 时,未预留足够的栈空间。
  • 超界的后果:覆盖代码段的指令或数据段的数据,导致程序执行错误(如跳转至错误地址)、数据丢失,严重时会导致系统死机。
  • 防范措施(必掌握)
    1. 合理规划栈大小:根据程序需求,分配足够的栈空间(如使用dw 128 dup(0)分配 256 字节栈空间);
    2. 规范栈操作:确保 push 和 pop 操作成对出现,避免只 push 不 pop;
    3. 初始化 SP 规范:初始 SP 应设为「栈段最高地址 + 2」,确保栈有最大可用空间(如栈段为 2000H~20FFH,最高地址为 20FFH,SP 设为 0100H);
    4. 调试时监控 SP:使用 Debug 工具,执行过程中实时查看 SP 的值,及时发现超界隐患。

示例(超界场景演示)

assume ss:stack
stack segment
    dw 2 dup(0)   ; 分配4字节栈空间(仅能入栈2个字)
stack ends

code segment
start:
    mov ax, stack
    mov ss, ax
    mov sp, 0004H ; 初始栈顶=0004H(栈段最高地址+2=0002H+2=0004H)
    
    push ax       ; 1次入栈,SP=0002H
    push bx       ; 2次入栈,SP=0000H(栈满)
    push cx       ; 3次入栈,SP=0FFFEH(超界!覆盖栈段外内存)
code ends
end start

说明:上述代码中,栈空间仅 4 字节(2 个字),第 3 次入栈时,SP 变为 0FFFEH,超出栈段范围(stack 段地址为 XXXH,物理地址 XXX0H~XXX3H),导致覆盖其他内存数据,程序会崩溃。

3.9 push、pop 指令

核心知识点(指令细节)

push 和 pop 是栈操作的核心指令,需掌握其完整格式、操作对象和注意事项,尤其要注意「不能操作 CS 寄存器」和「操作数必须是 16 位」这两个关键点。

1. 指令格式(完整列表)

push 和 pop 的操作对象只能是「16 位寄存器」「16 位内存单元」「段寄存器(CS 除外)」,具体格式如下:

push 指令(入栈)
  • push r16:将 16 位通用寄存器入栈(如push axpush bx);
  • push segreg:将段寄存器入栈(如push dspush sspush es,CS 不可入栈);
  • push mem16:将 16 位内存单元的字数据入栈(如push [0]push [bx])。
pop 指令(出栈)
  • pop r16:将栈顶字数据弹出到 16 位通用寄存器(如pop axpop bx);
  • pop segreg:将栈顶字数据弹出到段寄存器(如pop dspop sspop es,CS 不可出栈);
  • pop mem16:将栈顶字数据弹出到 16 位内存单元(如pop [0]pop [bx])。
2. 关键注意事项(必记)
  • 操作数必须是 16 位:push/pop 不能操作 8 位数据(如push alpop bl均为非法指令);
  • CS 寄存器不可操作:CS 存放代码段地址,直接 push/pop CS 会导致程序执行流混乱(如跳转至错误地址),CPU 禁止此类操作;
  • 栈操作的顺序:push 多个寄存器后,pop 的顺序需相反(后进先出),否则数据会错乱。

示例代码(规范操作)

; 功能:练习push/pop指令,掌握操作顺序和规范
mov ax, 1234H
mov bx, 5678H
mov ds, ax      ; 设置DS=1234H

push ax         ; 入栈ax(1234H),SP -= 2
push bx         ; 入栈bx(5678H),SP -= 2
push ds         ; 入栈ds(1234H),SP -= 2

pop cx          ; 出栈到cx(cx=1234H),SP += 2
pop dx          ; 出栈到dx(dx=5678H),SP += 2
pop bx          ; 出栈到bx(bx=1234H),SP += 2

; 注意:push顺序是ax→bx→ds,pop顺序是ds→bx→ax,需反向操作

易错点警示

❌ 误区 1:使用push alpop bl操作 8 位数据;

❌ 误区 2:使用push cspop cs操作 CS 寄存器;

❌ 误区 3:push 和 pop 顺序不一致(如 push ax→bx,pop ax→bx),导致数据错乱;

✅ 牢记:push/pop 仅支持 16 位操作,CS 不可操作,push 与 pop 顺序需相反。

3.10 栈段

核心知识点(编程规范)

栈段是专门用于栈操作的内存区域,与数据段、代码段并列,通过SS寄存器关联,SP寄存器指定栈顶位置 —— 规范定义栈段,是避免栈顶超界、确保程序稳定运行的关键。

  • 栈段的定义:使用segmentends伪指令定义栈段,配合dw伪指令分配栈空间(dw定义字,每个字 2 字节,便于栈操作)。
  • 段大小限制:实模式下,栈段最大为 64KB,SP 的取值范围为 0~FFFFH(16 位偏移地址)。
  • 栈段初始化规范(必遵循)
    1. 先设置SS寄存器(栈段基地址),再设置SP寄存器(栈顶偏移地址)—— 避免中间状态(如先设 SP,再设 SS,此时 SP 可能指向错误地址);
    2. 初始 SP 的设置:通常设为「栈段最高地址 + 2」,确保栈有最大可用空间。例如:栈段定义为dw 128 dup(0)(128 字 = 256 字节),栈段的偏移地址范围为 0~007FH(256 字节),最高偏移地址为 007FH,初始 SP 设为 0080H(007FH + 1,因 SP 指向栈顶,需多留 1 个偏移);
    3. 使用assume ss:stack伪指令,告知编译器SS与栈段的关联(仅编译时生效)。

标准示例代码(规范栈段定义)

assume ss:stack, cs:code  ; 告知编译器SS关联stack栈段,CS关联code代码段
stack segment             ; 定义栈段,段名stack
    dw 128 dup(0)         ; 分配128个字(256字节)的栈空间,初始值为0
                          ; 栈段偏移地址范围:0~007FH(256字节)
stack ends                ; 栈段结束

code segment             ; 定义代码段
start:                   ; 程序入口
    ; 栈段初始化:先设SS,再设SP
    mov ax, stack       
Logo

AtomGit 是由开放原子开源基金会联合 CSDN 等生态伙伴共同推出的新一代开源与人工智能协作平台。平台坚持“开放、中立、公益”的理念,把代码托管、模型共享、数据集托管、智能体开发体验和算力服务整合在一起,为开发者提供从开发、训练到部署的一站式体验。

更多推荐