从静态链接到动态链接,彻底理解ELF

提示:文章写完后,目录可以自动生成,如何生成可参考右边的帮助文档

文章目录

目录

 一. 编译的过程

 二. 静态链接



 一. 编译的过程

GCC 编译分为4个步骤, 预处理,编译,汇编和链接,下面以hello_world为例:

#include <stdio.h>

int main (int argc, char *argv[])
{
    printf("hello world");
}

预处理 (-E):

gcc  -E hello_world.c -o hello_world.i //生成预编译文件,替换宏定义和一些路径

编译 (-S)

gcc -S hello_world.i -o hello_world.s //生成汇编文件

汇编 (-o)

gcc -o hello_world.s -o hello_world.o //生成目标文件

链接 (ld):

ld hello_world.o -e main -o hello_world  

这句话编译器会报错误,因为printf是外部函数,链接器执行的时候需要链接所有相关的.o文件, 我们可以通过gcc -v参数去看正常编译链接的全过程。

gcc hello_world.c -o hello_world --> 这条命令等于上面的四条命令

cc -v hello_world.c -o hello_world
Using built-in specs.
Target: x86_64-redhat-linux
Configured with: ../configure --prefix=/usr --mandir=/usr/share/man --infodir=/usr/share/info --with-bugurl=http://bugzilla.redhat.com/bugzilla --enable-bootstrap --enable-shared --enable-threads=posix --enable-checking=release --with-system-zlib --enable-__cxa_atexit --disable-libunwind-exceptions --enable-gnu-unique-object --enable-languages=c,c++,objc,obj-c++,java,fortran,ada --enable-java-awt=gtk --disable-dssi --with-java-home=/usr/lib/jvm/java-1.5.0-gcj-1.5.0.0/jre --enable-libgcj-multifile --enable-java-maintainer-mode --with-ecj-jar=/usr/share/java/eclipse-ecj.jar --disable-libjava-multilib --with-ppl --with-cloog --with-tune=generic --with-arch_32=i686 --build=x86_64-redhat-linux
Thread model: posix
gcc version 4.4.7 20120313 (Red Hat 4.4.7-23) (GCC) 
COLLECT_GCC_OPTIONS='-v' '-o' 'hello_world' '-mtune=generic'
 /usr/libexec/gcc/x86_64-redhat-linux/4.4.7/cc1 -quiet -v hello_world.c -quiet -dumpbase hello_world.c -mtune=generic -auxbase hello_world -version -o /tmp/cc4wYAxQ.s
ignoring nonexistent directory "/usr/lib/gcc/x86_64-redhat-linux/4.4.7/include-fixed"
ignoring nonexistent directory "/usr/lib/gcc/x86_64-redhat-linux/4.4.7/../../../../x86_64-redhat-linux/include"
#include "..." search starts here:
#include <...> search starts here:
 /usr/local/include
 /usr/lib/gcc/x86_64-redhat-linux/4.4.7/include
 /usr/include
End of search list.
GNU C (GCC) version 4.4.7 20120313 (Red Hat 4.4.7-23) (x86_64-redhat-linux)
    compiled by GNU C version 4.4.7 20120313 (Red Hat 4.4.7-23), GMP version 4.3.1, MPFR version 2.4.1.
GGC heuristics: --param ggc-min-expand=100 --param ggc-min-heapsize=131072
Compiler executable checksum: f1e04b41791fd0c9eea88d5989031e7d
COLLECT_GCC_OPTIONS='-v' '-o' 'hello_world' '-mtune=generic'
 as -V -Qy -o /tmp/ccIvLpKe.o /tmp/cc4wYAxQ.s
