目录
为什么需要链接脚本
通常我们在编译链接代码文件时,只需要使用
$ gcc a.c
这个过程中,我们并没有指定链接脚本,是不是就真的不需要链接脚本呢?其实ld 在用户没有指定链接脚本的时候会使用默认链接脚本。 我们可以使用下面的命令行来查看ld默认的链接脚本:
# ld -verbose
GNU ld (GNU Binutils for Ubuntu) 2.26.1
Supported emulations:
elf_i386
i386linux
elf_iamcu
elf32_x86_64
elf_x86_64
elf_l1om
elf_k1om
i386pep
i386pe
using internal linker script:
==================================================
/* Script for -z combreloc: combine and sort reloc sections */
/* Copyright (C) 2014-2015 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-i386", "elf32-i386",
"elf32-i386")
OUTPUT_ARCH(i386)
ENTRY(_start)
SEARCH_DIR("=/usr/local/lib/i386-linux-gnu"); SEARCH_DIR("=/lib/i386-linux-gnu"); SEARCH_DIR("=/usr/lib/i386-linux-gnu"); SEARCH_DIR("=/usr/local/lib32"); SEARCH_DIR("=/lib32"); SEARCH_DIR("=/usr/lib32"); SEARCH_DIR("=/usr/local/lib"); SEARCH_DIR("=/lib"); SEARCH_DIR("=/usr/lib"); SEARCH_DIR("=/usr/i686-linux-gnu/lib32"); SEARCH_DIR("=/usr/i686-linux-gnu/lib");
SECTIONS
{
/* Read-only sections, merged into text segment: */
PROVIDE (__executable_start = SEGMENT_START("text-segment", 0x08048000)); . = SEGMENT_START("text-segment", 0x08048000) + 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.*)
*(.rel.ifunc)
}
.rel.plt :
{
*(.rel.plt)
PROVIDE_HIDDEN (__rel_iplt_start = .);
*(.rel.iplt)
PROVIDE_HIDDEN (__rel_iplt_end = .);
}
.init :
{
KEEP (*(SORT_NONE(.init)))
}
.plt : { *(.plt) *(.iplt) }
.plt.got : { *(.plt.got) }
.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) >= 12 ? 12 : 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 ? 32 / 8 : 1);
}
. = ALIGN(32 / 8);
. = SEGMENT_START("ldata-segment", .);
. = ALIGN(32 / 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_*) }
}
==================================================
以上就是默认的链接脚本, 存放在/usr/lib/ldscripts/下, 不同的机器平台、 输出文件格式都有相应的链接脚本。ld会根据命令行要求使用相应的链接脚本文件来控制链接过程, 当我们使用ld来链接生成一个可执行文件的时候, 它就会使用elf_i386.x作为链接控制脚本; 当我们使用ld来生成一个共享目标文件的时候, 它就会使用elf_i386.xs作为链接控制脚本。
链接脚本描述了如何组织输出的目标文件, 当然, 我们使用链接器提供的默认链接规则对目标文件进行链接,这在一般情况下是没有问题的, 但对于一些特殊要求的程序, 比如操作系统内核、 BIOS(Basic Input Output System) 或一些在没有操作系统的情况下运行的程序(如引导程序Boot Loader或者嵌入式系统的程序, 或者有一些脱离操作系统的硬盘分区软件PQMagic等) , 以及另外的一些须要特殊的链接过程的程序, 如一些内核驱动程序等, 它们往往受限于一些特殊的条件, 如须要指定输出文件的各个段虚拟地址、 段的名称、 段存放的顺序等, 因为这些特殊的环境, 特别是某些硬件条件的限制, 往往对程序的各个段的地址有着特殊的要求。
由于整个链接过程有很多内容须要确定: 使用哪些目标文件? 使用哪些库文件? 是否在最终可执行文件中保留调试信息、 输出文件格式(可执行文件还是动态链接库)? 还要考虑是否要导出某些符号以供调试器或程序本身或其他程序使用等。操作系统内核,从本质上来讲, 它本身也是一个程序。 比如Windows的内核ntoskrnl.exe就是一个我们平常看到的PE文件, 它的位置位于
\WINDOWS\system32\ntoskrnl.exe。 很多人误以为Window操作系统的内核很庞大, 由很多文件组成。 这是一个误解, 其实真正的Windows内核就是这个文件。
所以,链接脚本在链接过程中是必须的, 为了更加精确地控制链接过程, 我们可以自己编写一个脚本, 然后指定该脚本为链接控制脚本。比如可以使用-T参数:
$ ld –T link.script
链接脚本语法
我们以ld -verbose获取的默认链接脚本为例,进行说明,其中分隔符‘==================================================’中间的那部分才是真正的链接脚本内容。
无论是输出文件还是输入文件, 它们的主要的数据就是文件中的各种段, 我们把输入文件中的段称为输入段(Input Sections) , 输出文件中的段称为输出段(Output Sections) 。 简单来讲, 控制链接过程无非是控制输入段如何变成输出段, 比如哪些输入段要合并一个输出段, 哪些输入段要丢弃; 指定输出段的名字、 装载地址、 属性, 等等,因此,链接脚本的核心也就是实现了这些段的操作。
ENTRY(_start): 指定了程序的入口为_start()函数,_start为glibc的函数
入口地址(entry point)是指进程执行的第一条用户空间的指令在进程地址空间的地址,ld有多种方法设置进程入口地址, 按一下顺序: (编号越前, 优先级越高)
1, ld命令行的-e选项
2, 连接脚本的ENTRY(SYMBOL)命令
3, 如果定义了start符号, 使用start符号值
4, 如果存在.text section, 使用.text section的第一字节的位置值
5, 使用值0
SEARCH(dir): 指定链接库的搜索路径
INCLUDE filename : 包含其他名为filename的链接脚本,类似c程序内的的#include指令, 用以包含另一个链接脚本.脚本搜索路径由-L选项指定. INCLUDE指令可以嵌套使用, 最大深度为10. 即: 文件1内INCLUDE文件2, 文件2内INCLUDE文件3… , 文件10内INCLUDE文件11. 那么文件11内不能再出现 INCLUDE指令了. 我们可以在默认链接脚本中使用INCLUDE包含自定义的链接脚本,需要注意的是,自定义链接脚本用到的section需要单独指定链接地址。
. = 0x08048000 + SIZEOF_HEADERS :意思是将当前虚拟地址设置成0x08048000 + SIZEOF_HEADERS
SECTIONS语句比较复杂, 它又包含了一个赋值语句及一些SECTIONS语句所特有的映射规则。 其实除了SECTIONS命令语句之外, 其他命令语句都比较简单, 毕竟SECTIONS负责指定链接过程的段转换过程, 这也是链接的最核心和最复杂的部分。SECTIONS命令通常作为链接脚本的主题, 命令格式:
SECTIONS
{
...
secname : { contents }
...
}
这个命令指定了各种输入段到输出段的变换, SECTIONS后面紧跟着的一对大括号里面包含了SECTIONS变换规则。
secname表示输出段的段名, secname后面必须有一个空格符, 这样使得输出段名不会有歧义, 后面紧跟着冒号和一对大括号。 大括号里面的contents描述了一套规则和条件, 它表示符合这种条件的输入段将合并到这个输出段中。 输出段名的命名方法必须符合输出文件格式的要求。
有一个特殊的段名叫“/DISCARD/”, 如果使用这个名字作为输出段名, 那么所有符合后面contents所规定的条件的段都将被丢弃, 不输出到输出文件中。 contents中可以包含若干个条件, 每个条件之间以空格隔开, 如果输入段符合这些条件中的任意一个即表示这个输入段符合contents规则。 条件的写法如下:
filename(sections)
filename表示输入文件名, sections表示输入段名。 让我们举几个条件的例子来看看:
file1.o(.data) 表示输入文件中名为file1.o的文件中名叫.data的段符合条件。
file1.o(.data .rodata) 或 file1.o(.data, .rodata) 表示输入文件中名为file1.o的文件中的名叫.data或.rodata的段符合条件。*(.data) 所有输入文件中的名字为.data的文件符合条件。 * 是通配符, 类似于正则表达式中的 *, 我们还可以使用正则表达式中的 ?、 []等规则。
[a-z]*(.text*[A-Z]) 这个条件比较复杂, 它表示所有输入文件中以小写字母a到z开头的文件中所有段名以.text开头, 并且以大写字母A到Z结尾的段。
.interp : { *(.interp) } 意思是:所有输入文件中的.interp段都合并输出到.interp段中
PROVIDE关键字可以定义一个符号,比如’etext’。语法为PROVIDE(symbol = expression)。
SECTIONS
{
.text :
{
*(.text)
_etext = .;
PROVIDE(etext = .);
}
}
如果程序定义了etext,则链接器将给出重复定义错误,然而,如果程序定义了etext,则链接器将会默认使用程序中的定义,如果程序引用了etext而没有定义,则链接器将使用连接脚本中的定义。
PROVIDE_HIDDEN:和PROVIDE类似, 这种方式定义的符号将被隐藏且不会被输出
SEGMENT_START(segment, default):
返回名为segment的段的基地址。如果为段显式指定了值(使用命令行命令’-T’)(注:原文这里可能有误,’-T’应当是指定链接脚本)则值将会被返回否则值会使用默认值。在现在,’-T’命令行选项只能被用来设置“text”, “data”, 和“bss”段的基地址,但你可以使用SEGMENT_START搭配任何段名字
指定链接脚本编译
gcc main.c -Wl,-Ts.lds
参考
https://blog.csdn.net/qq_31208451/article/details/77430489