ELF静态链接(ELF Static link)

ELF Static link

image-20230428222115232

之前在elf文件解析时只是做了一下可执行文件elf从执行的视角看了如何依靠Program Header table加载到内存区执行的过程,这次补个从链接的视角看可重定位elf文件如何依靠Section Header table做静态链接成为可执行文件的

?????????????????????????????????????????丑小鸭变天鹅????????????????????????????????????

Program Header table其实就是把一些section看成一块

grxer@Ubuntu16 /m/h/s/l/chapter7> readelf -l xxx.so

Elf file type is DYN (Shared object file)
Entry point 0x420
There are 7 program headers, starting at offset 52

Program Headers:
Type Offset VirtAddr PhysAddr FileSiz MemSiz Flg Align
LOAD 0x000000 0x00000000 0x00000000 0x00678 0x00678 R E 0x1000
LOAD 0x000efc 0x00001efc 0x00001efc 0x0011c 0x00124 RW 0x1000
DYNAMIC 0x000f08 0x00001f08 0x00001f08 0x000e0 0x000e0 RW 0x4
NOTE 0x000114 0x00000114 0x00000114 0x00024 0x00024 R 0x4
GNU_EH_FRAME 0x0005b4 0x000005b4 0x000005b4 0x0002c 0x0002c R 0x4
GNU_STACK 0x000000 0x00000000 0x00000000 0x00000 0x00000 RW 0x10
GNU_RELRO 0x000efc 0x00001efc 0x00001efc 0x00104 0x00104 R 0x1

Section to Segment mapping:
Segment Sections...
00 .note.gnu.build-id .gnu.hash .dynsym .dynstr .gnu.version .gnu.version_r .rel.dyn .rel.plt .init .plt .plt.got .text .fini .eh_frame_hdr .eh_frame
01 .init_array .fini_array .jcr .dynamic .got .got.plt .data .bss
02 .dynamic
03 .note.gnu.build-id
04 .eh_frame_hdr
05
06 .init_array .fini_array .jcr .dynamic .got

可以看的只有前面两个lOAD类型块是被加载的到内存,其余都是辅助信息,通过下面的 Section to Segment mapping可以看出是把多个section当成一块加载到内存

这样做应该是为了减少一些空间碎片,因为映射是按照页大小来的。有太多的section不足一个页大小,会浪费空间,但是我们操作系统只关心section权限,所以把相同权限的section当成一个整体去映射

实验环境

grxer@Ubuntu16 /m/h/S/l/chapter3> gcc --version
gcc (Ubuntu 5.4.0-6ubuntu1~16.04.12) 5.4.0 20160609
Copyright (C) 2015 Free Software Foundation, Inc.
This is free software; see the source for copying conditions. There is NO
warranty; not even for MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.

中间会穿插一点11.3.0版本 just a little bit

grxer@Ubuntu22-wsl ~/s/nemu [SIGINT]> gcc --version
gcc (Ubuntu 11.3.0-1ubuntu1~22.04) 11.3.0
Copyright (C) 2021 Free Software Foundation, Inc.
This is free software; see the source for copying conditions. There is NO
warranty; not even for MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.

工具主要是GNU Binutils https://www.gnu.org/software/binutils/

以32位程序为例

编译漫谈

从古老的传说hello world说起。。。。。。

#include <stdio.h>
int main(){
printf("hello world");
return 0;
}

刚开始大部分人初学时基本上都是直接集成环境的ide一键编译运行,慢慢开始用一些编译工具,gcc,make等,

grxer@Ubuntu16 /m/h/S/l/chapter3> gcc test2.c
grxer@Ubuntu16 /m/h/S/l/chapter3> ./a.out
hello world⏎

但是gcc帮我们做的远比我们想象的多的多

gcc提供给了我们一个选项来看显示编译和链接具体过程

