Skip to content

第10讲:远指针、局部变量与断点

本周核心:理解远指针和近指针的区别,掌握局部变量在栈上的分配,学会远调用(跨段),了解软硬件断点原理。


一、远指针(Far Pointer)与近指针(Near Pointer)

1.1 概念

  • 远指针(Far Pointer):由「段地址 + 偏移地址」组合而成的完整逻辑地址。例如 0B800h:0000h
  • 近指针(Near Pointer):只有偏移地址,段地址隐含在某个段寄存器中。

1.2 在汇编中定义和使用远指针

远指针 A:B 形式被存储到内存中时,总是被保存为一个 32 位的值:偏移地址 A 在低 16 位,段地址 B 在高 16 位。

asm
data segment
video_addr1 dd 0B8000000h      ; 32位值: 偏移=0000h, 段=0B800h
                                 ; 内存中: 00, 00, 00, 0B8h(小端)
video_addr2 dw 0000h, 0B800h   ; 分开定义: 先偏移, 后段地址
                                 ; 内存中: 00, 00, 00, 0B8h(同上)
data ends

code segment
assume cs:code, ds:data
main:
   mov ax, data
   mov ds, ax
   ; --- 方式一:分开取偏移和段地址 ---
   mov di, video_addr2[0]   ; di = 0000h(偏移地址)
   mov es, video_addr2[2]   ; es = 0B800h(段地址)
   mov byte ptr es:[di], 'A'
   mov byte ptr es:[di+1], 71h   ; 颜色=71h, 表示白底蓝字
   ; --- 方式二:les 一条指令完成 ---
   les di, [video_addr1]    ; di = 0000h, es = 0B800h
   mov byte ptr es:[di], 'A'
   mov byte ptr es:[di+1], 71h
   mov ah, 4Ch
   mov al, 0
   int 21h
code ends
end main

les = Load ES + register:把内存中的低 16 位 → 目标寄存器(di),高 16 位 → es。类似还有 lds(→ ds)。

1.3 C 语言中的远指针

在 16 位编译器(如 TC)中:

  • char *p 是近指针,宽度为 2 字节,保存的是一个 16 位偏移地址
  • char far *p 是远指针,宽度为 4 字节,保存的是 16 位段地址 + 16 位偏移地址

在 32 位编译器(如 VC)中:

  • char *p 是近指针,宽度为 4 字节,保存的是一个 32 位偏移地址
  • 远指针的宽度为 6 字节 = 16 位段地址 + 32 位偏移地址,对应 fword ptr
c
/* 以下程序只能在16位编译器TC中编译 */
#include <stdio.h>
main()
{
   char far *p = (char far *)0xB8000000;
   *p = 'A';
   *(p+1) = 0x71;
}

二、函数内的局部非静态变量

全局变量和局部静态变量都是定义在数据段中,而局部非静态变量是定义在堆栈中

2.1 C 语言版本

c
int f(int a, int b)
{
   int c;
   c = a - b;
   return c;
}
main()
{
   int y;
   y = f(5, 3);
}

2.2 汇编版本(老师 1.asm,含完整栈迹)

asm
code segment
assume cs:code
f:
   push bp            ; (4)
   mov bp, sp         ; (*) 建立栈帧
   sub sp, 2          ; (5) ★ 为局部变量 c 分配 2 字节空间
   mov ax, [bp+4]     ; ax = a
   sub ax, [bp+6]     ; ax = a - b
   mov [bp-2], ax     ; c = a - b(局部变量在 bp 下方)
   mov ax, [bp-2]     ; ax = c(返回值)
   mov sp, bp         ; (6) ★ 释放所有局部变量空间
   pop bp             ; (7)
   ret                ; (8)
   
main:
   mov ax, 3
   push ax            ; (1) 参数2
   mov ax, 5
   push ax            ; (2) 参数1
   call f             ; (3) y = f(5, 3)
back:   
   add sp, 4          ; (9)
   mov ah, 4Ch
   mov al, 0
   int 21h
code ends
end main

以上程序在运行时的堆栈变化如下。设 ss=2000h, sp=1000h:

ss:0FE4              (5) sp → 局部变量 c 的空间
...
ss:0FF8  old bp      (4)(6) bp → (*)
ss:0FFA  back        (3)(7)
ss:0FFC  5           (2)(8)  参数1 = a
ss:0FFE  3           (1)     参数2 = b
ss:1000              (9) ← 调用前 sp

栈帧结构总结

ss:[bp+6]  参数2(先压入的)
ss:[bp+4]  参数1(后压入的)
ss:[bp+2]  返回地址(call 压入)
ss:[bp]    old bp(push bp 压入)
ss:[bp-2]  局部变量 c(sub sp, 2 分配)← sp

