基于qemu-riscv从0开始构建嵌入式linux系统ch14. 动态链接——elf文件的加载

busybox动态链接

之前我们配置busybox为静态编译,即是可以看到生成的二进制文件就几M的大小,如果后续我们在系统内添加更多的应用程序均为静态编译,可想而知对磁盘存储消耗很大,在嵌入式设备上是很不划算的,因此我们需要考虑动态链接库,将应用程序和C库分离,这样多个应用程序可以共享一个libc的共享库,不仅仅节省磁盘空间,同时还节约了多个应用程序同时运行时的内存开销。

busybox使用动态库编译选项仅需修改busybox-1.33.1/configs/quard_star_defconfig文件,CONFIG_STATIC配置为not set即可。

# CONFIG_STATIC is not set

重新编译生成/bin/busybox只有960K的大小,使用下面的命令检查下这个文件

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函数即为这个解释器对应的源码,其中还包含了将解释器用作可执行程序本身的一段注释,非常有趣味,摘抄如下,这也解释了这个解释器的为什么还拥有一些工具性质的功能。

 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目标机的文件系统内,这些动态库都是交叉编译工具链中自带的,因此直接从中拷贝即可。

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