You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.06.26 23:17:01