惯性聚合 高效追踪和阅读你感兴趣的博客、新闻、科技资讯
阅读原文 在惯性聚合中打开

推荐订阅源

Engineering at Meta
Engineering at Meta
D
Docker
IT之家
IT之家
博客园_首页
罗磊的独立博客
V
V2EX
OSCHINA 社区最新新闻
OSCHINA 社区最新新闻
美团技术团队
Y
Y Combinator Blog
博客园 - 聂微东
量子位
阮一峰的网络日志
阮一峰的网络日志
GbyAI
GbyAI
Microsoft Security Blog
Microsoft Security Blog
博客园 - Franky
Martin Fowler
Martin Fowler
Jina AI
Jina AI
大猫的无限游戏
大猫的无限游戏
C
Check Point Blog
月光博客
月光博客
G
Google Developers Blog
B
Blog
T
The Blog of Author Tim Ferriss
爱范儿
爱范儿

博客园 - ffl

[AI翻译]我如何用大模型写软件 [AI翻译]来自Zig的教训 [AI翻译] 标题:Agentic AI 手册:生产级模式 [AI翻译]:我们如何让 Python 的 packaging 库提速三倍 翻译:Akin 的航天器设计法则 Navida和Groq的交易资金多么? 翻译:这并不是未来 一个完整的软件开发过程,AI在哪些地方加速? 从业务读写流程出发,理解各种分布式系统组件的意义 类型标注,太多和太少一样糟糕 【AI翻译】什么是第三方API?优势、应用场景与最佳实践 【AI翻译】分布式系统中的心跳机制 【AI翻译】Python 3.14来了,有多快? Argo CD 与 Kubernetes 资源关系简述 技术文章阅读todo-list DLM(Diffusion Language Model) vs AR(Autoregressive) 标注的原理:少而完备,监督模型训练的根本 几何平均比算术平均对极值不敏感。 对比理解:什么是AI味浓厚的文章? 重要文章:Asymmetry of verification and verifier’s law
用AI像大一新生一样学东西
ffl · 2026-06-18 · via 博客园 - ffl

提示词:【解释 https://devblogs.microsoft.com/oldnewthing/20031008-00/?p=42223 ,我还是没看懂,因为低地址,高地址,fixup等等,实际上我以前理解过,但是又忘记了,你得把我当成大一新生一样,从基础开始,一步步解释,示意,每一个可能的「常识」都要教学。】

AI step by step 教学:

我会分成 6 级台阶,每一级都用 ASCII 图,不跳步。


0. 整体路线图(你先有个心理预期)

我们要爬的楼梯是这样的:

  1. 什么是“内存地址”?高地址 / 低地址是啥?
  2. 什么是 16 位 / 32 位数?为什么要“拆成高 16 + 低 16”?
  3. CPU 怎么把一个 32 位地址装进寄存器?(RISC 的限制)
  4. 什么是 DLL、什么是 relocation / fixup?为什么要“改地址”?
  5. 如果随便移动 DLL,会发生什么灾难?(这一步是关键)
  6. 所以:为什么 Windows 只能选 64KB 作为分配粒度?

你随时可以说“这一级再慢一点”,我们就停在某一级多举几个例子。


1. 什么是“内存地址”?高地址 / 低地址是啥?

先忘掉 CPU,忘掉 Windows,就想象一条很长的格子带:

内存(抽象示意):

  +--------+--------+--------+--------+--------+--------+
  |  单元  |  单元  |  单元  |  单元  |  单元  |  单元  |  ...
  +--------+--------+--------+--------+--------+--------+
    0x0000   0x0001   0x0002   0x0003   0x0004   0x0005
  • 每一个格子就是一个“内存单元”(比如 1 字节)。
  • 每个格子有一个编号,这个编号就是地址
  • 地址是一个整数,用十六进制写起来比较方便,比如 0x00000x0001

低地址 / 高地址:

  低地址  →  →  →  →  →  →  →  →  →  →  高地址
  0x0000   0x0001   0x0002   ...        0xFFFF
  • 数字小的叫“低地址”(low address)
  • 数字大的叫“高地址”(high address)

就像楼层:1 楼比 20 楼“低”。


2. 什么是 16 位 / 32 位数?为什么会有“高 16 位 / 低 16 位”?

