链接过程(4/13)

在一个 C 项目的编译中,编译器以 C 源文件为单位,将一个个 C 文件翻译成对应的目标文件。生成的每一个目标文件都是由代码段、数据段、BSS 段、符号表等 section 组成的。这些 section 从目标文件的零偏移地址开始按照顺序依次排放,每个段中的符号相对于零地址的偏移,其实就是每个符号的地址,这样程序中定义的变量、函数名等,都有了一个暂时的地址。

在后续的链接过程中,这些目标文件中的各个 section 会重新拆分组装,每个 section 的起始地址都会发生变化,导致每个 section 中定义的函数、全局变量等符号的地址也要随之发生变化,需要重新修改,即重定位。

链接主要分为 3 个过程:分段组装、符号决议和重定位。

分段组装

在这里插入图片描述

链接过程的第一步,就是将各个目标文件分段组装。链接器将编译器生成的各个可重定位目标文件重新分解组装:将各个目标文件的代码段放在一起,作为最终生成的可执行文件的代码段;将各个目标文件的数据段放在一起,作为可执行文件的数据段。其他 section 也会按照同样的方法进行组装。

链接器会在可执行文件中创建一个全局的符号表,收集各个目标文件符号表中的符号,然后统一将它们放到全局符号表中。通过这步操作,一个可执行文件中的所有符号都有了自己的地址,并保存在全局符号表中,但此时全局符号表中的地址还都是原来在各个目标文件中的地址,即相对于零地址的偏移。

在链接过程中,不同的代码段如何组装?链接生成的可执行文件最终是要被加载到内存中执行的,那么要加载到内存的什么地方?一般来讲,这个地址是链接起始地址。一个可执行程序肯定会有入口地址,一般是先执行的代码要放到前面,通过链接脚本即可指定程序的链接地址和各个段的组装顺序

链接脚本本质上是一个脚本文件,在这个脚本文件里,不仅规定了各个段的组装顺序、起始地址、位置对齐等信息,同时对输出的可执行文件格式、运行平台、入口地址等信息做了详细的描述。链接器就是根据链接脚本定义的规则来组装可执行文件的,并最终将这些信息以 section 的形式保存到可执行文件的 ELF Header 中。

假如在一个嵌入式系统中,内存 RAM 的起始地址是 0x60000000,在链接程序时,就可以在链接脚本中指定内存中的一个合法地址作为链接起始地址。程序运行时,加载器首先会解析可执行文件中的 ELF Header 头部信息,验证程序的运行平台和加载地址信息,然后将可执行文件加载到内存中对应的地址,程序就可以运行了。一般使用 编译器提供的默认链接脚本。

使用命令查看默认的链接脚本:

jiaming@jiaming-pc:~/Documents/CSDN_Project$ arm-linux-gnueabi-ld --verbose
GNU ld (GNU Binutils for Ubuntu) 2.34
  Supported emulations:
   armelf_linux_eabi
   armelfb_linux_eabi
using internal linker script:
==================================================
/* Script for -z combreloc */
/* Copyright (C) 2014-2020 Free Software Foundation, Inc.
   Copying and distribution of this script, with or without modification,
   are permitted in any medium without royalty provided the copyright
   notice and this notice are preserved.  */
OUTPUT_FORMAT("elf32-littlearm", "elf32-bigarm",
	      "elf32-littlearm")
