我正在尝试构建和链接单个映像以加载为针对 aarch64-unknown-none-softfloat 的操作系统内核(即在 QEMU 中(。我使用自定义linker.ld文件,该文件设置内核ENTRY(_reset)
的入口点并定位图像
. = 0x40080000
程序计数器 (PC( 处于重置状态的位置。
它工作正常,直到我将 0x40080000 的页面映射到内核将驻留的高内存并启用虚拟内存转换。为了保证切换后的调试信息网格,我将标称图像位置更改为
. = 0xffffff8200000000
并重建。
我发现访问:
- 对某些(酒吧外部(静态,以及
- 通过某些核心库函数
是通过从.rodata
某处读取绝对地址.这会在映射之前运行代码时中断代码。如果我把它改回来,它在映射后运行它时会破坏代码。
它生成的代码在 O1 中看起来有点像这样(间接通过 PC 相对页面(:
adrp x0, 0x10000 // page offset from PC up to rodata
add x0, 0x120 // byte offset from page in rodata
ldr x0, [x0] // use as address
我需要的是真正跨代码和数据定位独立的代码,以便它在内存中的两个位置工作,而无需引用任何存储的绝对地址,即使这些地址相对于PC可用。
我已经尝试了其他重新定位模型,包括 Pic 和 RopiRwpi,但我看不到它生成不同的代码。
谢谢!
编辑:非常感谢临时地图的建议。我见过用过的。我对编译器选项更感兴趣,这些选项将使-no-dynamic-linker能够工作,避免生成R_AARCH64_ABS64需要重新定位的代码,因为保证代码和数据将相距一定距离。
根据我的经验,大多数想要存在于高内存中的操作系统内核只是链接到它们的目标高内存地址,内核在启动时做的第一件事就是构建页表以将自己映射到高内存中。这样,操作系统内核设计人员只需要确保入口点的某些块与位置无关(或者实际上,只需将该启动代码链接到其真正的低内存位置,将其放置在特殊部分中并更改链接器脚本(。
请参阅示例入口点和链接器脚本(不是 Rust 内核/aarch64(。
然后,一个足够复杂的内核可以控制其内存分配器,然后可以考虑释放包含入口点代码的页面,如入口点部分的开头和结尾所标识的那样(并且实际上扩展它以相同的方式处理所有初始化代码(。
如果你能在启动时使用额外的内存,一个不错的 rust 选项也可以是为加载器编写一个单独的 crate,将所有内容与链接器脚本绑定在一起,并在启动后释放其内存。
您不能在任意代码执行过程中重定其基数并期望它继续运行。从字面上看,在变基时使用的每个地址都将变得无效。即使你删除了所有的静态引用,你将如何处理动态分配的内存?
在高地址映射后,您必须加载二进制文件的干净副本,或者您必须使用高加载地址进行编译并在运行任何其他代码之前将自己映射到那里。
如果您觉得映射逻辑太长/太复杂,无法在这样的阶段完成,则可以先将所有内容映射为 RWX 的琐碎页表,然后可以切换,调出库和其他代码,然后继续构建最终页表。这种"琐碎"的页表可以在编译时构建,应该容纳不超过 4 页的内存(每个 TTBR 两页(,使用它们切换到高地址最多需要 20 条指令。