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 程序开始分析:
#include 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 需要做以下几件事情:
众所周知,在 32 位 Linux 操作系统上有 4G 的地址空间,3G 以上为操作系统使用,0000000~4000000 归进程的代码段、数据段和堆段使用,从 3G 往下归栈段使用。基本上我们可以认为从 1G~3G 的地址空间可以用来指定动态库的加载地址,地址空间还是很丰富的。
凡事总有万一,如果地址空间不够怎么办呢?Prelink 关于这个问题,做了两个约定:
有了这两个约定之后,基本上就可以保证,为每个动态库指定加载地址,从而在运行前就能获知函数和全局变量等符号的地址。
更多有关 Prelink 的具体做法和细节可以参考 Prelink 的开发者 Jakub Jelinek 的专文介绍。以下两份文档提供了有关 prelink 的原理简介和性能评估,极具参考价值:
2.1. Prelink 的交叉编译
2.1.1 获取源码
原版的 prelink 不适用于嵌入式平台;需要使用 Yocto Project 下的 prelink-cross 版本:
也可以通过 git 获取最新的源码:
$ 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 过程所用到的重要参数解释如下:
1 2 3 4 5 6 7 |
$ tree -d -L 2 sys_root sys_root ├── target_dir │ └── target_bin ├── config ├── etc └── lib |
其他参数的解释请参考 man 手册。
单个可执行文件的 prelink 处理时间在秒级,如果对整个系统进行 prelink,可能要花几分钟或者十几分钟。
这里需要注意,使用 prelink 处理多个可执行文件时,如果每个文件运行时的动态库搜索路径不同,建议通过指定 LD_LIBRARY_PATH 来分别处理,而非通过 -a 参数一次性处理,否则可能会 prelink 错误的共享库,导致运行时 prelink 机制并没能发挥作用。
如果需要取消已经做过 Prelink 的 ELF 文件的,也非常简单:
1 |
$ prelink -au |
警告:在对本机的 ELF 文件进行 prelink 处理过程中,如果被强制中断,可能会将整个系统弄崩掉。
3.1. 地址无关代码
需要被 Prelink 的 ELF 文件,无论是共享库还是可执行文件,编译时必须加 -fpic/-fPIC 参数,生成目标无关地址代码。对于可执行文件,不能使用 -fpie/-fPIE 加 –pie 生成地址无关可执行文件,否则无法被 prelink。
$ cat main.cc #include 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。例如:
注意观察到 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 的节头:
Prelink 之后,再次查看节头:
对比 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 文件依赖图,仅供参考:
#!/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 |
使用方法如下:
$ ./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 方式打开的共享库没有效果。
https://joydig.com/prelink-elf-to-speed-up-startup-time/