OUTPUT_ARCH(arm)
ENTRY(_start)
SEARCH_DIR("=/usr/local/lib/arm-linux-gnueabi"); SEARCH_DIR("=/lib/arm-linux-gnueabi"); SEARCH_DIR("=/usr/lib/arm-linux-gnueabi"); SEARCH_DIR("=/usr/local/lib"); SEARCH_DIR("=/lib"); SEARCH_DIR("=/usr/lib"); SEARCH_DIR("=/usr/arm-linux-gnueabi/lib");
SECTIONS
{
    
    
  /* Read-only sections, merged into text segment: */
  PROVIDE (__executable_start = SEGMENT_START("text-segment", 0x00010000)); . = SEGMENT_START("text-segment", 0x00010000) + SIZEOF_HEADERS;
  .interp         : {
    
     *(.interp) }
  .note.gnu.build-id  : {
    
     *(.note.gnu.build-id) }
  .hash           : {
    
     *(.hash) }
  .gnu.hash       : {
    
     *(.gnu.hash) }
  .dynsym         : {
    
     *(.dynsym) }
  .dynstr         : {
    
     *(.dynstr) }
  .gnu.version    : {
    
     *(.gnu.version) }
  .gnu.version_d  : {
    
     *(.gnu.version_d) }
  .gnu.version_r  : {
    
     *(.gnu.version_r) }
  .rel.dyn        :
    {
    
    
      *(.rel.init)
      *(.rel.text .rel.text.* .rel.gnu.linkonce.t.*)
      *(.rel.fini)
      *(.rel.rodata .rel.rodata.* .rel.gnu.linkonce.r.*)
      *(.rel.data.rel.ro .rel.data.rel.ro.* .rel.gnu.linkonce.d.rel.ro.*)
      *(.rel.data .rel.data.* .rel.gnu.linkonce.d.*)
      *(.rel.tdata .rel.tdata.* .rel.gnu.linkonce.td.*)
      *(.rel.tbss .rel.tbss.* .rel.gnu.linkonce.tb.*)
      *(.rel.ctors)
      *(.rel.dtors)
      *(.rel.got)
      *(.rel.bss .rel.bss.* .rel.gnu.linkonce.b.*)
      PROVIDE_HIDDEN (__rel_iplt_start = .);
      *(.rel.iplt)
      PROVIDE_HIDDEN (__rel_iplt_end = .);
    }
  .rela.dyn       :
    {
    
    
      *(.rela.init)
      *(.rela.text .rela.text.* .rela.gnu.linkonce.t.*)
      *(.rela.fini)
      *(.rela.rodata .rela.rodata.* .rela.gnu.linkonce.r.*)
      *(.rela.data .rela.data.* .rela.gnu.linkonce.d.*)
      *(.rela.tdata .rela.tdata.* .rela.gnu.linkonce.td.*)
      *(.rela.tbss .rela.tbss.* .rela.gnu.linkonce.tb.*)
      *(.rela.ctors)
      *(.rela.dtors)
      *(.rela.got)
      *(.rela.bss .rela.bss.* .rela.gnu.linkonce.b.*)
      PROVIDE_HIDDEN (__rela_iplt_start = .);
      *(.rela.iplt)
      PROVIDE_HIDDEN (__rela_iplt_end = .);
    }
  .rel.plt        :
    {
    
    
      *(.rel.plt)
    }
  .rela.plt       :
    {
    
    
      *(.rela.plt)
    }
  .init           :
  {
    
    
    KEEP (*(SORT_NONE(.init)))
  }
  .plt            : {
    
     *(.plt) }
  .iplt           : {
    
     *(.iplt) }
  .text           :
  {
    
    
    *(.text.unlikely .text.*_unlikely .text.unlikely.*)
    *(.text.exit .text.exit.*)
    *(.text.startup .text.startup.*)
    *(.text.hot .text.hot.*)
    *(SORT(.text.sorted.*))
    *(.text .stub .text.* .gnu.linkonce.t.*)
    /* .gnu.warning sections are handled specially by elf.em.  */
    *(.gnu.warning)
    *(.glue_7t) *(.glue_7) *(.vfp11_veneer) *(.v4_bx)
  }
  .fini           :
  {
    
    
    KEEP (*(SORT_NONE(.fini)))
  }
  PROVIDE (__etext = .);
  PROVIDE (_etext = .);
  PROVIDE (etext = .);
  .rodata         : {
    
     *(.rodata .rodata.* .gnu.linkonce.r.*) }
  .rodata1        : {
    
     *(.rodata1) }
  .ARM.extab   : {
    
     *(.ARM.extab* .gnu.linkonce.armextab.*) }
  .ARM.exidx   :
    {
    
    
      PROVIDE_HIDDEN (__exidx_start = .);
      *(.ARM.exidx* .gnu.linkonce.armexidx.*)
      PROVIDE_HIDDEN (__exidx_end = .);
    }
  .eh_frame_hdr   : {
    
     *(.eh_frame_hdr) *(.eh_frame_entry .eh_frame_entry.*) }
  .eh_frame       : ONLY_IF_RO {
    
     KEEP (*(.eh_frame)) *(.eh_frame.*) }
  .gcc_except_table   : ONLY_IF_RO {
    
     *(.gcc_except_table .gcc_except_table.*) }
  .gnu_extab   : ONLY_IF_RO {
    
     *(.gnu_extab*) }
  /* These sections are generated by the Sun/Oracle C++ compiler.  */
  .exception_ranges   : ONLY_IF_RO {
    
     *(.exception_ranges*) }
  /* Adjust the address for the data segment.  We want to adjust up to
     the same address within the page on the next page up.  */
  . = DATA_SEGMENT_ALIGN (CONSTANT (MAXPAGESIZE), CONSTANT (COMMONPAGESIZE));
  /* Exception handling  */
  .eh_frame       : ONLY_IF_RW {
    
     KEEP (*(.eh_frame)) *(.eh_frame.*) }
  .gnu_extab      : ONLY_IF_RW {
    
     *(.gnu_extab) }
  .gcc_except_table   : ONLY_IF_RW {
    
     *(.gcc_except_table .gcc_except_table.*) }
  .exception_ranges   : ONLY_IF_RW {
    
     *(.exception_ranges*) }
  /* Thread Local Storage sections  */
  .tdata	  :
   {
    
    
     PROVIDE_HIDDEN (__tdata_start = .);
     *(.tdata .tdata.* .gnu.linkonce.td.*)
   }
  .tbss		  : {
    
     *(.tbss .tbss.* .gnu.linkonce.tb.*) *(.tcommon) }
  .preinit_array    :
  {
    
    
    PROVIDE_HIDDEN (__preinit_array_start = .);
    KEEP (*(.preinit_array))
    PROVIDE_HIDDEN (__preinit_array_end = .);
  }
  .init_array    :
  {
    
    
    PROVIDE_HIDDEN (__init_array_start = .);
    KEEP (*(SORT_BY_INIT_PRIORITY(.init_array.*) SORT_BY_INIT_PRIORITY(.ctors.*)))
    KEEP (*(.init_array EXCLUDE_FILE (*crtbegin.o *crtbegin?.o *crtend.o *crtend?.o ) .ctors))
    PROVIDE_HIDDEN (__init_array_end = .);
  }
  .fini_array    :
  {
    
    
    PROVIDE_HIDDEN (__fini_array_start = .);
    KEEP (*(SORT_BY_INIT_PRIORITY(.fini_array.*) SORT_BY_INIT_PRIORITY(.dtors.*)))
    KEEP (*(.fini_array EXCLUDE_FILE (*crtbegin.o *crtbegin?.o *crtend.o *crtend?.o ) .dtors))
    PROVIDE_HIDDEN (__fini_array_end = .);
  }
  .ctors          :
  {
    
    
    /* gcc uses crtbegin.o to find the start of
       the constructors, so we make sure it is
       first.  Because this is a wildcard, it
       doesn't matter if the user does not
       actually link against crtbegin.o; the
       linker won't look for a file to match a
       wildcard.  The wildcard also means that it
       doesn't matter which directory crtbegin.o
       is in.  */
    KEEP (*crtbegin.o(.ctors))
    KEEP (*crtbegin?.o(.ctors))
    /* We don't want to include the .ctor section from
       the crtend.o file until after the sorted ctors.
       The .ctor section from the crtend file contains the
       end of ctors marker and it must be last */
    KEEP (*(EXCLUDE_FILE (*crtend.o *crtend?.o ) .ctors))
    KEEP (*(SORT(.ctors.*)))
    KEEP (*(.ctors))
  }
  .dtors          :
  {
    
    
    KEEP (*crtbegin.o(.dtors))
    KEEP (*crtbegin?.o(.dtors))
    KEEP (*(EXCLUDE_FILE (*crtend.o *crtend?.o ) .dtors))
    KEEP (*(SORT(.dtors.*)))
    KEEP (*(.dtors))
  }
  .jcr            : {
    
     KEEP (*(.jcr)) }
  .data.rel.ro : {
    
     *(.data.rel.ro.local* .gnu.linkonce.d.rel.ro.local.*) *(.data.rel.ro .data.rel.ro.* .gnu.linkonce.d.rel.ro.*) }
  .dynamic        : {
    
     *(.dynamic) }
  . = DATA_SEGMENT_RELRO_END (0, .);
  .got            : {
    
     *(.got.plt) *(.igot.plt) *(.got) *(.igot) }
  .data           :
  {
    
    
    PROVIDE (__data_start = .);
    *(.data .data.* .gnu.linkonce.d.*)
    SORT(CONSTRUCTORS)
  }
  .data1          : {
    
     *(.data1) }
  _edata = .; PROVIDE (edata = .);
  . = .;
  __bss_start = .;
  __bss_start__ = .;
  .bss            :
  {
    
    
   *(.dynbss)
   *(.bss .bss.* .gnu.linkonce.b.*)
   *(COMMON)
   /* Align here to ensure that the .bss section occupies space up to
      _end.  Align after .bss to ensure correct alignment even if the
      .bss section disappears because there are no input sections.
      FIXME: Why do we need it? When there is no .bss section, we do not
      pad the .data section.  */
   . = ALIGN(. != 0 ? 32 / 8 : 1);
  }
  _bss_end__ = .; __bss_end__ = .;
  . = ALIGN(32 / 8);
  . = SEGMENT_START("ldata-segment", .);
  . = ALIGN(32 / 8);
  __end__ = .;
  _end = .; PROVIDE (end = .);
  . = DATA_SEGMENT_END (.);
  /* Stabs debugging sections.  */
  .stab          0 : {
    
     *(.stab) }
  .stabstr       0 : {
    
     *(.stabstr) }
  .stab.excl     0 : {
    
     *(.stab.excl) }
  .stab.exclstr  0 : {
    
     *(.stab.exclstr) }
  .stab.index    0 : {
    
     *(.stab.index) }
  .stab.indexstr 0 : {
    
     *(.stab.indexstr) }
  .comment       0 : {
    
     *(.comment) }
  .gnu.build.attributes : {
    
     *(.gnu.build.attributes .gnu.build.attributes.*) }
  /* DWARF debug sections.
     Symbols in the DWARF debugging sections are relative to the beginning
     of the section so we begin them at 0.  */
  /* DWARF 1 */
  .debug          0 : {
    
     *(.debug) }
  .line           0 : {
    
     *(.line) }
  /* GNU DWARF 1 extensions */
  .debug_srcinfo  0 : {
    
     *(.debug_srcinfo) }
  .debug_sfnames  0 : {
    
     *(.debug_sfnames) }
  /* DWARF 1.1 and DWARF 2 */
  .debug_aranges  0 : {
    
     *(.debug_aranges) }
  .debug_pubnames 0 : {
    
     *(.debug_pubnames) }
  /* DWARF 2 */
  .debug_info     0 : {
    
     *(.debug_info .gnu.linkonce.wi.*) }
  .debug_abbrev   0 : {
    
     *(.debug_abbrev) }
  .debug_line     0 : {
    
     *(.debug_line .debug_line.* .debug_line_end) }
  .debug_frame    0 : {
    
     *(.debug_frame) }
  .debug_str      0 : {
    
     *(.debug_str) }
  .debug_loc      0 : {
    
     *(.debug_loc) }
  .debug_macinfo  0 : {
    
     *(.debug_macinfo) }
  /* SGI/MIPS DWARF 2 extensions */
  .debug_weaknames 0 : {
    
     *(.debug_weaknames) }
  .debug_funcnames 0 : {
    
     *(.debug_funcnames) }
  .debug_typenames 0 : {
    
     *(.debug_typenames) }
  .debug_varnames  0 : {
    
     *(.debug_varnames) }
  /* DWARF 3 */
  .debug_pubtypes 0 : {
    
     *(.debug_pubtypes) }
  .debug_ranges   0 : {
    
     *(.debug_ranges) }
  /* DWARF Extension.  */
  .debug_macro    0 : {
    
     *(.debug_macro) }
  .debug_addr     0 : {
    
     *(.debug_addr) }
  .gnu.attributes 0 : {
    
     KEEP (*(.gnu.attributes)) }
  .note.gnu.arm.ident 0 : {
    
     KEEP (*(.note.gnu.arm.ident)) }
  /DISCARD/ : {
    
     *(.note.GNU-stack) *(.gnu_debuglink) *(.gnu.lto_*) }
}


