musl libc ldso 动态加载研究笔记:02

前言

  • 本篇继续研究 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_fileszelf_ppnt->p_vaddr 是这个文件段的起始地址。

  • 这里需要提一下: 段的 p_offset,这个是相对文件本身的偏移,通过情况下, p_offsetp_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 节的存在

猜你喜欢

转载自blog.csdn.net/tcjy1000/article/details/132375504
今日推荐