verbose
Enable showing the tree dump for each statement.
grxer@Ubuntu16 /m/h/S/l/chapter3> gcc --verbose test2.c
Using built-in specs.
COLLECT_GCC=gcc
COLLECT_LTO_WRAPPER=/usr/lib/gcc/x86_64-linux-gnu/5/lto-wrapper
Target: x86_64-linux-gnu
Configured with: ../src/configure -v --with-pkgversion='Ubuntu 5.4.0-6ubuntu1~16.04.12' --with-bugurl=file:///usr/share/doc/gcc-5/README.Bugs --enable-languages=c,ada,c++,java,go,d,fortran,objc,obj-c++ --prefix=/usr --program-suffix=-5 --enable-shared --enable-linker-build-id --libexecdir=/usr/lib --without-included-gettext --enable-threads=posix --libdir=/usr/lib --enable-nls --with-sysroot=/ --enable-clocale=gnu --enable-libstdcxx-debug --enable-libstdcxx-time=yes --with-default-libstdcxx-abi=new --enable-gnu-unique-object --disable-vtable-verify --enable-libmpx --enable-plugin --with-system-zlib --disable-browser-plugin --enable-java-awt=gtk --enable-gtk-cairo --with-java-home=/usr/lib/jvm/java-1.5.0-gcj-5-amd64/jre --enable-java-home --with-jvm-root-dir=/usr/lib/jvm/java-1.5.0-gcj-5-amd64 --with-jvm-jar-dir=/usr/lib/jvm-exports/java-1.5.0-gcj-5-amd64 --with-arch-directory=amd64 --with-ecj-jar=/usr/share/java/eclipse-ecj.jar --enable-objc-gc --enable-multiarch --disable-werror --with-arch-32=i686 --with-abi=m64 --with-multilib-list=m32,m64,mx32 --enable-multilib --with-tune=generic --enable-checking=release --build=x86_64-linux-gnu --host=x86_64-linux-gnu --target=x86_64-linux-gnu
Thread model: posix
gcc version 5.4.0 20160609 (Ubuntu 5.4.0-6ubuntu1~16.04.12)
COLLECT_GCC_OPTIONS='-v' '-mtune=generic' '-march=x86-64'
/usr/lib/gcc/x86_64-linux-gnu/5/cc1 -quiet -v -imultiarch x86_64-linux-gnu test2.c -quiet -dumpbase test2.c -mtune=generic -march=x86-64 -auxbase test2 -version -fstack-protector-strong -Wformat -Wformat-security -o /tmp/ccZSA6XB.s
GNU C11 (Ubuntu 5.4.0-6ubuntu1~16.04.12) version 5.4.0 20160609 (x86_64-linux-gnu)
compiled by GNU C version 5.4.0 20160609, GMP version 6.1.0, MPFR version 3.1.4, MPC version 1.0.3
GGC heuristics: --param ggc-min-expand=100 --param ggc-min-heapsize=131072
ignoring nonexistent directory "/usr/local/include/x86_64-linux-gnu"
ignoring nonexistent directory "/usr/lib/gcc/x86_64-linux-gnu/5/../../../../x86_64-linux-gnu/include"
#include "..." search starts here:
#include <...> search starts here:
/usr/lib/gcc/x86_64-linux-gnu/5/include
/usr/local/include
/usr/lib/gcc/x86_64-linux-gnu/5/include-fixed
/usr/include/x86_64-linux-gnu
/usr/include
End of search list.
GNU C11 (Ubuntu 5.4.0-6ubuntu1~16.04.12) version 5.4.0 20160609 (x86_64-linux-gnu)
compiled by GNU C version 5.4.0 20160609, GMP version 6.1.0, MPFR version 3.1.4, MPC version 1.0.3
GGC heuristics: --param ggc-min-expand=100 --param ggc-min-heapsize=131072
Compiler executable checksum: 8087146d2ee737d238113fb57fabb1f2
COLLECT_GCC_OPTIONS='-v' '-mtune=generic' '-march=x86-64'
as -v --64 -o /tmp/ccyxTcBb.o /tmp/ccZSA6XB.s
GNU assembler version 2.26.1 (x86_64-linux-gnu) using BFD version (GNU Binutils for Ubuntu) 2.26.1
COMPILER_PATH=/usr/lib/gcc/x86_64-linux-gnu/5/:/usr/lib/gcc/x86_64-linux-gnu/5/:/usr/lib/gcc/x86_64-linux-gnu/:/usr/lib/gcc/x86_64-linux-gnu/5/:/usr/lib/gcc/x86_64-linux-gnu/
LIBRARY_PATH=/usr/lib/gcc/x86_64-linux-gnu/5/:/usr/lib/gcc/x86_64-linux-gnu/5/../../../x86_64-linux-gnu/:/usr/lib/gcc/x86_64-linux-gnu/5/../../../../lib/:/lib/x86_64-linux-gnu/:/lib/../lib/:/usr/lib/x86_64-linux-gnu/:/usr/lib/../lib/:/usr/lib/gcc/x86_64-linux-gnu/5/../../../:/lib/:/usr/lib/
COLLECT_GCC_OPTIONS='-v' '-mtune=generic' '-march=x86-64'
/usr/lib/gcc/x86_64-linux-gnu/5/collect2 -plugin /usr/lib/gcc/x86_64-linux-gnu/5/liblto_plugin.so -plugin-opt=/usr/lib/gcc/x86_64-linux-gnu/5/lto-wrapper -plugin-opt=-fresolution=/tmp/ccvOb2eL.res -plugin-opt=-pass-through=-lgcc -plugin-opt=-pass-through=-lgcc_s -plugin-opt=-pass-through=-lc -plugin-opt=-pass-through=-lgcc -plugin-opt=-pass-through=-lgcc_s --sysroot=/ --build-id --eh-frame-hdr -m elf_x86_64 --hash-style=gnu --as-needed -dynamic-linker /lib64/ld-linux-x86-64.so.2 -z relro /usr/lib/gcc/x86_64-linux-gnu/5/../../../x86_64-linux-gnu/crt1.o /usr/lib/gcc/x86_64-linux-gnu/5/../../../x86_64-linux-gnu/crti.o /usr/lib/gcc/x86_64-linux-gnu/5/crtbegin.o -L/usr/lib/gcc/x86_64-linux-gnu/5 -L/usr/lib/gcc/x86_64-linux-gnu/5/../../../x86_64-linux-gnu -L/usr/lib/gcc/x86_64-linux-gnu/5/../../../../lib -L/lib/x86_64-linux-gnu -L/lib/../lib -L/usr/lib/x86_64-linux-gnu -L/usr/lib/../lib -L/usr/lib/gcc/x86_64-linux-gnu/5/../../.. /tmp/ccyxTcBb.o -lgcc --as-needed -lgcc_s --no-as-needed -lc -lgcc --as-needed -lgcc_s --no-as-needed /usr/lib/gcc/x86_64-linux-gnu/5/crtend.o /usr/lib/gcc/x86_64-linux-gnu/5/../../../x86_64-linux-gnu/crtn.o

