5.7 KiB
5.7 KiB
FreeBSD 内核:linker sets / SYSINIT / KLD
FreeBSD 表面上和 Linux 很像——**但一个关键差异会让整套"可达性"推理失效**:SYSINIT() 并不是生成 "_start → call init"
而是把一个 struct sysinit 放进 linker set
内核启动 / 模块加载时统一排序后执行
所以:
foo_init() ← 没有任何 call 交叉引用
可能完全正常。 应当恢复的是
func 地址 → struct sysinit → linker set
而不是把它标成 dead code。"找不到调用者"在 FreeBSD 上首先是一条关于 linker set 的线索,而不是一条关于死代码的线索。 </CORE RULE>
一、SYSINIT / SYSUNINIT:带序号的注册项
SYSINIT()创建一个struct sysinit放进启动 linker set;SYSUNINIT()放进关闭 linker set- 结构包含四个字段:子系统标识(
SI_SUB_*)、子系统内次序(SI_ORDER_*)、函数指针、传给它的数据指针 - 启动时扫描启动 linker set,生成排好序的初始化例程表再依次执行
- 排序键:
SI_SUB_*为主键,SI_ORDER_*为次键,各自升序 - 同一 sub 且同一 order 的两个例程,相对顺序未定义——不要从"源码里的先后"推断运行顺序
SI_SUB_DUMMY一类条目会被跳过;条目执行后会被标记(如SI_SUB_DONE)
二、linker set 机制本身:识别它,才谈得上识别"注册"
- linker set 利用链接器把多个编译单元里静态声明的数据聚成一个连续、可寻址的整体
- 现代 ELF 实现用 "magic ld symbols" 定界:
__start_set_<集合名>/__stop_set_<集合名>,条目落在形如set_<集合名>的 section 里 - **老实现(a.out 时代)**是
struct linker_set { int ls_length; void *ls_items[...]; },且条目列表以 NULL 结尾 - 现代实现里列表不再以 NULL 结尾——按"结尾是 NULL"去扫描会直接越界。范围只能由
__start_set_*/__stop_set_*边界算出 - 宏层提供遍历/计数(
SET_BEGIN/SET_LIMIT/SET_FOREACH/SET_COUNT)——反编译器一侧看到的是纯数据指针数组,没有这些 API 名
通用判据(值得跨系统复用):
大量连续排布的 pointer / object
+ section 名形如 set_*
→ 优先按 linker-set 语义解释,而不是 jump table / 混淆表
而且 linker set 不止 SYSINIT 一处在用——FreeBSD 内核里有很多 set_...._set 形式的集合。看到 set_ 前缀先想 linker set。
三、KLD:运行期还有一层 kernel linker
- 预加载模块(随内核由 boot loader 载入):其启动 linker set 在
SI_SUB_KLD子系统初始化时被扫描、排序并并入内核的启动表,随后在启动过程中执行 - 由此产生一条反直觉结论:预加载模块里排序在
SI_SUB_KLD之前的初始化例程,实际会在SI_SUB_KLD之后才运行——"它声明得很早,为什么跑得晚"有确定的机制解释 - 运行期加载的模块(
kldload):加载时扫描其启动 set、排序、执行 - 卸载:扫描关闭 set、排序、执行,拆除顺序与初始化顺序相反
- 内核与已加载模块的拆除例程在系统关机时并不执行——"关机时应该走某条清理路径"这类推断不成立
决策树
拿到 FreeBSD 内核 / 模块目标
├─ 某个 init/注册函数没有 call 交叉引用
│ ├─ 查它是否被 SYSINIT/SYSUNINIT 引用(func 地址 → struct sysinit)
│ ├─ 查它落在哪个 linker set(section 名 set_*)
│ └─ 别标 dead code
├─ 一大段连续指针/对象、section 名 set_*
│ └─ 按 linker set 解释(可能有 count 字段、可能没有;现代 ELF 无结尾 NULL)
├─ 顺序问题
│ ├─ 排序键是 SI_SUB_* 主 + SI_ORDER_* 次
│ ├─ 同 sub + 同 order → 相对顺序未定义,别从源码顺序推运行顺序
│ └─ 预加载模块:早于 SI_SUB_KLD 的项实际在 SI_SUB_KLD 之后才跑
├─ 模块相关
│ ├─ 加载:扫描-排序-执行启动 set
│ ├─ 卸载:执行关闭 set,顺序与初始化相反
│ └─ 系统关机不执行内核与已加载模块的拆除例程
└─ 与 Linux 对照的常见误判 → 见下方"坑"
工具与验证
- 定位注册项:由函数地址回溯到
struct sysinit,再由它所在的 section 归属到具体 linker set - 枚举集合:按
__start_set_*/__stop_set_*边界计算范围与条目数(不要依赖结尾 NULL 或固定 count 字段) - 模块生命周期:预加载 vs
kldload两条路径分别核对(排序时机不同) - 验证:能同时说清「注册方式(linker set / 直接调用)+ 所属集合与排序键 + 加载路径(内建/预加载/运行期)」
该平台的坑(汇总)
- 找不到 call 交叉引用就判 init 不会运行:
SYSINIT()通过 linker set 注册,源码层面本来就没有调用点 - 按"结尾 NULL"扫描 linker set:那是 a.out 时代的老结构;现代 ELF 用边界符号定界,按 NULL 扫会越界
- 把
set_*的指针数组当 jump table / 混淆表:它更可能是内核自己的注册集合 - 从源码里的先后推断初始化顺序:排序由
SI_SUB_*+SI_ORDER_*决定,同 sub 同 order 的相对顺序未定义 - 以为预加载模块里声明得早的初始化就跑得早:它们在
SI_SUB_KLD时才被并入内核启动表 - 以为关机时会执行内核/模块的拆除例程:不会
- 把卸载时的执行顺序当成初始化顺序:拆除顺序与初始化相反
- 拿 Linux 的
module_init/__initcall模型直接套 FreeBSD:注册机制与排序键都不同(对照见 linux-kernel)