如何定位进程中的所有可执行代码区域(含动态链接代码)
解决方案
1. 明确可执行映射的两种类型
不要默认所有可执行映射都是完整ELF文件,实际存在两种核心场景:
- 完整ELF映射:主程序、动态链接器、共享库的完整文件映射(包含ELF头、程序头表等),这类映射的起始地址就是ELF在内存中的加载基地址。
- 单个ELF段映射:动态加载时可能单独映射某个可执行段(比如.text),内存中只有段内容,没有ELF头和程序头表。
2. 正确解析ELF程序头的流程
针对/proc/<pid>/maps中标记为可执行(含x权限)的条目,按以下步骤处理:
- 提取映射关键信息:从每行解析出起始地址、结束地址、文件偏移、文件路径(第四列的pathname)。
- 区分文件映射与匿名映射:
- 若文件路径非空且不是
[anon]/[stack]等特殊标记:- 读取磁盘上对应路径的ELF文件,解析ELF头(
Elf64_Ehdr/Elf32_Ehdr),获取程序头表的偏移e_phoff和数量e_phnum。 - 遍历所有程序头(
Elf64_Phdr/Elf32_Phdr),筛选出p_type=PT_LOAD且p_flags包含PF_X的可执行段:- 计算该段的内存起始地址:
加载基地址 + p_vaddr。其中加载基地址的获取分两种情况:- 若当前maps条目是完整ELF映射:加载基地址就是maps的起始地址(因为ELF头在映射起始处)。
- 若当前maps条目是单个段映射:加载基地址 =
maps起始地址 - p_vaddr(通过程序头的p_vaddr反向推导)。
- 验证匹配性:
加载基地址 + p_vaddr需等于maps起始地址,加载基地址 + p_vaddr + p_memsz需落在maps的结束地址范围内。
- 计算该段的内存起始地址:
- 读取磁盘上对应路径的ELF文件,解析ELF头(
- 若为匿名可执行映射:大概率是JIT生成的代码,需额外追踪
mprotect系统调用(很多JIT会先映射可写内存,再修改为可执行权限)。
- 若文件路径非空且不是
3. 解决“段偏移超出映射空间”的问题
你遇到的偏移溢出问题,本质是错误地从单个段的内存映射中读取ELF头/段头——这类映射里根本没有这些结构。正确的做法是从磁盘上的原始ELF文件解析程序头,再对应到内存中的映射区域,而不是直接从内存映射里找ELF结构。
举个实际例子:
maps条目:7f1234567000-7f1234568000 r-xp 00001000 08:01 123456 /lib/x86_64-linux-gnu/libc.so.6
- 磁盘上的libc.so.6中,存在一个程序头的
p_offset=0x1000、p_vaddr=0x1000、p_flags=PF_R|PF_X。 - 计算加载基地址:
7f1234567000 - 0x1000 = 7f1234566000。 - 该段在内存中的范围就是
7f1234566000 + 0x1000到7f1234566000 + 0x1000 + p_memsz,完全覆盖maps条目范围。
4. 动态链接场景的补充处理
- 每次捕获到
mmap/mprotect系统调用后重新读取/proc/<pid>/maps是正确的,需注意:- 动态链接器本身也是一个ELF,其可执行映射也要按上述流程解析。
- PLT/GOT表所在的段通常标记为可执行,在程序头中会有对应的
PT_LOAD条目,无需特殊处理,按常规可执行段解析即可。
5. 避坑要点
- ASLR影响:加载基地址会随机变化,必须从maps实时获取,不能硬编码。
- 页对齐处理:ELF段的虚拟地址
p_vaddr可能不是页对齐的,但内存映射的起始地址一定是页对齐的,计算时需用getpagesize()获取系统页大小,处理对齐差异。 - 只关注PT_LOAD段:ELF中只有
PT_LOAD类型的段会被映射到内存,其他类型(如PT_NOTE、PT_DEBUG)无需处理。
内容的提问来源于stack exchange,提问作者Olle
相关产品推荐
相关产品推荐

