Repository navigation
[dlmodule] fix: uninitialized globals in relocatable modules - #11915
usualtreee wants to merge 3 commits into
Conversation
Fix the loader of relocatable (ET_REL) dynamic modules so that uninitialized global variables get their own storage and correct addresses: 1. Advance the allocation pointer after loading a BSS (SHT_NOBITS) section, so multiple NOBITS sections no longer overlap in memory. 2. Relocate section symbols with section_load_addr - sec_sh_addr + sym->st_value, matching how text symbols are already handled. 3. Allocate a common block for SHN_COMMON symbols (tentative definitions of global variables) and relocate every reference to it instead of silently skipping them. 4. Build the module symbol table from .symtab for ET_REL modules so that dlsym() can resolve the module's global functions. Verified on bsp/qemu-vexpress-a9 with CONFIG_RT_USING_MODULE=y: before the fix an ET_REL module fails dlsym(), after the fix its globals get correct non-overlapping addresses and round-trip reads/writes succeed, and ET_DYN (-shared) modules are unaffected. Refs: RT-Thread#5705
|
👋 感谢您对 RT-Thread 的贡献!Thank you for your contribution to RT-Thread! 为确保代码符合 RT-Thread 的编码规范,请在你的仓库中执行以下步骤运行代码格式化工作流(如果格式化CI运行失败)。 🛠 操作步骤 | Steps
完成后,提交将自动更新至 如有问题欢迎联系我们,再次感谢您的贡献!💐 |
📌 Code Review Assignment🏷️ Tag: componentsReviewers: @Maihuanyi Changed Files (Click to expand)
🏷️ Tag: components_libcReviewers: @GorrayLi @mysterywolf Changed Files (Click to expand)
📊 Current Review Status (Last Updated: 2026-10-28 15:50 CST)
📝 Review Instructions
|
|
|
|
||
| /* the common block (uninitialized global variables) starts right after | ||
| * the loaded sections; it was zeroed together with the module space */ | ||
| common_addr = (rt_ubase_t)ptr; |
There was a problem hiding this comment.
[P1] 对齐 COMMON 块基址,并为填充预留空间
这里直接将已加载段的结束地址作为 common_addr,但 dlmodule_common_block() 只对齐相对于块起点的偏移,不能保证最终符号地址满足 st_value 的对齐要求。使用当前加载函数复现:8 字节 text 后接 1 字节 data,要求 4 字节对齐的 COMMON 变量被放在 mem_space + 9,最终地址模 4 为 1。这会让要求对齐的访问在目标平台上产生异常。请按 COMMON 符号的最大对齐要求对齐块基址,并在 module_size 中预留相应填充空间;只对齐块大小不足以解决此问题。
There was a problem hiding this comment.
感谢老师的细致评审。已根据建议修正:现在会记录 COMMON 符号的最大对齐要求,在 module_size 中预留相应的填充空间,并对齐 common_addr,不再只对齐块内偏移。针对 4 字节和 16 字节对齐场景的测试均已通过,麻烦老师有空再帮忙确认一下!
| } | ||
| } | ||
|
|
||
| module->symtab = (struct rt_module_symtab *)rt_malloc(count * sizeof(struct rt_module_symtab)); |
There was a problem hiding this comment.
[P1] 检查符号表和符号名称的分配结果
新增的符号表数组分配没有检查返回值,后续会直接写入 module->symtab[count];第 664 行的名称分配也未经检查就传给 rt_memset()。在模块空间分配成功后,分别令这两处 rt_malloc() 返回空指针,均复现了空指针访问,低内存时会使模块加载触发系统异常。请检查两处分配结果,失败时清理已分配资源并返回错误;同时让 module->nsym 只统计已完成初始化的条目,避免调用方的 dlmodule_destroy() 遍历未初始化项。
There was a problem hiding this comment.
感谢老师提醒!这里确实考虑得不够周全。现已补充符号表和符号名称分配结果的检查,失败时返回 -RT_ENOMEM 并正确清理资源;同时,module->nsym 只统计完整初始化的条目,避免销毁时访问未初始化项。两类分配失败的注入测试均已通过,麻烦你再帮忙看一下!
| length = rt_strlen((const char *)(strtab + symtab[i].st_name)) + 1; | ||
|
|
||
| module->symtab[count].addr = | ||
| (void *)((rt_uint8_t *)module->mem_space - module_addr + symtab[i].st_value); |
There was a problem hiding this comment.
[P2] 根据符号所属段计算导出函数地址
ET_REL 中已定义符号的 st_value 是 st_shndx 所指段内的偏移,不能统一加到模块基址上。使用两个各 8 字节的可执行段、每段一个 st_value = 0 的全局函数复现时,两个导出地址都变成了 mem_space,而第二个函数实际位于 mem_space + 8。保留多个代码段的模块(例如使用 -ffunction-sections 后进行可重定位链接)会因此让 dlsym() 返回错误地址;若涉及 module_init,加载阶段就可能调用错误函数。请记录各段的实际加载地址,再按 st_shndx 和段内偏移计算导出地址。
There was a problem hiding this comment.
感谢老师。现已记录各段的实际加载地址,并根据符号的 st_shndx、sh_addr 和段内偏移计算导出函数地址。多代码段测试中,两个函数的导出地址现在均能指向各自正确的位置,麻烦您再帮忙复核一下!
| rt_size_t length; | ||
|
|
||
| if ((ELF_ST_BIND(symtab[i].st_info) != STB_GLOBAL) || | ||
| (ELF_ST_TYPE(symtab[i].st_info) != STT_FUNC)) |
There was a problem hiding this comment.
[P2] 从导出表中排除未定义函数
这里仅检查 STB_GLOBAL 和 STT_FUNC,因此也会收集 st_shndx == SHN_UNDEF 的外部函数引用。构造一个 GLOBAL/FUNC/UNDEF、st_value = 0 的符号后,当前代码将其加入导出表,dlsym() 返回模块基址,而该函数并没有在此模块中定义;调用这个地址会执行错误代码。请在前面的计数循环及这里的填表循环中同时排除未定义符号,避免将导入引用作为模块自身的函数导出。
There was a problem hiding this comment.
感谢指出!现已在计数和填表两个循环中同时排除 SHN_UNDEF,并补充段索引范围及已加载段检查,避免将外部函数引用误作为模块自身函数导出。相关测试均已通过,纯未定义符号场景下的导出数量为 0。若还有考虑不周之处,也麻烦您继续指出。
Reserve COMMON alignment padding and check symbol-table allocations. Use per-section load addresses for exported functions and exclude undefined symbols.
Rbb666
left a comment
There was a problem hiding this comment.
基于提交 5d6e93f933 补充三条行内审查意见,均附复现步骤和结果。验证使用当前加载代码、ARM 重定位代码及交叉编译生成的 ELF,在主机上检查地址和内存内容;未做 QEMU 或实板验证。
| addr = (Elf_Addr)(common_addr + dlmodule_common_block( | ||
| symtab, | ||
| (rt_ubase_t)(shdr[shdr[index].sh_link].sh_size / sizeof(Elf_Sym)), | ||
| sym)); | ||
| dlmodule_relocate(module, rel, addr); |
There was a problem hiding this comment.
[P1] COMMON 重定位需要写到目标段,不能直接使用段内偏移
ET_REL 的 r_offset 是目标段内的偏移。这里把原始重定位项直接传给 dlmodule_relocate(),而 ARM 实现按 module->mem_space + rel->r_offset 计算写入位置。引用只要不在第一个加载段,就会写错地方。
复现代码:
int common_value;
int *common_pointer = &common_value;
int function(void) { return 42; }编译命令:
arm-none-eabi-gcc -marm -mcpu=arm926ej-s -fcommon -fno-pic -c common.c -o common.o
arm-none-eabi-ld -r common.o -o common.mo生成的 .text 为 28 字节,随后是 4 字节的 .data;.rel.data 中引用 common_value 的项为 R_ARM_ABS32,r_offset=0。
将本提交的加载函数和 ARM 重定位函数放入主机测试程序,模块内存固定在 0x10000000,得到以下结果:
- COMMON 变量分配在
0x10000020。 .data中的common_pointer应写入该地址,实际仍为零。.text的第一条指令由0xe52db004被改成0xf52db024。- 加载函数仍返回成功。
请根据重定位段的 sh_info 找到目标段,用 section_load_addr 换算写入位置后再执行重定位。上述验证检查了内存内容,未运行模块中的 ARM 指令,也未做 QEMU 或实板验证。
There was a problem hiding this comment.
感谢老师指出,已在提交 6880fad 中修复。现在根据重定位节的 sh_info 查找目标节实际加载地址,将 r_offset 换算为模块内偏移后再调用架构重定位函数。已补充 COMMON 跨节重定位测试,主机检查及 QEMU 验证均通过。
| else if (rt_strncmp((const char *)(shstrab + shdr[sym->st_shndx].sh_name), ELF_BSS, 5) == 0) | ||
| { | ||
| /* relocate bss section */ | ||
| LOG_D("bss"); | ||
| addr = (Elf_Addr)bss_addr + sym->st_value; | ||
| addr = (Elf_Addr)(bss_addr - sec_sh_addr + sym->st_value); | ||
| } | ||
| else if (rt_strncmp((const char *)(shstrab + shdr[sym->st_shndx].sh_name), ".common", 7) == 0) | ||
| { | ||
| /* relocate common section (uninitialized globals) */ | ||
| LOG_D("common"); | ||
| addr = (Elf_Addr)(bss_addr - sec_sh_addr + sym->st_value); |
There was a problem hiding this comment.
[P2] 多个 NOBITS 段仍被重定位到同一个 bss_addr
加载 NOBITS 段后虽然推进了 ptr,但 bss_addr 只保存最后一个 NOBITS 段的地址。这里处理 .bss 和新增的 .common 分支时都使用它,两个段中的变量仍可能指向同一块内存。
复现代码:
int bss_value = 0;
int common_value;
int get_values(void) { return bss_value + common_value; }用下面的链接脚本把两类变量分别放入 .bss 和 .common:
SECTIONS
{
.text 0 : { *(.text*) }
.data 0 : { *(.data*) }
.bss 0 : { *(.bss*) }
.common 0 : { *(COMMON) }
}编译命令:
arm-none-eabi-gcc -marm -mcpu=arm926ej-s -fcommon -c multibss.c -o multibss.o
arm-none-eabi-ld -r -d -T multibss.ld multibss.o -o multibss.mo使用本提交的加载函数和 ARM 重定位函数做主机测试,结果如下,偏移均相对于 module->mem_space:
| 变量 | 所属段 | 实际分配偏移 | 重定位后引用的偏移 |
|---|---|---|---|
bss_value |
.bss |
52 | 56 |
common_value |
.common |
56 | 56 |
两个变量仍共用地址,写入其中一个会覆盖另一个。请按 sym->st_shndx 使用已经记录的 section_load_addr,不要再用单个 bss_addr 代表所有 NOBITS 段。本次为主机验证,未做 QEMU 或实板验证。
There was a problem hiding this comment.
感谢老师指出!已在提交 6880fad 中移除单个 bss_addr 的处理,统一通过 section_load_addr[st_shndx] + st_value 计算符号地址。已验证 .bss 和 .common 的地址独立,写入不同特征值后互不覆盖。
| module->symtab[module->nsym].addr = | ||
| (void *)(section_load_addr[symtab[i].st_shndx] - | ||
| shdr[symtab[i].st_shndx].sh_addr + symtab[i].st_value); |
There was a problem hiding this comment.
[P2] ET_REL 的 st_value 已是段内偏移,不应再减 sh_addr
ET_REL 中,已定义符号的 st_value 是相对于所属段起点的偏移。这里再减去 sh_addr,会在段地址非零时把导出地址算到模块之外。前面 rodata、bss、data 重定位中新增的同类减法也有这个问题。
复现代码:
int initialized = 7;
int get_value(void) { return initialized; }链接脚本:
SECTIONS
{
.text 0x1000 : { *(.text*) }
.data 0x2000 : { *(.data*) }
.bss 0x3000 : { *(.bss*) }
}编译命令:
arm-none-eabi-gcc -marm -mcpu=arm926ej-s -c nonzero.c -o nonzero.o
arm-none-eabi-ld -r -T nonzero.ld nonzero.o -o nonzero.moreadelf 显示:get_value 的 st_value=0,所属 .text 的 sh_addr=0x1000;initialized 的 st_value=0,所属 .data 的 sh_addr=0x2000。
将生成的 ELF 交给本提交的加载函数和 ARM 重定位函数,在主机上得到:
get_value实际加载在模块起点,导出地址却是mem_space - 4096,位于分配范围之外。.data实际加载在mem_space + 36,initialized的引用却被写成mem_space + 36 - 0x2000。- 对照基线
9e6dec3b14,同一 ELF 的数据引用为正确的mem_space + 36。
请按“所属段的实际加载地址 + st_value”计算地址,并一起修正前面的段符号重定位公式。参见 ELF 符号值规则。上述结果来自主机验证,未做 QEMU 或实板验证。
There was a problem hiding this comment.
感谢老师纠正,之前对 ET_REL 的符号值规则理解有误。提交 6880fad 已将已定义符号重定位和函数导出地址统一改为所属节实际加载地址加 st_value,不再减去 sh_addr。非零节地址场景的主机检查及 QEMU 验证均通过。
Convert relocation offsets to module-relative addresses using the target section's load address. Use per-section load addresses for defined symbols and ET_REL exports without subtracting sh_addr. Skip relocations whose target sections were not loaded, such as debug sections. Verified with 12 host scenarios using ASan/UBSan and seven modules on qemu-vexpress-a9, including COMMON references in data, multiple NOBITS sections, nonzero sh_addr, and -g debug relocations.
|
本轮修复已推送至 6880fad,请各位老师帮忙复审。另外补充处理了带 -g 模块的调试节重定位:跳过目标节未加载的重定位项,避免生成模块外写入地址。 当前 12 组主机回归测试在 ASAN、UBSAN 下全部通过;qemu-vexpress-a9 的 7 个测试模块均通过,包括 COMMON 跨节引用、多个 NOBITS 节、非零节地址和带调试信息模块。尚未做实板及全部架构验证。 |
拉取/合并请求描述:(PR description)
为什么提交这份PR (why to submit this PR)
修复 issue #5705(动态模块中未初始化全局变量访问异常,2022-03 报告,至今 open)。该问题在最新 master(1c801efa)上仍可复现:
dlmodule_load_relocated_object()(ET_REL 动态模块加载路径,Cortex-M 等使用 RT_USING_MODULE 的 BSP 走此路径)存在以下缺陷,导致动态模块中未初始化全局变量的地址重叠/无法重定位:你的解决方案是什么 (what is your solution)
ptr += shdr[index].sh_size;;section_load_addr - sec_sh_addr + sym->st_value,与 text 段处理方式一致;st_value(对齐要求)对齐、st_size累计计入模块总大小;重定位阶段先于段名匹配处理 SHN_COMMON(避免越界读 shdr[0xfff2]),地址 = 公共块基址 + 符号在块内偏移(与尺寸核算共用同一算法,保证一致);修改文件:components/libc/posix/libdl/dlelf.c、dlelf.h(新增 SHN_COMMON 宏)。
请提供验证的bsp和config (provide the config and bsp)
BSP: bsp/qemu-vexpress-a9
.config: 需要开启
CONFIG_RT_USING_MODULE=y(RT_USING_DFS / RT_USING_DFS_ROMFS / RT_USING_POSIX_FS 为该 BSP 默认配置,无需改动)验证方法:qemu-system-arm 运行时加载三个测试动态模块:
-shared -fPIC -fcommon流程)-r、非 PIC、-fcommon;rx_sem/serial 为 SHN_COMMON),覆盖本次修复路径每个模块检查未初始化全局变量地址互不重叠,并做特征值写入回读(serial=0xdeadbeef、rx_sem=0xaa55)验证内存互不踩踏。
dlsym FAILED;action: (待触发;可在 fork 仓库 Actions 中运行 manual_dist 编译 qemu-vexpress-a9 后贴链接)
当前拉取/合并请求的状态 Intent for your PR
必须选择一项 Choose one (Mandatory):
代码质量 Code Quality:
我在这个拉取/合并请求中已经考虑了 As part of this pull request, I've considered the following:
#if 0代码,不包含已经被注释了的代码 All redundant code is removed and cleaned up