
有一类启动失败很反直觉:程序一直好好的,只是旁边多了一个模块,或者多装了一个带注入功能的工具,它就起不来了。
原因不在文件缺失,而在模块被加载到哪里。这一层涉及基址、重定位,以及 32 位进程的地址空间——都属于"DLL修复工具 补文件补不出来"的问题。
每个可执行文件和模块(DLL)在编译时,都会在映像头里写入一个期望基址。
加载时,加载器优先把它放在期望基址。如果那个地址已经被别的模块占用了,加载器只能把它"搬到别处"——这就是重定位。
搬家的过程不是把文件挪一下那么简单:映像内部有大量"指向自身"的绝对地址引用,这些位置全部需要按偏移修正。映像里有一张重定位表记录着这些位置,加载器照着它逐项修改。
所以重定位是"被迫搬家 %2B 全屋改门牌号",代价是实实在在的。
现象一:多挂一个模块,老程序就起不来。
新来的模块占用了老模块的期望基址。如果那个老模块不能重定位,它就没了落脚点——直接启动失败。
现象二:开启某项安全加固后,一批老程序集体失败。
强制随机化类的加固项,要求所有模块都加载到随机地址上。而这等价于"要求所有模块都能重定位"——不支持的程序只能失败。
现象三:同一组模块,有时好有时坏。
因为谁先加载,谁就抢占基址。加载顺序稍有变化(多一个注入型工具、启动路径不同),抢占结果就变了。
两种情况会做出"必须加载到期望基址"的模块:
一是编译时关掉了可重定位特性。 把期望基址当成硬约束,不允许搬家。
二是一些加壳与保护方案。 它们需要代码地址保持固定,重定位会让保护逻辑失效,于是干脆禁止重定位。
这两类模块在基址被占时没有退路——加载器只能报告失败。
判断的入口是现象特征(这类问题用 DLL修复工具 扫描不会有结果——文件一个不少、版本也没错):
处理方向(按代价从低到高):
要看一个进程里模块的基址分布与加载顺序,可以列出模块的基址并排序:
Get-Process -Name app -ErrorAction SilentlyContinue %7C ForEach-Object { $_.Modules } %7C
Select-Object ModuleName, BaseAddress %7C
Sort-Object BaseAddress按基址排序后,相邻模块的占位情况一眼可见——如果两个模块的期望基址区间有重叠,冲突的可能性就存在。
第一,不要手工指定基址去"优化启动"。
这是早期常见的做法,收益很小——而代价是让模块失去搬家能力,遇到抢占就只能失败。现代系统里不值得这么做。
第二,保留可重定位能力。
默认保留即可,不要为了省一点重定位开销就把它关掉。省下的是加载时的几毫秒,换来的是"在某些机器上直接起不来"。
第三,控制 32 位进程的模块数量。
32 位进程的地址空间是稀缺资源。模块越多,越容易出现"地址不够用"和"基址冲突"。大地址感知能缓解总量问题,但缓解不了碎片与冲突。
关于这一层,记住四条:
最后一条给开发者:保留可重定位性,别手工指定基址。 它换来的那点启动收益,远不值"在部分机器上起不来"这个风险。
https://www.ijinshan.com/functions/repairdll.html?channel=4068
原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。
如有侵权,请联系 cloudcommunity@tencent.com 删除。