先看一个 32 位的数,比如:

0x1234ABCD

用二进制表示是 32 个 bit(位):

0x1234ABCD = 0001 0010 0011 0100 1010 1011 1100 1101
              ↑               ↑
            高 16 位         低 16 位

我们可以把它拆成两半:

高 16 位:0x1234
低 16 位:0xABCD

ASCII 图:

0x1234ABCD
  └─┬──┘└─┬──┘
   高16   低16

为什么要拆?

因为有些 CPU 的指令集里:

  • 一条指令装不下完整的 32 位立即数(immediate)
  • 只能一次装 16 位
  • 所以要用两条指令:先装高 16,再装低 16

这就引出了下一步。


3. CPU 怎么把一个 32 位地址装进寄存器?(RISC 的限制)

在很多 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 要修两处”的根源。


4. 什么是 DLL?什么是 relocation / fixup?

4.1 DLL 是什么?

简单讲:

  • EXE / DLL 里有代码和数据
  • 编译时,编译器不知道它们最终会被放到内存的哪个地址
  • 所以会先假设一个“理想地址”,比如:
    • 这个 DLL 以后会被放在 0x10000000 开头

比如 DLL 里有一段代码要访问一个全局变量 g_value

extern int g_value;

int foo() {
    return g_value + 1;
}

编译器会把 g_value 当成“在某个固定地址”,比如:

g_value 假设在:0x10008000

于是生成的机器码里,会出现“访问 0x10008000”这样的地址。

4.2 问题来了:实际加载时地址可能变了

当系统真正把 DLL 加载进内存时:

  • 可能 0x10000000 这块已经被别的 DLL 占了
  • 于是系统只好把这个 DLL 放到别的地方,比如 0x20000000

这时,原来写死在代码里的地址 0x10008000不对了,应该变成:

0x20008000

这就叫 relocation(重定位)
而对每一个需要修改的地方做的“改地址动作”,就叫 fixup

ASCII 图:

编译时假设:
  DLL 基址:0x10000000
  g_value: 0x10008000

实际加载:
  DLL 被放到:0x20000000
  g_value 实际:0x20008000

需要做的事:
  把代码里所有用到 0x10008000 的地方
  改成 0x20008000   ← 这一步就是 fixup

5. 如果 DLL 随便移动,会发生什么灾难?

现在把前面两件事放在一起:

  1. 地址是 32 位,要拆成高 16 + 低 16 来加载
  2. DLL 可能被移动,导致地址要改(fixup)

5.1 理想情况:只改低 16 位就够了

假设原地址是:

原地址:0x12340000
新地址:0x12348000   (只加了 0x8000 = 32KB)

拆开:

原:高 16 = 0x1234,低 16 = 0x0000
新:高 16 = 0x1234,低 16 = 0x8000

ASCII 图:

0x1234 0000  →  0x1234 8000
   高不变         高不变
   低变了         低变了

这种情况很好:

  • 高 16 位不变 → 不用改高位指令
  • 只改低 16 位 → fixup 简单、便宜

5.2 糟糕情况:跨过 64KB 边界,高 16 位也变了

看 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 位也变了

这会导致:

  • 低 16 位要改
  • 高 16 位也要改
  • 而且两者之间还有进位 / 借位的问题
  • 编译器可能把“加载高 16 位”的指令放在很远的地方,fixup 很难找到它

这就是:如果 DLL 可以以 4KB 粒度随便移动,fixup 会变得又多又复杂。

5.3 Alpha 的 signed 低 16 位,让事情更糟

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 就会非常麻烦。


6. 所以:为什么 Windows 选 64KB 作为分配粒度?

现在我们把所有东西串起来:

  1. RISC CPU(Alpha/MIPS/PowerPC 等)
    • 加载 32 位地址 = 高 16 位指令 + 低 16 位指令
  2. DLL 可能因为地址冲突被移动
    • 需要对代码里的地址做 fixup
  3. 如果移动量不是 64KB 的整数倍
    • 可能跨 64KB 边界 → 高 16 位也要改
    • Alpha 还要处理 signed 低 16 位 → 更复杂
    • 编译器把高 16 位加载指令放得很远 → 很难找到一起改
  4. 如果强行支持“任意 4KB 粒度移动 DLL”
    • fixup 数量暴增
    • 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 位)