GNU assembler version 2.20.51.0.2 (x86_64-redhat-linux) using BFD version version 2.20.51.0.2-5.48.el6_10.1 20100205
COMPILER_PATH=/usr/libexec/gcc/x86_64-redhat-linux/4.4.7/:/usr/libexec/gcc/x86_64-redhat-linux/4.4.7/:/usr/libexec/gcc/x86_64-redhat-linux/:/usr/lib/gcc/x86_64-redhat-linux/4.4.7/:/usr/lib/gcc/x86_64-redhat-linux/:/usr/libexec/gcc/x86_64-redhat-linux/4.4.7/:/usr/libexec/gcc/x86_64-redhat-linux/:/usr/lib/gcc/x86_64-redhat-linux/4.4.7/:/usr/lib/gcc/x86_64-redhat-linux/
LIBRARY_PATH=/usr/lib/gcc/x86_64-redhat-linux/4.4.7/:/usr/lib/gcc/x86_64-redhat-linux/4.4.7/:/usr/lib/gcc/x86_64-redhat-linux/4.4.7/../../../../lib64/:/lib/../lib64/:/usr/lib/../lib64/:/usr/lib/gcc/x86_64-redhat-linux/4.4.7/../../../:/lib/:/usr/lib/
COLLECT_GCC_OPTIONS='-v' '-o' 'hello_world' '-mtune=generic'
 /usr/libexec/gcc/x86_64-redhat-linux/4.4.7/collect2 --eh-frame-hdr --build-id -m elf_x86_64 --hash-style=gnu -dynamic-linker /lib64/ld-linux-x86-64.so.2 -o hello_world /usr/lib/gcc/x86_64-redhat-linux/4.4.7/../../../../lib64/crt1.o /usr/lib/gcc/x86_64-redhat-linux/4.4.7/../../../../lib64/crti.o /usr/lib/gcc/x86_64-redhat-linux/4.4.7/crtbegin.o -L/usr/lib/gcc/x86_64-redhat-linux/4.4.7 -L/usr/lib/gcc/x86_64-redhat-linux/4.4.7 -L/usr/lib/gcc/x86_64-redhat-linux/4.4.7/../../../../lib64 -L/lib/../lib64 -L/usr/lib/../lib64 -L/usr/lib/gcc/x86_64-redhat-linux/4.4.7/../../.. /tmp/ccIvLpKe.o -lgcc --as-needed -lgcc_s --no-as-needed -lc -lgcc --as-needed -lgcc_s --no-as-needed /usr/lib/gcc/x86_64-redhat-linux/4.4.7/crtend.o /usr/lib/gcc/x86_64-redhat-linux/4.4.7/../../../../lib64/crtn.o
 

加粗部分CC1相当于编译 gcc -S, collect2 相当于链接 ld, 我们可以看到链接除了需要hello_world.o之外还需要其他的库和一些.o文件。

 二. 静态链接

在一个大型的项目里,我们会有很多.c文件, 但最终经过编译器编译之后只会有一个可执行文件,因此编译器做的主要工作就是将同一个项目下的所有目标文件(.o) 链接成最终的可执行文件。

2.1 目标文件

再讲静态链接之前需要简单了解下什么是目标文件, 目标文件就是.o文件, 由.c文件编译而成,每个.c文件都有自己对应的.o文件。

这次我们以Lib.c 和 Program1.c 为例:

/* a.c */                                         /* b.c */
extern int shared;                                int shared
                                                             
int main()                                        void swap (int *a, int *b)
{                                                 {
    int a = 100;                                      *a ^= *b ^= *a ^= *b;     
    swap (&a, &shared);                                    
}                                                 }

首先分别编译两个.c问件生成对应的.o文件:

gcc -c a.c -o a.o

gcc -c b.c  -o  b.o

objdump -h a.o 

Sections:
Idx Name          Size      VMA               LMA               File off  Algn
  0 .text         00000027  0000000000000000  0000000000000000  00000040  2**2
                  CONTENTS, ALLOC, LOAD, RELOC, READONLY, CODE
  1 .data         00000000  0000000000000000  0000000000000000  00000068  2**2
                  CONTENTS, ALLOC, LOAD, DATA
  2 .bss          00000000  0000000000000000  0000000000000000  00000068  2**2
                  ALLOC
  3 .comment      0000002e  0000000000000000  0000000000000000  00000068  2**0
                  CONTENTS, READONLY
  4 .note.GNU-stack 00000000  0000000000000000  0000000000000000  00000096  2**0
                  CONTENTS, READONLY
  5 .eh_frame     00000038  0000000000000000  0000000000000000  00000098  2**3
                  CONTENTS, ALLOC, LOAD, RELOC, READONLY, DATA