tmd,这合理吗? 哈,这很合理!

尝试着去看一下,会发现一些我们原本熟悉的东西

刚开始输出了一些我们gcc的版本信息等

熟悉的cc1去做预处理并对预处理过的文件做编译(不知道为什么没有看见cpp去做预处理的身影),这一步是本身就极其复杂的,语法树什么的,更别提代码优化等等,具体的东西可能要去补补编译原理 ps:这学期给自己挖的坑够多了,只能下次一定

/usr/lib/gcc/x86_64-linux-gnu/5/cc1 -quiet -v -imultiarch x86_64-linux-gnu test2.c -quiet -dumpbase test2.c -mtune=generic -march=x86-64 -auxbase test2 -version -fstack-protector-strong -Wformat -Wformat-security -o /tmp/ccZSA6XB.s

打印出他搜索头文件的路径

ignoring nonexistent directory "/usr/local/include/x86_64-linux-gnu"
ignoring nonexistent directory "/usr/lib/gcc/x86_64-linux-gnu/5/../../../../x86_64-linux-gnu/include"
#include "..." search starts here:
#include <...> search starts here:
/usr/lib/gcc/x86_64-linux-gnu/5/include
/usr/local/include
/usr/lib/gcc/x86_64-linux-gnu/5/include-fixed
/usr/include/x86_64-linux-gnu
/usr/include
End of search list.

as 汇编器去做汇编,就是机械的翻译汇编到机器码

as -v --64 -o /tmp/ccyxTcBb.o /tmp/ccZSA6XB.s

后面从 /usr/lib/gcc/x86_64-linux-gnu/5/collect2到最后/x86_64-linux-gnu/crtn.o都是在用collect2做链接 collect2是ld链接器的一个封装

我们要这次要研究的就是ld究竟怎么做的链接,怎么把多个可能互相引用的可重定位文件链接成一个可执行文件的

重生之我是linker

把自己想象成linker,我们有的是可重定位目标文件,我们要把他变为可执行文件,首先要对有编译器和汇编器生成的可重定位目标文件做个了解,编译器给了他什么,如果什么都没有我们linker凭什么或者说何德何能把他变为可执行目标文件

linker: ?什么都没有?不好意思我做不到!

Relocatable object file

ELF里的一些定义符号

The following types are used for  N-bit  architectures  (N=32,64,  ElfN
stands for Elf32 or Elf64, uintN_t stands for uint32_t or uint64_t):

ElfN_Addr       Unsigned program address, uintN_t
ElfN_Off        Unsigned file offset, uintN_t
ElfN_Section    Unsigned section index, uint16_t
ElfN_Versym     Unsigned version symbol information, uint16_t
Elf_Byte        unsigned char
ElfN_Half       uint16_t
ElfN_Sword      int32_t
ElfN_Word       uint32_t
ElfN_Sxword     int64_t
ElfN_Xword      uint64_t

simple_section.c

int printf(const char* format, ...);

int global_init_var = 84;
int global_uninit_var;
static int global_static_var;

extern int reference_to_out;

void func1(int i) {
printf("%d\n", i);
}

int main(void) {
static int static_var = 85;
static int static_var2;

int a = 1;
int b;

func1(static_var + static_var2 + a + b);

return a;
}
grxer@Ubuntu16 /m/h/S/l/chapter3> gcc -m32 -c simple_section.c
grxer@Ubuntu16 /m/h/S/l/chapter3> file simple_section.o
simple_section.o: ELF 32-bit LSB relocatable, Intel 80386, version 1 (SYSV), not stripped

ELF文件,链接我们依靠的是他的section head table ,linker哪里找到他呢?

ELF header里

#define EI_NIDENT   16

typedef struct {
unsigned char e_ident[EI_NIDENT]; /* ELF文件标识 */
Elf32_Half e_type; /* 文件类型 */
Elf32_Half e_machine; /* 机器类型 */
Elf32_Word e_version; /* 文件版本 */
Elf32_Addr e_entry; /* 程序入口地址 */
Elf32_Off e_phoff; /* 程序头表偏移 */
Elf32_Off e_shoff; /* 节头表偏移 */
Elf32_Word e_flags; /* 文件标志 */
Elf32_Half e_ehsize; /* ELF头大小 */
Elf32_Half e_phentsize; /* 程序头表项大小 */
Elf32_Half e_phnum; /* 程序头表项数量 */
Elf32_Half e_shentsize; /* 节头表项大小 */
Elf32_Half e_shnum; /* 节头表项数量 */
Elf32_Half e_shstrndx; /* 节头表字符串表索引 */
} Elf32_Ehdr;

e_shoff(section header table offset)给出了在文件里的偏移,e_shnum,e_shentsize节头表项大小和数量

grxer@Ubuntu16 /m/h/S/l/chapter3> readelf -h simple_section.o
ELF Header:
Magic: 7f 45 4c 46 01 01 01 00 00 00 00 00 00 00 00 00
Class: ELF32
Data: 2's complement, little endian
Version: 1 (current)
OS/ABI: UNIX - System V
ABI Version: 0
Type: REL (Relocatable file)
Machine: Intel 80386
Version: 0x1
Entry point address: 0x0
Start of program headers: 0 (bytes into file)
Start of section headers: 868 (bytes into file)
Flags: 0x0
Size of this header: 52 (bytes)
Size of program headers: 0 (bytes)
Number of program headers: 0
Size of section headers: 40 (bytes)
Number of section headers: 13
Section header string table index: 10

