• 程序员的自我修养-编译链接


    常见场景

    你是在工作中遇到如下问题或者疑问:

    1. undefined reference to “function”。链接过程中出现未定义引用。
    2. .a和.so文件分别是什么?什么情况下使用?
    3. extern "C"有什么作用?
      等等...

    编译过程

    我们平时编译,如果没有加任何编译参数将默认执行预处理,编译,汇编,链接等步骤。

    ELF文件格式

    每一个cpp文件会生成一个.o文件。.o文件里面有什么信息?多个.o文件如何合并成一个可执行文件。可执行文件的文件里有都有什么信息?
    看下下面的例子:

    int global_init_var = 84;
    int global_uninit_var;
    
    void func1(int i) {
        printf("%d\n", i);
    }
    
    int main() {
        static int static_var = 85;
        static int static_var2;
        int a = 1;
        int b;
        func1(static_var + static_var2 + a + b);
        return 0;
    }
    

    为了探究.o文件内容,只编译不链接gcc -c whats_in_elf.c -o whats_in_elf.o
    ELF可以用objdump,readelf等工具查看内容。这里用readelf -S whats_in_elf.o查看section headers:

    # daihaonan link_load $ readelf -S whats_in_elf.o
    There are 11 section headers, starting at offset 0x114:
    
    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 000051 00  AX  0   0  4
      [ 2] .rel.text         REL             00000000 000424 000028 08      9   1  4
      [ 3] .data             PROGBITS        00000000 000088 000008 00  WA  0   0  4
      [ 4] .bss              NOBITS          00000000 000090 000004 00  WA  0   0  4
      [ 5] .rodata           PROGBITS        00000000 000090 000004 00   A  0   0  1
      [ 6] .comment          PROGBITS        00000000 000094 00002d 01  MS  0   0  1
      [ 7] .note.GNU-stack   PROGBITS        00000000 0000c1 000000 00      0   0  1
      [ 8] .shstrtab         STRTAB          00000000 0000c1 000051 00      0   0  1
      [ 9] .symtab           SYMTAB          00000000 0002cc 0000f0 10     10  10  4
      [10] .strtab           STRTAB          00000000 0003bc 000065 00      0   0  1
    Key to Flags:
      W (write), A (alloc), X (execute), M (merge), S (strings)
      I (info), L (link order), G (group), x (unknown)
      O (extra OS processing required) o (OS specific), p (processor specific)
    

    可以看到.o文件由很多section组成,每个section都有size, file off等描述其在文件内位置的属性。元信息记录在File header中,其中有e_shoff字段指向Section Header Table,Section Header Table是个数组结构保存每个Section信息。
    查看Header:

    .o文件总体格式如下:

    当然还有很多其它section,.text,.data,.rodata,.symtab,.rel.text段是最主要的段,分别保存代码信息,全局数据,全局只读数据,符号表,代码段重定位表。

    .text Section

    将代码反汇编objdump -s -d whats_in_elf.o

    # daihaonan link_load $ objdump -d whats_in_elf.o
    
    whats_in_elf.o:     file format elf64-x86-64
    
    
    Disassembly of section .text:
    
    0000000000000000 :
       0:	55                   	push   %rbp
       1:	48 89 e5             	mov    %rsp,%rbp
       4:	48 83 ec 10          	sub    $0x10,%rsp
       8:	89 7d fc             	mov    %edi,-0x4(%rbp)
       b:	8b 45 fc             	mov    -0x4(%rbp),%eax
       e:	89 c6                	mov    %eax,%esi
      10:	bf 00 00 00 00       	mov    $0x0,%edi
      15:	b8 00 00 00 00       	mov    $0x0,%eax
      1a:	e8 00 00 00 00       	callq  1f 
      1f:	c9                   	leaveq
      20:	c3                   	retq
    
    0000000000000021 
    : 21: 55 push %rbp 22: 48 89 e5 mov %rsp,%rbp 25: 48 83 ec 10 sub $0x10,%rsp 29: c7 45 f8 01 00 00 00 movl $0x1,-0x8(%rbp) 30: 8b 15 00 00 00 00 mov 0x0(%rip),%edx # 36 36: 8b 05 00 00 00 00 mov 0x0(%rip),%eax # 3c 3c: 8d 04 02 lea (%rdx,%rax,1),%eax 3f: 03 45 f8 add -0x8(%rbp),%eax 42: 03 45 fc add -0x4(%rbp),%eax 45: 89 c7 mov %eax,%edi 47: e8 00 00 00 00 callq 4c 4c: b8 00 00 00 00 mov $0x0,%eax 51: c9 leaveq 52: c3 retq

    可以看到func1和main两个函数的反汇编代码。
    顺便可以了解下gcc函数调用约定。

    规则如下:

    1. 执行call指令前,函数调用者将参数入栈,按照函数列表从右到左的顺序入栈。
    2. call指令会自动将当前eip入栈,ret指令将自动从栈中弹出该值到eip寄存器。
    3. 被调用函数负责:将ebp入栈,esp的值赋给ebp。所以反汇编一个函数会发现开头两个指令都是push %ebp, mov %esp,%ebp
      一个例子:

    .data和.rodat Section

    Contents of section .data:
     0000 54000000 55000000                    T...U...
    Contents of section .rodata:
     0000 25640a00                             %d..
    

    可以看到.data Section有8个字节,分别是0x54和0x55对应全局变量static_var和global_init_var。
    .rodata Section只有4个字节保存%d\n三个字符。

    从这里可以直观看到全局有初值的变量是会在ELF文件中分配空间的,而a,b这种栈上分配的变量不会ELF文件中分配空间,只会在运行到该函数的是在栈上动态分配。

    .symtab Section

    可以用readelf -s whats_in_elf.o查看符号表

    # daihaonan link_load $ readelf -s whats_in_elf.o
    
    Symbol table '.symtab' contains 16 entries:
       Num:    Value          Size Type    Bind   Vis      Ndx Name
         0: 0000000000000000     0 NOTYPE  LOCAL  DEFAULT  UND
         1: 0000000000000000     0 FILE    LOCAL  DEFAULT  ABS whats_in_elf.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    5
         6: 0000000000000004     4 OBJECT  LOCAL  DEFAULT    3 static_var.1600
         7: 0000000000000000     4 OBJECT  LOCAL  DEFAULT    4 static_var2.1601
         8: 0000000000000000     0 SECTION LOCAL  DEFAULT    7
         9: 0000000000000000     0 SECTION LOCAL  DEFAULT    8
        10: 0000000000000000     0 SECTION LOCAL  DEFAULT    6
        11: 0000000000000000     4 OBJECT  GLOBAL DEFAULT    3 global_init_var
        12: 0000000000000004     4 OBJECT  GLOBAL DEFAULT  COM global_uninit_var
        13: 0000000000000000    33 FUNC    GLOBAL DEFAULT    1 func1
        14: 0000000000000000     0 NOTYPE  GLOBAL DEFAULT  UND printf
        15: 0000000000000021    50 FUNC    GLOBAL DEFAULT    1 main
    

    从上面可以得到如下信息:

    1. 该.o文件中有static_var.1600,static_var2.1601,global_init_var,global_uninit_var,func1,printf,main等符号
    2. 每个符号在.o文件中的位置,比如func1,Ndx是1,对应.text Section,Value为0,Size为33,说明func1从.text Section起始字节开始,占了33个字节。
    3. printf这个符号在.o文件中并没有定义,所以它的Ndx是UND

    g++ whats_in_elf.c -o whats_in_elf2.o重新编译,会发现

    # daihaonan link_load $ readelf -s whats_in_elf2.o
    
    Symbol table '.symtab' contains 17 entries:
       Num:    Value          Size Type    Bind   Vis      Ndx Name
         0: 0000000000000000     0 NOTYPE  LOCAL  DEFAULT  UND
         1: 0000000000000000     0 FILE    LOCAL  DEFAULT  ABS whats_in_elf.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    5
         6: 0000000000000004     4 OBJECT  LOCAL  DEFAULT    3 _ZZ4mainE10static_var
         7: 0000000000000004     4 OBJECT  LOCAL  DEFAULT    4 _ZZ4mainE11static_var2
         8: 0000000000000000     0 SECTION LOCAL  DEFAULT    7
         9: 0000000000000000     0 SECTION LOCAL  DEFAULT    8
        10: 0000000000000000     0 SECTION LOCAL  DEFAULT    6
        11: 0000000000000000     4 OBJECT  GLOBAL DEFAULT    3 global_init_var
        12: 0000000000000000     4 OBJECT  GLOBAL DEFAULT    4 global_uninit_var
        13: 0000000000000000    33 FUNC    GLOBAL DEFAULT    1 _Z5func1i
        14: 0000000000000000     0 NOTYPE  GLOBAL DEFAULT  UND __gxx_personality_v0
        15: 0000000000000000     0 NOTYPE  GLOBAL DEFAULT  UND _Z6printfPKcz
        16: 0000000000000021    50 FUNC    GLOBAL DEFAULT    1 main
    

    原来的func1变成了_Z5func1i,为了防止符号冲突,C++引入了符号修饰的概念。
    所以在C++里如果希望动态库中某个函数能被正确加载,需要加上extern "C"方式符号被修饰,比如:

    extern "C"
    {
        ProcessorBase* create_processor(const std::string& processor_name)
        {
            ...
        }
    }
    

    加载该符号的地方才能正确找到create_processor这个符号。(PFUNC_CREATE_PROCESSOR_CALL)dlsym(handle,"create_processor");

    .rel.text Section

    对于可重定位的ELF文件,必须包含重定位Section,一个ELF文件中可能有多个重定位Section,比如.text有需要重定位的地方,那么会有一个.rel.text表,详细见下文。

    静态链接

    为什么需要链接?

    考虑如下程序:
    a.c

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

    b.c

    int shared = 1;
    
    void swap(int* a, int* b) {
        *a ^= *b ^= *a ^= *b;
    }
    

    分别将a.c和b.c进行编译,然后查看代码段反汇编。

    # daihaonan link_load $ objdump -d a.o
    
    a.o:     file format elf64-x86-64
    
    
    Disassembly of section .text:
    
    0000000000000000 
    : 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: 8b 15 00 00 00 00 mov 0x0(%rip),%edx # 15 15: 8b 45 fc mov -0x4(%rbp),%eax 18: 89 d6 mov %edx,%esi 1a: 89 c7 mov %eax,%edi 1c: b8 00 00 00 00 mov $0x0,%eax 21: e8 00 00 00 00 callq 26 26: c9 leaveq 27: c3 retq

    main中会引用全局变量shared,调用swap函数,但是shared和swap都不是定义在a.o中的,而是定义在b.o中。所以a.o中对shared的引用为0x0(%rip),%rip寄存器中保存的是当前执行指令的地址,对swap调用为e8 00 00 00 00,这是一条近址相对位移调用指令,e8是指令码,00 00 00 00是操作数,也就是被调用函数相对于调用指令的下一条指令的偏移量。这里因为不知道swap函数在哪,所以暂时用00 00 00 00来代替。

    所以我们可以得出链接的一个主要作用是对一些全局变量,函数引用指令进行修正。

    链接后达到什么效果?

    将a.o和b.o链接在一起。ld a.o b.o -e main -o ab
    然后再来看下ab中main的反汇编代码

    # daihaonan link_load $ objdump -S ab
    
    ab:     file format elf64-x86-64
    
    
    Disassembly of section .text:
    
    00000000004000e8 
    : 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: 8b 15 bb 00 20 00 mov 0x2000bb(%rip),%edx # 6001b8 4000fd: 8b 45 fc mov -0x4(%rbp),%eax 400100: 89 d6 mov %edx,%esi 400102: 89 c7 mov %eax,%edi 400104: b8 00 00 00 00 mov $0x0,%eax 400109: e8 02 00 00 00 callq 400110 40010e: c9 leaveq 40010f: c3 retq

    链接后再来看main函数的反汇编代码。有三个地方变动了mov 0x0(%rip),%edx变成了mov 0x2000bb(%rip),%edxe8 00 00 00 00变成了e8 02 00 00 00。最左侧的地址变成了全局的虚拟地址,这说明链接还会分配虚拟地址空间,链接结束,每个函数,每个全局变量在虚拟地址空间内的地址就确定了。
    callq下一条指令地址为0x40010e再加上0x02,等于0x400110。所以swap函数代码起始地址应该是0x400110。用objdump -S ab来验证下。

    0000000000400110 :
      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
    

    果然swap起始地址是0x400110。

    a.o+b.o到ab的过程大致如下图:

    第一步对a.o和b.o相同的Section进行并合。
    第二步将ab映射到进行虚拟地址空间,并确定各符号在进行虚拟地址空间中的地址。
    第三步修正各符号引用,使其指向符号最终的地址。

    怎么链接?

    链接一般分为两步:

    1. 空间和地址分配。扫码所有输入目标文件,搜集符号定义和引用,放到全局符号表,并对Section进行并合。
    2. 符号解析和重定位。

    符号重定位依赖重定位表+符号表

    # daihaonan link_load $ objdump -r a.o
    
    a.o:     file format elf64-x86-64
    
    RELOCATION RECORDS FOR [.text]:
    OFFSET           TYPE              VALUE
    0000000000000011 R_X86_64_PC32     shared-0x0000000000000004
    0000000000000022 R_X86_64_PC32     swap-0x0000000000000004
    

    重定位表中记录了哪些地方需要修正,这里可以看到.text的0x11偏移处引用了shared变量,所以需要修正,.text的0x22偏移处引用了swap函数,也需要修正,
    而.symtab Section记录了符号所在的位置。

    # daihaonan link_load $ readelf -s b.o
    
    Symbol table '.symtab' contains 10 entries:
       Num:    Value          Size Type    Bind   Vis      Ndx Name
         0: 0000000000000000     0 NOTYPE  LOCAL  DEFAULT  UND
         1: 0000000000000000     0 FILE    LOCAL  DEFAULT  ABS b.c
         2: 0000000000000000     0 SECTION LOCAL  DEFAULT    1
         3: 0000000000000000     0 SECTION LOCAL  DEFAULT    2
         4: 0000000000000000     0 SECTION LOCAL  DEFAULT    3
         5: 0000000000000000     0 SECTION LOCAL  DEFAULT    5
         6: 0000000000000000     0 SECTION LOCAL  DEFAULT    6
         7: 0000000000000000     0 SECTION LOCAL  DEFAULT    4
         8: 0000000000000000     4 OBJECT  GLOBAL DEFAULT    2 shared
         9: 0000000000000000    76 FUNC    GLOBAL DEFAULT    1 swap
    

    链接器有了这俩信息,可以轻而易举完成符号重定位。

    动态链接

    静态链接VS动态链接

    动态链接优点:

    1. 方便发布。模块A依赖模块B,如果模块B实现发生了改变,在静态链接的情况下,模块A需要重新编译。
    2. 内存占用。模块A和模块B都依赖模块C中的某个函数func,在果静态链接的情况下,模块A/B同时运行时,func需要在内存中存在两份。

    动态链接缺点:

    1. 执行效率不如静态链接高。

    动态链接效果

    静态共享库

    如图假设A.so又依赖B.so中的a变量和foo函数,当调用foo的时候,动态链接器会将B.so加载到内存load_address处,foo在B.so内是固定的y字节偏移出。所以foo在进程内的虚拟地址就是load_address+y。然后动态链接器修改A.so中call foo指令出代码,将foo地址修改为load_address+y。至此动态链接完成。和静态链接的区别在于动态链接将地址重定位推迟到了运行时。

    动态共享库

    上面这种静态共享库有个问题,就是指令部分没法在多个进程之间共享,从而失去了节省内存的优点。
    假设有两个进程,做的事情都是A.so中调用B.so中的foo函数和引用a变量。
    进程1A.so被加载到a0虚拟地址,进程2中A.so被动态加载到a1虚拟地址,静态共享库的虚拟内存分布如下:

    A.so中的代码会被重定位,并且重定位值不一样,进程1中a变量在虚拟地址load_address1+x处,而在进程2中a变量在虚拟地址load_address2+x处。所以A.so的代码在内存中需要保存多份。

    如果我们把需要重定位的地方单独抽出来放到数据区,这样a变量被加载到哪个地址,代码部分都不需要变动,那么两个进程可以只在物理内存中加载一份代码。使用这种机制的共享库叫做动态共享库。
    相同的动态共享库的虚拟内存分布如下:

    这种模式下,代码中需要被重定位的地方被放到了GOT中,动态加载重定位的时候只需要修改GOT就可以了,代码部分不需要被修改。缺点也很明显就是多了一层索引。

    这就是-fPIC链接选项的作用。该链接选项指定生成的动态库为动态共享库。

  • 相关阅读:
    Windows 10 + Jenkins 2.4 安装插件时https 的证书问题及解决
    Spring(四)
    使用chat-GPT接口提取合同中关键信息
    vue2构建一个后端权限管理模板
    django已连接数据库,但是前端不显示任何信息(包括数据标题和数据内容)
    (附源码)springboot篮球场地预约系统 毕业设计 345655
    Newtonsoft.Json 在安卓上报错
    父组件给子组件传值动态传入的值一直显示的都是第一次传入的值,vue子组件监听父组件的传值
    安装k8s集群
    Java Character.SubSet equals()方法具有什么功能呢?
  • 原文地址:https://www.cnblogs.com/gatsby123/p/17258435.html