objdump -h b.o

Sections:
Idx Name          Size      VMA               LMA               File off  Algn
  0 .text         0000004c  0000000000000000  0000000000000000  00000040  2**2
                  CONTENTS, ALLOC, LOAD, READONLY, CODE
  1 .data         00000004  0000000000000000  0000000000000000  0000008c  2**2
                  CONTENTS, ALLOC, LOAD, DATA
  2 .bss          00000000  0000000000000000  0000000000000000  00000090  2**2
                  ALLOC
  3 .comment      0000002e  0000000000000000  0000000000000000  00000090  2**0
                  CONTENTS, READONLY
  4 .note.GNU-stack 00000000  0000000000000000  0000000000000000  000000be  2**0
                  CONTENTS, READONLY
  5 .eh_frame     00000038  0000000000000000  0000000000000000  000000c0  2**3
                  CONTENTS, ALLOC, LOAD, RELOC, READONLY, DATA
 

链接a,o和 b.o, 这次就可以连接成功,因为我们的例子中没有引用其他外部函数,比如标准C库函数 printf 或stdio.h 头文件,因此整个链接过程只需要a.o 和 b.o参与。

ld a.o b.o -e main -o ab

objdump -h ab

Sections:
Idx Name          Size      VMA               LMA               File off  Algn
  0 .text         00000074  00000000004000e8  00000000004000e8  000000e8  2**2
                  CONTENTS, ALLOC, LOAD, READONLY, CODE
  1 .eh_frame     00000058  0000000000400160  0000000000400160  00000160  2**3
                  CONTENTS, ALLOC, LOAD, READONLY, DATA
  2 .data         00000004  00000000006001b8  00000000006001b8  000001b8  2**2
                  CONTENTS, ALLOC, LOAD, DATA
  3 .comment      0000002d  0000000000000000  0000000000000000  000001bc  2**0
                  CONTENTS, READONLY

下面开始做一个简单的解释,在.o文件和可执行文件中我们都看到了.text段和 .data段,我们称之为section, 区别就是.o文件中的各个section VMA (虚拟地址)LMA(加载地址)是有值的,因此我们可以知道连接器会在链接完成之后确定程序的虚拟地址,这个虚拟地址可以理解成实际程序运行所在的地址空间。

因此链接的目的就是将各个.o中的相同section合并到一起,至于怎么合并,因为每个.o文件中都会引用一些变量,因此每个.o文件里还有一个叫符号表的东西,连接器根据各个.o文件中的符号表最终链接生成可执行文件。

下图摘自程序员的自我修养, 我们可以看到 .o文件中只有文件偏移的概念,图中的text段是0x34, 但我们这边是0x40

2.2.符号解析重定位

这节主要简单描述下连接器连接成 ab可执行文件的符号表解析过程, 上例子

objdump -d a.o 

Disassembly of section .text:

0000000000000000 <main>:
   0:    55                       push   %rbp
   1:    48 89 e5                 mov    %rsp,%rbp
   4:    48 83 ec 10              sub    $0x10,%rsp
   8:    c7 45 fc 64 00 00 00     movl   $0x64,-0x4(%rbp)
   f:     48 8d 45 fc              lea    -0x4(%rbp),%rax
  13:    be 00 00 00 00           mov    $0x0,%esi
  18:    48 89 c7                 mov    %rax,%rdi
  1b:    b8 00 00 00 00           mov    $0x0,%eax
  20:    e8 00 00 00 00           callq  25 <main+0x25>
  25:    c9                       leaveq 
  26:    c3                       retq  

