前言
-
本篇继续研究 musl libc ldso 的动态加载过程中遇到的关键性的概念:到底要加载ELF 文件的哪些内容到 内存
-
当前如果遇到 ELF 动态加载,当前系统需要有【文件系统】,并且有较大的内存,因为 ELF 文件是无法直接运行的,首先通过解析 ELF 头部 获取入口函数,把需要载入到内存中的文件内容复制到指定内存区域,然后执行ELF 的入口函数,通常不是 ELF的 main 函数,而是更早的执行函数,如
_start
或者_dlstart
函数。此时 PC 指针指向 ELF 加载的基地址 + ELF 入口函数。
ELF 加载基地址
- 一个 ELF 文件,是否可以随意的加载?
当前验证发现: ELF 文件包括我们通常见到的 可以执行的文件,以及 共享库(如 xx.so)。共享库没有连接地址,基址是 0,但入口函数不一定是 0,如果遇到入口函数也是 0 的,需要注意这个 偏移地址 为 0 的入口函数,是否只是个空的符号,无法执行
-
为何有的 xxx.so 也称作 ELF 文件? 比如 musl libc.so,本身是个 库,但是它 有入口函数,并且可以执行。 当前 musl libc.so 确实如此,通常我们一般区分 执行文件与 库,库不用于执行。但是 musl libc.so 具备执行的功能,就像是我们见到的普通的执行文件,但是它依旧具备普通库的功能,为其他动态编译的应用程序提供共享库。
-
作为 共享库与可执行 集成在一起的 musl libc.so,基地址:0,入口函数不为0,基地址为0 可以重定位加载,如手动把 libc.so 加载到 0x200000 地址, 那么 libc.so 的入口函数就是: 0x200000 + libc.so 入口地址
-
普通静态或者动态链接的ELF 文件,由于基地址 不为0,就无法手动加载到 随意的地址。
-
如下 ELF 文件:基地址 0x200000,这个基地址跟链接脚本中的链接地址有关系,可以查看这个 elf 的连接脚本配置
-
入口点:入口地址,这个地址 已经是基于基地址 0x200000的,所以这个地址就不能随机加载了。如果想改变 这个 elf 的基地址,需要更改 相应的 链接脚本 链接地址的设置
动态加载需要加载哪些 ELF 内容到内存
-
有的 ELF 文件特别的大,尤其是开启了 【DEBUG】的,比如编译时使用
-O0 -g
, gdb 的调试信息都加入 ELF 文件了, ELF 文件不同于单片机的烧写文件 bin 文件,里面还有一些内容,如调试信息,是不需要加载到内存的,那么到底需要加载什么内容呢? -
这部分可以查看 Linux 内核代码 elf 加载部分,如
linux-6.3.8/fs/binfmt_elf.c
中的load_elf_binary
-
Linux 系统由于默认支持 mmu,执行文件的 mmap 映射,所以没有文件没有使用常规的 内存分配,不过依旧是先把文件内容映射 到用户地址空间,之所以不填充,是因为 Linux文件mmap 有缺页异常机制,需要访问时才会真正载入文件内容到内存,这样有很多好处,开始只映射(占位子)不加载,这样节省了加载时间,一个 ELF 文件,不可能上来全部执行到,可能只会执行部分内容,这样采用 访问时再加载,将会节省数量可观的内存,节省大量的加载时间。加上文件 mmap 有 cache 功能,如果加载过后,缓存暂时不清掉,这样下次执行就不再重复加载了。Linux 这个文件mmap 映射加载机制,对于 ELF 加载非常的有用。
-
经过熟悉 Linux 的
load_elf_binary
,发现只需要 加载PT_LOAD
段 -
那么 ELF 的
PT_LOAD
段,真的覆盖 ELF 的所有需要加载到内存中的内容范围吗?有没有漏下的?或者说 elf 不是还有 重定位、符号、.text
、.data
、等等吗?这些包含在里面吗? -
通过 elf 查看工具,加上对实际加载到内存的内容进行反向 dump 出来,肯定的一点就是: ELF 的
PT_LOAD
段 包含了所有需要加载到内存的文件内容,是所有,如果在其他的系统上,发现动态加载后, 内存中的文件内容不正确,或者部分内容为0,需要查看文件加载部分是否有处理不当的地方。
查看 PT_LOAD
段
- 可以使用
Die
这个工具,查看 ELF文件
-
这里了解到,
PT_LOAD
段 第一个段 文件偏移是 0,也就是把 ELF 文件头部也加入了内存 -
两个
PT_LOAD
段 的大小:Program 中的 p_filesz 就是当前的段大小,总大小: 0x23528 + 0x9f8 = 0x23f20,之所以这么计算,是因为 当前的两个段 是连在一起的。 -
由于段有多个节(section),可以查看 节 信息,
-
通过 计算
PT_LOAD
段的总大小,知道 这个 elf 文件 前面0 ~ (0x23f20 -1)
,也就是 0x23f20 个字节已经加载到内存,剩下 的节,.bss
没有实际内容,但内存中需要留位置,并且清 0。其他的节全部是 调试信息 debug 相关的。 -
所以通过加载
PT_LOAD
段,确实实现了整个 ELF 必需文件内容的全部加载
加载大小
-
这里需要提一下:段的加载大小,不是 段的 p_filesz,而是 段的 p_memsz, p_memsz 一般等于或者大于 p_filesz,超出的大小,就是
.bss
section 的大小,这部分大小需要手动清零,不清零,可能引发程序启动后的异常,比如定义了一个变量,但是没有初始化就使用,而程序员默认没有初始化的变量会被初始化 为 0。 清零.bss
就是清零PT_LOAD
段 中p_memsz - p_filesz
大小的区域,这个区域的起始地址应该是:base +elf_ppnt->p_vaddr + elf_ppnt->p_filesz
,如果是静态连接编译的 elf 程序, base 是0,也就是elf_ppnt->p_vaddr + elf_ppnt->p_filesz
。elf_ppnt->p_vaddr
是这个文件段的起始地址。 -
这里需要提一下: 段的 p_offset,这个是相对文件本身的偏移,通过情况下,
p_offset
与p_vaddr
是相同的,但也有不相同的。所以在文件填充时,需要把 文件内容 偏移p_offset
后,读取到内存地址p_vaddr
的位置,也就是说: 文件内容的存放位置 与 文件映射到内存的地址,并非一一对应。
小结
-
本篇注意讲解一下 ELF文件在 动态加载时需要加载哪些内容到内存,注意这里的动态加载,是动态加载 ELF 文件,这个 ELF文件,不单是 动态编译链接的 ELF,也包括静态编译链接的 ELF 以及 经常遇到的 动态共享库 (xx.so)
-
需要熟悉 ELF 的 头部、Program Header、了解 各个 Segment 段,了解 Section 节信息,这样对理解 动态加载程序,熟悉 动态加载非常有用。
-
需要了解操作系统的进程、线程机制,文件映射 mmap 机制。注意需要反复确认 内存的文件内容是否正确、完整。可以同 dump 的方式,把内存中的文件内容 dump 成一个文件,然后与实际的文件进行内容对比。
-
需要深刻了解 文件段的本身的偏移 :p_offset 与 内存地址 p_vaddr 的关系,也需要了解 段真实文件大小 p_filesz 与 p_memsz 的关系,也就是
.bss
节的存在