核心规律:

  • 参数在 bp 上方:[bp+4], [bp+6], ...
  • 局部变量在 bp 下方:[bp-2], [bp-4], ...
  • 退出函数时 mov sp, bp 一步释放所有局部变量,pop bp 恢复上一层的 bp

三、远调用(Far Call)与远返回(Retf)

3.1 近调用 vs 远调用

近调用:调用者和被调用者在同一个代码段内。call 只 push ip(偏移地址),ret 只 pop ip。

远调用:调用者和被调用者不在同一个代码段内。call far ptr push cs + ip,retf pop ip + cs。

3.2 远调用完整示例(老师 1.asm)

asm
code1 segment
assume cs:code1
f:
   push bp
   mov bp, sp
   mov ax, [bp+6]     ; 参数1(注意:远调用多了一个 cs 在栈中)
   sub ax, [bp+8]     ; 参数2(偏移从 bp+8 开始,比近调用多了 2)
   pop bp
   retf               ; ★ 远返回指令
                      ; CPU 执行 retf 时做以下操作:
                      ;   ① ip = word ptr ss:[sp]
                      ;   ② cs = word ptr ss:[sp+2]
                      ;   ③ sp = sp + 4
                      ; 也可概括为: ①pop ip ②pop cs
code1 ends

code2 segment
assume cs:code2
main:
   mov ax, 3
   push ax            ; 参数2
   mov ax, 5
   push ax            ; 参数1
   call far ptr f     ; ★ 远调用: ax = f(5, 3)
                      ; CPU 在执行远调用指令时做以下操作:
                      ;   ① push cs
                      ;   ② push offset back
                      ;   ③ jmp far ptr f
back:
   add sp, 4
   mov ah, 4Ch
   mov al, 0
   int 21h
code2 ends
end main

远调用 vs 近调用的栈帧对比

近调用远调用
call 压栈push ippush cs + push ip
返回指令ret (pop ip)retf (pop ip + pop cs)
栈帧参数位置[bp+4] = 参数1[bp+6] = 参数1

远调用比近调用多压了一个 cs(2 字节),所以参数位置后移了 2 字节。


四、编译与调试

需要调试 hello.asm 时的编译和链接步骤:

bash
tasm /zi hello       # /zi 加入调试信息
tlink /v hello       # /v 加入调试信息
td hello             # Turbo Debugger 调试
# 或 ldr hello

五、软断点与硬断点

5.1 软断点(Software Breakpoint)

软断点的原理:把断点处指令的第一个字节替换成 0CCh(int 3 指令的机器码)

CPU 执行到 0CCh 时触发 int 3 中断,调试器接管控制权。恢复执行时,调试器把原指令字节写回去,再重新设置断点。

5.2 硬断点(Hardware Breakpoint)

硬断点不修改代码内容。把断点所在指令字节的地址(或多个地址)保存到 CPU 内部的调试寄存器中,当 CPU 访问这些地址并试图执行那里的指令时,CPU 会自动引发中断 int 1,通知调试器暂停程序的运行。

硬断点常用于调试 ROM 中的代码(无法修改代码内容)。

5.3 检测软断点的示例程序(老师 1.asm 中的 int3.asm)

asm
code segment
assume cs:code
main:
   mov cx, 10
next:
   mov ah, 2           ; 此处可设一个软断点
   mov dl, 'A'
   int 21h
   mov al, byte ptr cs:[next]    ; byte ptr 相当于 char *
   ;cmp byte ptr cs:[next], 0CCh ; 等价写法
   cmp al, 0CCh
   je done              ; 检测到断点 → 退出
   sub cx, 1
   jnz next
done:
   mov ah, 4Ch
   int 21h
code ends
end main

原理:如果调试器在 next: 处设了软断点,该处的首字节已被替换为 0CCh。程序运行时检查 cs:[next] 的首字节,如果是 0CCh 就说明有断点。


六、本周易错点

  1. 远指针存储到内存时分不清偏移和段地址的位置(偏移在低地址)
  2. 使用 les/lds 时忘记目标寄存器是哪个(es/ds 在右边)
  3. 有局部变量时 sub sp, n 分配、mov sp, bp 释放,位置写反
  4. 远调用时栈中多了 cs,参数偏移算错([bp+6] / [bp+8] vs [bp+4] / [bp+6]
  5. 近调用用了 retf 或远调用用了 ret,导致返回地址错乱

七、速查表

操作写法
加载远指针到 es:diles di, [addr]
加载远指针到 ds:silds si, [addr]
分配局部变量sub sp, n
释放局部变量mov sp, bp
远调用call far ptr func
远返回retf
软断点检测cmp byte ptr cs:[addr], 0CCh
编译(调试信息)tasm /zi / tlink /v