==================================================

在嵌入式裸机环境下编译程序,尤其是编译 ARM 底层代码,很多时候我们要根据开发板的不同硬件配置、内存大小和地址,灵活指定链接地址,或者显示指定链接脚本,有时候甚至自己编写链接脚本。

u-boot 源码编译的链接脚本 u-boot.lds 一般放在 u-boot 源码的顶层目录下。 Linux 内核编译的链接脚本 vmlinux.lds 一般放在 arch/arm/boot/compressed/ 目录下面。在 IDE 中可以直接通过 Debug Setting 界面设置代码段、数据段的起始地址,通过链接器的 layout 选项,可以设置程序的入口地址。

符号决议

一个公司的项目通常由多人组成的软件团队共同开发。一个项目一般由产品经理定义功能需求,由架构师进行系统分析和模块划分,然后将各个模块的具体实现分配给不同的人员。开发人员在实现各自模块的编程中,可能会产生一个问题:位于不同模块或不同文件中的全局变量、函数可能存在重名冲突。

当这些全局变量在多个文件中定义时,链接器在链接过程中就会发现:各个文件中定义了相同的全局变量名或函数名,发生了符号冲突,最终该使用可执行文件中的哪一个?链接器具有专门的符号决议规则来解决符号冲突:

强符号和弱符号:函数名、初始化的全局变量是强符号,未初始化的全局变量则是弱符号。强符号不允许多次定义,强符号和弱符号可以在一个项目中共存,当强弱符号共存时,强符号会覆盖掉弱符号,链接器会选择强符号作为可执行文件中的最终符号。

