调试
-
裁剪脚本
查找当前目录大于指定大小 #!/bin/bash # 遍历当前目录及子目录中的所有文件 find . -type f -exec du -b {} + | while read size file; do # 如果文件大小为50KB(即50 * 1024字节) if [ "$size" -gt 51200 ]; then echo "文件: $file, 大小: $((size / 1024)) KB" fi done 查找当前的库是哪些应用依赖 #!/bin/bash # 当前目录 dst_lib=libblkid.so search_dir="." # 递归查找所有可执行文件 find "$search_dir" | while read -r file; do # 使用 ldd 检查依赖 if riscv32-linux-musl-readelf -d "$file" 2>/dev/null | grep -q "$dst_lib"; then echo "$file 依赖 $dst_lib" fi done -
Cmake构建模版
# 指定 CMake 的最低版本要求。它应该是 CMakeLists.txt 文件中的第一个命令。 cmake_minimum_required(VERSION 3.10) # 定义项目的名称 project(mpp_webrtc) # 要编译的源文件,先收集在SRCS变量中。 file(GLOB SRCS "*.c" "common/*.c") # 编译源文件的头文件路径 include_directories( ${CMAKE_SOURCE_DIR}/src ${CMAKE_SOURCE_DIR}/example/common /lib/xxx/a /lib/xxx/b ) # 需要链接的库路径 link_directories( /lib/A /lib/B /lib/C ) #可执行文件名称,以及依赖的源文件 add_executable(mpp_webrtc ${SRCS}) # 需要链接的库,如libwav.a, libaac.a等。其中可以使用-Wl,--start-group # 和-Wl,--end-group编译参数。 target_link_libraries(mpp_webrtc peer pthread -Wl,--start-group wav aac VE vencoder ... -Wl,--end-group ) -
Linux系统编译生成镜像流程
编译内核 清除内核 命令 ${MAKE} O= mrproper 示例 make CROSS_COMPILE=riscv32-unknown-linux- ARCH=riscv -j16 O=kernel/build KERNEL_SRC=kernel/linux-5.4 INSTALL_MOD_PATH=kernel/staging O= mrproper 配置defconfig 命令 defconfig:${MAKE} defconfig KBUILD_DEFCONFIG=${LICHEE_KERN_DEFCONF_RELATIVE} 示例 make CROSS_COMPILE=nds32le-linux-glibc-v5d/bin/riscv32-unknown-linux- ARCH=riscv -j16 O=kernel/build KERNEL_SRC=kernel/linux-5.4 INSTALL_MOD_PATH=kernel/staging defconfig KBUILD_DEFCONFIG=linux-5.4/xxx_defconfig 编译 ${MAKE} $MAKE_ARGS 示例 MAKE_ARGS+=' INSTALL_HDR_PATH=kernel/build/user_headers headers_install' $COMP_TYPE = Image.bz2/Image.lz4 根据LICHEE_COMPRESS是bzip2,gzip等。这里的rv选择的是Image.gz MAKE_ARGS+$COMP_TYPE make CROSS_COMPILE=nds32le-linux-glibc-v5d/bin/riscv32-unknown-linux- ARCH=riscv -j16 O=kernel/build KERNEL_SRC=kernel/linux-5.4 INSTALL_MOD_PATH=kernel/staging modules all INSTALL_HDR_PATH=kernel/build/user_headers headers_install Image.gz 安装modules 命令 ${MAKE} modules_install 示例 make CROSS_COMPILE=nds32le-linux-glibc-v5d/bin/riscv32-unknown-linux- ARCH=riscv -j16 O=kernel/build KERNEL_SRC=kernel/linux-5.4 INSTALL_MOD_PATH=kernel/staging modules_install 额外编译 单独额外编译的模块 make CROSS_COMPILE=nds32le-linux-glibc-v5d/bin/riscv32-unknown-linux- ARCH=riscv -j16 O=kernel/build KERNEL_SRC=kernel/linux-5.4 INSTALL_MOD_PATH=kernel/staging -C kernel/linux-5.4/xxx/modules/nand M=kernel/linux-5.4/xxx/modules/nand -j1 设备树 cpp \ -Wp,-MD,${dep}/.${outname}.d.pre.tmp \ -nostdinc \ -I ${LICHEE_KERN_DIR}/include \ -I ${LICHEE_KERN_DIR}/bsp/include \ -I ${die_dtsi_path} \ -I ${chip_dtsi_path} \ -undef \ -D__DTS__ \ -x assembler-with-cpp \ -o ${dep}/.${outname}.dts.tmp \ ${dtsfile} $DTC \ -O dtb \ -o ${outpath}/${outname} \ -W no-unit_address_vs_reg \ -W no-unit_address_format \ -W no-unique_unit_address \ -W no-graph_child_address \ -W no-simple_bus_reg \ -b 0 \ -@ \ -i ${LICHEE_CHIP_CONFIG_DIR}/configs/default/${LICHEE_KERN_VER} \ -d ${dep}/.${outname}.d.dtc.tmp ${dep}/.${outname}.dts.tmp 示例 cpp -Wp,-MD,kernel/staging/dts_dep/.xxx.dtb.d.pre.tmp -nostdinc -I kernel/linux-5.4/include -I kernel/linux-5.4/bsp/include -I bsp/configs/linux-5.4 -I configs/default -undef -D__DTS__ -x assembler-with-cpp -o kernel/staging/dts_dep/.xxx.dtb.dts.tmp configs/perf2/board.dts scripts/dtc/dtc -O dtb -o kernel/staging/xxx.dtb -W no-unit_address_vs_reg -W no-unit_address_format -W no-unique_unit_address -W no-graph_child_address -W no-simple_bus_reg -b 0 -@ -i configs/default/linux-5.4 -d kernel/staging/dts_dep/.xxx.dtb.d.dtc.tmp kernel/staging/dts_dep/.xxx.dtb.dts.tmp 将内核与dtb打包生成img ${MKBOOTIMG} --kernel ${BIMAGE} \ $(check_whether_use_ramdisk && echo "--ramdisk $RAMDISK") \ --board ${CHIP}_${LICHEE_ARCH} \ --base ${BASE} \ --kernel_offset ${KERNEL_OFFSET} \ $(check_whether_use_ramdisk && echo "--ramdisk_offset ${RAMDISK_OFFSET}") \ --dtb ${DTB} \ --dtb_offset ${DTB_OFFSET} \ --header_version 2 \ -o $STAGING_DIR/${IMAGE_NAME} 示例 tools/pack/pctools/linux/android/mkbootimg --kernel kernel/staging/Image.gz --board xxx300i_riscv32 --base 0x80000000 --kernel_offset 0x0 --dtb kernel/staging/sunxi.dtb --dtb_offset 10485760 --header_version 2 -o kernel/staging/boot.img 根文件系统 制作根文件系统 mksquashfs4 [rootfs-dir] [img-name] -noappend:不将squashfs选项传递给内核 -root-owned:设置root目录为root所有 -comp xz:使用xz压缩算法 -b 256k:设置块大小为256KB -p '/dev d 755 0 0' 和 -p '/dev/console c 600 0 0 5 1':设置文件系统的权限和所有权。 -processors 1:使用一个处理器进行压缩。 使用dd of生成rootfs.img dd if=root.squashfs of=openwrt/rootfs.img bs=128k conv=sync 编译uboot 略 打包整个镜像 命令: dragonxx imagexx.cfg partitionxx.fex 使用打包工具dragonxx进行打包,其中image.cfg和partition.fex中的内容为要打包的文件。 -
内存地址对齐
内存地址对齐是在内存中的数据(具体为变量的地址、内存块的地址)按照指定地址长度对齐,包含了基本的变量数据对齐和结构体数据对齐。 为什么需要内存对齐? 可以提高CPU和内存交互的效率,比如一个32位的系统,CPU读取内存,硬件设计上只支持4字节或4字节的倍数对齐进行地址访问,CPU在每次访问内存时,一个周期可以访问4字节,如果要访问的数据是4字节对齐的地址,CPU一次就可以把数据访问完毕;如果访问的数据不是4字节对齐,cpu就需要分两次才能把4字节数据访问完成。 什么时候完成的内存对齐? 为了与具体的arch设计提高运行效率,编译器会自动完成内存对齐操作,在编译程序时,对应基本的数据类型,如int,char,short,float等,会按照其数据类的大小进行地址对齐,这样对齐方式分配的存储地址,CPU一次就可以访问完毕。这样即使会造成内存的空洞,浪费一些内存单位,但是对于硬件设计和运行效率可以极大的简化和提升。除了了基本的数据类型外,包括一些复合数据类型如结构体也要满足对齐要求。 数据类型对齐 32位系统,编译对齐规则 char: 1字节对齐 short:2字节对齐 init:4字节对齐 float:4字节对齐 double:8字节对齐 指针:4字节对齐 64为系统,编译对齐规则 指针:8字节对齐 结构体类型对齐 结构体数据内存对齐,具体是结构体内的各个数据对齐。结构体作为一种复合数据类型,编译器在分配存储空间时,不仅要考虑结构体内各个基本成员的地址对齐,还要考虑结构体整体的对齐。 成员变量对齐:按照各自成员变量类型对齐,如32位系统 int 为4字节对齐 结构体整体对齐:成员变量最大对齐字节或其整数倍对齐,在尾部补齐。如最大成员对齐是4,那么就需要是4的整数倍。 结构体的大小为什么需要按照最大成员变量填充对齐? 当数据按照一定的对齐规则进行排列时,CPU可以更高效地访问这些数据。这是因为CPU在读取内存数据时,通常是按照固定的大小块来读取的。例如,在64位系统中,CPU从内存的0-7位置开始读取,然后是8-15,16-23以此类推。如果结构体的成员没有按照最大成员变量的大小进行对齐,那么在连续的结构体数据(如结构体数组)中,变量的位置可能不再合理,导致读取效率下降。例如,如果一个int64类型的变量原本应该从0-7位置开始连续读取,但如果前面的成员没有对齐,下一个结构体的起始位置就可能不对,导致需要多次读取和拼接数据,进而影响性能1。 示例1 struct data { char a; //1字节对齐, 实际占用4字节,由于后面的c变量 int c ; //4字节对齐, 因此a后面要填补3字节数据,相当于a占用了4字节 short b ; //2字节对齐,但是后面要补2字节,因为3个成员变量占了4+4+2=10, //成员中最大的变量长度是4字节对齐,因此整个数据结构要是4个整数倍 //因此后面要补2字节。 }; sizeof(struct data) = 12 示例2 struct data{ char a; //1字节对齐,由于后面的short是2字节对齐,因此后面补了1字节 short b ; //2字节对齐 int c ; //4字节对齐 }; sizeof(struct data) = 8 从上面的示例可知,结构体中的变量排列会影响实际的空间大小,因此在定义结构体时,尽量的从小变量到大变量排列,这样节省内存。 aligned与packed GNU C通过 atttribute 来声明 aligned 和 packed 属性,指定一个变量或类型的对齐方式。这两个属性用来告诉编译器:在给变量分配存储空间时,要按指定的地址对齐方式给变量分配地址。 aligned 如果你想定义一个变量,在内存中以8字节地址对齐,就可以这样定义。 int a __attribute__((aligned(8)); 通过 aligned 属性,我们可以直接显式指定变量 a 在内存中的地址对齐方式。aligned 有一个参数,表示要按几字节对齐,使用时要注意地址对齐的字节数必须是2的幂次方,否则编译就会出错。 struct data{ char a; //4 short b __attribute__((aligned(4))); //4 int c ; //4 }; sizeof(struct data) = 12 上述的示例表示struct data成员变量中的short b要按4字节强行对齐。 struct data{ char a; //4 short b; //4 int c ; //8 }__attribute__((aligned(16))); sizeof(struct data) = 16 可以显示指定了整个结构体的对齐方式,如上示例显式指定结构体整体以16字节对齐,所以编译器就会在这个结构体的末尾填充8个字节以满足16字节对齐的要求,导致结构体的总长度变为16字节。 packed aligned属性一般用来增大变量的地址对齐,元素之间因为地址对齐会造成一定的内存空洞。而 packed属性则与之相反,用来减少地址对齐,用来指定变量或类型使用最可能小的地址对齐方式。 struct data{ char a; //1 short b __attribute__((packed)); //2 int c __attribute__((packed)); //4 }; sizeof(strut data) =7 使用了packed属性,告诉编译器使用最小的对齐方式。 aligned与packed一起使用 struct data{ char a; //1 short b ; //2 int c ; //5 }__attribute__((packed,aligned(8))); sizeof(struct data) = 8 aligned 和 packed 一起使用,即对一个变量或类型同时使用 aligned 和 packed 属性声明。这样做的好处是,既避免了结构体内因地址对齐产生的内存空洞,又指定了整个结构体的对齐方式。 -
kasan原理
kasan是什么?(基本原理) kasan是用于内存检测的工具,能够检测内存以下异常。 buffer-overflow in heap,stack and globals use-after-free uninitialized-memory-read user-memory-access 如若要支持kasan需要多划分1/8的内存用于内存检测的管理,如上图所示。分配的内存地址都是按字节对齐的,这样做为了提高cpu的效率,本章默认是8字节对齐。 如何实现分配内存地址对齐了?首先在初始化内存时,会在前后舍弃一定的字节数,保证整个地址空间起始和结束地址是8字节对齐的,在分配内存时,分配的大小也按照字节对齐来分配,如分配3字节时,会自动补齐5字节,即实际分配到的空间是8字节,只是5字节对申请者不可见,这样就可以实现分配的内存地址是按8字节对齐的了。 因为内存分配都是按照8字节对齐的,而用户申请的空间并不是按照8字节来,正如上所说请求分配了3字节,实际分配到8字节,剩余5字节对用户不可见,当用户写到了该5字节的内存也是不合法,所以了对应内存的状态一共有以下9种,影子区1字节就可以记录这9种状态。 0字节可访问:说明这8字节内存都不可访问,影子区记录为-1。 前K字节可以访问:K值记录在影子区,如当K为2,表示0~1字节可访问。K取值为1~7。 全都可以访问:影子区写0。 从上可知,对访问内存合法性的可以根据影子区的值来判断。判断公式如下: (1) 对于分配的内存是8字节情况 *a = ... char *shadow = ( a>> 3) + offset; if (*shadow) report_error(a); *a = .... (2)对于分配内存小于8字节情况 *a = ... char *shadow = (a >> 3) + Offset; if (*shadow && *shadow < (a & 7) + N) report_error(a); *a = ... 示例如上图,依旧是用户申请了3个字节空间,shadow = 3。当客户从第2个空间访问2字节时,但是shadow < 1 + 2,条件不成立,因此合法。如果从2个空间访问3字节时,*shadow < 1 + 3,条件成立,即为非法。 通过上面的方法可以解决use-after-free的问题,但是还不能解决buffer-overflow的问题。解决buffer-overflow,就是在分配内存的前后填充redzone,这段填区也将进行影子区映射,那么当访问越界时就会检查到影子区的内存,提示非法,实现原理参考下图所示。 简单小结一下kasan的基本原理: 内存分配地址和空间都是8n字节对齐的,本章默认8字节对齐。 总内存被分为n份,每份大小为8字节内存,每份用1字节内存标识,形成一一映射关系,标识区称为影子区域。 当请求字节不是8的整数倍时,如分配了3字节,实际分配的也是8字节,仅是另外5字节对分配者非法。因此8字节内存的访问权限一共有9种状态,其当前的状态在分配内存时记录到映射的影子区域中。 在分配内存时,为了支持越界访问操作,会在请求内存的前后填充red zone,red zone一般前后各8字节,也有对应的影子区域,该值填充0xfe。 编译器会在指令访问(读写)内存时,进行自动插桩代码,检查要访问的目标内存对应的影子区,判断是否合法,当检查非法时就会报错。 kasan何时设置影子区 (1)malloc的时候会分配2* redsize + wantsize,并对内存进行映射填充值。 redzone映射区域填充0xfe,读写区若8字节对齐则全填充0x0;若不对齐,余下不足8字节的实际值填充到最后1字节的影子区域。内存实际分配的还是8字节对齐的,只是对应应用来说,可访问的不足8字节,剩下的当redzone。 (2)free的时候对齐对应的影子区填充0xff。 申请内存 malloc _malloc_r pvPortMalloc __internal_malloc kasan_malloc_small //addr = 0xc178938, size = 16 kasan_unpoison_shadow kasan_poison_shadow(address, size, 0); //分配空间对应的地址设置为0 shadow_start = kasan_mem_to_shadow(address); shadow_end = kasan_mem_to_shadow(address + size); memset(shadow_start, value, shadow_end - shadow_start); kasan_poison_shadow(left_rz, sizeof(debug_magic.redzone), KASAN_KMALLOC_REDZONE); //设置内存左redzone kasan_poison_shadow(right_rz, sizeof(debug_magic.redzone), KASAN_KMALLOC_REDZONE); //设置内存右redzone //实际分配内存左右预留一个空间,然后这预留的空间对应的shadow区域也要填充值,假设分配了16字节,实际分配了8+16+8=32字节空间。然后对应的shadow区域是1+2+1字节大小。在heap4中malloc函数中,会多分配16字节长度,如下。 _internal_malloc(size_t xWanteSize) xWantSize +=2*xSlabDegbuMgicSize; 申请内存,前后的内存redzone对应的shadow区域写0xFE,表示表示红区。实际内存对应的shadow区域写0,表示内存已经被申请,表示可写。 释放内存 free __internal_free kasan_free_large kasan_poison_shadow(page, size, KASAN_FREE_PAGE); //设置0xff到对应的shadow区域,表示该内存已经释放。不能再写了。 kasan如何检查内存合法性 初始化 初始化部分kasan_early_init/kasan_init/do_ctors kasan_early_init kasan_shadow_init_nommu for() *(xxx) = 0 将所有的shadaow memory设置为0 kasan_init kasan_init_nommu kasan_init_report kasan_flags |= KASAN_REPORT_INIT_FLAG; //设置kasan标志位 kasan_enable_report kasan_flags |= KASAN_REPORT_SHOW_FLAG; rt_malloc_small_sethook(rt_malloc_small_func_hook); rt_malloc_small_hook = hook; //设置回调函数,heap4 __internal_malloc的时候调用 rt_free_small_sethook(rt_free_small_func_hook); rt_free_small_hook = hook; //设置回调函数,heap4 __internal_free的时候调用 影子区映射 adress:0+CONFIG_ARCH_MEM_LENGTH ------------------- |XXXXXXXXXXXXXXXXXX| adress:0+x -------------------- KASAN_SHADOW_START | | | | | | adress:0 ------------------- CONFIG_ARCH_START_ADDRESS #define KASAN_SHADOW_SIZE (CONFIG_ARCH_MEM_LENGTH>>3) // 相当于除以8 #define KASAN_SHADOW_START (CONFIG_ARCH_START_ADDRESS + CONFIG_ARCH_MEM_LENGTH - KASAN_SHADOW_SIZE) #define KASAN_SHADOW_OFFSET (KASAN_SHADOW_START - (CONFIG_ARCH_START_ADDRESS>>3)) static inline void *kasan_mem_to_shadow(const void *addr) { return (void *)((unsigned long)addr >> KASAN_SHADOW_SCALE_SHIFT) + KASAN_SHADOW_OFFSET; 实际= KASAN_SHADOW_START + addr >> KASAN_SHADOW_SCALE_SHIFT - CONFIG_ARCH_START_ADDRESS>>3 addr肯定比CONFIG_ARCH_START_ADDRESS要大,也就是说,shadow的开始地址要先减掉 arch_start之前的地址,这样避免浪费空间。 } 通过分配的地址查找对应shadow地址,内存分配是8字节对齐分配,如分配3字节,实际也是8字节空间。 __asan_load/store定义 #define DEFINE_ASAN_LOAD_STORE(size) \\ void __asan_load##size(unsigned long addr) \\ { \\ KASAN_CHECK_ADDR_FILTER(addr);\\ check_memory_region_inline(addr, size, false, _RET_IP_);\\ } \\ __alias(__asan_load##size) \\ void __asan_load##size##_noabort(unsigned long); \\ void __asan_store##size(unsigned long addr) \\ { \\ KASAN_CHECK_ADDR_FILTER(addr);\\ check_memory_region_inline(addr, size, true, _RET_IP_); \\ } \\ __alias(__asan_store##size) \\ void __asan_store##size##_noabort(unsigned long); \\ DEFINE_ASAN_LOAD_STORE(1); DEFINE_ASAN_LOAD_STORE(2); DEFINE_ASAN_LOAD_STORE(4); DEFINE_ASAN_LOAD_STORE(8); DEFINE_ASAN_LOAD_STORE(16); 上面的函数展开后得到 void __asan_load1(unsigned long addr) { KASAN_CHECK_ADDR_FILTER(addr); check_memory_region_inline(addr, 1, false, _RET_IP_); } __alias(__asan_load1) void __asan_load1_noabort(unsigned long); void __asan_store1(unsigned long addr) { KASAN_CHECK_ADDR_FILTER(addr); check_memory_region_inline(addr, 1, true, _RET_IP_); } __alias(__asan_store1) void __asan_store1_noabort(unsigned long); void __asan_load2(unsigned long addr) { KASAN_CHECK_ADDR_FILTER(addr); check_memory_region_inline(addr, 2, false, _RET_IP_); } __alias(__asan_load2) void __asan_load2_noabort(unsigned long); void __asan_store2(unsigned long addr) { KASAN_CHECK_ADDR_FILTER(addr); check_memory_region_inline(addr, 2, true, _RET_IP_); } __alias(__asan_store2) void __asan_store2_noabort(unsigned long); void __asan_load4(unsigned long addr) { KASAN_CHECK_ADDR_FILTER(addr); check_memory_region_inline(addr, 4, false, _RET_IP_); } __alias(__asan_load4) void __asan_load4_noabort(unsigned long); void __asan_store4(unsigned long addr) { KASAN_CHECK_ADDR_FILTER(addr); check_memory_region_inline(addr, 4, true, _RET_IP_); } __alias(__asan_store4) void __asan_store4_noabort(unsigned long); void __asan_load8(unsigned long addr) { KASAN_CHECK_ADDR_FILTER(addr); check_memory_region_inline(addr, 8, false, _RET_IP_); } __alias(__asan_load8) void __asan_load8_noabort(unsigned long); void __asan_store8(unsigned long addr) { KASAN_CHECK_ADDR_FILTER(addr); check_memory_region_inline(addr, 8, true, _RET_IP_); } __alias(__asan_store8) void __asan_store8_noabort(unsigned long); void __asan_load16(unsigned long addr) { KASAN_CHECK_ADDR_FILTER(addr); check_memory_region_inline(addr, 16, false, _RET_IP_); } __alias(__asan_load16) void __asan_load16_noabort(unsigned long); void __asan_store16(unsigned long addr) { KASAN_CHECK_ADDR_FILTER(addr); check_memory_region_inline(addr, 16, true, _RET_IP_); } __alias(__asan_store16) void __asan_store16_noabort(unsigned long); __alias是给函数起一个别名,从上可知,定义了如下几个函数的实现。 __asan_loadx __asan_loadx __asan_loadx_noabort 其中x为1/2/4/8/16 __asan_loadx { KASAN_CHECK_ADDR_FILTER(addr); unsigned long t = (unsigned long)addr; if ((t < CONFIG_ARCH_START_ADDRESS) || (t > (CONFIG_ARCH_START_ADDRESS + CONFIG_ARCH_MEM_LENGTH))) return ; 上面代码的意思就是,只对特定范围的地址做检测,不再该范围的不检查,比如XIP的代码,没必要检测。 check_memory_region_inline(addr, X, true, _RET_IP_); } 检测 1/2/4/8/16字节 以4字节的来做示例分析,其他的类似。 static __always_inline bool memory_is_poisoned_4(unsigned long addr) { uint8_t *shadow_addr = (uint8_t *)kasan_mem_to_shadow((void *)addr); //获取影子区的值 //如果影子区的值不等于继续检查,可能是不对齐的情况,比如分配了3字节,有5字节属于redzone if (unlikely(*shadow_addr)) { //判断第4个字节是否可访问,如果不可访问说明非法。 if (memory_is_poisoned_1(addr + 3)) { return true; } /* * If single shadow byte covers 4-byte access, we don't * need to do anything more. Otherwise, test the first * shadow byte. */ //如果第4字节为合法,满足*shadow >= (addr & 7) + N 则合法,否则非法。 if (likely(((addr + 3) & KASAN_SHADOW_MASK) >= 3)) { return false; } return unlikely(*(uint8_t *)shadow_addr); } return false; } static __always_inline bool memory_is_poisoned_1(unsigned long addr) { int8_t shadow_value = *(int8_t *)kasan_mem_to_shadow((void *)addr); //获取影子区的值 //如果影子区的值不等于0,即进一步判断 if (unlikely(shadow_value)) { //判断原理与前面算法一致: *shadow < (addr & 7) + N为非法,否则为合法。 int8_t last_accessible_byte = addr & KASAN_SHADOW_MASK; return unlikely(last_accessible_byte >= shadow_value); } return false; } N字节 * 判断内存对应的影子内存中,起始和结束shadow值是否都为 0 结束地址是对应地址长度的影子地址的下一个影子地址 */ ret = memory_is_zero(kasan_mem_to_shadow((void *)addr), kasan_mem_to_shadow((void *)addr + size - 1) + 1); if (ret) { unsigned long last_byte = addr + size - 1; int8_t *last_shadow = (int8_t *)kasan_mem_to_shadow((void *)last_byte); /*如果ret!=last_shadow 那么在连续的内存检测过程中,就已经检测到了 一个非法权限,即有问题 */ /* ||后面的检测方案和 memory_is_poisoned_1 实现是相同的 */ if (unlikely(ret != (unsigned long)last_shadow || ((long)(last_byte & KASAN_SHADOW_MASK) >= *last_shadow))) { return true; } return false; } kasan全局变量实现 (1)当使能使能了asan-globals=1参数后,编译器会自动为每个全局变量填充red_zone。 redzone的大小为63-(size-1)%32,为了保证与32字节对齐。 (2)填充redzone后,编译器会为每个变量自动生成一个xxx_name的构造函数,该构造函数会调用__asan_register_globals进行注册,将全局变量的内存区域与影子区域建立映射并填充影子区的值,填充方法与heap一致。 以上操作全是编译器自动行为,应用层最终调用do_ctors回调调用构造函数即可。访问时判断内存合法性与前面算法一致。 全局变量影子区初始化 do_ctors *fn = &_ctors_start for(;fn< _ctors_end;fn++) (*fn)(); 调用_ctors_start和_ctors_end直接的回调函数,用于初始化全局变量的shadow 上面的地址在链接脚本的 #if (defined(CONFIG_KASAN)) /* .ctors */ . = ALIGN(8); __ctors_start__ = .; KEEP(*(.ctors)) KEEP(*(SORT(.init_array.*))) KEEP(*(.init_array)) __ctors_end__ = .; #endif 先调用do_ctors,调用每个工具链为每个变量生成的构造函数,构造函数调用__asan_register_globals。 static void register_global(struct kasan_global *global) { KASAN_CHECK_ADDR_FILTER(global->beg); size_t aligned_size = round_up(global->size, KASAN_SHADOW_SCALE_SIZE); kasan_unpoison_shadow(global->beg, global->size); kasan_poison_shadow(global->beg + aligned_size, global->size_with_redzone - aligned_size, KASAN_GLOBAL_REDZONE); } void __asan_register_globals(struct kasan_global *globals, size_t size) { int i; for (i = 0; i < size; i++) { register_global(&globals[i]); } } kasan局部变量实现 左边填充32字节,右边填充63-(size-1)% 32 字节。 shadow区域计算公式: shadow = (addr >>3) +koffset。 koffset由编译参数-fasan-shadow-offset=xxx指定。 shadow区域 KSAN_STACK_可读写区 0x00 KASAN_STACK_LEFT 0xF1 KASAN_STACK_MID 0xF2 --只有一个变量且是32字节对齐的,不会填充 KASAN_STACK_RIGHT 0xF3 KASAN_STACK_PARTAL 0xF4 示例: (int *) (0xc18a6d0 >> 3 + 0xaf00000) = KASAN_STACK_LEFT 以上操作全是编译器自动行为,用户只需要使能编译参数即可。 void foo() { char rz1[32]; // 32-byte aligned char a[328]; char rz2[24]; char rz3[32]; int *shadow = (&rz1 >> 3) + kOffset; //计算变量映射shadow区的起始地址 shadow[0] = 0xffffffff; // poison rz1 shadow[11] = 0xffffff00; // poison rz2 shadow[12] = 0xffffffff; // poison rz3 <------------- CODE -------------> shadow[0] = shadow[11] = shadow[12] = 0; } 官方解释算法如上,当工具链接加上--param asan-stack=1,局部变量redzone填充以及shadow映射填充值全工具链自动完成。对应局部变量需要左右都需要填充redzone,所以可以划分为以下4个部分。 left redzone: 32字节。 变量长度:实际的变量长度 mid redzone:该区域有两个用途①是补齐变量长度,让其32字节对齐。②是当存在多个变量时,用于中间区域的隔离。当变量长度不是32字节对齐的,填充长度是变量长度+mid redzone长度能够32字节对齐。mid redzone= 63-(size-1)%32 -32。当存在多个变量中,变量与变量之间至少要有32字节用于隔离。 right redzone:32字节。 对于上面4个内存区域,也可以将shadow映射区划分为4个部分 left redzone shadow: 填充0xF1 变量长度:都填充0x0; mid redzone: 填充0xF2 right redzone: 填充0xF3; 注意: 当只有一个变量且该变量长度是32字节对齐,则没有mid redzone区域。 下面来段实验,定义了这么一段代码。 void test_asan_xxx(void) { char a[328] = {1}; memset(a,0x0,328); } 下面是反汇编的结果 void test_asan_xxx(void) { c0efd34: 7121 addi sp,sp,-448 //开辟448空间内存 c0efd36: f1f1f7b7 lui a5,0xf1f1f //a5=0xf1f1f<<12 c0efd3a: fb22 sd s0,432(sp) //将s0存储到sp+432位置 c0efd3c: f726 sd s1,424(sp) //将s1存储到sp+424位置 c0efd3e: f34a sd s2,416(sp) //将s2存储到sp+416位置 c0efd40: ff06 sd ra,440(sp) //将ra存储到sp+440位置 //开辟一个栈空间为448字节,并且将a5=0xf1f1f000 //将s0,s1,s2,ra压入栈中。 c0efd42: 840a mv s0,sp //s0指向sp,即填充32字节的地址 c0efd44: 1f17879b addiw a5,a5,497 //a5 = 0xf1f1f1f1 c0efd48: 800d srli s0,s0,0x3 //s0 = s0 >> 3 c0efd4a: 0af00937 lui s2,0xaf00 //s2 = 0xaf00000,即koffset c0efd4e: 4089578b srw a5,s2,s0,0 //*(s0>>0 + s2)=a5 //写KASAN_STACK_LEFT //对变量的前32字节 shadow映射区填充0xf1f1f1f1(8字节映射1字节) //int *shadow = (sp >> 3) + koffset; //shadow区域是按照4字节对齐存储,对应的变量就是32字节。 // shadow[0] = 0xf1f1f1f1 c0efd52: f2f2f7b7 lui a5,0xf2f2f //a5 = 0xf2f2f000 c0efd56: 2007879b addiw a5,a5,512 //a5 = 0xf2f2f200 c0efd5a: 008904b3 add s1,s2,s0 //s1 = s2 +s0,即32字节映射区地址 c0efd5e: d4dc sw a5,44(s1) //从32字节映射区偏移44字节写入0xf2f2f200 //写KASAN_STACK_MID(包含数据区对齐32字节后剩余部分的shadow) //因为数组长度是328字节,对应的shadow是41字节,shodow是每4字节存储,所以剩余1字节 //就随着REDZONE映射shadow区域一起填充,余下的3字节就填充f2。 //所以STACK_MID就是偏移44的位置。 c0efd60: f3f3f7b7 lui a5,0xf3f3f c0efd64: 3f37879b addiw a5,a5,1011 c0efd68: d89c sw a5,48(s1) //0xc73150a地址内容=0xf3f3f3f3 //写KASAN_STACK_RIGHT //最后余下32字节redzone,对应4字节填充f3 char a[328] = {1}; memset(a,0x0,328); c0efd6a: 1008 addi a0,sp,32 c0efd6c: 14800613 li a2,328 c0efd70: 4581 li a1,0 c0efd72: acfc20ef jal ra,c0b2840 <memset> { c0efd76: 4089500b srw zero,s2,s0,0 } c0efd7a: 70fa ld ra,440(sp) c0efd7c: 745a ld s0,432(sp) { c0efd7e: 0204b623 sd zero,44(s1) } c0efd82: 791a ld s2,416(sp) c0efd84: 74ba ld s1,424(sp) c0efd86: 6139 addi sp,sp,448 c0efd88: 8082 ret 当定义两个变量时示例 void test_asan_xxx(void) { char a[328] = {1}; char b[144] = {1}; memset(a,0x0,328); memset(b,0x0,128); } left值依旧是32字节没有变化,在a数组和b数组中间填充了48字节的mid redzone,其中mid[16]字节是为了对齐b数组让其32字节对齐的,而mid[32]是用于隔离a[328]和b[144]数组的。 -
静态链接与动态链接
经过[ELF格式解析](http://www.laumy.tech/1085.html \\"ELF格式解析\\")章节描述,对ELF文件有了一定的了解,本章节继续来探讨处于链接阶段的:静态链接与动态链接。 静态链接 以下是本小节的实验代码。 ----> a.c extern int shared; int s; int main() { int a = 100; swap(&a, &shared); return 0; } ----> b.c int shared = 1; int z; void swap(int *a, int *b) { *a ^= *b ^= *a ^= *b; } 编译生产a.o和b.o命令:gcc -c a.c b.c 链接生成ab:ld a.o b.o -e main -o ab,-e main表示将main函数作为程序入口,因为ld链接器默认使用c库_start作为入口。 静态链接是一种将可执行文件与其依赖的库文件(也称为静态库,实际上是多个.o的打包)打包成一个单独的、独立的二进制文件的技术。在静态链接中,编译器将被引用的库文件的代码和数据复制到最终的可执行文件中,因此这些库的函数和变量不再需要单独的共享库文件。静态链接适用于需要独立部署的应用程序或小规模的程序,对于大型程序和库,使用动态链接更为合适。 以上是静态链接的概念,来自Chatgpt。 空间与地址分配 这里的空间和地址有两个含义:一个是输出的可执行文件空间,另一个是装载运行后的虚拟地址空间。对于有实际数据的段,如.text和.data来说,它们在可执行文件中和虚拟地址中都要分配空间。而对于.bss,不需要分配可执行文件空间,因为他在文件中并没有内容(不占用文件大小),但是需要分配虚拟地址空间。 需要注意的是:对于使用MMU的OS来说,使用的是虚拟地址,而如果没有MMU那么直接使用的就是物理地址空间。 上图中a.o和b.o中的.text,.data,.bss依次进行了合并,.bss不占用实际空间,只是方便描述。在空间与地址分配过程中,扫描所有的输入目标文件,获取各个目标文件中的各个section的长度、属性、位置,然后将相同属性的section合并,然后计算出输出文件中各个段合并后的长度和位置。 上图是使用objdump -h 分别查看a.o,b.o以及输出的ab链接前后地址分配情况。VMA就是虚拟地址空间(如果没有使用MMU,那么代表的就是时间的物理地址空间),对应虚拟地址空间的分配也主要关注VMA这部分,因为VMA代表的是程序被加载到内核时间运行的地址范围。可以看出a.o和b.o中各个sections的VMA都是为0,因为a.o和b.o没有经过链接器进行链接,所以还没有分配地址空间。当使用链接器ld将a.o和b.o进行链接生成ab后,可以看出各个section都分配了VMA地址。需要注意的是上图中的size是VMA的size,.text和.data实际占用大小与VMA大小相等,而.bss实际占用大小为0,VMA却不是。 总结:链接器的第一步是输出一个可执行文件空间(合并后的section范围)和装载运行后的虚拟地址空间(VMA范围),简称为空间与地址分配。 符号解析与重定位 先来思考一个问题,在示例代码中a.c调用了外部swap函数,而swap在b.c中使用gcc -c a.c不会编译报错?之所以不报错,是因为编译器当遇到外部符号是,先占个\\\\"坑\\\\",等后续链接阶段再来填。 如上图,通过objdump -d a.o和objdump -d ab可以看出,在a.o中执行swap函数时,偏移值是0(e8是callq操作码),而到ab中偏移值已经修正为(0x07)。 因此在单独编译每个目标文件(模块)时,遇到外部符号会暂时使用地址0(或其他值)占个\\\\"坑\\\\"代替,把真正地址计算工作留给链接器处理,在上一小节中链接器完成地址和空间分配之后已经确定了所有符号的虚拟地址(也就是说swap函数地址已确定),那么链接器就可以根据符号的地址对每个需要重定位的指令进行修正(修正的是地址不是操作码)。 在链接阶段需要重定位的指令做修正,那么链接器是如何判断哪些指令地址需要修正了?在ELF文件中,专门一个重定位表(.rel.xxx)专门用来保存需要修正的地址。如代码段.text需要被重定位,在会有一个.rel.text保存重定位信息,数据段.data需要被重定位,则会有一个.rel.data来保存重定位信息,可以使用objdump -r来查询目标文件重定位表。 如上图可知a.o中.text和.eh_frame 这两个sections有需要被重定位,b.o中有.eh_frame一处需要被重定位,ab是没有的,在a.o中.text OFFSET表明了修正的偏移位置。 链接器根据重定位表知道哪些位置需要被修正,就会去查找有所有输入目标文件的符号表组成的全局符号表(这个过程就是符号解析),找到对应的符号后获取地址进行修正。 可以使用readelf -s xx来查询符号表。 小结 静态链接可以让各程序文件(模块)单独编译生产目标文件,最终通过链接器合并起来,链接器会将各个模块需要外部代码复制进来打包成一个可执行文件。链接器在链接阶段主要使用了空间和地址分配、符号解析和重定位方法,前者确定了最终可执行文件空间和虚拟地址,后者使得程序在编译的时候可以独立编译最终在链接阶段整合。符号解析和重定位是静态链接的核心内容,主要核心思想是使用.rel.xxx重定位表记录要重定向的地址,然后通过解析全局符号搜索地址完成重定位地址的修正。 动态链接 以下是本小节的实验代码。 ----> a.c extern int shared; int s; int main() { int a = 100; swap(&a, &shared); while(1) sleep(10); return 0; } ----> b.c int shared = 1; int z; void swap(int *a, int *b) { *a ^= *b ^= *a ^= *b; } 编译生成libt.so:gcc -fPIC -shared -o libt.so b.c 链接成a.out: gcc a.c ./libt.so 静态链接和动态链接是两种不同的链接方式,下面对比下静态链接的优缺点。 静态链接(Static Linking):静态链接在编译时进行,将所有的函数和库代码复制到最终可执行文件中。执行程序时,所有的代码和依赖项都包含在一个单独的可执行文件中。 优点:可执行文件独立,不需要依赖外部库文件。简单、快速,没有额外的运行时开销。 缺点:造成代码冗余,多个应用程序使用相同的库会导致重复的代码。更新库需要重新编译整个程序。占用更多的磁盘空间。 动态链接(Dynamic Linking):动态链接在运行时进行,程序在执行前并不包含完整的依赖项。执行程序时,运行时链接器将程序与所需的共享库动态加载到内存中。共享库可以被多个程序共享,减少了代码冗余。 优点:节省内存和磁盘空间,共享库在内存中只需加载一次。更新库时,不需要重新编译整个程序,只需替换共享库文件即可。支持库版本管理和动态升级。 缺点:需要依赖外部的共享库文件。运行时链接会增加性能开销,包括加载和解析共享库的时间。 为什么要有动态链接? 节省资源:动态链接使多个程序可以共享同一个库,减少了代码冗余和内存占用。 灵活性和可扩展性:通过动态链接,可以更方便地更新和升级共享库,而不需要重新编译整个程序。 模块化:动态链接使得程序可以使用第三方库来提供特定功能,促进了模块化开发和代码复用。 共享库的维护:多个应用程序使用同一个共享库,可以更容易地进行库的维护和更新,减少了开发和测试的工作量。 总而言之,动态链接在节省资源和提高灵活性方面具有优势,但可能会带来一些运行时开销。选择使用哪种链接方式取决于具体的应用场景和需求。 地址无关代码 地址无关代码在编译时地址是从0开始,不是实际运行的地址,只有在装载的时候由装载器确定地址。 地址无关代码可以做到程序模块中共享指令部分在装载时不需要因为装载地址的改变而改变,实现的基本思想就是把指令中哪些需要被修改的部分分离出来,跟数据部分放在一起,这样指令部分就可以保持不变,对于共享模块的地址引用可以按照模块划分为两类:模块内部引用和模块外部引用,按照引用方式可以分为指令引用(函数)和数据引用(变量),所以可以分为以下四种: 模块内部函数调用、跳转 模块内部数据访问、如全局变量、静态变量。 模块外部函数调用、跳转 模块外部数据访问、如定义其他模块中的全局变量 模块内部调用,调用的函数与调用者都在同一个模块中,他们之间的相对位置是固定的,根据相对地址的调用就可以实现,因此这种调用不需要重定位。 模块间的调用,模块间的数据访问目标地址需要等到装载时才能确定,所以在装载运行时才能找到目标地址,那么只能把跟地址相关的部分放到数据段里面,这些其他模块的全局变量地址跟模块装载地址有关。这个数据段里面存储的是指向全局变量的地址,称为全局偏移表(GOT)。为引用外部符号生成一个全局GOT表,这个GOT在编译期间已经生成,只是这期间的内容不是指向正确的地址,因为目标地址还不知道,等到运行装载时,目标地址确定了,由装载器进行更新这个表,这样就可以通过GOT表访问到外部符号。 使用静态链接时,调用外部模块的符号时可以通过链接器进行符号解析并重定位修正地址,而要使用动态链接时,因为在链接时外部符号的地址是不确定的,不清楚加载到那个位置,因为可执行程序和动态库是独立的不是一个整体,只有在运行的时候才加载动态库,也就是说在运行阶段才能进行修正,那么问题又来了,在运行阶段.text是只读属性,没法进行修改了指令了,要解决这个问题,ELF的做法就是在数据段里面建立一个表用于存储要修改的符号地址,称为全局偏移表(Gloabal Offset Table),当需要修正的代码统一先跳转到GOT的位置,而GOT又是在数据段便可以进行修改,在运行阶段扫描获取外部符号的运行地址进行填充,那么解决了这个问题。 如上图a.out应用了libt.so中的变量b和swap函数,在编译链接生成a.out时,解析出b和swap是外部符号,那么就统一跳转到GOT表中,根据GOT中变量所对应的表项找到目标地址执行。 GOT表的填充是在程序加载时(要运行前)就完成了,加载程序会解析a.out中的.interp找到要加载的动态链接器ld的位置(解释器),然后通过.dymamic段获取动态库的信息(依赖哪些动态库、动态链接符号表位置等),最后通过.symtab动态符号表(与静态链接的符号表类似)和.rel.xxx(.rel.dyn和.rel.plt分别对应数据,函数的修正)符号解析和重定位。 地址和空间分配 先来看下a.out实际运行时,地址的范围情况: a.out实际运行时,虚拟地址空间中的模块可以分为几个部分:a.out、libc-2.19.so、libt.so、ld-2.19.so、stack等,a.out的可执行代码地址范围0x00400000~0x00401000,数据段:0x00601000~0x00602000;libt.so的地址范围:0x7f1d921ab000-0x7f1d923ad000。 使用objdump -h a.out查看section情况,VMA与落在运行时的地址范围。 使用objdump -h libt.so查看section情况如下。 因为libt.so编译时使用了-fPIC参数,表示地址无关,地址是从0x00000000开始的,这地址不是实际运行的地址,代表的只是偏移,实际地址落在0x7f1d921ab000-0x7f1d923ad000中。共享对象的最终装载地址在编译时时不确定的,而是在装载时,装载器根据当前地址空间空闲情况,动态分配一块足够大小的虚拟地址空间给相应的共享对象。 可执行文件与静态链接地址和空间分配没什么差异,而动态库是有差别的,虚拟地址空间分配的是相对值,其实际运行的虚拟地址需要到被调用的可执行文件运行时才会分配。 延迟绑定PLT 前面描述了动态链接在运行时重定位比静态链接更灵活,但是也有不少缺点: 外部符号的访问需要先定位GOT表,然后再间接跳转,如此一来运行速度降低,据统计动态链接比静态链接运行性能慢1%~5%不等。 动态链接的链接工作在程序开始执行时完成,链接器寻找并装载所需要的共享对象,然后进行符号查找地址重定位,这个过程就会减慢程序启动速度。 正如上面第二点缺点,程序开始执行前动态链接会消耗不少时间用于解决模块之间的函数引用的符号查找并进行重定位工作,但是一个程序在运行过程中,可能很多外部函数符号在执行结束都可能用不到,比如一些错误处理等,如果一开始就把所有外部函数符号都进行一次性链接那么实际上是种浪费,因此ELF采用了延迟绑定(Lazy Bingding)来进行优化,核心思想就是函数第一次被用到时才进行绑定(符号查找,重定位),如果没有用到则不进行绑定。 引入延时绑定后ELF的构成就变成如上图所示。 .plt: 每个外部符号都对应一个plt项,plt中的内容存放着几条指令,主要的作用是先跳转到got表中查询是否有目标地址,如果没有就调用_dl_runtime_resolve查询全局符号表将目标地址填充到.got表中,跳转运行。填充好后,下次再调用时就可以直接从got表中跳转,不需要再调用_dl_runtime_resolve查询。 .got:保存全局变量引用的地址。 .got.plt:保存函数引用的地址,与.got还有一个特殊的地方就是前3项存储的是.dynamic的地址,模块的ID,_dl_runtime_resolve地址。 上图以调用外部swap函数为例,说明流程。 ①:执行到swap函数,跳转到swap@plt。 ②:swap@plt在.plt的第4个表项PLT[3],从上图中PLT[3]存储了3条指令,第一条指令jmpq GOT[3]处,GOT[3]中存储的是400616,即下一条指令,因为是第一次调用,swap在got表项并没有目标地址,GOT[3]中存储的是PLT[3]第二条指令地址,先入栈偏移量,这里的偏移是swap函数在got.plt的偏移量,便于查找到实际的swap地址填充到.got.plt表中,接着再下一条指令就是跳转到PLT[0]的地址执行。 ③:PLT[0]的第一条指令是pushq *GOT[1],而GOT[1]存储的是依赖库的module id,该id是用于寻找库的,将module ID压入栈,接着jmpq到*GOT[2],而GOT[2]保存的正好是_dl_runtime_resolve的地址。由此可知PLT[0]是公共表项,存储的2条指令用于跳转到_dl_runtime_resolve运行。 ④:调用_dl_runtime_resolve执行符号解析和重定位工作,并将swap的目标地址填充到GOT[3]。 至此,第一次调用流程完成,swap的地址已经填充到GOT表中,下次再执行swap函数时,就从GOT[3]获取到地址直接跳转,就没有查询过程。 下面是通过反汇编objdump -D获取.plt实际情况。 动态链接相关结构 .interp interp是interpreter解释器的缩写,决定动态链接器ld的位置,里面一般就是一个字符串\\\\"/lib/ld-linux.so.2\\\\" $ riscv64-unknown-linux-gnu-objdump -s a.out a.out: file format elf64-littleriscv Contents of section .interp: 10238 2f6c6962 2f6c642d 6c696e75 782d7269 /lib/ld-linux-ri 10248 73637636 342d6c70 3634642e 736f2e31 scv64-lp64d.so.1 10258 00 . .dynmaic .dynamic:保存动态链接器ld需要的基本信息,如依赖哪些共享对象、动态链接符号表位置、动态链接重定位表位置、共享对象初始化代码地址等。 typedef struct dynamic{ Elf32_Sword d_tag; union{ Elf32_Sword d_val; Elf32_Addr d_ptr; } d_un; } Elf32_Dyn; typedef struct { Elf64_Sxword d_tag; /* entry tag value */ union { Elf64_Xword d_val; Elf64_Addr d_ptr; } d_un; } Elf64_Dyn; $ riscv64-unknown-linux-gnu-readelf -d qemu-opensbi/build/platform/generic/firmware/fw_jump.elf Dynamic section at offset 0x21980 contains 13 entries: Tag Type Name/Value 0x0000000000000004 (HASH) 0x8001bd30 0x000000006ffffef5 (GNU_HASH) 0x8001be68 0x0000000000000005 (STRTAB) 0x8001b9f0 0x0000000000000006 (SYMTAB) 0x8001c000 0x000000000000000a (STRSZ) 825 (bytes) 0x000000000000000b (SYMENT) 24 (bytes) 0x0000000000000015 (DEBUG) 0x0 0x0000000000000007 (RELA) 0x8001c3a8 0x0000000000000008 (RELASZ) 9240 (bytes) 0x0000000000000009 (RELAENT) 24 (bytes) 0x000000006ffffffb (FLAGS_1) Flags: PIE 0x000000006ffffff9 (RELACOUNT) 385 0x0000000000000000 (NULL) 0x0 .dynsym 为了完成动态链接,最关键的是所依赖符号和相关文件的信息,其存储在该段中,使用readelf -sD xxx可以获取查询。 riscv64-unknown-linux-gnu-readelf -sD qemu-opensbi/build/platform/generic/firmware/fw_jump.elf Symbol table for image contains 39 entries: Num: Value Size Type Bind Vis Ndx Name 0: 0000000000000000 0 NOTYPE LOCAL DEFAULT UND 1: 0000000080000000 0 SECTION LOCAL DEFAULT 1 2: 00000000800208a8 8 OBJECT GLOBAL DEFAULT 8 sbi_hart_expecte[...] 3: 0000000080020090 8 OBJECT GLOBAL DEFAULT 8 fdt_reset_driver[...] 4: 00000000800200c8 8 OBJECT GLOBAL DEFAULT 8 fdt_irqchip_driv[...] 5: 0000000080020078 8 OBJECT GLOBAL DEFAULT 8 fdt_serial_drive[...] 6: 0000000080020070 8 OBJECT GLOBAL DEFAULT 8 fdt_timer_driver[...] 7: 0000000080020300 48 OBJECT GLOBAL DEFAULT 8 platform_overrid[...] 8: 0000000080021388 16 OBJECT GLOBAL DEFAULT 8 fdt_gpio_drivers 9: 00000000800031fc 26 FUNC GLOBAL DEFAULT 1 sbi_tlb_local_fence_i 10: 0000000080021188 24 OBJECT GLOBAL DEFAULT 8 fdt_irqchip_drivers 11: 00000000800209b8 16 OBJECT GLOBAL DEFAULT 8 fdt_timer_drivers 12: 00000000800032f0 134 FUNC GLOBAL DEFAULT 1 sbi_tlb_local_hf[...] 13: 00000000800200e8 8 OBJECT GLOBAL DEFAULT 8 fdt_i2c_adapter_[...] 14: 000000008000da80 0 NOTYPE GLOBAL DEFAULT 1 __thead_pre_star[...] 15: 000000008000318a 114 FUNC GLOBAL DEFAULT 1 sbi_tlb_local_sf[...] 16: 000000008000da98 0 NOTYPE GLOBAL DEFAULT 1 __reset_thead_cs[...] 17: 0000000080020a80 72 OBJECT GLOBAL DEFAULT 8 fdt_serial_drivers 18: 0000000080020810 152 OBJECT GLOBAL DEFAULT 8 root 19: 000000008000328a 102 FUNC GLOBAL DEFAULT 1 sbi_tlb_local_hf[...] 20: 00000000800370d8 56 OBJECT GLOBAL DEFAULT 13 plicsw 21: 0000000080003216 116 FUNC GLOBAL DEFAULT 1 sbi_tlb_local_hf[...] 22: 0000000080020040 4 OBJECT GLOBAL DEFAULT 8 last_hartid_havi[...] 23: 0000000080003136 84 FUNC GLOBAL DEFAULT 1 sbi_tlb_local_sf[...] 24: 00000000800200d0 8 OBJECT GLOBAL DEFAULT 8 fdt_ipi_drivers_size 25: 0000000080020058 8 OBJECT GLOBAL DEFAULT 8 platform_overrid[...] 26: 0000000080020068 8 OBJECT GLOBAL DEFAULT 8 sbi_ecall_exts_size 27: 00000000800212c8 16 OBJECT GLOBAL DEFAULT 8 fdt_ipi_drivers 28: 00000000800200d8 8 OBJECT GLOBAL DEFAULT 8 fdt_gpio_drivers_size 29: 0000000080004690 0 NOTYPE GLOBAL DEFAULT 1 __ae350_enable_c[...] 30: 0000000080020ec0 56 OBJECT GLOBAL DEFAULT 8 fdt_reset_drivers 31: 0000000080020570 96 OBJECT GLOBAL DEFAULT 8 sbi_ecall_exts 32: 000000008002efe0 1024 OBJECT GLOBAL DEFAULT 13 hartid_to_domain[...] 33: 0000000080036c80 40 OBJECT GLOBAL DEFAULT 13 plmt 34: 000000008002e660 1024 OBJECT GLOBAL DEFAULT 13 hartid_to_scratc[...] 35: 000000008000da78 0 NOTYPE GLOBAL DEFAULT 1 __fdt_reset_thea[...] 36: 0000000080021900 16 OBJECT GLOBAL DEFAULT 8 fdt_i2c_adapter_[...] 37: 000000008000be28 0 NOTYPE GLOBAL DEFAULT 1 __sbi_expected_t[...] 38: 0000000080003376 108 FUNC GLOBAL DEFAULT 1 sbi_tlb_local_hf[...] .rel.xxx .rel.dyn/plt对应的是.rel.text和.rel.data,是动态链接重定位表,.rel.dyn表示对数据引用的修正,其修正的位置位于.got数据段;.rel.plt对函数引用的修正,所修正的位置位于.got.plt段。 可以使用readelf -r xxx。 riscv64-unknown-linux-gnu-readelf -r qemu-opensbi/build/platform/generic/firmware/fw_jump.elf Relocation section '.rela.dyn' at offset 0x1d3a8 contains 385 entries: Offset Info Type Sym. Value Sym. Name + Addend 000080020018 000000000003 R_RISCV_RELATIVE 80000000 000080020028 000000000003 R_RISCV_RELATIVE 80038000 000080020120 000000000003 R_RISCV_RELATIVE 80000e7c 000080020158 000000000003 R_RISCV_RELATIVE 80000e8c 000080020160 000000000003 R_RISCV_RELATIVE 80001316 000080020168 000000000003 R_RISCV_RELATIVE 80020168 000080020170 000000000003 R_RISCV_RELATIVE 80020168 000080020198 000000000003 R_RISCV_RELATIVE 80002a18 0000800201a0 000000000003 R_RISCV_RELATIVE 80002a28 0000800201c8 000000000003 R_RISCV_RELATIVE 800034ae 0000800201d0 000000000003 R_RISCV_RELATIVE 8000358a 0000800201d8 000000000003 R_RISCV_RELATIVE 80003474 000080020238 000000000003 R_RISCV_RELATIVE 80020250 000080020248 000000000003 R_RISCV_RELATIVE 8002eac8 000080020250 000000000003 R_RISCV_RELATIVE 800040b0 000080020258 000000000003 R_RISCV_RELATIVE 8000429a 000080020260 000000000003 R_RISCV_RELATIVE 800040dc 000080020268 000000000003 R_RISCV_RELATIVE 800042f0 000080020270 000000000003 R_RISCV_RELATIVE 80004146 000080020278 000000000003 R_RISCV_RELATIVE 8000416c 000080020290 000000000003 R_RISCV_RELATIVE 80004192 000080020298 000000000003 R_RISCV_RELATIVE 80004230 0000800202a0 000000000003 R_RISCV_RELATIVE 80004212 0000800202a8 000000000003 R_RISCV_RELATIVE 800042be 0000800202b0 000000000003 R_RISCV_RELATIVE 800041ec 0000800202b8 000000000003 R_RISCV_RELATIVE 8000f9cc 0000800202c0 000000000003 R_RISCV_RELATIVE 8000f97e 0000800202c8 000000000003 R_RISCV_RELATIVE 800108b2 0000800202d0 000000000003 R_RISCV_RELATIVE 80010892 0000800202d8 000000000003 R_RISCV_RELATIVE 800041c0 0000800202e0 000000000003 R_RISCV_RELATIVE 8000be80 0000800202e8 000000000003 R_RISCV_RELATIVE 8000be60 0000800202f0 000000000003 R_RISCV_RELATIVE 80004104 0000800202f8 000000000003 R_RISCV_RELATIVE 80004122 000080020300 000000000003 R_RISCV_RELATIVE 80021478 000080020308 000000000003 R_RISCV_RELATIVE 80021518 000080020310 000000000003 R_RISCV_RELATIVE 80021618 000080020318 000000000003 R_RISCV_RELATIVE 800216d8 000080020320 000000000003 R_RISCV_RELATIVE 80020370 000080020328 000000000003 R_RISCV_RELATIVE 800204e0 000080020350 000000000003 R_RISCV_RELATIVE 800045e4 000080020358 000000000003 R_RISCV_RELATIVE 8000455c ELF加载过程 检查ELF可执行文件格式的有效性,比如魔术、程序头表段的数量。 寻找动态链接的.interp段,设置动态链接器路径。 根据ELF可执行文件的程序头表描述,对ELF文件进行映射,比如代码、数据、只读数据。 初始化ELF进程环境,比如进程启动时寄存器配置。 将系统调用的返回地址修改为ELF可执行文件的入口点,如果是静态链接的ELF就是ELF文件中e_entry地址,如果是动态链接的ELF就是动态链接器LD。 总结下,常用工具链调试命令。 最后总结下动态链接的核心思想:编译成位置无关的库,当需要调用到这些库中的符号时,在数据段生成.got(外部变量表)和.got.plt(外部函数表),在程序开始要运行时,就执行查找到依赖的外部符号(变量或函数)地址填充到表中,即可运行。为了解决不需要把所有的符号在运行时一下全填充进.got中,且降低刚启动寻址的时间,使用了延时绑定技术,先生成.plt表,表中存储在动态库中搜索符号的逻辑指令填充到.got表中,第一次运行找不到符号时就在库中查找填充到.got表中,下次就直接可以在.got跳转运行。 参考:<程序员的自我修养 链接、装载与库> -
ELF格式解析
编译过程 工具链把程序源文件翻译成可执行文件一般经理4个步骤:预处理、编译、汇编、链接。本章节关于静态链接和动态链接的过程主要就是在第4个过程。链接器会将输入目标文件(.o)经过加工后合并成一个输出文件,这里的输出文件以linux的ELF为例。链接器会将相同性质的段合并在一起,如将所有输入文件的\".text\"合并到输出文件的\".text\"段,接着是\".data\"段,\".bss\"段等。 ELF格式 ELF一般有3种类型: 可重定位文件(Reloacatable File):xxx.o。 可执行文件(Executable File):xx.out。 共享目标文件(Sharead object File):xxx.so。 ELF提供了两种视图:链接视图和执行视图。 链接视图: 以section为单位,在链接阶段,如将多个模块的.text合并为一个.text。 执行视图:以segment为单位,在执行阶段,对应于linux的VMA,.text和.radata为一个整体。 ELF可以组成可以分为四类:ELF header、program header table、固定的sections、sections header table。 ELF header typedef struct elf32_hdr{ unsigned char e_ident[EI_NIDENT]; Elf32_Half e_type; Elf32_Half e_machine; Elf32_Word e_version; Elf32_Addr e_entry; /* Entry point */ Elf32_Off e_phoff; Elf32_Off e_shoff; Elf32_Word e_flags; Elf32_Half e_ehsize; Elf32_Half e_phentsize; Elf32_Half e_phnum; Elf32_Half e_shentsize; Elf32_Half e_shnum; //描述了有多少个sections。 Elf32_Half e_shstrndx; } Elf32_Ehdr; 这些字段是 ELF 文件头中的一部分,它们包含了 ELF 文件的基本信息,如文件类型、目标机器架构、入口地址、程序头表和节区头表等,为 ELF 文件的加载和执行提供了必要的信息。 使用命令readelf -h xxx可以读取上面的信息。 program header table typedef struct elf32_phdr{ Elf32_Word p_type; Elf32_Off p_offset; Elf32_Addr p_vaddr; Elf32_Addr p_paddr; Elf32_Word p_filesz; Elf32_Word p_memsz; Elf32_Word p_flags; Elf32_Word p_align; } Elf32_Phdr; ELF 程序头表(Program Header Table)包含了多个程序头(Program Header)的条目,每个程序头描述了一个段(Segment)的信息。段是 ELF 文件中的逻辑组织单元,包含了可执行代码、数据、只读数据等。程序头表通过指定每个段在文件中和内存中的位置、大小、属性等信息,为系统加载器(Loader)提供了必要的信息,以正确加载和执行 ELF 文件。 使用readelf -l 可以查看有哪些segment 上图中Type 为 LOAD的,表示该节区包含可执行文件或共享库的可加载部分。在加载过程中,这些节区的内容将被加载到内存中。 固定的sections 使用readelf -S 可以查看有哪些sections .text:包含可执行代码的节区,通常用于存储程序的指令。 .data:包含已初始化的全局和静态变量的节区,用于存储程序的全局数据。 .bss:包含未初始化的全局和静态变量的节区,用于存储程序的全局数据,但在文件中不占用实际空间。 .rodata:包含只读数据的节区,通常用于存储常量、字符串等不可修改的数据。 .comment:包含编译器生成的注释信息的节区,通常用于存储编译器版本、编译时间等信息。 .eh_frame:包含异常处理框架信息的节区,用于支持 C++ 异常处理和调用堆栈展开。 .dynsym:包含动态链接符号表的节区,用于存储动态链接所需的符号信息。 .shstrtab:包含节区名称字符串的节区,用于存储所有节区的名称。 .strtab:包含字符串表的节区,用于存储字符串常量。 .symtab:包含符号表的节区,用于存储链接器符号信息。 .got:全局偏移表(Global Offset Table),用于存储动态链接所需的全局变量和函数地址。 .plt:过程链接表(Procedure Linkage Table),用于实现函数调用的延迟绑定。 .rel.xxx:用于存储重定位信息的节区,其中 xxx 表示需要进行重定位的节区名称,如.rel.text,.rel.data。 .dynamic:包含动态链接器信息的节区,用于存储动态链接器所需的信息。 .bss 节区存储未初始化的全局和静态变量,这些变量在编译时没有明确赋初始值,因此在 ELF 文件中不需要占用实际空间。相反,当程序被加载到内存中时,操作系统会分配一段 BSS 段的内存,大小由 .bss 节区中所有变量的总大小决定,然后将该内存区域初始化为 0。这种方式可以有效地节省文件大小,节约磁盘空间和加载时间。如果 .bss 节区中的变量在文件中被显式地初始化了,那么它们就会被分配到 .data 节区中,该节区会在文件中占用实际空间。需要注意的是,.bss 节区的存在并不是必须的,编译器可以将未初始化的全局和静态变量直接放入 .data 节区中,但这样会导致 .data 节区的大小增加,从而增加了文件大小和加载时间。因此,使用 .bss 节区可以更好地优化程序的性能和空间占用。 sections header table typedef struct elf32_shdr { Elf32_Word sh_name; Elf32_Word sh_type; Elf32_Word sh_flags; Elf32_Addr sh_addr; Elf32_Off sh_offset; Elf32_Word sh_size; Elf32_Word sh_link; Elf32_Word sh_info; Elf32_Word sh_addralign; Elf32_Word sh_entsize; } Elf32_Shdr; ELF文件中section head table (SHT)来描述ELF文件中有哪些具体的sections,每个section描述了这个段的信息,比如每个段的段名、段的长度、在文件中的偏移、读写权限及段的其它属性。 使用命令使用readelf -S 可以查看有section head table信息,与上一节固定的sections对应,实际上section head table就是描述上一节的信息。