C语言中获取已加载共享对象内存地址与大小的最优方案
问题描述
我正在用C语言开发一个64位内存扫描库,需要扫描共享对象所在的内存区域,因此需要获取模块的地址和大小,效果类似/proc/self/maps的输出。需忽略不可读段,仅扫描指定模块(如/usr/lib/libXau.so或/usr/lib/libdbus-1.so):
7a9d2f92e000-7a9d2f930000 r--p 00000000 103:03 15505103 /usr/lib/libXdmcp.so.6.0.0 7a9d2f930000-7a9d2f932000 r-xp 00002000 103:03 15505103 /usr/lib/libXdmcp.so.6.0.0 7a9d2f932000-7a9d2f934000 r--p 00004000 103:03 15505103 /usr/lib/libXdmcp.so.6.0.0 7a9d2f934000-7a9d2f935000 r--p 00005000 103:03 15505103 /usr/lib/libXdmcp.so.6.0.0 7a9d2f935000-7a9d2f936000 rw-p 00006000 103:03 15505103 /usr/lib/libXdmcp.so.6.0.0 7a9d2f936000-7a9d2f937000 r--p 00000000 103:03 15505107 /usr/lib/libXau.so.6.0.0 7a9d2f937000-7a9d2f938000 r-xp 00001000 103:03 15505107 /usr/lib/libXau.so.6.0.0 7a9d2f938000-7a9d2f939000 r--p 00002000 103:03 15505107 /usr/lib/libXau.so.6.0.0 7a9d2f939000-7a9d2f93a000 r--p 00002000 103:03 15505107 /usr/lib/libXau.so.6.0.0 7a9d2f93a000-7a9d2f93b000 rw-p 00003000 103:03 15505107 /usr/lib/libXau.so.6.0.0 7a9d2f93b000-7a9d2f949000 r--p 00000000 103:03 15490157 /usr/lib/libdbus-1.so.3.32.4 7a9d2f949000-7a9d2f977000 r-xp 0000e000 103:03 15490157 /usr/lib/libdbus-1.so.3.32.4 7a9d2f977000-7a9d2f989000 r--p 0003c000 103:03 15490157 /usr/lib/libdbus-1.so.3.32.4 7a9d2f989000-7a9d2f98b000 r--p 0004e000 103:03 15490157 /usr/lib/libdbus-1.so.3.32.4 7a9d2f98b000-7a9d2f98c000 rw-p 00050000 103:03 15490157 /usr/lib/libdbus-1.so.3.32.4
此前我使用扩展版的link_map结构体(参考dlopen(3)),通过link->l_addr获取起始地址,link->phdr[0].p_memsz计算结束地址:
#include <dlfcn.h> /* dlopen() */ #include <link.h> /* link_map */ struct my_link_map { /* Base from link.h */ ElfW(Addr) l_addr; const char* l_name; ElfW(Dyn) * l_ld; struct my_link_map* l_next; struct my_link_map* l_prev; /* Added */ struct my_link_map* real; long int ns; struct libname_list* moduleName; ElfW(Dyn) * info[DT_NUM + DT_VERSIONTAGNUM + DT_EXTRANUM + DT_VALNUM + DT_ADDRNUM]; const ElfW(Phdr) * phdr; }; struct my_link_map* link = dlopen(module, RTLD_NOLOAD | RTLD_NOW); if (!link) return; uint8_t* start = (uint8_t*)link->l_addr; uint8_t* end = start + link->phdr[0].p_memsz; /* ... */
但当我尝试用这段代码获取主64位可执行文件的起始和结束地址(即向dlopen()传入NULL而非模块名)时,赋值end时程序崩溃。我考虑过解析/proc/self/maps,虽已实现过滤指定模块的函数,但认为这并非最优方案。
核心问题:我知道通过dlopen()后使用link_map->l_addr获取已加载模块的起始地址,但获取模块大小/结束地址的最优方法是什么?
最优解决方案
1. 正确遍历程序头表(Program Header Table)
崩溃原因是直接访问phdr[0],但主可执行文件的phdr并非从索引0开始就是有效可加载段,且你自定义的my_link_map遗漏了标准link_map中的l_phnum字段(记录程序头表的条目数量)。标准link_map结构体本身就包含l_phdr和l_phnum,不需要自定义扩展,正确做法是遍历所有可加载且可读的段,计算模块的整体覆盖范围:
#include <dlfcn.h> #include <link.h> #include <stdint.h> #include <stddef.h> typedef struct { uint8_t* start; uint8_t* end; } module_range_t; module_range_t get_module_range(const char* module) { module_range_t range = {NULL, NULL}; struct link_map* link = dlopen(module, RTLD_NOLOAD | RTLD_NOW); if (!link) { return range; } uintptr_t min_addr = (uintptr_t)-1; uintptr_t max_addr = 0; // 遍历所有程序头条目 for (int i = 0; i < link->l_phnum; i++) { const ElfW(Phdr)* phdr = &link->l_phdr[i]; // 只处理可加载段(PT_LOAD)且拥有读权限的段 if (phdr->p_type == PT_LOAD && (phdr->p_flags & PF_R)) { uintptr_t seg_start = link->l_addr + phdr->p_vaddr; uintptr_t seg_end = seg_start + phdr->p_memsz; if (seg_start < min_addr) { min_addr = seg_start; } if (seg_end > max_addr) { max_addr = seg_end; } } } if (min_addr != (uintptr_t)-1 && max_addr > min_addr) { range.start = (uint8_t*)min_addr; range.end = (uint8_t*)max_addr; } dlclose(link); return range; }
关键说明:
- 标准
link_map结构体(定义在link.h)原生包含l_phdr和l_phnum,自定义结构体可能与运行时实际布局不匹配,导致内存访问越界。 - 主可执行文件的
l_addr通常为0,通过link->l_addr + phdr->p_vaddr可计算段的实际加载地址。 - 必须遍历所有符合条件的段,因为一个模块会被拆分为多个段(只读数据、代码、读写数据等),取所有段的最小起始和最大结束地址,才能得到模块的完整内存范围。
2. 两种方案的优缺点对比
- 使用
link_map的优势:- 直接调用动态链接器接口,无需解析文本文件,性能更高。
- 不受
/proc文件系统可用性限制(如chroot环境或部分嵌入式系统可能未挂载/proc)。
- 解析
/proc/self/maps的优势:- 能获取更详细的段权限信息,便于精确过滤扫描目标。
- 不依赖
dlopen,可处理特殊加载的模块。
如果你的场景中/proc一定可用,两种方案均可,但link_map方式更贴合系统原生接口,是最优选择。
内容的提问来源于stack exchange,提问作者trxgnyp1
相关产品推荐
相关产品推荐