链接器也允许一个项目中出现多个弱符号共存。在程序编译期间,编译器在分析每个文件中未初始化的全局变量时,并不知道该符号在链接阶段是被采用还是被丢弃,因此在程序编译期间,未初始化的全局变量并没有被直接放置在 BSS 段中,而是将这些弱符号放到一个叫作 COMMON 的临时块中,在符号表中使用一个未定义的 COMMON 来标记,在目标文件中也没有给它们分配存储空间。

在链接期间,链接器会比较多个文件中的弱符号,选择占用空间最大的那一个,作为可执行文件中的最终符号,此时弱符号的大小已经确定,并被直接放到了可执行文件的 BSS 段中。

正常情况下,初始化的全局变量、函数名默认都是强符号,未初始化的全局变量默认是弱符号。如果在项目中特殊需求,也可以将一些强符号显示转化为弱符号。

__attribute__((weak)) int n = 100;

和强符号、弱符号对应的,还有强引用、弱引用的概念。在一个程序中,我们可以定义多个函数和变量,变量名和函数名都是符号,这些符号的本质,或者说这些符号值,其实就是地址。在另一个文件中,我们可以通过函数名去调用该函数,通过变量名去访问该变量。通过符号去调用一个函数或访问一个变量,通常称之为引用,强符号对应强引用,弱符号对应弱引用。