看我加粗的部分,首先所有.o文件都是从虚拟地址0开始的,因为这时还不是可执行文件, 第二点看第13条指令"be 00 00 00 00",这条指令是上面的a,c中shared赋值语句,但因为shared是在b.c中定义的,因此这里先用 0代替, 第20条调转指令一样,因为swap是外部函数,目前不知道地址,因此也用0代替。

再来看最终的可执行文件ab

objdump -d ab

Disassembly of section .text:

00000000004000e8 <main>:
  4000e8:    55                       push   %rbp
  4000e9:    48 89 e5                 mov    %rsp,%rbp
  4000ec:    48 83 ec 10              sub    $0x10,%rsp
  4000f0:    c7 45 fc 64 00 00 00     movl   $0x64,-0x4(%rbp)
  4000f7:    48 8d 45 fc              lea    -0x4(%rbp),%rax
  4000fb:    be b8 01 60 00           mov    $0x6001b8,%esi
  400100:    48 89 c7                 mov    %rax,%rdi
  400103:    b8 00 00 00 00           mov    $0x0,%eax
  400108:    e8 03 00 00 00           callq  400110 <swap>
  40010d:    c9                       leaveq 
  40010e:    c3                       retq   
  40010f:    90                       nop

0000000000400110 <swap>:
  400110:    55                       push   %rbp
  400111:    48 89 e5                 mov    %rsp,%rbp
  400114:    53                       push   %rbx
  400115:    48 89 7d f0              mov    %rdi,-0x10(%rbp)
  400119:    48 89 75 e8              mov    %rsi,-0x18(%rbp)
  40011d:    48 8b 45 f0              mov    -0x10(%rbp),%rax
  400121:    8b 10                    mov    (%rax),%edx
  400123:    48 8b 45 e8              mov    -0x18(%rbp),%rax
  400127:    8b 08                    mov    (%rax),%ecx
  400129:    48 8b 45 f0              mov    -0x10(%rbp),%rax
  40012d:    8b 18                    mov    (%rax),%ebx
  40012f:    48 8b 45 e8              mov    -0x18(%rbp),%rax
  400133:    8b 00                    mov    (%rax),%eax
  400135:    31 c3                    xor    %eax,%ebx
  400137:    48 8b 45 f0              mov    -0x10(%rbp),%rax
  40013b:    89 18                    mov    %ebx,(%rax)
  40013d:    48 8b 45 f0              mov    -0x10(%rbp),%rax
  400141:    8b 00                    mov    (%rax),%eax
  400143:    31 c1                    xor    %eax,%ecx
  400145:    48 8b 45 e8              mov    -0x18(%rbp),%rax
  400149:    89 08                    mov    %ecx,(%rax)
  40014b:    48 8b 45 e8              mov    -0x18(%rbp),%rax
  40014f:    8b 00                    mov    (%rax),%eax
  400151:    31 c2                    xor    %eax,%edx
  400153:    48 8b 45 f0              mov    -0x10(%rbp),%rax
  400157:    89 10                    mov    %edx,(%rax)
  400159:    5b                       pop    %rbx
  40015a:    c9                       leaveq 
  40015b:    c3                       retq   
 

我们可以看到可执行文件ab包含swap和main连个函数,并且虚拟地址不再是0,而是 4000e8,

在main数中   "4000fb:    be b8 01 60 00           mov    $0x6001b8,%esi"

比较a.o中  : 13:         be 00 00 00 00           mov    $0x0,%esi
shared已经有值了(0x6001b8), 具体这个值是如何算出来的,我们可以理解为连接器内部(LD)有自己算法,当他加载各个.o文件的时候,会读取各个文件里面一个叫符号表的section,然后合并每个.o文件中相同的段,并解析符号表来确定最终的合并地址,这里仅限于静态链接,动态链接要复杂写,稍后再讲。

2.3 符号表

可以先用nm查看符号结果

nm a.o

0000000000000000 T main
                 U shared
                 U swap

符号表也是文件中的一个段,section名叫symtab,

readelf -s a.o

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

