Linker Script

Photo by Dominik Kempf on Unsplash
Photo by Dominik Kempf on Unsplash
在將 source files 編譯成 executable file 時,GCC 會將 source files 編譯成 object files。然後,linker 依據 linker script 將 object files 鏈結成 executable file。

在將 source files 編譯成 executable file 時,GCC 會將 source files 編譯成 object files。然後,linker 依據 linker script 將 object files 鏈結成 executable file。本文章將介紹 linker script 的基本語法。

Linkers

Linkers 和 Object Files

Linker 結合數個輸入檔成一個輸出檔。輸入檔可以是 relocatable files 也可以是 shared object files,而輸出檔則是 executable files,如下圖。

Object code libraries, from Linkers & Loaders.
Object code libraries, from Linkers & Loaders.

Relocatable files、shared object files、和 executable files 都是 object files。Object file 是一種特殊的檔案格式的檔案,稱為 object file format。現今有數種 object file format,其中在 Linux 上使用的是 ELF。想要了解 Linkers,必須要先了解一種 object file format。因為 Linkers 從數個 object files 中讀取資料,並輸出成一個 object file。如果想要了解 ELF 的話,可以參考以下文章。

在了解 ELF 之後,我們知道一個 object file 是由多個 sections 組成。因此,Linkers 從輸入檔中讀取 sections,經過一些處理後,再將這些 sections 輸出到輸出檔。在這個過程中,我們可以藉由 linker script 告訴 linkers 如何將這些 sections 擺放至輸出檔。我們可以使用 ld -T linker_script_file 來指定 linker script。

GCC 預設使用的 Linker Script

在 Linux 的命令列裡,我們可以執行 ld --verbose 來取得預設的 linker script,如下。當我們用 GCC 編譯 source files 成一個 executable file 時,GCC 會將每一個 source file 編譯成一個 relocatable file,其副檔名為 .o。再將這些 relocatable files 與 libraries 如 libc.so,一起鏈結成一個 executable file。在我們沒有指定一個 linker script 時,GCC 會使用此預設的 linker script 來鏈結成一個 executable file。

