使用 Prelink 加速程序启动
一、What is Prelink?
1.1 Prelink 简介
Prelink 是 Red Hat 开发者 Jakub Jelinek 所设计的工具。正如其名字所示,Prelink 利用事先链接代替运行时链接的方法来加速共享库的加载。它不仅可以加快起动速度,还可以减少部分内存开销,是各种 Linux 架构上用于减少程序加载时间、缩短系统启动时间和加快应用程序启动的很受欢迎的一个工具。
Linux 系统运行时的动态链接尤其是重定位 (Relocation) 的开销,对于大型系统来说是很大的。相比之下,早期 UNIX 下的 a.out 格式的老式链接方法在速度和占用内存方面有明显的优势(但不如ELF格式更灵活,能方便的构建动态共享库)。Prelink 工具是试图在保持一部分灵活性的基础上,借鉴 a.out 格式在速度和占用内存方面的优点,对 ELF 文件进行一些改进。
Prelink 工具的原理主要基于这样一个事实:动态链接和加载的过程开销很大,并且在大多数的系统上,函数库并不会常常被更动,每次程序被执行时所进行的链接动作都是完全相同的,对于嵌入式系统来说尤其如此。因此,这一过程可以改在运行时之前就可以预先处理好,即花一些时间利用 Prelink 工具对动态共享库和可执行文件进行处理,修改这些二进制文件并加入相应的重定位等信息,节约了本来在程序启动时的比较耗时的查询函数地址等工作,这样可以减少程序启动的时间,同时也减少了内存的耗用。
Prelink 的这种做法当然也有代价:每次更新动态共享库时,相关的可执行文件都需要重新执行一遍 Prelink 才能保证有效,因为新的共享库中的符号信息、地址等很可能与原来的已经不同了。这种代价对于嵌入式系统的开发者来说可能稍微带来一些复杂度,不过好在对用户来说几乎是可以忽略的。
很多 Linux 发行版上已经预装了或者已经使用了 Prelink 工具,不过我们需要适用于嵌入式平台,比如 ARM 的版本,这样我们需要到下载 Prelink 的源代码并重新编译。
1.2 Prelink 机理
从我们最熟悉的 hello world 程序开始分析:
1 2 3 4 5 | #include <stdio.h> int main(int argc, const char* argv[]) { printf("Hello, World!\n"); return 0; } |
我们知道,printf 是在 c语言运行库 libc 中定义的。如果不使用动态库,也就是使用glibc 的静态库版本,链接到 a.out 中的话,那么 printf 函数的地址在运行之前就是已知的,很简单的一句地址转移就可以完成了。
可是使用动态库的话,在程序编译阶段,我们是无法得知 printf 的函数地址,因为动态库的加载的内存地址是随机的。那么对于动态库的情况,针对 printf 是如何寻址的呢?
在程序启动时,当调用 printf 的时候,程序会将处理权交给 loader,由其负责在进程以及其链接的动态库中查找 printf 的函数地址。由于 loader 不知道 printf 是在哪个动态库,所以它将在整个进程和动态库的范围内查找。更糟糕的是在 C++ 程序中,符号的命名是类名+函数名,这导致在做字符串比较时,往往直到字符串的结尾才能获得结果。
这就导致了,在进程启动过程中,符号查找往往占据了大部分时间。据统计,在 Linux 的 KDE 进程中启动过程中,符号查找表竟占据了进程启动 80% 的时间。有没有办法来改进呢?
如果进程在运行前,就能获知动态库的加载地址,那么函数调用的地址就应该是已知的,我们就可以通过修改执行程序,来避免符号的查找。从而节省进程启动的时间。
实际上 Prelink 正是这么做的。Prelink 最早是在 Redhat 中引用的,用来加速 KDE 的启动速度。那时侯 Prelink 作为系统的一个进程,不定期的启动,对系统中的进程和动态库进行优化,这在系统中进程和动态库不怎么变化的情况下非常有用。
在做 Prelink 时,需要为其指定需要做 Prelink 的进程和动态库的目录。Prelink 需要做以下几件事情:
- 分析所有的进程和动态库,为每个动态库指定一块唯一的(虚拟)内存地址;
- 分析进程和动态库中,所有需要重定位的函数、全局变量等,用 loader 进行符号查找,对齐地址进行解析;
- 修改进程中和动态库的二进制文件;
众所周知,在 32 位 Linux 操作系统上有 4G 的地址空间,3G 以上为操作系统使用,0000000~4000000 归进程的代码段、数据段和堆段使用,从 3G 往下归栈段使用。基本上我们可以认为从 1G~3G 的地址空间可以用来指定动态库的加载地址,地址空间还是很丰富的。
凡事总有万一,如果地址空间不够怎么办呢?Prelink 关于这个问题,做了两个约定:
- 总是一同出现的动态库,其动态库的加载地址一定不能重叠;
- 总是不同时间段出现的动态库,其动态库的加载地址可以重叠;
有了这两个约定之后,基本上就可以保证,为每个动态库指定加载地址,从而在运行前就能获知函数和全局变量等符号的地址。
更多有关 Prelink 的具体做法和细节可以参考 Prelink 的开发者 Jakub Jelinek 的专文介绍。以下两份文档提供了有关 prelink 的原理简介和性能评估,极具参考价值:
- https://elinux.org/images/1/19/MIPS-Prelinker.pdf
- https://elinux.org/images/a/af/Evaluation_of_MIPS_Prelinking.pdf
二、How to prelink?
2.1. Prelink 的交叉编译
2.1.1 获取源码
原版的 prelink 不适用于嵌入式平台;需要使用 Yocto Project 下的 prelink-cross 版本:
1 2 3 | $ wget http://git.yoctoproject.org/cgit/cgit.cgi/prelink-cross/snapshot/prelink-cross-20151030_cross.tar.gz $ tar -zxvpf prelink-cross-20151030_cross.tar.gz $ cd prelink-cross-20151030_cross/ |
也可以通过 git 获取最新的源码:
1 2 3 | $ git clone https://git.yoctoproject.org/git/prelink-cross $ cd prelink-cross $ git checkout 20151030_cross |
注意,需要切换到 cross 分支。
2.1.2.交叉编译
prelink 工具类似于 gcc 等工具链,如果处理的 ELF 文件所属系统架构不同于宿主系统架构(也就是当前的操作系统),则需要指定交叉编译参数。例如,如果目标软件运行的平台为 arm,需要将 -target 参数指定为 arm-linux。
此外,还需要加上 –without-sysroot 参数,使得我们编译出来的 prelink 工具可以在运行时指定 sysroot 路径。
1 2 | $ autoreconf -if $ ./configure --prefix=$HOME/local --without-sysroot --target=arm-linux |
2.2. Prelink 的使用详解
针对目标程序 target_bin 的 prelink 过程如下:
1 2 3 4 5 6 | $ /usr/sbin/arm-linux-prelink \ --root=$HOME/prelink/sys_root \ --cache-file=/etc/prelink.cache \ --config-file=/etc/prelink.conf \ --ld-library-path="/usr/lib:/lib" \ -h /path-to/target-bin -vmRfi |
对于上述 prelink 过程所用到的重要参数解释如下:
- –root 选项指定包含目标程序和系统共享库的 sysroot 路径,也就是嵌入式系统的根目录拷贝到宿主操作系统上的路径;后续所有路径都可指定为 sysroot 的相对路径。上述操作中,–root 参数指定的目录,其层级结构应与板子上根目录层级结构一致:
1 2 3 4 5 6 7 | $ tree -d -L 2 sys_root sys_root ├── target_dir │ └── target_bin ├── config ├── etc └── lib |
- –cache-file 参数用于指定prelink 建立索引过程中的 cache 文件;
- –config_file 参数用于指定 prelink 的配置文件,默认为 /etc/prelink.conf 。里面是所有需要进行 prelink 的 ELF 文件路径;如果 prelink 后加 -a 选项,则会处理此文件中所有的目录或文件;
- –ld-library-path 参数用于指定目标可执行文件运行时的共享库搜索路径。由于我们需要从 target_bin 开始进行 prelink,因此指定其运行时的 LD_LIBRARY_PATH。
- -h 和 -l 参数用于指定对目录为目录的软连接的不同处理方式;加 -h 参数时,会处理软链接文件指向的目标目录;加 -l 参数时,如果目标目录跨文件系统,则会忽略。
- -b 参数用于添加黑名单,所有用 –b 参数指定的目标都不会处理。
- -a 参数表示会处理配置文件中所有添加的路径或文件。
- -m 节省虚拟定址分配;如果有大量的共享库需要 prelink 就会需要这个选项。
- -R 参数会为共享库选择随机的基址;这个是为安全考虑。
- -f 强制重新 prelink 已经做过 prelink 的 ELF 文件。prelink 默认会忽略之前已经被 prelink 的 ELF 文件。
- -v 参数表示会输出中间的详细处理过程。
其他参数的解释请参考 man 手册。
单个可执行文件的 prelink 处理时间在秒级,如果对整个系统进行 prelink,可能要花几分钟或者十几分钟。
这里需要注意,使用 prelink 处理多个可执行文件时,如果每个文件运行时的动态库搜索路径不同,建议通过指定 LD_LIBRARY_PATH 来分别处理,而非通过 -a 参数一次性处理,否则可能会 prelink 错误的共享库,导致运行时 prelink 机制并没能发挥作用。
如果需要取消已经做过 Prelink 的 ELF 文件的,也非常简单:
1 | $ prelink -au |
警告:在对本机的 ELF 文件进行 prelink 处理过程中,如果被强制中断,可能会将整个系统弄崩掉。
三、Prelink 注意事项
3.1. 地址无关代码
需要被 Prelink 的 ELF 文件,无论是共享库还是可执行文件,编译时必须加 -fpic/-fPIC 参数,生成目标无关地址代码。对于可执行文件,不能使用 -fpie/-fPIE 加 –pie 生成地址无关可执行文件,否则无法被 prelink。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 | $ cat main.cc #include <iostream> int main(int argc, const char* argv[]) { std::cout << "Hello, World!" << std::endl; return 0; } $ g++ main.cc $ prelink --cache-file=./prelink.cache -h a.out -vm Laying out 1 libraries in virtual address space 0000003000000000-0000004000000000 Assigned virtual address space slots for libraries: /lib64/ld-linux-x86-64.so.2 0000003748800000-0000003748a29150 /lib/x86_64-linux-gnu/libc.so.6 0000003748c00000-0000003748fdfa60 /lib/x86_64-linux-gnu/libgcc_s.so.1 0000003749400000-0000003749616430 /lib/x86_64-linux-gnu/libm.so.6 0000003749000000-0000003749355328 /lib/x86_64-linux-gnu/libpthread-2.26.so 000000329c800000-000000329ca1e468 /usr/lib/x86_64-linux-gnu/libstdc++.so.6 0000003749800000-0000003749b86000 a.out Prelinking /home/huangjj/prelink/sample/a.out prelink: /home/huangjj/prelink/sample/a.out: R_X86_64_COPY reloc in shared library? $ g++ main.cc -fPIC –o fpic-a.out $ prelink --cache-file=./prelink.cache -h fpic-a.out -vm Assigned virtual address space slots for libraries: /lib64/ld-linux-x86-64.so.2 0000003748800000-0000003748a29150 /lib/x86_64-linux-gnu/libc.so.6 0000003748c00000-0000003748fdfa60 /lib/x86_64-linux-gnu/libgcc_s.so.1 0000003749400000-0000003749616430 /lib/x86_64-linux-gnu/libm.so.6 0000003749000000-0000003749355328 /lib/x86_64-linux-gnu/libpthread-2.26.so 000000329c800000-000000329ca1e468 /usr/lib/x86_64-linux-gnu/libstdc++.so.6 0000003749800000-0000003749b86000 fpic-a.out |
这个结论是根据上述测试程序得出的,其中的详细机理有待进一步研究。
3.2. 检查Prelink 状态
可以使用 readelf 和 objdump 工具来检查一个 ELF 文件是否已经被 prelink。例如:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 | $ prelink --cache-file=./prelink.cache -h fpic-a.out -vm Dynamic section at offset 0xd88 contains 30 entries: 标记 类型 名称/值 0x0000000000000001 (NEEDED) 共享库:[libstdc++.so.6] 0x0000000000000001 (NEEDED) 共享库:[libc.so.6] 0x000000000000000c (INIT) 0x3000000730 0x000000000000000d (FINI) 0x30000009d4 0x0000000000000019 (INIT_ARRAY) 0x3000200d70 0x000000000000001b (INIT_ARRAYSZ) 16 (bytes) 0x000000000000001a (FINI_ARRAY) 0x3000200d80 0x000000000000001c (FINI_ARRAYSZ) 8 (bytes) 0x000000006ffffef5 (GNU_HASH) 0x3000000298 0x0000000000000005 (STRTAB) 0x30000003f0 0x0000000000000006 (SYMTAB) 0x30000002b8 0x000000000000000a (STRSZ) 355 (bytes) 0x000000000000000b (SYMENT) 24 (bytes) 0x0000000000000015 (DEBUG) 0x0 0x0000000000000003 (PLTGOT) 0x3000200f88 0x0000000000000002 (PLTRELSZ) 96 (bytes) 0x0000000000000014 (PLTREL) RELA 0x0000000000000017 (JMPREL) 0x30000006d0 0x0000000000000007 (RELA) 0x30000005b0 0x0000000000000008 (RELASZ) 288 (bytes) 0x0000000000000009 (RELAENT) 24 (bytes) 0x000000000000001e (FLAGS) BIND_NOW 0x000000006ffffffb (FLAGS_1) 标志: NOW PIE 0x000000006ffffffe (VERNEED) 0x3000000570 0x000000006fffffff (VERNEEDNUM) 2 0x000000006ffffff0 (VERSYM) 0x3000000554 0x000000006ffffff9 (RELACOUNT) 4 0x000000006ffffdf8 (CHECKSUM) 0x31b79b4 0x000000006ffffdf5 (GNU_PRELINKED) 2018-05-03T13:30:05 0x0000000000000000 (NULL) 0x0 |
注意观察到 6~14 行,对比没有被 prelink 之前的状态,INIT、FINI、STRTAB、SYMTAB 等 section 的地址已经修改为运行时进程空间的虚拟内存地址。第 30 行,RELACOUNT 表示已经预先进行重定位的符号的数量;第 31 行是 prelink 根据 ELF 所直接依赖的共享库计算的 MD5 值,该值用于判断该 ELF 所以来的共享库是否被修改过;从第 32 行可以看出该 ELF 已被加上 PRELINKED 标记和时间戳。
但是,并非所有被成功 prelink 的 ELF 文件都会加上 PRELINKED 的标记和时间戳。在用 prelink 处理完我们的SDK的后,发现 target_bin 所有的依赖项都有 PRELINKED 标记,target_bin 自身并没有此标记。但是通过测试其启动速度,确有巨大的提升,证明 prelink 在 target_bin 上确实发挥了作用。
至于为什么没有这个标记,暂时还没有调查清楚,仍待进一步研究。
对于上述情况,通过 objdump 等工具查看ELF文件的 section header,我们仍然可以发现 prelink 处理后留下的蛛丝马迹。
Prelink 之前,查看 target_bin 的节头:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 | $ objdump -hw /path-to/target_bin /path-to/target_bin: 文件格式 elf32-little 节: Idx Name Size VMA LMA File off Algn 标志 0 .interp 00000013 00008174 00008174 00000174 2**0 CONTENTS, ALLOC, LOAD, READONLY, DATA 1 .note.ABI-tag 00000020 00008188 00008188 00000188 2**2 CONTENTS, ALLOC, LOAD, READONLY, DATA 2 .hash 00011620 000081a8 000081a8 000001a8 2**2 CONTENTS, ALLOC, LOAD, READONLY, DATA 3 .dynsym 000359b0 000197c8 000197c8 000117c8 2**2 CONTENTS, ALLOC, LOAD, READONLY, DATA 4 .dynstr 000a3cb1 0004f178 0004f178 00047178 2**0 CONTENTS, ALLOC, LOAD, READONLY, DATA 5 .gnu.version 00006b36 000f2e2a 000f2e2a 000eae2a 2**1 CONTENTS, ALLOC, LOAD, READONLY, DATA 6 .gnu.version_r 000001e0 000f9960 000f9960 000f1960 2**2 CONTENTS, ALLOC, LOAD, READONLY, DATA 7 .rel.dyn 00000348 000f9b40 000f9b40 000f1b40 2**2 CONTENTS, ALLOC, LOAD, READONLY, DATA 8 .rel.plt 000047c8 000f9e88 000f9e88 000f1e88 2**2 CONTENTS, ALLOC, LOAD, READONLY, DATA 9 .init 0000000c 000fe650 000fe650 000f6650 2**2 CONTENTS, ALLOC, LOAD, READONLY, CODE 10 .plt 000070a8 000fe65c 000fe65c 000f665c 2**2 CONTENTS, ALLOC, LOAD, READONLY, CODE 11 .text 01e789e2 00105740 00105740 000fd740 2**6 CONTENTS, ALLOC, LOAD, READONLY, CODE 12 .fini 00000008 01f7e124 01f7e124 01f76124 2**2 CONTENTS, ALLOC, LOAD, READONLY, CODE 13 .rodata 002e4b1c 01f7e200 01f7e200 01f76200 2**8 CONTENTS, ALLOC, LOAD, READONLY, DATA 14 .ARM.extab 00024440 02262d1c 02262d1c 0225ad1c 2**2 CONTENTS, ALLOC, LOAD, READONLY, DATA 15 .ARM.exidx 000efd00 0228715c 0228715c 0227f15c 2**2 CONTENTS, ALLOC, LOAD, READONLY, DATA 16 .eh_frame_hdr 00000014 02376e5c 02376e5c 0236ee5c 2**2 CONTENTS, ALLOC, LOAD, READONLY, DATA 17 .eh_frame 00000030 02376e70 02376e70 0236ee70 2**2 CONTENTS, ALLOC, LOAD, READONLY, DATA 18 .init_array 00000160 0237f730 0237f730 0236f730 2**2 CONTENTS, ALLOC, LOAD, DATA 19 .fini_array 00000004 0237f890 0237f890 0236f890 2**2 CONTENTS, ALLOC, LOAD, DATA 20 .jcr 00000004 0237f894 0237f894 0236f894 2**2 CONTENTS, ALLOC, LOAD, DATA 21 .data.rel.ro 001335b8 0237f898 0237f898 0236f898 2**3 CONTENTS, ALLOC, LOAD, DATA 22 .dynamic 000002d8 024b2e50 024b2e50 024a2e50 2**2 CONTENTS, ALLOC, LOAD, DATA 23 .got 0000bed8 024b3128 024b3128 024a3128 2**2 CONTENTS, ALLOC, LOAD, DATA 24 .data 0001f224 024bf000 024bf000 024af000 2**3 CONTENTS, ALLOC, LOAD, DATA 25 .bss 00025af0 024de230 024de230 024ce224 2**4 ALLOC 26 .comment 00000036 00000000 00000000 024ce224 2**0 CONTENTS, READONLY 27 .ARM.attributes 00000039 00000000 00000000 024ce25a 2**0 CONTENTS, READONLY |
Prelink 之后,再次查看节头:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 | $ objdump -hw /path-to/target_bin /path-to/target_bin: 文件格式 elf32-little 节: Idx Name Size VMA LMA File off Algn 标志 0 .interp 00000013 00008194 00008194 00000194 2**0 CONTENTS, ALLOC, LOAD, READONLY, DATA 1 .note.ABI-tag 00000020 000081a8 000081a8 000001a8 2**2 CONTENTS, ALLOC, LOAD, READONLY, DATA 2 .hash 00011620 000081c8 000081c8 000001c8 2**2 CONTENTS, ALLOC, LOAD, READONLY, DATA 3 .dynsym 000359b0 000197e8 000197e8 000117e8 2**2 CONTENTS, ALLOC, LOAD, READONLY, DATA 4 .gnu.liblist 0000067c 0004f198 0004f198 00047198 2**2 CONTENTS, ALLOC, LOAD, READONLY, DATA 5 .gnu.conflict 0002d4a4 0004f814 0004f814 00047814 2**2 CONTENTS, ALLOC, LOAD, READONLY, DATA 6 .gnu.version 00006b36 000f2e2a 000f2e2a 000eae2a 2**1 CONTENTS, ALLOC, LOAD, READONLY, DATA 7 .gnu.version_r 000001e0 000f9960 000f9960 000f1960 2**2 CONTENTS, ALLOC, LOAD, READONLY, DATA 8 .rel.dyn 00000348 000f9b40 000f9b40 000f1b40 2**2 CONTENTS, ALLOC, LOAD, READONLY, DATA 9 .rel.plt 000047c8 000f9e88 000f9e88 000f1e88 2**2 CONTENTS, ALLOC, LOAD, READONLY, DATA 10 .init 0000000c 000fe650 000fe650 000f6650 2**2 CONTENTS, ALLOC, LOAD, READONLY, CODE 11 .plt 000070a8 000fe65c 000fe65c 000f665c 2**2 CONTENTS, ALLOC, LOAD, READONLY, CODE 12 .text 01e789e2 00105740 00105740 000fd740 2**6 CONTENTS, ALLOC, LOAD, READONLY, CODE 13 .fini 00000008 01f7e124 01f7e124 01f76124 2**2 CONTENTS, ALLOC, LOAD, READONLY, CODE 14 .rodata 002e4b1c 01f7e200 01f7e200 01f76200 2**8 CONTENTS, ALLOC, LOAD, READONLY, DATA 15 .ARM.extab 00024440 02262d1c 02262d1c 0225ad1c 2**2 CONTENTS, ALLOC, LOAD, READONLY, DATA 16 .ARM.exidx 000efd00 0228715c 0228715c 0227f15c 2**2 CONTENTS, ALLOC, LOAD, READONLY, DATA 17 .eh_frame_hdr 00000014 02376e5c 02376e5c 0236ee5c 2**2 CONTENTS, ALLOC, LOAD, READONLY, DATA 18 .eh_frame 00000030 02376e70 02376e70 0236ee70 2**2 CONTENTS, ALLOC, LOAD, READONLY, DATA 19 .init_array 00000160 0237f730 0237f730 0236f730 2**2 CONTENTS, ALLOC, LOAD, DATA 20 .fini_array 00000004 0237f890 0237f890 0236f890 2**2 CONTENTS, ALLOC, LOAD, DATA 21 .jcr 00000004 0237f894 0237f894 0236f894 2**2 CONTENTS, ALLOC, LOAD, DATA 22 .data.rel.ro 001335b8 0237f898 0237f898 0236f898 2**3 CONTENTS, ALLOC, LOAD, DATA 23 .dynamic 000002d8 024b2e50 024b2e50 024a2e50 2**2 CONTENTS, ALLOC, LOAD, DATA 24 .got 0000bed8 024b3128 024b3128 024a3128 2**2 CONTENTS, ALLOC, LOAD, DATA 25 .data 0001f224 024bf000 024bf000 024af000 2**3 CONTENTS, ALLOC, LOAD, DATA 26 .bss 00025af0 024de230 024de230 024ce224 2**4 ALLOC 27 .dynstr 000a3e8c 0250e224 0250e224 024ce224 2**0 CONTENTS, ALLOC, LOAD, READONLY, DATA 28 .comment 00000036 00000000 00000000 025720b0 2**0 CONTENTS, READONLY 29 .ARM.attributes 00000039 00000000 00000000 025720e6 2**0 CONTENTS, READONLY 30 .gnu.prelink_undo 000005fc 00000000 00000000 02572120 2**2 CONTENTS, READONLY |
对比 prelink 前后的节头信息,我们发现 prelink 后每个节的地址都有了调整,增加了.gnu.liblist , .gnu.conflict 和 .gnu.prelink_undo 这三个节。同时 .dynstr 节的 size 由 0xa3cb1 增加到了 0xa3e8c。这些都是 prelink 之后 ELF 的 size 有所增大的原因。
3.3. 查看ELF依赖树
Prelink 的处理过程是从目标 ELF 文件开始,检查其依赖树。从叶子节点开始处理,自底向上,直至根节点。若中间任何节点处理异常,则目标文件都无法被 prelink。同理,如果已经被 prelink 处理的 ELF 文件,如果其依赖树的中任何节点对应的 ELF 文件有更改,则需要从根开始重新 prelink。如果被更改的 ELF 所处的层级较低,被很多可执行文件依赖,则可能整个系统的 ELF 都需要重新进行 Prelink 处理。
可以使用 lddtree 查看 ELF 文件的依赖树。但是这个工具比较鸡肋,只适用于处理本机的 ELF 文件,无法像 prelink 一样可以在运行时指定 sysroot 和 LD_LIBRARY_PATH。
下面提供了一个 shell 脚本,可以指定 sysroot 和 LD_LIBRARY_PATH,输出 ELF 文件依赖图,仅供参考:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 | #!/bin/bash #========================================================= # Author : Junjie Huang # Email : [email protected] # Last modified : 2018-05-07 17:43 # Filename : elf_dependencies_analyzer.sh # Description : a tool to analyze dependencies for elf #========================================================= ROOT_ELF="" MAX_DEPTH=0 SYS_ROOT=$(pwd) LD_LIB_PATH="" CHECK_PRELINK=0 PRINT_PATH=0 IGNORE_SCANNED=0 DRAW_WITH_DOT=0 SAVE_IN_CSV=0 function usage() { echo -e "\nUsage : `basename $0` [OPTION...] [FILES]" echo "Note :" echo " -c Check if the elf file has been prelinked or not" echo " -p Print the path of elf; otherwise print name as default" echo " -i Ignore the elf file which has been scanned, to be unique" echo " -g | --graph Draw the dependencies graphs with graphviz and dot" echo " -t | --table Generate the depencies table in cvs format" echo " -h | --help Get help" echo " --depth=MAX_DEPTH The max depth to scan" echo " --root=ROOT_PATH Prefix all paths with ROOT_PATH " echo " --ld-library-path=LD_LIBRARY_PATH What LD_LIBRARY_PATH should be used" echo "" } if [[ $# -lt 1 ]]; then usage exit 1 fi # parse the arguments for i in "$@"; do case $i in -c) CHECK_PRELINK=1 shift ;; -p) PRINT_PATH=1 shift ;; -i) IGNORE_SCANNED=1 shift ;; -g|--graph) DRAW_WITH_DOT=1 shift ;; -t|--table) SAVE_IN_CSV=1 shift ;; -d=*|--depth=*) MAX_DEPTH="${i#*=}" shift ;; -r=*|--root=*) SYS_ROOT="${i#*=}" shift ;; -l=*|--ld-library-path=*) LD_LIB_PATH="${i#*=}" shift ;; -h|--help) usage exit 1 ;; *) ROOT_ELF=$i ;; esac done ROOT_ELF_PATH=${SYS_ROOT}${ROOT_ELF} ROOT_ELF_NAME=$(basename ${ROOT_ELF_PATH}) LD_LIB_LIST=($(echo ${LD_LIB_PATH} | tr : ' ')) DOT_SOURCE_FILE=${ROOT_ELF_NAME}".dot" DOT_OUTPUT_FILE=${ROOT_ELF_NAME}".png" CSV_SOURCE_FILE=${ROOT_ELF_NAME}".csv" BLACK=$(tput setaf 0) RED=$(tput setaf 1) GREEN=$(tput setaf 2) YELLOW=$(tput setaf 3) BLUE=$(tput setaf 4) MAGENTA=$(tput setaf 5) CYAN=$(tput setaf 6) WHITE=$(tput setaf 7) RESET=$(tput sgr0) last_child_mask=() scanned_elf_list=() function print_hierarchy_symbols() { if [[ $depth -gt 0 ]]; then local i=1 while [[ $i -lt $depth ]]; do if [[ ${last_child_mask[$i]} -eq 0 ]]; then echo -n '│ ' else echo -n ' ' fi i=$((i+1)) done if [[ ${last_child_mask[$depth]} -eq 0 ]]; then echo -n '├──' else echo -n '└──' fi fi } # PARAMETER 1: ELF file path function print_file_path() { local path=$1 local name=$(basename $path) if [[ SAVE_IN_CSV -eq 1 ]]; then save_in_cvs ${name} fi print_hierarchy_symbols if [[ ${CHECK_PRELINK} -eq 1 ]]; then if [[ $(readelf -S ${path} | grep -i "prelink") ]]; then if [[ ${PRINT_PATH} -eq 1 ]]; then echo "${GREEN}${path} [y]${RESET}" else echo "${GREEN}${name} [y]${RESET}" fi else if [[ ${PRINT_PATH} -eq 1 ]]; then echo "${RED}${path} [n]${RESET}" else echo "${RED}${name} [n]${RESET}" fi fi else if [[ ${PRINT_PATH} -eq 1 ]]; then echo "${MAGENTA}${path}${RESET}" else echo "${MAGENTA}${name}${RESET}" fi fi } function generate_dot_header() { if [[ -f ${DOT_SOURCE_FILE} ]]; then rm -rf ${DOT_SOURCE_FILE} fi cat << EOT >> ${DOT_SOURCE_FILE} digraph "${ROOT_ELF_NAME}" { graph [ rankdir=LR bgcolor=white fontsize=10 ] edge [ color=blue ] node [ shape=record fontcolor=red color=black style=filled fillcolor=white ] EOT } # PARAMETER 1: parent elf path # PARAMETER 2: current elf path function draw_in_dot() { local pre=$(basename $1) local cur=$(basename $2) echo " \"$pre\" -> \"$cur\";" >> ${DOT_SOURCE_FILE} } function generate_dot_footer() { echo "}" >> ${DOT_SOURCE_FILE} } # PARAMETER 1: ELF file name function save_in_cvs() { local elf=$1 if [[ $depth -gt 0 ]]; then local i=1 while [[ $i -le $depth ]]; do echo -n ', ' >> ${CSV_SOURCE_FILE} i=$((i+1)) done fi echo $elf >> ${CSV_SOURCE_FILE} } # PARAMETER 1: ELF file path function list_dependencies() { local elf_list=$(readelf -d $1 | grep NEEDED | \ awk -F'[' '{print $2}' | awk -F']' '{print $1}') echo ${elf_list} } depth=0 # PARAMETER 1: ELF file path function dependencies_scan() { local target=$1 local dep_list=($(list_dependencies ${target})) #array if [[ ${IGNORE_SCANNED} -eq 1 ]]; then if [[ ${scanned_elf_list[@]} =~ (^|[[:space:]])"${target}"($|[[:space:]]) ]]; then return else scanned_elf_list+=(${target}) fi fi for elf_name in "${dep_list[@]}"; do local file_list=() if [[ ${#LD_LIB_LIST[@]} -ne 0 ]]; then for lib in "${LD_LIB_LIST[@]}"; do lib=${SYS_ROOT}${lib} if [[ -d ${lib} ]]; then file_list=($(find ${lib} -name ${elf_name})) if [[ ${#file_list[@]} -ne 0 ]]; then break fi fi done else file_list=($(find . -name ${elf_name})) fi if [[ ${#file_list[@]} -eq 0 ]]; then #file_list=($(find . -name ${elf_name})) #echo "could not found ${elf_name}" continue fi ((depth++)) if [[ ${dep_list[-1]} = ${elf_name} ]]; then last_child_mask[$depth]=1 else last_child_mask[$depth]=0 fi file_path=${file_list[0]} print_file_path ${file_path} if [[ ${DRAW_WITH_DOT} -eq 1 ]]; then draw_in_dot ${target} ${file_path} fi if [[ $(echo ${file_path} | grep "") ]]; then dependencies_scan ${file_path} elif [[ $depth -lt ${MAX_DEPTH} ]]; then dependencies_scan ${file_path} fi ((depth--)) done } function main() { if [[ ${DRAW_WITH_DOT} -eq 1 ]]; then generate_dot_header fi if [[ ${SAVE_IN_CSV} -eq 1 ]]; then rm -rf ${CSV_SOURCE_FILE} fi print_file_path ${ROOT_ELF_PATH} dependencies_scan ${ROOT_ELF_PATH} if [[ ${DRAW_WITH_DOT} -eq 1 ]]; then generate_dot_footer if [[ $(which dot) ]]; then dot -Tpng -o ${DOT_OUTPUT_FILE} ${DOT_SOURCE_FILE} else echo "Could not find \"dot\" command, please install graphviz!" fi fi } main |
使用方法如下:
1 2 3 | $ ./elf_dependencies_analyzer.sh --root=sys_root \ --ld-library-path="/usr/lib:/lib" \ /path-to/target_bin -i -c –g -t |
此脚本可以指定 sysroot 和 LD_LIBRARY_PATH 输出任意 ELF 文件的依赖图,并同时检查各节点的 prelink 状态,支持 graphviz / dot 和 csv 格式。
3.4. 不必要的依赖项
如果 prelink 在处理某个 ELF 文件(记为 A)的过程中,发现 A 并没有使用其直接依赖的另一个 ELF 文件(记为 B),而 A 又通过 C 间接依赖到 B,并且 B 已经被 prelink 处理。此时,A 将无法被 prelink。
3.5. 动态加载的共享库
Prelink 对于通过 dlopen 方式打开的共享库没有效果。