在程序链接过程中,若对一个符号的引用为强引用,链接时找不到其定义,则链接不会报错,不会影响最终可执行文件的生成。可执行文件在运行时如果没有找到该符号的定义才会报错。

利用链接器对弱引用的处理规则,在引用一个符号之前可以先判断该符号是否存在定义。这样做的好处是:当我们引用一个未定义符号时,在链接阶段不会报错,在运行阶段通过判断运行,也可以避免运行错误。

在模块实现的过程中,我们可以将提供给用户的一系列 API 函数声明为弱符号:

  • 当我们对库中的某些 API 函数的实现不是很满意,或者这些 API 存在 BUG,有更好的实现时,可以自定义与库函数同名的函数,直接调用他们不会发生冲突。
  • 在库的实现过程中,可以将某些扩展功能模块中还未完成的一些 API 定义为弱引用。应用程序在调用这些 API 之前,要先判断该函数是否实现,然后才调用运行。这样做的好处就是未来发布新版本库时,无论这些接口是否已经实现,或者已经删除,都不会影响应用程序的正常链接和运行。

重定位

经过符号决议,我们解决了链接过程中多文件符号冲突的问题,经过处理之后,可执行文件的符号表中的每个符号虽然都确定了下来,但是还存在一个问题:符号表中的每个符号值,就是每个函数、全局变量的地址,还是原来各个目标文件中的指,还都是基于零地址的偏移,链接器将各个目标文件重新分解组装后,各个段的起始地址都发生了变化。各个段的符号地址也要跟着发生变化。接下来要修改可执行文件中全局符号表中的符号的指,将它们的真实地址更新到符号表中。修改为完毕后,当我们想通过符号引用去调用一个函数或访问一个变量时,就能找到它们在内存中的真实地址了。

通过文件的重定位表,链接器可以知道哪些符号需要重定位。重定位的核心工作就是修正指令中的符号地址,是链接过程中的最后一步,也是最核心、最重要的一步,前面两步的操作,其实都是为这一步服务的。

在编译阶段,编译器在将各个 C 源文件生成目标文件的过程中,遇到未定义的符号一般不会报错,编译器会认为这些符号可能会在其它地方定义。在链接阶段,链接器会在其他地方找不到该符号的定义,才会报链接错误。编译器在链接阶段会搜集这些未定义的符号,生成一个重定位表,用来告诉链接器,这些符号在文件中被引用,但是在本文件中没有找到定义,请在链接过程中查看。

重定位表中有一个信息比较重要:需要重定位的符号在指令代码中的偏移地址 offset,链接器修正指令代码中各个符号的指时要根据这个地址信息才能从茫茫的二进制代码中找到它们。链接器读取各个目标文件中的重定位表,根据这些符号在可执行文件中的新地址,进行符号重定位,修改指令代码中引用这些符号的地址,并生成新的符号表。

整个编译流程至此结束,我们得到了一个可执行目标文件。

猜你喜欢

转载自blog.csdn.net/weixin_39541632/article/details/132031475