第10讲:远指针、局部变量与断点
本周核心:理解远指针和近指针的区别,掌握局部变量在栈上的分配,学会远调用(跨段),了解软硬件断点原理。
一、远指针(Far Pointer)与近指针(Near Pointer)
1.1 概念
- 远指针(Far Pointer):由「段地址 + 偏移地址」组合而成的完整逻辑地址。例如
0B800h:0000h。 - 近指针(Near Pointer):只有偏移地址,段地址隐含在某个段寄存器中。
1.2 在汇编中定义和使用远指针
远指针 A:B 形式被存储到内存中时,总是被保存为一个 32 位的值:偏移地址 A 在低 16 位,段地址 B 在高 16 位。
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 mainles = 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
/* 以下程序只能在16位编译器TC中编译 */
#include <stdio.h>
main()
{
char far *p = (char far *)0xB8000000;
*p = 'A';
*(p+1) = 0x71;
}二、函数内的局部非静态变量
全局变量和局部静态变量都是定义在数据段中,而局部非静态变量是定义在堆栈中。
2.1 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,含完整栈迹)
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)
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 ip | push cs + push ip |
| 返回指令 | ret (pop ip) | retf (pop ip + pop cs) |
| 栈帧参数位置 | [bp+4] = 参数1 | [bp+6] = 参数1 |
远调用比近调用多压了一个 cs(2 字节),所以参数位置后移了 2 字节。
四、编译与调试
需要调试 hello.asm 时的编译和链接步骤:
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)
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 就说明有断点。
六、本周易错点
- 远指针存储到内存时分不清偏移和段地址的位置(偏移在低地址)
- 使用
les/lds时忘记目标寄存器是哪个(es/ds 在右边) - 有局部变量时
sub sp, n分配、mov sp, bp释放,位置写反 - 远调用时栈中多了 cs,参数偏移算错(
[bp+6]/[bp+8]vs[bp+4]/[bp+6]) - 近调用用了
retf或远调用用了ret,导致返回地址错乱
七、速查表
| 操作 | 写法 |
|---|---|
| 加载远指针到 es:di | les di, [addr] |
| 加载远指针到 ds:si | lds si, [addr] |
| 分配局部变量 | sub sp, n |
| 释放局部变量 | mov sp, bp |
| 远调用 | call far ptr func |
| 远返回 | retf |
| 软断点检测 | cmp byte ptr cs:[addr], 0CCh |
| 编译(调试信息) | tasm /zi / tlink /v |