

























提示词:【解释 https://devblogs.microsoft.com/oldnewthing/20031008-00/?p=42223 ,我还是没看懂,因为低地址,高地址,fixup等等,实际上我以前理解过,但是又忘记了,你得把我当成大一新生一样,从基础开始,一步步解释,示意,每一个可能的「常识」都要教学。】
AI step by step 教学:
我会分成 6 级台阶,每一级都用 ASCII 图,不跳步。
我们要爬的楼梯是这样的:
你随时可以说“这一级再慢一点”,我们就停在某一级多举几个例子。
先忘掉 CPU,忘掉 Windows,就想象一条很长的格子带:
内存(抽象示意):
+--------+--------+--------+--------+--------+--------+
| 单元 | 单元 | 单元 | 单元 | 单元 | 单元 | ...
+--------+--------+--------+--------+--------+--------+
0x0000 0x0001 0x0002 0x0003 0x0004 0x0005
0x0000、0x0001。低地址 / 高地址:
低地址 → → → → → → → → → → 高地址
0x0000 0x0001 0x0002 ... 0xFFFF
就像楼层:1 楼比 20 楼“低”。
先看一个 32 位的数,比如:
0x1234ABCD
用二进制表示是 32 个 bit(位):
0x1234ABCD = 0001 0010 0011 0100 1010 1011 1100 1101
↑ ↑
高 16 位 低 16 位
我们可以把它拆成两半:
高 16 位:0x1234
低 16 位:0xABCD
ASCII 图:
0x1234ABCD
└─┬──┘└─┬──┘
高16 低16
为什么要拆?
因为有些 CPU 的指令集里:
这就引出了下一步。
在很多 RISC 架构(Alpha / MIPS / PowerPC / ARM)里,没有这种指令:
MOV R1, 0x1234ABCD ; 一次性把 32 位常量装进寄存器
它们只能这样干(伪代码):
LDAH R1, 0x1234 ; Load high 16 bits
LDA R1, 0xABCD(R1) ; 在 R1 的基础上加上低 16 位
ASCII 图:
步骤 1:LDAH R1, 0x1234
R1 = 0x1234 0000
步骤 2:LDA R1, 0xABCD(R1)
R1 = 0x1234 0000 + 0xABCD
= 0x1234 ABCD
所以你可以记住一个简单事实:
在这些 CPU 上,“加载一个 32 位地址” = “高 16 位指令 + 低 16 位指令”。
这就是后面“fixup 要修两处”的根源。
简单讲:
0x10000000 开头比如 DLL 里有一段代码要访问一个全局变量 g_value:
extern int g_value;
int foo() {
return g_value + 1;
}
编译器会把 g_value 当成“在某个固定地址”,比如:
g_value 假设在:0x10008000
于是生成的机器码里,会出现“访问 0x10008000”这样的地址。
当系统真正把 DLL 加载进内存时:
0x10000000 这块已经被别的 DLL 占了0x20000000这时,原来写死在代码里的地址 0x10008000 就不对了,应该变成:
0x20008000
这就叫 relocation(重定位),
而对每一个需要修改的地方做的“改地址动作”,就叫 fixup。
ASCII 图:
编译时假设:
DLL 基址:0x10000000
g_value: 0x10008000
实际加载:
DLL 被放到:0x20000000
g_value 实际:0x20008000
需要做的事:
把代码里所有用到 0x10008000 的地方
改成 0x20008000 ← 这一步就是 fixup
现在把前面两件事放在一起:
假设原地址是:
原地址:0x12340000
新地址:0x12348000 (只加了 0x8000 = 32KB)
拆开:
原:高 16 = 0x1234,低 16 = 0x0000
新:高 16 = 0x1234,低 16 = 0x8000
ASCII 图:
0x1234 0000 → 0x1234 8000
高不变 高不变
低变了 低变了
这种情况很好:
看 Raymond Chen 文里的例子:
原地址:0x1234F000
新地址:0x12350000 (只加了 0x1000 = 4KB)
拆开:
原:高 16 = 0x1234,低 16 = 0xF000
新:高 16 = 0x1235,低 16 = 0x0000
ASCII 图:
0x1234 F000 → 0x1235 0000
高:1234 高:1235 ← 变了!
低:F000 低:0000
虽然只移动了 4KB,但因为跨过了一个 64KB 边界(0x1234FFFF → 0x12350000),
高 16 位也变了。
这会导致:
这就是:如果 DLL 可以以 4KB 粒度随便移动,fixup 会变得又多又复杂。
Alpha 的低 16 位是有符号数,所以它经常这样写:
LDAH t1, 0x1235
LDA t1, -0x5433(t1)
用来表示 0x1234ABCD。
ASCII 图(你可以慢慢看几遍):
目标地址:0x1234ABCD
高 16 位:0x1235
低 16 位:-0x5433 (因为 0x10000 - 0xABCD = 0x5433)
组合过程:
0x1235 0000
- 5433
-----------
0x1234 ABCD
如果 DLL 移动后,地址从“64KB 的上半区”变到“下半区”,
signed 低 16 位的写法会完全变掉,
这就不是“简单加个偏移”能搞定的了。
所以:
只要 DLL 的移动不是 64KB 的整数倍,fixup 就会非常麻烦。
现在我们把所有东西串起来:
于是 Windows 做了一个非常务实的决定:
地址空间分配粒度 = 64KB
也就是说:
- DLL 只会被移动到 64KB 对齐的位置
- 移动量永远是 64KB 的整数倍
- 这样就可以保证:
- 很多情况下只需要改低 16 位
- 即使要改高 16 位,模式也比较简单
- 编译器的优化(共享高 16 位加载)仍然成立
再用一个 ASCII 总结图收个尾:
地址空间(简化):
... ┆ 0x12330000
┆ 0x12340000 ← 64KB 对齐
┆ 0x12350000 ← 64KB 对齐
┆ 0x12360000
... ┆
规则:
DLL 只能从一个 64KB 边界
移动到另一个 64KB 边界
✔ 0x12340000 → 0x12350000 (OK,移动 64KB)
✘ 0x1234F000 → 0x12350000 (NO,只移动 4KB,会搞乱高 16 位)
此内容由惯性聚合(RSS阅读器)自动聚合整理,仅供阅读参考。 原文来自 — 版权归原作者所有。