/* Script for -z combreloc: combine and sort reloc sections */
/* Copyright (C) 2014-2016 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("elf64-x86-64", "elf64-x86-64", "elf64-x86-64")
OUTPUT_ARCH(i386:x86-64)
ENTRY(_start)
SEARCH_DIR("=/usr/x86_64-redhat-linux/lib64"); SEARCH_DIR("=/usr/lib64"); SEARCH_DIR("=/usr/local/lib64"); SEARCH_DIR("=/lib64"); SEARCH_DIR("=/usr/x86_64-redhat-linux/lib"); SEARCH_DIR("=/usr/local/lib"); SEARCH_DIR("=/lib"); SEARCH_DIR("=/usr/lib");
SECTIONS
{
  /* Read-only sections, merged into text segment: */
  PROVIDE (__executable_start = SEGMENT_START("text-segment", 0x400000)); . = SEGMENT_START("text-segment", 0x400000) + 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) }
  .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.*)
      *(.rela.ldata .rela.ldata.* .rela.gnu.linkonce.l.*)
      *(.rela.lbss .rela.lbss.* .rela.gnu.linkonce.lb.*)
      *(.rela.lrodata .rela.lrodata.* .rela.gnu.linkonce.lr.*)
      *(.rela.ifunc)
  }
  .rela.plt       :
  {
      *(.rela.plt)
      PROVIDE_HIDDEN (__rela_iplt_start = .);
      *(.rela.iplt)
      PROVIDE_HIDDEN (__rela_iplt_end = .);
  }
  .init           :
  {
    KEEP (*(SORT_NONE(.init)))
  }
  .plt            : { *(.plt) *(.iplt) }
  .plt.got        : { *(.plt.got) }
  .plt.bnd        : { *(.plt.bnd) }
  .text           :
  {
    *(.text.unlikely .text.*_unlikely .text.unlikely.*)
    *(.text.exit .text.exit.*)
    *(.text.startup .text.startup.*)
    *(.text.hot .text.hot.*)
    *(.text .stub .text.* .gnu.linkonce.t.*)
    /* .gnu.warning sections are handled specially by elf32.em.  */
    *(.gnu.warning)
  }
  .fini           :
  {
    KEEP (*(SORT_NONE(.fini)))
  }
  PROVIDE (__etext = .);
  PROVIDE (_etext = .);
  PROVIDE (etext = .);
  .rodata         : { *(.rodata .rodata.* .gnu.linkonce.r.*) }
  .rodata1        : { *(.rodata1) }
  .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 .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 .exception_ranges*) }
  /* Thread Local Storage sections  */
  .tdata	  : { *(.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) }
  .got            : { *(.got) *(.igot) }
  . = DATA_SEGMENT_RELRO_END (SIZEOF (.got.plt) >= 24 ? 24 : 0, .);
  .got.plt        : { *(.got.plt)  *(.igot.plt) }
  .data           :
  {
    *(.data .data.* .gnu.linkonce.d.*)
    SORT(CONSTRUCTORS)
  }
  .data1          : { *(.data1) }
  _edata = .; PROVIDE (edata = .);
  . = .;
  __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 don't
       pad the .data section.  */
    . = ALIGN(. != 0 ? 64 / 8 : 1);
  }
  .lbss   :
  {
    *(.dynlbss)
    *(.lbss .lbss.* .gnu.linkonce.lb.*)
    *(LARGE_COMMON)
  }
  . = ALIGN(64 / 8);
  . = SEGMENT_START("ldata-segment", .);
  .lrodata   ALIGN(CONSTANT (MAXPAGESIZE)) + (. & (CONSTANT (MAXPAGESIZE) - 1)) :
  {
    *(.lrodata .lrodata.* .gnu.linkonce.lr.*)
  }
  .ldata   ALIGN(CONSTANT (MAXPAGESIZE)) + (. & (CONSTANT (MAXPAGESIZE) - 1)) :
  {
    *(.ldata .ldata.* .gnu.linkonce.l.*)
    . = ALIGN(. != 0 ? 64 / 8 : 1);
  }
  . = ALIGN(64 / 8);
  _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) }
  /* 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) }
  .gnu.attributes 0 : { KEEP (*(.gnu.attributes)) }
  /DISCARD/ : { *(.note.GNU-stack) *(.gnu_debuglink) *(.gnu.lto_*) }
}

Linker Script

我們將會介紹一些 linker script 的語法,其他的細節請參照 Linker Scripts。了解這些之後,你應該有能力可以了解上面所列出的 GCC 預設使用的 linker script。

VMA & LMA

每一個 loadable 和 allocatable sections 都有兩個 addresses,分別是 VMA 和 LMA。VMA(virtual memory address)是當 section 在執行時的 address。LMA(load memory address)是當 section 被 loaded 時的 address。

在大部分的情況下,這兩個 addresses 是相同的。一個它們不相同的例子是,data 被 loaded 到 ROM,此時它的 address 是 LMA。之後,它被執行時,它會被 loaded 被 RAM,此時它的 address 是 VMA。

定義 Symbols

在 linker script 中,我們可以定義 symbols,並且指定 symbols 的 addresses。如下範例中,我們定義一個 symbol 叫 start,其 address 則設定為 0x7C00。

start = 0x7C00;

Location Counter

Location counter 是一個特別的 symbol '.'。它指著下一個 output section 的位址。在 linker script 中,location counter 的起始值是 0。當我們輸出一個 section 後,location counter 會加上該 section 的大小。

SECTIONS – 對應 Input Sections 到 Output Sections

SECTIONS command 是最重要的 command 了。它告訴 linker 要如何對應 input sections 到 output sections,以及如何在 memory 中擺放 output sections,其語法如下。

SECTIONS
{
    SectionName1 [VMA] [(type)] : [AT(LMA)]
    {
        output-sections-command
    } [> region] [: phdr : phdr ...] [=fillexp]

    SectionName2 [address] [(type)] : [AT(LMA)]
    {
        output-sections-command
    } [> region] [: phdr : phdr ...] [=fillexp]

    ...
} 

在 SECTIONS command 裡,我們可以定義很多 sections。定義一個 output section 時,必須要指定它的名稱,以及它包含哪些 input sections。如下的範例中,我們定義一個 output section 叫 .text,它會包含所有 input files 的 .text sections。這邊的 * 是指所有的 input files。我們也可以指定某個檔案的 sections。如下一行中,我們定義一個 output section 叫 .data,而它包含檔案 data.o.data section。

SECTIONS
{
    .text : { *(.text) }
    .data : { data.o(.data) }
}

在上面的範例中,我們沒有指定 output section 的位址,這時候它會使用 location counter 的位址。所以,上面的範例等同於下面的範例。Linker 會依據 linker script 定義 sections 的順序,依序地擺放 output sections 到 output file 裡。

SECTIONS
{
    .text . : { *(.text) }
    .data . : { data.o(.data) }
}

我們也可以改變 output section 的 VMS。下面的範例中,我們藉由改變 location counter 的 address,來設定 output section .text 的 VMA。

SECTIONS
{
    . = 0x7C00;
    .text : { *(.text) }
}

或是,下面的範例中,我們則是直接設定 output section .text 的 VMA。

SECTIONS
{
    .text 0x7C00 : { *(.text) }
}

如果你了解以上所述的 SECTIONS command 的語法的話,再看一次 GCC 預設使用的 linker script,你應該可以了解一半以上。其他 SECTIONS command 的細節,請參照 Linker Script

另外,如果我們沒有設定 LMA 的話,linker 會使用 VMA 的值來設定 LMA。由於大部分的情況下,VMA 和 LMA 相同,所以我們只需要設定 VMA。

PROVIDE – 定義 Symbols 如果還沒有被定義

如果該 symbol 還沒有被定義,則定義該 symbol。如果該 symbol 已經被定義在某個 input file 中,則忽略。

SECTIONS
{
    PROVIDE (__etext = .);
    PROVIDE (_etext = .);
    PROVIDE (etext = .);
}

KEEP – 保留 Symbols

KEEP command 用來告訴 linker 保留 symbols,即使該 symbols 沒有被 referenced。

SECTIONS
{
    .jcr : { KEEP (*(.jcr)) }
}

ENTRY – 設定 Entry Point

ENTRY command 可以用來設定 executable file 的 entry point,其語法如下。它的參數是一個 symbol 名稱。另外,我們也可以使用 ld -e symbol 來設定 entry point。

ENTRY(symbol)

INPUT – 指定輸入檔案

INPUT command 可以用來指定輸入檔案,不過我們一般是在 ld 指令後面指定輸入的 .o 檔案。

INPUT(file, file, ...)
INPUT(file file ...)

OUTPUT – 指定輸出檔名

OUTPUT command 可以用來指定輸出檔名,不過我們一般使用 ld -o file 來指定輸出檔名。假如兩者都被使用的話,則 ld -o file 會先被使用。這樣的話,我們就可以使用 OUTPUT command 來指定預設的輸出檔名。

OUTPUT(filename)

OUTPUT_FORMAT – 設定輸出 BFD 格式

OUTPUT_FORMAT 可以用來指定輸出檔案的 BFD 格式。我們也可以使用 ld -oformat bfdname

OUTPUT_FORMAT(bfdname)
OUTPUT_FORMAT(default, big, little)
// Example
OUTPUT_FORMAT(elf64-x86-64, elf64-x86-64, elf64-x86-64)

OUTPUT_ARCH – 設定輸出的 Machine Architecture

OUTPUT_ARCH 可以用來指定輸出的 machine architecture。

OUTPUT_ARCH(bfdarch)
// Example
OUTPUT_ARCH(i386:x86-64)

Built-in Functions

ALIGN – Aligning Location Counter

ALIGN function 會將 location counter 的值 aligned 到指定的 alignment,並回傳新的值。

SECTIONS
{
    .text . : { *(.text) }
    .data ALIGN(0x8) : { *(.data) }
}

ADDR – 取得 Section 的位址

ADDR function 回傳 section 的 VMA。

SECTIONS
{
    .text . : { *(.text) }
    .data ADDR(.text) + 0x200 : { *(.data) }
}

SIZEOF – 取得 Section 的大小

SIZEOF function 回傳 section 的大小。

SECTIONS
{
    .text . : { *(.text) }
    .data ADDR(.text) + SIZEOF(.text) : { *(.data) }
}

結語

現在在編譯程式碼時,GCC 不只編譯程式碼,還鏈結 object files 成 executable file。所以,我們雖然知道 linking 這個步驟,但是不了解其內部的運作。藉由了解 linker script,我們可以更具體的了解 linking 是在做什麼。

參考

  • ld, Linux manual page.
  • LD.
  • Using ld.
  • John R. Levine, Linkers and Loaders.
發佈留言

發佈留言必須填寫的電子郵件地址不會公開。 必填欄位標示為 *

You May Also Like
Photo by Timothée Geenens on Unsplash
Read More

x86 Memory Map

在 x86 PC 起動後,它會處在 real mode。此時,我們可以存取 1 MB 以下的 memory。然而,BIOS 也會使用一些 memory。因此,我們必須要知道 BIOS 佔據哪些範圍,才能夠避開它們
Read More
Photo by Lanju Fotografie on Unsplash
Read More

Makefile

Makefile 是 Linux 中最常用的編譯工具了。Stuart Feldman 在 1967 的 Bell Labs 裡創造了它。雖然它可能比你我的年紀都還大,但是它現在還是依然地活躍。本文章將介紹 Makefile 的一些基本語法。
Read More