看一下各个section

There are 13 section headers, starting at offset 0x364:

Section Headers:
[Nr] Name Type Addr Off Size ES Flg Lk Inf Al
[ 0] NULL 00000000 000000 000000 00 0 0 0
[ 1] .text PROGBITS 00000000 000034 000062 00 AX 0 0 1
[ 2] .rel.text REL 00000000 0002cc 000028 08 I 11 1 4
[ 3] .data PROGBITS 00000000 000098 000008 00 WA 0 0 4
[ 4] .bss NOBITS 00000000 0000a0 000008 00 WA 0 0 4
[ 5] .rodata PROGBITS 00000000 0000a0 000004 00 A 0 0 1
[ 6] .comment PROGBITS 00000000 0000a4 000036 01 MS 0 0 1
[ 7] .note.GNU-stack PROGBITS 00000000 0000da 000000 00 0 0 1
[ 8] .eh_frame PROGBITS 00000000 0000dc 000064 00 A 0 0 4
[ 9] .rel.eh_frame REL 00000000 0002f4 000010 08 I 11 8 4
[10] .shstrtab STRTAB 00000000 000304 00005f 00 0 0 1
[11] .symtab SYMTAB 00000000 000140 000110 10 12 12 4
[12] .strtab STRTAB 00000000 000250 000079 00 0 0 1
Key to Flags:
W (write), A (alloc), X (execute), M (merge), S (strings)
I (info), L (link order), G (group), T (TLS), E (exclude), x (unknown)
O (extra OS processing required) o (OS specific), p (processor specific)

section head table可以理解为一个Elf32_Shdr结构体的数组 Elf32_Shdr结构来描述具体的section属性