简单说下, main前面那个1代表 section 1,section 1也就是代码段,代表main位于代码段

a.o 全部section, 我们可以看到.text是在 1. GLOBA 代表全局变量, UND 就是undefine,证明符号是个外部符号。

readelf -S a.o

Section Headers:
  [Nr] Name              Type             Address           Offset
       Size              EntSize          Flags  Link  Info  Align
  [ 0]                   NULL             0000000000000000  00000000
       0000000000000000  0000000000000000           0     0     0
  [ 1] .text             PROGBITS         0000000000000000  00000040
       0000000000000027  0000000000000000  AX       0     0     4
  [ 2] .rela.text        RELA             0000000000000000  00000550
       0000000000000030  0000000000000018          10     1     8
  [ 3] .data             PROGBITS         0000000000000000  00000068
       0000000000000000  0000000000000000  WA       0     0     4
  [ 4] .bss              NOBITS           0000000000000000  00000068
       0000000000000000  0000000000000000  WA       0     0     4
  [ 5] .comment          PROGBITS         0000000000000000  00000068
       000000000000002e  0000000000000001  MS       0     0     1
  [ 6] .note.GNU-stack   PROGBITS         0000000000000000  00000096
       0000000000000000  0000000000000000           0     0     1
  [ 7] .eh_frame         PROGBITS         0000000000000000  00000098
       0000000000000038  0000000000000000   A       0     0     8
  [ 8] .rela.eh_frame    RELA             0000000000000000  00000580
       0000000000000018  0000000000000018          10     7     8
  [ 9] .shstrtab         STRTAB           0000000000000000  000000d0
       0000000000000059  0000000000000000           0     0     1
  [10] .symtab           SYMTAB           0000000000000000  00000430
       0000000000000108  0000000000000018          11     8     8
  [11] .strtab           STRTAB           0000000000000000  00000538
       0000000000000016  0000000000000000           0     0     1
 

那么连接器是怎么知道哪些指令是要被调整的呢 ? 这些指令那部分要调整,怎么调整 ?

事实上 ELF文件中有个重定位表的结构用来保存所有重定位信息:

objdump -r a.o

RELOCATION RECORDS FOR [.text]:
OFFSET           TYPE              VALUE 
0000000000000014 R_X86_64_32       shared
0000000000000021 R_X86_64_PC32     swap-0x0000000000000004


RELOCATION RECORDS FOR [.eh_frame]:
OFFSET           TYPE              VALUE 
0000000000000020 R_X86_64_PC32     .text
 

OFFSET 14 21就是需要重定位的位置, 对照a.o反汇编:正好对应 shared赋值和call swap函数。

objdump -d a.o 

Disassembly of section .text:

0000000000000000 <main>:
   0:    55                       push   %rbp
   1:    48 89 e5                 mov    %rsp,%rbp
   4:    48 83 ec 10              sub    $0x10,%rsp
   8:    c7 45 fc 64 00 00 00     movl   $0x64,-0x4(%rbp)
   f:     48 8d 45 fc              lea    -0x4(%rbp),%rax
  13:    be 00 00 00 00           mov    $0x0,%esi
  18:    48 89 c7                 mov    %rax,%rdi
  1b:    b8 00 00 00 00           mov    $0x0,%eax
  20:    e8 00 00 00 00           callq  25 <main+0x25>
  25:    c9                       leaveq 
  26:    c3                       retq  

总结: 之所以链接是因为我们目标文件中用到的符号被定义在其他目标文件, 所以要把他们链接起来, 连接器会加载各个.o文件读取他们的重定位表,其实重定位过程也是符号解析过程,每个目标文件都可以定义一些符号,也可能引用到另一在其他目标文件的符号,重定位过程,每个重定位入口都对应一个符号引用,这时连接器就回去查找由所有目标文件的符号表组恒的全局符号表,找到相应的符号进行重定位。

根据符号表 "readelf -s a.o" 进行指令修正。

2.4 如何根据符号表重定位表进行指令修正


  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值