# 基于qemu-riscv从0开始构建嵌入式linux系统ch14. 动态链接——elf文件的加载 ### busybox动态链接 之前我们配置busybox为静态编译,即是可以看到生成的二进制文件就几M的大小,如果后续我们在系统内添加更多的应用程序均为静态编译,可想而知对磁盘存储消耗很大,在嵌入式设备上是很不划算的,因此我们需要考虑动态链接库,将应用程序和C库分离,这样多个应用程序可以共享一个libc的共享库,不仅仅节省磁盘空间,同时还节约了多个应用程序同时运行时的内存开销。 busybox使用动态库编译选项仅需修改busybox-1.33.1/configs/quard_star_defconfig文件,CONFIG_STATIC配置为not set即可。 ```shell # CONFIG_STATIC is not set ``` 重新编译生成/bin/busybox只有960K的大小,使用下面的命令检查下这个文件 ```shell file busybox ``` 得到如下输出结果 ``` busybox: ELF 64-bit LSB executable, UCB RISC-V, version 1 (SYSV), dynamically linked, interpreter /lib/ld-linux-riscv64-lp64.so.1, for GNU/Linux 5.4.0, stripped ``` 对比修改前可以发现其区别 ``` busybox: ELF 64-bit LSB executable, UCB RISC-V, version 1 (SYSV), statically linked, for GNU/Linux 5.4.0, stripped ``` ### linux elf加载机制 在kernel的配置选项里有允许加载允许的可执行文件的格式类型的使能,一般都会配置CONFIG_BINFMT_ELF和CONFIG_BINFMT_SCRIPT,分别对应elf和shell脚本的加载,实现源码分别在linux-5.10.42/fs/binfmt_elf.c和binfmt_script.c,本节我们主要分析binfmt_elf.c:820:load_elf_binary函数,内核执行程序时,会加载目标成的数据并匹配格式,如果是elf格式则会进入这个函数。(注意本文在撰写时笔者已经把交叉编译工具链改为自己源码编译的riscv64-unknown-linux-gnu-gcc,不过不影响项目在ch14节使用的riscv64-linux-gcc这个工具链是完全一样的原理只是版本区别)。 我们可以使用编译工具链的riscv64-unknown-linux-gnu-readelf -h busybox来查看elf文件头信息。 ``` ELF Header: Magic: 7f 45 4c 46 02 01 01 00 00 00 00 00 00 00 00 00 Class: ELF64 Data: 2's complement, little endian Version: 1 (current) OS/ABI: UNIX - System V ABI Version: 0 Type: EXEC (Executable file) Machine: RISC-V Version: 0x1 Entry point address: 0x18c30 Start of program headers: 64 (bytes into file) Start of section headers: 959136 (bytes into file) Flags: 0x5, RVC, double-float ABI Size of this header: 64 (bytes) Size of program headers: 56 (bytes) Number of program headers: 9 Size of section headers: 64 (bytes) Number of section headers: 26 Section header string table index: 25 ``` 首先是load_elf_phdrs函数用于elf头信息,然后根据头信息中的段进行解析加载。关于elf段依然可以通过工具链查看。riscv64-unknown-linux-gnu-readelf -h busybox。 ``` Elf file type is EXEC (Executable file) Entry point 0x18c30 There are 9 program headers, starting at offset 64 Program Headers: Type Offset VirtAddr PhysAddr FileSiz MemSiz Flags Align PHDR 0x0000000000000040 0x0000000000010040 0x0000000000010040 0x00000000000001f8 0x00000000000001f8 R 0x8 INTERP 0x0000000000000238 0x0000000000010238 0x0000000000010238 0x0000000000000021 0x0000000000000021 R 0x1 [Requesting program interpreter: /lib/ld-linux-riscv64-lp64d.so.1] LOAD 0x0000000000000000 0x0000000000010000 0x0000000000010000 0x00000000000e816c 0x00000000000e816c R E 0x1000 LOAD 0x00000000000e8de8 0x00000000000f9de8 0x00000000000f9de8 0x00000000000013d9 0x0000000000001b10 RW 0x1000 DYNAMIC 0x00000000000e8e00 0x00000000000f9e00 0x00000000000f9e00 0x0000000000000200 0x0000000000000200 RW 0x8 NOTE 0x000000000000025c 0x000000000001025c 0x000000000001025c 0x0000000000000020 0x0000000000000020 R 0x4 GNU_EH_FRAME 0x00000000000e812c 0x00000000000f812c 0x00000000000f812c 0x0000000000000014 0x0000000000000014 R 0x4 GNU_STACK 0x0000000000000000 0x0000000000000000 0x0000000000000000 0x0000000000000000 0x0000000000000000 RW 0x10 GNU_RELRO 0x00000000000e8de8 0x00000000000f9de8 0x00000000000f9de8 0x0000000000000218 0x0000000000000218 R 0x1 Section to Segment mapping: Segment Sections... 00 01 .interp 02 .interp .note.ABI-tag .hash .gnu.hash .dynsym .dynstr .gnu.version .gnu.version_r .rela.dyn .rela.plt .plt .text .rodata .eh_frame_hdr .eh_frame 03 .preinit_array .init_array .fini_array .dynamic .data .got .sdata .sbss .bss 04 .dynamic 05 .note.ABI-tag 06 .eh_frame_hdr 07 08 .preinit_array .init_array .fini_array .dynamic ``` binfmt_elf.c:862开始的for循环会依次对各个上述段进行处理,申请内存,装填数据,完成加载后进入进程开始执行。这里有个关于动态链接的程序和静态链接程序最大的不同就是.interp段的存在,这个段保存了一段字符串信息,指向/lib/ld-linux-riscv64-lp64d.so.1,即为动态库加载的“解释器”,而解释器本身作为so文件也是个elf文件,原可执行文件的动态库加载需要依赖这个解释器的工作,而且,其实这个解释器本身是个伪装成so文件的可执行文件,我们可以直接执行这个解释器文件,/lib/ld-linux-riscv64-lp64d.so.1 --help,会看到这样的输出。 ``` [~]#/lib/ld-linux-riscv64-lp64d.so.1 --help Usage: /lib/ld-linux-riscv64-lp64d.so.1 [OPTION]... EXECUTABLE-FILE [ARGS-FOR-PROGRAM...] You have invoked 'ld.so', the program interpreter for dynamically-linked ELF programs. Usually, the program interpreter is invoked automatically when a dynamically-linked executable is started. You may invoke the program interpreter program directly from the command line to load and run an ELF executable file; this is like executing that file itself, but always uses the program interpreter you invoked, instead of the program interpreter specified in the executable file you run. Invoking the program interpreter directly provides access to additional diagnostics, and changing the dynamic linker behavior without setting environment variables (which would be inherited by subprocesses). --list list all dependencies and how they are resolved --verify verify that given object really is a dynamically linked object we can handle --inhibit-cache Do not use /etc/ld.so.cache --library-path PATH use given PATH instead of content of the environment variable LD_LIBRARY_PATH --glibc-hwcaps-prepend LIST search glibc-hwcaps subdirectories in LIST --glibc-hwcaps-mask LIST only search built-in subdirectories if in LIST --inhibit-rpath LIST ignore RUNPATH and RPATH information in object names in LIST --audit LIST use objects named in LIST as auditors --preload LIST preload objects named in LIST --argv0 STRING set argv[0] to STRING before running --list-tunables list all tunables with minimum and maximum values --help display this help and exit --version output version information and exit This program interpreter self-identifies as: /lib/ld-linux-riscv64-lp64d.so.1 Shared library search path: /lib (system search path) /usr/lib (system search path) /usr/local/lib (LD_LIBRARY_PATH) (libraries located via /etc/ld.so.cache) /lib (system search path) /usr/lib (system search path) No subdirectories of glibc-hwcaps directories are searched. Legacy HWCAP subdirectories under library search path directories: tls (supported, searched) ``` 尝试执行/lib/ld-linux-riscv64-lp64d.so.1 --list /bin/busybox,你会惊奇的发现会打印展示一个需要动态链接库所有的依赖,如果你的系统环境缺少了其中的一些库,则程序无法启动,这条命令也会显示类似libc.so.6 => not found这样的内容。 ``` [~]#/lib/ld-linux-riscv64-lp64d.so.1 --list /bin/busybox linux-vdso.so.1 (0x0000003fc2246000) libm.so.6 => /lib/libm.so.6 (0x0000003fc21c2000) libresolv.so.2 => /lib/libresolv.so.2 (0x0000003fc21b2000) libc.so.6 => /lib/libc.so.6 (0x0000003fc20a7000) /lib/ld-linux-riscv64-lp64d.so.1 (0x0000003fc2248000) ``` 如果熟悉linux系统的朋友想必都熟悉一个ldd的命令,用它操作可执行文件同样可以得到这个输出结果。其实ldd并不是二进制可执行文件,而是一个脚本,其原理就是用这个执行解释器,并配置LD_TRACE_LOADED_OBJECTS=1,和--list选项可以达成一样的效果。 这些探究只能对这个“解释器”做一感性的探究,而笔者作为一个喜欢对事物探求本源的人,当然是希望能阅读“解释器”的源码学习其工作机理,glibc/elf/rtld.c:1124:dl_main函数即为这个解释器对应的源码,其中还包含了将解释器用作可执行程序本身的一段注释,非常有趣味,摘抄如下,这也解释了这个解释器的为什么还拥有一些工具性质的功能。 ```c if (*user_entry == (ElfW(Addr)) ENTRY_POINT) { /* Ho ho. We are not the program interpreter! We are the program itself! This means someone ran ld.so as a command. Well, that might be convenient to do sometimes. We support it by interpreting the args like this: ld.so PROGRAM ARGS... The first argument is the name of a file containing an ELF executable we will load and run with the following arguments. To simplify life here, PROGRAM is searched for using the normal rules for shared objects, rather than $PATH or anything like that. We just load it and use its entry point; we don't pay attention to its PT_INTERP command (we are the interpreter ourselves). This is an easy way to test a new ld.so before installing it. */ rtld_is_main = true; …… ``` OK,其实对于elf的详细加载过程笔者并没有给出非常详细的讲解,不过有很博主有很好的讲解博客,虽然可能不是使用riscv的,不过也是完全相同的原理,这里给大家推荐这篇博客 https://blog.csdn.net/gatieme/article/details/51628257/ 。笔者虽然是希望将本系列博客编写炒成一个比较完整的学习文档,但是部分关键内容其他博客已给出较好的描述时,这里就只做援引链接。 博客到这里大家一定发现了笔者是一个喜欢阅读源码实现来梳理自己感兴趣的所有细节实现的,因此对于想要真正学好嵌入式相关知识,大家也一定要深入源码阅读,由于笔者的笔力有限,因此博客只能进行一些抛砖引玉的工作。 ### 更新合成文件系统映像脚本 由于我们使用了动态链接的busybox,因此需要将上文提到的解释器以及相关lib库都拷贝到qemu目标机的文件系统内,这些动态库都是交叉编译工具链中自带的,因此直接从中拷贝即可。 ```shell CROSS_COMPILE_DIR=/opt/riscv64--glibc--bleeding-edge-2020.08-1 CROSS_PREFIX=$CROSS_COMPILE_DIR/bin/riscv64-linux # 合成文件系统映像 MAKE_ROOTFS_DIR=$SHELL_FOLDER/output/rootfs TARGET_ROOTFS_DIR=$MAKE_ROOTFS_DIR/rootfs TARGET_BOOTFS_DIR=$MAKE_ROOTFS_DIR/bootfs if [ ! -d "$MAKE_ROOTFS_DIR" ]; then mkdir $MAKE_ROOTFS_DIR fi if [ ! -d "$TARGET_ROOTFS_DIR" ]; then mkdir $TARGET_ROOTFS_DIR fi if [ ! -d "$TARGET_BOOTFS_DIR" ]; then mkdir $TARGET_BOOTFS_DIR fi cd $MAKE_ROOTFS_DIR if [ ! -f "$MAKE_ROOTFS_DIR/rootfs.img" ]; then dd if=/dev/zero of=rootfs.img bs=1M count=1024 pkexec $SHELL_FOLDER/build_rootfs/generate_rootfs.sh $MAKE_ROOTFS_DIR/rootfs.img $SHELL_FOLDER/build_rootfs/sfdisk fi cp $SHELL_FOLDER/output/linux_kernel/Image $TARGET_BOOTFS_DIR/Image cp $SHELL_FOLDER/output/uboot/quard_star_uboot.dtb $TARGET_BOOTFS_DIR/quard_star.dtb $SHELL_FOLDER/u-boot-2021.07/tools/mkimage -A riscv -O linux -T script -C none -a 0 -e 0 -n "Distro Boot Script" -d $SHELL_FOLDER/dts/quard_star_uboot.cmd $TARGET_BOOTFS_DIR/boot.scr cp -r $SHELL_FOLDER/output/busybox/* $TARGET_ROOTFS_DIR/ cp -r $SHELL_FOLDER/target_root_script/* $TARGET_ROOTFS_DIR/ if [ ! -d "$TARGET_ROOTFS_DIR/proc" ]; then mkdir $TARGET_ROOTFS_DIR/proc fi if [ ! -d "$TARGET_ROOTFS_DIR/sys" ]; then mkdir $TARGET_ROOTFS_DIR/sys fi if [ ! -d "$TARGET_ROOTFS_DIR/dev" ]; then mkdir $TARGET_ROOTFS_DIR/dev fi if [ ! -d "$TARGET_ROOTFS_DIR/tmp" ]; then mkdir $TARGET_ROOTFS_DIR/tmp fi if [ ! -d "$TARGET_ROOTFS_DIR/lib" ]; then mkdir $TARGET_ROOTFS_DIR/lib cd $TARGET_ROOTFS_DIR ln -s ./lib ./lib64 cd $MAKE_ROOTFS_DIR fi cp $CROSS_COMPILE_DIR/riscv64-buildroot-linux-gnu/sysroot/lib/* $TARGET_ROOTFS_DIR/lib/ cp $CROSS_COMPILE_DIR/riscv64-buildroot-linux-gnu/sysroot/usr/bin/* $TARGET_ROOTFS_DIR/usr/bin/ pkexec $SHELL_FOLDER/build_rootfs/build.sh $MAKE_ROOTFS_DIR ``` 本节将busybox编译为动态链接库的版本,到这里我们就可以移植更多的用户态应用程序,来完善我们自己的文件系统,甚至创造自己的linux "发行版",下一节我们将对linux多用户管理进行探究并为我们的系统添加多用户管理。 > 本教程的
github仓库:https://github.com/QQxiaoming/quard_star_tutorial
gitee仓库:https://gitee.com/QQxiaoming/quard_star_tutorial
本节所在tag:ch14