typedef struct {
Elf32_Word sh_name; // 节名称在 .shstrtab 节中的索引
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;

sh_name

节名称在 .shstrtab 节中的索引, .shstrtab是节头表字符串表索引

具体来看下我们例子的 .shstrtab是的name是怎么解析的,linker首先从elf header里得到.shstrtab在section head table的第十个section e_shoff+e_shstrndx*e_shentsize=904+10*40=1304=0x508,找到这个section的描述结构体

image-20230428232104339

前四个字节就是sh_name在.shstrtab 节中的索引,找到sh_offset为0x0328,即为shstrtab 节在文件中的位置,加上sh_name这个索引0x0328+0x11=0x339

image-20230428232706551

就可以找到这个名字

之所以要这样这样存储名字,是因为我们的ELF必须要有一个对所有文件固定的格式,但是名字往往是不固定的长度,如果直接存储这个字符串,就不能用一个统一的结构体去描述,存储索引完美解决

sh_type

决定了节的类型 SHT_SYMTAB,SHT_RELA,SHT_DYNAMIC,SHT_REL等

sh_flags

SHF_WRITE     This section  contains  data  that  should  be writable during process execution.

SHF_ALLOC This section occupies memory during process execution. Some control sections do notreside in the memory image of an object file. This attribute is off for those sections.表示该节在进程空间里要分配内存,有些节是一些控制信息不会被加载到进程空间,一般代码段数据段,和.bss段都会有这个表示,另外一些节完成任务之后就扔了尼,嘿嘿

SHF_EXECINSTR This section contains executable machineinstructions.

SHF_MASKPROC All bits included in this mask are reserved for processor-specific semantics.

sh_linksh_info只有节的类型是与链接相关时才会有用

image-20230429003128446

sh_entsize

有一些节的内容是一张表,其中每一个表项的大小是固定的,比如符号表。 对于这种表来说,本成员指定其每一个表项的大小。

a.c

extern int shared;

int main()
{
int a = 6;
swap(&a, &shared);
}

b.c

int shared = 1;

void swap(int* a, int* b) {
*a ^= *b ^= *a ^= *b;
}
grxer@Ubuntu16 /m/h/S/l/chapter4> gcc -m32 -fno-stack-protector -c a.c b.c
a.c: In function ‘main’:
a.c:6:5: warning: implicit declaration of function ‘swap’ [-Wimplicit-function-declaration]
swap(&a, &shared);
^
grxer@Ubuntu16 /m/h/S/l/chapter4> ld -m elf_i386 a.o b.o -e main -o ab
grxer@Ubuntu16 /m/h/S/l/chapter4> file a.o b.o ab
a.o: ELF 32-bit LSB relocatable, Intel 80386, version 1 (SYSV), not stripped
b.o: ELF 32-bit LSB relocatable, Intel 80386, version 1 (SYSV), not stripped
ab: ELF 32-bit LSB executable, Intel 80386, version 1 (SYSV), statically linked, not stripped

把canary关了,不然链接是找不到__stack_chk_fail,因为我们并没有ld标准链接库,

grxer@Ubuntu16 /m/h/S/l/chapter4> gcc -m32  -c a.c b.c
a.c: In function ‘main’:
a.c:6:5: warning: implicit declaration of function ‘swap’ [-Wimplicit-function-declaration]
swap(&a, &shared);
^
grxer@Ubuntu16 /m/h/S/l/chapter4> checksec a.o
[*] '/mnt/hgfs/Share/link-load-library-code/chapter4/a.o'
Arch: i386-32-little
RELRO: No RELRO
Stack: Canary found
NX: NX enabled
PIE: No PIE (0x0)
grxer@Ubuntu16 /m/h/S/l/chapter4> gcc -m32 -fno-stack-protector -c a.c b.c
a.c: In function ‘main’:
a.c:6:5: warning: implicit declaration of function ‘swap’ [-Wimplicit-function-declaration]
swap(&a, &shared);
^
grxer@Ubuntu16 /m/h/S/l/chapter4> checksec a.o
[*] '/mnt/hgfs/Share/link-load-library-code/chapter4/a.o'
Arch: i386-32-little
RELRO: No RELRO
Stack: No canary found
NX: NX enabled
PIE: No PIE (0x0)

ld时-e指定下入口为mian,不然ld时找不到默认的_start

gcc11.3还需要-fno-pie把pie关了,不然他的重定位类型似乎是按照共享库来的,我们先搞静态,gcc5加不加都行

grxer@Ubuntu16 /m/h/S/l/chapter4> readelf -S a.o
There are 12 section headers, starting at offset 0x220:

Section Headers:
[Nr] Name Type Addr Off Size ES Flg Lk Inf Al
[ 0] NULL 00000000 000000 000000 00 0 0 0
[ 1] .text PROGBITS 00000000 000034 000039 00 AX 0 0 1
[ 2] .rel.text REL 00000000 0001b0 000010 08 I 10 1 4
[ 3] .data PROGBITS 00000000 00006d 000000 00 WA 0 0 1
[ 4] .bss NOBITS 00000000 00006d 000000 00 WA 0 0 1
[ 5] .comment PROGBITS 00000000 00006d 000036 01 MS 0 0 1
[ 6] .note.GNU-stack PROGBITS 00000000 0000a3 000000 00 0 0 1
[ 7] .eh_frame PROGBITS 00000000 0000a4 000044 00 A 0 0 4
[ 8] .rel.eh_frame REL 00000000 0001c0 000008 08 I 10 7 4
[ 9] .shstrtab STRTAB 00000000 0001c8 000057 00 0 0 1
[10] .symtab SYMTAB 00000000 0000e8 0000b0 10 11 8 4
[11] .strtab STRTAB 00000000 000198 000016 00 0 0 1
Key to Flags:
W (write), A (alloc), X (execute), M (merge), S (strings)
I (info), L (link order), G (group), T (TLS), E (exclude), x (unknown)
O (extra OS processing required) o (OS specific), p (processor specific)
grxer@Ubuntu16 /m/h/S/l/chapter4> readelf -S b.o
There are 11 section headers, starting at offset 0x1f4:

Section Headers:
[Nr] Name Type Addr Off Size ES Flg Lk Inf Al
[ 0] NULL 00000000 000000 000000 00 0 0 0
[ 1] .text PROGBITS 00000000 000034 000039 00 AX 0 0 1
[ 2] .data PROGBITS 00000000 000070 000004 00 WA 0 0 4
[ 3] .bss NOBITS 00000000 000074 000000 00 WA 0 0 1
[ 4] .comment PROGBITS 00000000 000074 000036 01 MS 0 0 1
[ 5] .note.GNU-stack PROGBITS 00000000 0000aa 000000 00 0 0 1
[ 6] .eh_frame PROGBITS 00000000 0000ac 000038 00 A 0 0 4
[ 7] .rel.eh_frame REL 00000000 000198 000008 08 I 9 6 4
[ 8] .shstrtab STRTAB 00000000 0001a0 000053 00 0 0 1
[ 9] .symtab SYMTAB 00000000 0000e4 0000a0 10 10 8 4
[10] .strtab STRTAB 00000000 000184 000011 00 0 0 1
Key to Flags:
W (write), A (alloc), X (execute), M (merge), S (strings)
I (info), L (link order), G (group), T (TLS), E (exclude), x (unknown)
O (extra OS processing required) o (OS specific), p (processor specific)
grxer@Ubuntu16 /m/h/S/l/chapter4> readelf -S ab
There are 8 section headers, starting at offset 0x2e4:

Section Headers:
[Nr] Name Type Addr Off Size ES Flg Lk Inf Al
[ 0] NULL 00000000 000000 000000 00 0 0 0
[ 1] .text PROGBITS 08048094 000094 000072 00 AX 0 0 1
[ 2] .eh_frame PROGBITS 08048108 000108 000064 00 A 0 0 4
[ 3] .data PROGBITS 0804916c 00016c 000004 00 WA 0 0 4
[ 4] .comment PROGBITS 00000000 000170 000035 01 MS 0 0 1
[ 5] .shstrtab STRTAB 00000000 0002aa 00003a 00 0 0 1
[ 6] .symtab SYMTAB 00000000 0001a8 0000d0 10 7 7 4
[ 7] .strtab STRTAB 00000000 000278 000032 00 0 0 1
Key to Flags:
W (write), A (alloc), X (execute), M (merge), S (strings)
I (info), L (link order), G (group), T (TLS), E (exclude), x (unknown)
O (extra OS processing required) o (OS specific), p (processor specific)

空间与地址分配

linker把所有的输入目标文件相同的节合并成一个,从大小也可以看出来是合并了,然后重新生成一个elf header来帮助我们加载时找到新段

符号解析与重定位

链接的本质就是把不同目标文件粘在一起,符号是粘合剂,符号包括变量名,函数名,还有段名等等,符号标记了各种信息,他被存储在我们的.symtab section标志的符号表里

typedef struct
{
Elf32_Word st_name; /* Symbol name (string tbl index) */
Elf32_Addr st_value; /* Symbol value */
Elf32_Word st_size; /* Symbol size */
unsigned char st_info; /* Symbol type and binding */
unsigned char st_other; /* Symbol visibility */
Elf32_Section st_shndx; /* Section index */
} Elf32_Sym;

st_name

在.strtab 字符串表里的下标

st_shndx

  • 符号定义在本文件
    • 符号所在段下标
  • 不在本文文件或者某些特殊值
    • SHN_ABS 表示该符号包含一个绝对的值
    • SHN_COMMON 表示该符号是一个COMMON块类型的符号
    • SHN_UNDEF 表示该符号未定义

st_info

符号的类型和绑定信息

绑定信息

  • STB_LOCAL局部符号
  • STB_GLOBAL全局符号
  • STB_WEAK弱符号

符号类型

  • STT_NOTYPE 未知
  • STT_OBJECT 数据对象,比如变量
  • STT_FUNC 函数或可执行代码
  • STT_SECTION 表示一个段
  • STT_FILE表示文件名 st_shndx一定是SHN_ABS

st_size

符号大小,对于包含数据的符号,这个值是数据类型的大小,比如我们上面a.o的main就是main函数的大小,0x39=57,int 类型的shared大小为4

st_value

  • 在可重定位目标文件里函数或变量符号
    • 符号类型不在common块里,则表示符号在段里的偏移
    • 在common块里,则表示符号对齐属性
  • 可执行目标文件里
    • 表示符号的虚拟地址

st_other暂时没用

grxer@Ubuntu16 /m/h/S/l/chapter4> readelf -s a.o b.o ab

File: a.o

Symbol table '.symtab' contains 11 entries:
Num: Value Size Type Bind Vis Ndx Name
0: 00000000 0 NOTYPE LOCAL DEFAULT UND
1: 00000000 0 FILE LOCAL DEFAULT ABS a.c
2: 00000000 0 SECTION LOCAL DEFAULT 1
3: 00000000 0 SECTION LOCAL DEFAULT 3
4: 00000000 0 SECTION LOCAL DEFAULT 4
5: 00000000 0 SECTION LOCAL DEFAULT 6
6: 00000000 0 SECTION LOCAL DEFAULT 7
7: 00000000 0 SECTION LOCAL DEFAULT 5
8: 00000000 57 FUNC GLOBAL DEFAULT 1 main
9: 00000000 0 NOTYPE GLOBAL DEFAULT UND shared
10: 00000000 0 NOTYPE GLOBAL DEFAULT UND swap

File: b.o

Symbol table '.symtab' contains 10 entries:
Num: Value Size Type Bind Vis Ndx Name
0: 00000000 0 NOTYPE LOCAL DEFAULT UND
1: 00000000 0 FILE LOCAL DEFAULT ABS b.c
2: 00000000 0 SECTION LOCAL DEFAULT 1
3: 00000000 0 SECTION LOCAL DEFAULT 2
4: 00000000 0 SECTION LOCAL DEFAULT 3
5: 00000000 0 SECTION LOCAL DEFAULT 5
6: 00000000 0 SECTION LOCAL DEFAULT 6
7: 00000000 0 SECTION LOCAL DEFAULT 4
8: 00000000 4 OBJECT GLOBAL DEFAULT 2 shared
9: 00000000 57 FUNC GLOBAL DEFAULT 1 swap

File: ab

Symbol table '.symtab' contains 13 entries:
Num: Value Size Type Bind Vis Ndx Name
0: 00000000 0 NOTYPE LOCAL DEFAULT UND
1: 08048094 0 SECTION LOCAL DEFAULT 1
2: 08048108 0 SECTION LOCAL DEFAULT 2
3: 0804916c 0 SECTION LOCAL DEFAULT 3
4: 00000000 0 SECTION LOCAL DEFAULT 4
5: 00000000 0 FILE LOCAL DEFAULT ABS a.c
6: 00000000 0 FILE LOCAL DEFAULT ABS b.c
7: 080480cd 57 FUNC GLOBAL DEFAULT 1 swap
8: 0804916c 4 OBJECT GLOBAL DEFAULT 3 shared
9: 08049170 0 NOTYPE GLOBAL DEFAULT 3 __bss_start
10: 08048094 57 FUNC GLOBAL DEFAULT 1 main
11: 08049170 0 NOTYPE GLOBAL DEFAULT 3 _edata
12: 08049170 0 NOTYPE GLOBAL DEFAULT 3 _end

为什么有些名字显示?

可以看的没显示的类型都为STT_SECTION他们的名字其实就是st_shndx代表的段的段名字

readelf没有显示,objdump给显示出来了

grxer@Ubuntu16 /m/h/S/l/chapter4> objdump -t a.o

a.o: file format elf32-i386

SYMBOL TABLE:
00000000 l df *ABS* 00000000 a.c
00000000 l d .text 00000000 .text
00000000 l d .data 00000000 .data
00000000 l d .bss 00000000 .bss
00000000 l d .note.GNU-stack 00000000 .note.GNU-stack
00000000 l d .eh_frame 00000000 .eh_frame
00000000 l d .comment 00000000 .comment
00000000 g F .text 00000039 main
00000000 *UND* 00000000 shared
00000000 *UND* 00000000 swap
什么是common块&&强符号弱符号&&弱引用强引用

我们用前面的simple section.c来说

.bss段存放未初始化的全局变量和局部静态变量(这里编译器其实为了节约磁盘空间把初始化为0的变量也放入了bss),但是global_uninit_var却在COM块

Num:    Value  Size Type    Bind   Vis      Ndx Name  
14: 00000004 4 OBJECT GLOBAL DEFAULT COM global_uninit_var

也就是说gcc把没有初始化的全局变量放到了COMMON块 当然gcc5也可以通过 -fno-common或者用__attribute__((nocommon))取消掉这个机制,这样多重定义的全局符号时,会触发一个错误

这个我在gcc11试了试发现好像默认没有放到common,而是都放到了bss

这种未初始化全局变量的符号叫做弱符号,相反的就叫做强符号

  • 不允许一个强符号被多次定义,否则链接器会报错
  • 如果一个符号在某个目标文件中是强符号,链接时选强符号
  • 一个符号在所有可重定位文件里都是弱符号,选字节最大的

所以我们下面这样会自动选择强符号

test.c
#include <stdio.h>
void p1(void);
double d = 666666;
int p= 200;
int main() {
p1();
printf("%p\n",&d);
printf("%f,%d",d,p);
}
p1.c
#include <stdio.h>
int d;
void p1(void) {
printf("%p\n",&d);
d =0xffffffff;
}

我们这样搞是不会报错的(实际上我在gcc11上报了multiple definition of d的错了,因为他默认不用common,可以通过-fcommon启用这个机制,之后也没有报错)

grxer@Ubuntu16 /m/h/S/NEMU> gcc -m32  test.c p1.c
grxer@Ubuntu16 /m/h/S/NEMU> ./a.out
0x804a020
0x804a020
666666.500000,200

当然我们的弱符号的字节大小是不能大于强符号的否则在ld链接过程有一个warning

test.c
#include <stdio.h>
void p1(void);
int d = 100;
int p= 200;
int main() {
p1();
printf("%p\n",&d);
printf("%x,%x",d,p);
}
p1.c
#include <stdio.h>
double d;
void p1(void) {
printf("%p\n",&d);
d =1.0;
}
grxer@Ubuntu16 /m/h/S/NEMU> gcc -m32  test.c p1.c
/usr/bin/ld: Warning: alignment 4 of symbol `d' in /tmp/ccEf1boS.o is smaller than 8 in /tmp/cc2ZKeXO.o
grxer@Ubuntu16 /m/h/S/NEMU> ./a.out
0x804a01c
0x804a01c
0,3ff00000

gcc提供给我们__attribute__((weak))来来定义任何一个符号为弱符号。

_attribute_((weak)) double d这样就消除了这条warning

但是这个输出显然是有问题的,d和p都变得奇怪起来

这个就是链接造成的,p1.c编译成一个可重定位模块时,他根本不会知道自己以后会干什么,在他眼里double d就是全部,所以他对d的赋值完全是按照double来的

但是到了链接的时候,对d的赋值是对test.c的int d强符号,用给double赋值的code对int 赋值

08048460 <p1>:
8048460: 55 push %ebp
8048461: 89 e5 mov %esp,%ebp
8048463: 83 ec 08 sub $0x8,%esp
8048466: 83 ec 08 sub $0x8,%esp
8048469: 68 1c a0 04 08 push $0x804a01c
804846e: 68 1a 85 04 08 push $0x804851a
8048473: e8 68 fe ff ff call 80482e0 <printf@plt>
8048478: 83 c4 10 add $0x10,%esp
804847b: d9 e8 fld1
804847d: dd 1d 1c a0 04 08 fstpl 0x804a01c
8048483: 90 nop
8048484: c9 leave
8048485: c3 ret

fld1把1这个浮点常数压倒x87栈的st0,fstpl再把他取出来放到&d

double的1就是 3FF0000000000000

image-20230429234155494

这种bug如果在一个大型项目里找起来,那真的是栓q,所以extern很有必要

与之对应的还有强引用和弱引用 _attribute_ (weakref)来修饰

弱引用可以在找不到定义的时候链接成功,但是在执行时他的值为0

这类强弱机制存在的意义是在库里实现一些非常规的自定义操作

符号问题还有c++的符号修饰,c++的函数重载名称空间等依靠这个机制,c++filt可以帮我们解析这些修饰过的符号

grxer@Ubuntu16 /m/h/s/NEMU> c++filt _ZN3foo3barE
foo::bar

为了和c兼容可以用extern “c” int x; extern ”c” { intx ;inty} 来不修饰符号

一些c里函数为了在c++不被当初c++函数修饰导致无法使用,头文件里采用了#ifdef宏来处理

#ifdef _cplusplus
extern “c” {
#endif
void *memset(void *,int,size_t);
#ifdef _cplusplus
}
#endif

说了这么多关于符号的东西,看看符号地址是怎么确定的,符号是怎么解析的

符号地址

符号在各个段里的偏移是确定的,我们linker把各个段拼接并分配虚拟地址,段基址我们自然知道,再加上原本的偏移就可以确定了

符合解析

链接器维护了三个集合E U D

  • E 将被合并组成可执行文件的可重定位文件集合
  • U 当前引用但未解析的符合集合
  • D 当前所有定义符号的集合

会从左到右扫描命令行出现的可重定位目标文件和静态库文件

当扫描到可重定位目标文件时 把它定义的符号加入D集合,引用单未定义的加入U

扫描到静态链接库时,依次检测库里的目标文件是不是包含U里面的未定义符号,包含则把这个符号从U里拿出来放入D,并把这个目标文件加入E,不包含直接丢弃该目标文件

扫描完全部时U是非空,链接器会报错终止,否则就开始合并E集合里的文件

这也就要求了我们静态链接时库的顺序问题,一定库放到最后,库之间如果有引用,还要调整一下顺序

重定位

a.c中引用的 share和swap在编译器编译时是不确定的

grxer@Ubuntu16 /m/h/s/l/chapter4> objdump -dS a.o

a.o: file format elf32-i386


Disassembly of section .text:

00000000 <main>:
extern int shared;
int main()
{
0: 8d 4c 24 04 lea 0x4(%esp),%ecx
4: 83 e4 f0 and $0xfffffff0,%esp
7: ff 71 fc pushl -0x4(%ecx)
a: 55 push %ebp
b: 89 e5 mov %esp,%ebp
d: 51 push %ecx
e: 83 ec 14 sub $0x14,%esp
int a = 6;
11: c7 45 f4 06 00 00 00 movl $0x6,-0xc(%ebp)
swap(&a, &shared);
18: 83 ec 08 sub $0x8,%esp
1b: 68 00 00 00 00 push $0x0
20: 8d 45 f4 lea -0xc(%ebp),%eax
23: 50 push %eax
24: e8 fc ff ff ff call 25 <main+0x25>
29: 83 c4 10 add $0x10,%esp
2c: b8 00 00 00 00 mov $0x0,%eax
}
31: 8b 4d fc mov -0x4(%ebp),%ecx
34: c9 leave
35: 8d 61 fc lea -0x4(%ecx),%esp
38: c3 ret
1b:   68 00 00 00 00          push   $0x0 ;对shared引用
24: e8 fc ff ff ff call 25 <main+0x25>;对swap引用

就在刚刚我们才解析了符号的地址,所以现在我们需要利用解析的符号地址把代码里引用的地址值给修正了,

我们怎么知道要去修正谁,那里修正,怎么修正,这一切都在.rel.xxx重定位表里

typedef struct
{
Elf32_Addr r_offset; /* Address */
Elf32_Word r_info; /* Relocation type and symbol index */
} Elf32_Rel;

r_offset表示要修正的位置第一个字节相对于这个段的偏移,作为linker我们知道段基址,自然可以找到要修正地址

r_info 低8位表示要重定位类型,高24位是符号在符号表里的下标

grxer@Ubuntu16 /m/h/s/l/chapter4> readelf -r a.o

Relocation section '.rel.text' at offset 0x1b0 contains 2 entries:
Offset Info Type Sym.Value Sym. Name
0000001c 00000901 R_386_32 00000000 shared
00000025 00000a02 R_386_PC32 00000000 swap

Info高24位 9和a正好对应

readelf里的Sym. Name表示要用哪个符号修正

Num:    Value  Size Type    Bind   Vis      Ndx Name
9: 00000000 0 NOTYPE GLOBAL DEFAULT UND shared
10: 00000000 0 NOTYPE GLOBAL DEFAULT UND swap

offset 1c和25对应要修改的位置

1b:   68 00 00 00 00          push   $0x0 ;对shared引用
24: e8 fc ff ff ff call 25 <main+0x25>;对swap引用

重定位类型类型有好多种,不同平台也可能不一样 https://bbs.kanxue.com/thread-246373.htm

这里就看两种最基本最常用的

R_386_32 绝对寻址修正

R_386_PC32 相对寻址修正

这个时候我们已经知道了符号的实际地址S和保存在被修正位置的原值A(偏移)

绝对寻址,直接把实际地址S+被修正位置的原值A填入offset就可以

相对寻址 比上面多了减去被修正位置P,得到偏移的过程 S+A-P

重定位后的main

08048094 <main>:
8048094: 8d 4c 24 04 lea 0x4(%esp),%ecx
8048098: 83 e4 f0 and $0xfffffff0,%esp
804809b: ff 71 fc pushl -0x4(%ecx)
804809e: 55 push %ebp
804809f: 89 e5 mov %esp,%ebp
80480a1: 51 push %ecx
80480a2: 83 ec 14 sub $0x14,%esp
80480a5: c7 45 f4 06 00 00 00 movl $0x6,-0xc(%ebp)
80480ac: 83 ec 08 sub $0x8,%esp
80480af: 68 6c 91 04 08 push $0x804916c
80480b4: 8d 45 f4 lea -0xc(%ebp),%eax
80480b7: 50 push %eax
80480b8: e8 10 00 00 00 call 80480cd <swap>
80480bd: 83 c4 10 add $0x10,%esp
80480c0: b8 00 00 00 00 mov $0x0,%eax
80480c5: 8b 4d fc mov -0x4(%ebp),%ecx
80480c8: c9 leave
80480c9: 8d 61 fc lea -0x4(%ecx),%esp
80480cc: c3 ret
     解析后的符号表
7: 080480cd 57 FUNC GLOBAL DEFAULT 1 swap
8: 0804916c 4 OBJECT GLOBAL DEFAULT 3 shared

80480af: 68 6c 91 04 08 push $0x804916c;对shared引用
80480b8: e8 10 00 00 00 call 80480cd <swap>;对swap引用

6c 91 04 08 即0x804916c shared的地址

10 00 00 00 即0x00000010 0x80480b8+0x5+0x10=0x80480CD

相对寻址为什么原来里面偏移要填-4呢,即fffffffc(-4)?

修正的时候是按照重定位表里的当前要修正位置

程序执行是相对寻址是相对于eip的,相对于的是当前指令的下一条指令

修正时S+A-P相当于s-(p+4),p+4即下一条指令,就统一起来