操作系统:CentOS Linux release 7.7.1908
内核版本:3.10.0-1062.1.1.el7.x86_64
运行平台:x86_64
参考文献: https://sourceware.org/binutils/docs-2.27/ld/index.html
通过执行$ gcc -m32 -Wl,--verbose tanglinux.c -o tanglinux命令(即给链接器/usr/bin/ld.bfd传递命令行参数--verbose)来编译程序,可以获得编译该程序所使用的链接脚本(在打印信息的两条分割线===之间)。内置链接脚本保存在链接器ld.bfd的.rodata section中。如下所示:
/* 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("elf32-i386", "elf32-i386",
"elf32-i386")
OUTPUT_ARCH(i386)
ENTRY(_start)
SEARCH_DIR("=/usr/i386-redhat-linux/lib32"); SEARCH_DIR("=/usr/x86_64-redhat-linux/lib32"); SEARCH_DIR("=/usr/local/lib32"); SEARCH_DIR("=/lib32"); SEARCH_DIR("=/usr/lib32"); SEARCH_DIR("=/usr/i386-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", 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_*) }
}
链接器的主要作用是将多个输入文件组合成一个输出文件。通常,称输入文件为目标文件,称输出文件为可执行文件(动态库与可执行文件本质上是相同的)。它们都由一系列的section构成,输入文件中的section叫作input section,同样,输出文件中的section叫作output section。所有的section都有名字和大小,当然也有相应的数据。
链接脚本是文本文件,由一系列的指令组成。每个指令要么是关键字(可能后带参数),要么是一个针对符号的赋值表达式。指令之间可以使用分号分隔。
链接脚本中的文件名可以包含在双引号之间,多个文件名之间使用逗号分隔。
链接脚本中的注释与C语言中的相同,包含在/*和*/两者之间。在语义上,注释相当于空白字符,会被链接器所忽略。
一、简易指令
1、入口点指令
链接脚本使用ENTRY(symbol)指令来将symbol设置程序的入口点(程序执行的起点),参数symbol是该程序中的一个符号。
链接器可以按顺序使用下面所列的方式来设置程序的入口点,直到有一个成功为止。
a、通过链接器命令行参数-e或--entry来设置;
b、链接脚本中的ENTRY(symbol)指令;
c、目标平台特定的符号,如start;
d、.text section的首地址;
e、0地址。
2、设置依赖库搜索目录
链接脚本通过SEARCH_DIR(path)指令来添加依赖库的搜索路径。跟链接器的命令行参数-L或--library-path的意义相同。如果两种方式都有使用,则设置的路径都有效,不过通过命令行参数设置的路径优先使用。
3、输出格式指令
链接脚本使用OUTPUT_FORMAT(bfdname)或OUTPUT_FORMAT(default, big, little)指令来设置输出文件的格式,其中bfdname表示BFD 格式名称,default表示默认格式,big表示大端字节序格式,little表示小端字节序格式。
OUTPUT_FORMAT(bfdname)指令跟链接器命令行参数--oformat的意义相同,不过命令行参数的优先级更高。
OUTPUT_FORMAT(default, big, little)指令中的三个参数到底使用哪个参数跟链接器命令行参数-EB和-EL的设置有关,如果-EB和-EL两者都没设置,则使用default格式,设置了-EB则使用big格式,设置了-EL则使用little格式。例子中,OUTPUT_FORMAT("elf32-i386", "elf32-i386", "elf32-i386")表示无论如何都使用相同的格式。
4、设置体系架构
链接脚本使用OUTPUT_ARCH(bfdarch)指令来设置输出文件的体系架构。参数bfdarch对应的名称来自于BFD动态库,如当前系统中的libbfd-2.27-41.base.el7_7.1.so。
二、符号的赋值操作
在链接脚本中,可以为一个符号赋值。这里的赋值操作相当于定义了一个全局符号,并且该符号会被添加进输出文件的符号表中。
1、普通赋值操作
普通赋值操作表达式类似于C语言中的赋值操作,如下所示:
symbol = expression ;
symbol += expression ;
symbol -= expression ;
symbol *= expression ;
symbol /= expression ;
symbol <<= expression ;
symbol >>= expression ;
symbol &= expression ;
symbol |= expression ;
这里的分号是必需的。符号赋值操作可以出现在链接脚本的任何地方。如下例所示:
floating_point = 0;
SECTIONS
{
.text :
{
*(.text)
_etext = .;
}
_bdata = (. + 3) & ~ 3;
.data : { *(.data) }
}
其中,floating_point、_etext和_bdata都会被定义为符号,但它们的地址各有不同。floating_point的地址为0;_etext的地址紧随最后一个.text input section之后;_bdata的地址紧随.text output section之后,并且4字节对齐。
2、PROVIDE(symbol = expression)和PROVIDE_HIDDEN
关键字PROVIDE表示链接脚本并不会真正定义一个符号(不会出现在输出文件的符号表中),但在程序中可以直接引用它们,它们的值是其所在位置的虚拟地址。如实例中的etext、_etext和edata等等。如果程序中有定义同名的变量,则同名的PROVIDE所创建的符号会被这个程序所定义的变量所覆盖。如下例所示:
/* tanglinux01.c */
#include <stdio.h>
unsigned long etext = 0x3;
extern unsigned long _etext[];
unsigned long _edata = 0x33;
extern unsigned long edata[];
int main()
{
printf("etext = %#lx, _etext = %#lx\n", etext, _etext);
printf("edata = %#lx, _edata = %#lx\n", edata, _edata);
return 0;
}
编译并执行,如下所示:
$ gcc -m32 tanglinux01.c -o tanglinux01
$ ./tanglinux01
etext = 0x3, _etext = 0x80484e8
edata = 0x804a020, _edata = 0
其中,etext、_etext和edata都有由PROVIDE所创建,从中可以看出,符号etext已经被新的定义所覆盖,但是_edata没有(它不是由PROVIDE所创建)。链接脚本中所创建的符号不能赋值。
PROVIDE_HIDDEN的语义与PROVIDE基本相同,唯一的区别是它所定义的符号是隐藏的(相当于局部符号)。
三、表达式
1、常量
链接脚本中的常量都是整数,包括0开头的八进制整数、0x或0X开头的十六进制整数。也可以使用整数加后缀的方式来表示不同的进制数,如h或H表示十六进制整数、o或O表示八进制整数、b或B表示二进制整数以及d或D表示十进制整数。没带前缀或后缀的整数都是十进制的。后缀K表示1024,M表示1024乘以1024。
2、符号常量
链接脚本还有两个特定于目标平台的常量,MAXPAGESIZE和COMMONPAGESIZE,分别表示内存页的最大尺寸和默认尺寸。它们都通过CONSTANT(name)运算符来使用,其中name的值就是这两者之一。
3、orphan section
出现在输入文件中的section,但在链接脚本中没有关于这些section应该保存在输出文件中什么位置的描述,那么这些section就被叫做orphan section。链接器仍然会将它们的数据拷贝到输出文件中,一般把它们保存在与它们属性相似的非orphan section的后面。
4、地址计数器
链接脚本使用点(.)来表示地址计数器,它是一个变量,总是代表输出文件中的一个地址(根据输入文件section的大小不断增加,不能倒退)。地址计数器只用于SECTIONS指令中。默认情况下,地址计数器的初始值为0。
(1)、给地址计数器赋值会直接改变它的地址,可能会导致在输出文件中留下一段空隙。如下例所示:
SECTIONS
{
output :
{
file1(.text)
. = . + 1000;
file2(.text)
. += 1000;
file3(.text)
} = 0x12345678;
}
在上例中,file1文件中的.text section会保存在输出文件中‘output’section的开头,紧随其后依次是1000个字节的空隙、文件file2的.text section、1000个字节的空隙、文件file3的.text section。“= 0x12345678”表示空隙中所填充的数据。
(2)、地址计数器如果出现在SECTIONS指令的普通语句中,那么它的值会被当作绝对地址,但如果出现在output section description语句中,则它的值会被看作是相对于该section的偏移量。如下例所示:
SECTIONS
{
. = 0x100
.text: {
*(.text)
. = 0x200
}
. = 0x500
.data: {
*(.data)
. += 0x600
}
}
在上例中,. = 0x100表示这时的地址计数器被设置为绝对地址0x100(也是输出文件中.text section的起始地址),而. = 0x200的意思是输出文件中的.text section将有0x200个字节的大小,而不管所有输入文件中的.text section的总大小有没有0x200个字节(如果超出0x200个字节则会产生一个错误)。同样的,输出文件中的.data section的起始地址为0x500(通过普通语句. = 0x500),它的内容为所有输入文件中的.data section,外加0x600个字节的空隙(通过output section description语句. += 0x600)。
(3)、在SECTIONS指令的普通语句中,将地址计数器赋值给一个符号,这个符号的值可能跟预想的不一致,因为链接器可能在其中插入了orphan section的数据。如下例所示:
SECTIONS
{
start_of_text = . ;
.text: { *(.text) }
end_of_text = . ;
start_of_data = . ;
.data: { *(.data) }
end_of_data = . ;
}
如果输入文件中存在.rodata section,但链接脚本中没有关于该种section的描述,那么链接器可能将该section保存在上例中的“start_of_data = . ;”和“end_of_data = . ;”之间,如下所示:
SECTIONS
{
start_of_text = . ;
.text: { *(.text) }
end_of_text = . ;
start_of_data = . ;
.rodata: { *(.rodata) }
.data: { *(.data) }
end_of_data = . ;
}
那么,这时的start_of_data和end_of_data就不能准确地反应出输出文件中.data section的前后地址。有一个方法可以解决这个问题,那就是将地址计数器赋值给自身,如下所示:
SECTIONS
{
start_of_text = . ;
.text: { *(.text) }
end_of_text = . ;
. = . ;
start_of_data = . ;
.data: { *(.data) }
end_of_data = . ;
}
这时,.rodata section这个orphan section将会被链接器保存在end_of_text和start_of_data之间,也就是普通语句“. = . ;”所在的地方。
5、运算符
链接脚本中所使用的运算符跟C语言中相应的运算符的意义相同,它们的优先级和求值顺序如下所示:
优先级 求值顺序 操作符
(高)
1 左 ! - ~
2 左 * / %
3 左 + -
4 左 >> <<
5 左 == != > < <= >=
6 左 &
7 左 |
8 左 &&
9 左 ||
10 右 ? :
11 右 &= += -= *= /=
(低)
6、内置函数
(1)、ABSOLUTE(exp)
ABSOLUTE(exp)返回表达式exp的绝对地址。主要用于给一个符号赋予一个绝对地址,而这个符号在正常情况下它的值为相对值。如下例所示:
SECTIONS
{
.data : { *(.data) _edata = ABSOLUTE(.); }
}
在上例中,如果不使用ABSOLUTE函数,那么_edata的值就是相对于.data section的偏移量,否则它的值就是紧随.data section的绝对地址。
(2)、ALIGN(align)和ALIGN(exp,align)
ALIGN返回地址计数器或表达式exp相对于align对齐的值。ALIGN(align)等效于ALIGN(ABSOLUTE(.), align)。
(3)、DATA_SEGMENT_ALIGN(maxpagesize, commonpagesize)和DATA_SEGMENT_END(exp)
DATA_SEGMENT_ALIGN的主要作用是使data segment相对系统内存页对齐。在链接脚本中,关于DATA_SEGMENT_ALIGN有一段注释,如“We want to adjust up to the same address within the page on the next page up.”,意思是会将当前地址调整到下一个内存页的相同偏移量。如果当前地址为0x08048560,内存页大小为0x1000,则这个地址会被“. = DATA_SEGMENT_ALIGN (CONSTANT(MAXPAGESIZE), CONSTANT(COMMONPAGESIZE));”调整到0x08049560,其中偏移量0x560是相同的。
DATA_SEGMENT_ALIGN(maxpagesize, commonpagesize)等效于“(ALIGN(maxpagesize) + (. & (maxpagesize - 1)))”或“(ALIGN(maxpagesize) + ((. + commonpagesize - 1) & (maxpagesize - commonpagesize)))”这两个表达式。其中,commonpagesize的值不能大于maxpagesize。如果commonpagesize等于maxpagesize则选用前一个表达式,如果commonpagesize小于maxpagesize则选用后一个表达式。选用后一个表达式,则意味着程序在运行时会节省commonpagesize个字节数的内存,但在ELF文件中会浪费最多commonpagesize个字节的空间。如下例所示:
/* align.c */
#include <stdio.h>
static unsigned long align_n(unsigned long value, unsigned long align)
{
if (align <= 1)
return value;
value = (value + align - 1) / align;
return value * align;
}
unsigned long data_segment_align(unsigned long value,
unsigned long maxpagesize, unsigned long commonpagesize)
{
if (commonpagesize > maxpagesize)
return value;
if (maxpagesize == commonpagesize)
return align_n(value, maxpagesize) + (value & (maxpagesize - 1));
return align_n(value, maxpagesize) + ((value + commonpagesize - 1)
& (maxpagesize - commonpagesize));
}
int main()
{
printf("value = %#lx, %#lx\n", data_segment_align(0x08048560, 0x1000, 0x1000),
data_segment_align(0x08048560, 0x1000, 0x400));
return 0;
}
编译并执行,输出如下:
$ gcc -m32 align.c -o align
$ ./align
value = 0x8049560, 0x8049800
其中,当commonpagesize取值为0x400时,则在链接所生成的ELF文件中会多出0x2A0(0x8049800减去0x8049560)个字节的空隙。
DATA_SEGMENT_ALIGN在链接脚本中只能使用一次,并且只能使用在SECTIONS指令的普通语句中,不能使用在output section description语句中。
DATA_SEGMENT_END(exp)与DATA_SEGMENT_ALIGN(maxpagesize, commonpagesize)配对使用,表示data segment的结尾地址。
(4)、DATA_SEGMENT_RELRO_END(offset, exp)
DATA_SEGMENT_RELRO_END表示PT_GNU_RELRO segment的结尾地址。只有当链接器指定-z relro命令行参数时才会创建PT_GNU_RELRO segment。实际上,对于链接器/usr/bin/ld.bfd,不用指定命令行参数-z relro,它是默认创建PT_GNU_RELRO segment。除非通过命令行参数-z norelro取消创建PT_GNU_RELRO segment。如果不创建PT_GNU_RELRO segment,则DATA_SEGMENT_RELRO_END函数在链接脚本中无效。
通过是否创建PT_GNU_RELRO segment分别编译tanglinux.c,并对比两次编译所生成的可执行文件tanglinux中关键的section的地址,可以发现一个很有趣的现象。tanglinux.c源码如下所示:
/* tanglinux.c */
int main() { return 0; }
不创建PT_GNU_RELRO segment,编译并查看其关键section的地址,如下所示:
$ gcc -m32 -Wl,-znorelro tanglinux.c -o tanglinux
$ readelf -S tanglinux
Section Headers:
[Nr] Name Type Addr Off Size ES Flg Lk Inf Al
[ 0] NULL 00000000 000000 000000 00 0 0 0
[18] .eh_frame PROGBITS 08048490 000490 0000b0 00 A 0 0 4
[19] .init_array INIT_ARRAY 08049540 000540 000004 04 WA 0 0 4
[23] .got PROGBITS 08049634 000634 000004 04 WA 0 0 4
[24] .got.plt PROGBITS 08049638 000638 000010 04 WA 0 0 4
其中, “. = DATA_SEGMENT_ALIGN (CONSTANT (MAXPAGESIZE), CONSTANT (COMMONPAGESIZE));”表达式在.eh_frame和.init_array两个section之间,地址0x08049540相对于0x08048540(0x08048490+0xb0)刚好是在下一个内存页(大小为0x1000)的相同偏移量上(0x540)。在.got和.got.plt两个section之间的“. = DATA_SEGMENT_RELRO_END (SIZEOF (.got.plt) >= 12 ? 12 : 0, .);”表达式没有起作用。
创建PT_GNU_RELRO segment,编译并查看其关键section的地址,如下所示:
$ gcc -m32 tanglinux.c -o tanglinux
$ readelf -S tanglinux
Section Headers:
[Nr] Name Type Addr Off Size ES Flg Lk Inf Al
[ 0] NULL 00000000 000000 000000 00 0 0 0
[18] .eh_frame PROGBITS 080484b0 0004b0 0000b0 00 A 0 0 4
[19] .init_array INIT_ARRAY 08049f08 000f08 000004 04 WA 0 0 4
[20] .fini_array FINI_ARRAY 08049f0c 000f0c 000004 04 WA 0 0 4
[21] .jcr PROGBITS 08049f10 000f10 000004 00 WA 0 0 4
[22] .dynamic DYNAMIC 08049f14 000f14 0000e8 08 WA 6 0 4
[23] .got PROGBITS 08049ffc 000ffc 000004 04 WA 0 0 4
[24] .got.plt PROGBITS 0804a000 001000 000010 04 WA 0 0 4
在这个例子中,DATA_SEGMENT_RELRO_END函数有效,它的有效导致一个有趣的现象,即DATA_SEGMENT_ALIGN函数所在的地址不再是相对于下一个内存页的相同的偏移量上,理论上的地址应该为0x08049560,但实际上却是0x08049f08。这个现象的发生正是DATA_SEGMENT_RELRO_END函数的语义所在。
DATA_SEGMENT_RELRO_END(offset, exp)函数的语义是offset加上exp的和相对于内存页对齐,并且DATA_SEGMENT_ALIGN和DATA_SEGMENT_RELRO_END之间所有section的数据都向这个地址靠拢保存。如在上例中,DATA_SEGMENT_ALIGN和DATA_SEGMENT_RELRO_END之间有五个section,它们的总大小为0xf8(即0x4+0x4+0x4+0xe8+0x4)个字节,所以“. = DATA_SEGMENT_ALIGN (CONSTANT (MAXPAGESIZE), CONSTANT (COMMONPAGESIZE));”表达式的地址被修正为0x08049f08,即0x0804a000减去0xf8所得。当然修正后的地址必须是大于0x08049560的。
在链接脚本中,DATA_SEGMENT_RELRO_END必须出现在DATA_SEGMENT_ALIGN和DATA_SEGMENT_END两者之间。
(5)、SEGMENT_START(segment, default)
SEGMENT_START(segment, default)函数返回名称为segment(即第一个参数)的segment的首地址。如果链接器有使用命令行参数-T为这个segment设置地址,那么SEGMENT_START函数就返回命令行参数所设置的地址值,否则就返回参数default的值。链接器/usr/bin/ld.bfd的命令行参数-T的详情如下所示:
-Tbss ADDRESS Set address of .bss section
-Tdata ADDRESS Set address of .data section
-Ttext ADDRESS Set address of .text section
-Ttext-segment ADDRESS Set address of text segment
-Trodata-segment ADDRESS Set address of rodata segment
-Tldata-segment ADDRESS Set address of ldata segment
(6)、SIZEOF(section)
SIZEOF(section)函数返回名称为section(即参数)的section的大小(字节数)。该section在输出文件中必须是实际存在的,否则链接器会报错。
(7)、SIZEOF_HEADERS或sizeof_headers
对于ELF文件来说,SIZEOF_HEADERS等于ELF文件头的大小加上所有program header的总大小。
四、SECTIONS指令
SECTIONS指令是链接脚本中的主要内容,用来指示链接器如何将输入文件中的section保存到输出文件中。SECTIONS指令的格式如下所示:
SECTIONS
{
sections-command
sections-command
...
}
其中,每个sections-command只能是以下四种语句之一:
a、一个ENTRY指令;
b、一个符号的赋值操作;
c、一个output section description语句;
d、一个overlay description语句。
如果一个链接脚本中没有使用SECTIONS指令,那么输出文件中各类section的布局就跟所有输入文件中该类section第一次出现的顺序有关,并且所有section的起始地址为0。
1、在SECTIONS指令中可以使用ENTRY指令和符号的赋值操作,以便它们可以使用地址计数器。
2、output section description语句
SECTIONS指令中的主要内容为output section description语句,它的格式如下所示:
section [address] [(type)] :
[AT(lma)]
[ALIGN(section_align) | ALIGN_WITH_INPUT]
[SUBALIGN(subsection_align)]
[constraint]
{
output-section-command
output-section-command
...
} [>region] [AT>lma_region] [:phdr :phdr ...] [=fillexp] [,]
其中,section、冒号和大括号是必须的,其它中括号中关于section的属性都是可选的。换行和多余的空格也是可选的。如果使用=fillexp,并且下一个sections-command看起来是这个表达式的延续,那么它后面的逗号就是必需的。
(1)、格式中的section表示output section的名称,如.text、.data、.bss等等。
如果名称为“/DISCARD/”,则表示链接器会丢弃输入文件中所有特定类型的section,这些特定的类型由大括号中的语句所描述。在实例中,所有输入文件中的.note.GNU-stack、.gnu_debuglink等类型的section都会被丢弃。
(2)、格式中的address是关于计算output section的虚拟地址的表达式,表达式的结果为该output section的虚拟地址。为非空的output section指定address属性将改变地址计数器的值。
实例中,调试信息相关section(如.stab和.debug等等)的address值都为0。
(3)、格式中的(type)表示output section的类型,类型值包含在小括号中。可能的类型值有NOLOAD、DSECT、COPY、INFO和OVERLAY等五种。NOLOAD表示在程序运行时该类型的section不会被加载到内存中。后四种表示在程序运行时都不会为这些类型的section分配内存。
通常情况下,链接器是根据input section的类型来确认相应output section的类型。
(4)、每个section都有一个虚拟地址(VMA,virtual memory address)和一个加载地址(LMA,load memory address)。格式中的关键字AT或AT>都是用来指定section的加载地址。AT使用一个表达式来作为它的参数,而AT>是使用一个内存区域的名称(参考MEMORY 指令)来作为它的参数。如下例所示:
SECTIONS
{
.text 0x1000 : { *(.text) _etext = . ; }
.mdata 0x2000 :
AT ( ADDR (.text) + SIZEOF (.text) )
{ _data = . ; *(.data); _edata = . ; }
.bss 0x3000 :
{ _bstart = . ; *(.bss) *(COMMON) ; _bend = . ;}
}
在上例中,链接器将会创建三个output section,其中.text的虚拟地址为0x1000,.bss的虚拟地址为0x3000,.mdata的虚拟地址为0x2000,但.mdata的加载地址是紧随.text section之后。符号_data的地址为0x2000,表明地址计数器的计算是针对虚拟地址的,而不是针对加载地址。
(5)、使用ALIGN来增加output section的对齐方式的数值,并且可以使用ALIGN_WITH_INPUT属性强制VMA和LMA之间的差异在整个output section中保持不变。
SUBALIGN强制所有input section以它的参数中指定的值对齐。
(6)、格式中的constraint用来设置限制条件。只有满足限制条件的input section才会被保存到相应的output section中。限制条件有ONLY_IF_RO(只读)和ONLY_IF_RW(读写)。
如实例中的.eh_frame、.gnu_extab、.gcc_except_table和.exception_ranges,当它们各自的input section满足ONLY_IF_RW限制条件时,它们就会被分别保存在data segment中,否则会被保存在只读data segment中。
(7)、通过>region为一个section指定一个内存区域。如下例所示:
MEMORY { rom : ORIGIN = 0x1000, LENGTH = 0x1000 }
SECTIONS { ROM : { *(.text) } >rom }
(8)、通过:phdr为一个section指定一个或多个program segment。一个section可以属于多个segment。如果一个section不属于任何segment,则可以将:phdr设置为:NONE。如下例所示:
PHDRS { text PT_LOAD ; }
SECTIONS { .text : { *(.text) } :text }
(9)、对于output section当中所有未指定有效值的区域(如由于对齐而留下的空隙)都可以使用=fillexp表达式当中的值来填充。该值以大端字节序保存,并且可以被FILL指令所改变。
(10)、格式中的每个output-section-command语句只可能是下列四种之一:
a、一个符号的赋值操作;
b、一个input section description语句;
c、一个直接包含的数据值;
d、一个特殊的output section关键字。
其中,最常见的output-section-command就是input section description语句。
1)、在output-section-command语句中也可以使用符号的赋值操作。
2)、input section description语句
一个input section description语句由一个输入文件名,和紧随其后的包含在小括号之中的一系列section的名称组成。输入文件名可以使用通配符来表示。如data.o(.data)、 *(.text)等。
*(.text)表示将所有输入文件中的.text section都保存到输出文件中。在链接脚本中,可以使用EXCLUDE_FILE排除某些文件。如*(EXCLUDE_FILE (*crtend.o *otherfile.o) .ctors),表示包含除文件名匹配*crtend.o或*otherfile.o等两个通配符模式之外的所有文件中的.ctors section。
可以使用以下两种方式来为同一output section包含多种不同名称的section,如下所示:
*(.text .rdata)
*(.text) *(.rdata)
两者主要的区别在于保存到output section的顺序,前者是按照.text和.rdata在输入文件中出现的次序,后者是严格按照它们在链接脚本中所述的顺序,即.text保存在前,.rdata保存在后。
在input section description语句中,文件名和section名称都可以使用通配符模式。“*”表示可以匹配任意数量的字符;“?”表示匹配任意的单个字符;“[chars]”表示匹配中括号当中的任一字符;“\”表示转义元字符。文件名通配符模式只匹配命令行参数或INPUT指令中出现过的文件。如果一个文件名匹配多个通配符模式(包括直接匹配,即该文件名直接出现在input section description语句中),则链接器会使用在链接脚本中首先匹配的模式。
对于输出文件,其中section的保存次序是按照它们在链接脚本中所述的顺序排列,如在实例中,.interp排在最前,.bss排在最后。对于输入文件,通常情况下是按照它们在链接过程中被处理的顺序排列(也就是在命令行参数中出现的顺序),而对于输入文件当中的section,则按照它们在该输入文件当中出现的前后位置排序。不过,输入文件名或input section名称的通配符模式可以通过SORT_BY_NAME来改变它们的次序,如SORT_BY_NAME(.text*)。
SORT_BY_NAME(SORT是它的别名)按照section名称的升序排序;SORT_BY_ALIGNMENT按照对齐字节数排序,对齐字节数大的排前面;SORT_BY_INIT_PRIORITY则按照GCC init_priority属性的值从小到大排序。这些输入文件中的section就按照它们排序之后的顺序依次保存到输出文件相应的section中。
SORT_BY_NAME和SORT_BY_ALIGNMENT,它们之间可以相互嵌套使用,如SORT_BY_NAME(SORT_BY_ALIGNMENT(wildcard section pattern)),它表示先按名称排序,然后名称相同的再按对齐字节数排序;它们也可以嵌套自己使用,如SORT_BY_NAME(SORT_BY_NAME(wildcard section pattern)),不过它的意义跟SORT_BY_NAME(wildcard section pattern)相同。链接脚本当中的排序指令(即SORT_BY_NAME和SORT_BY_ALIGNMENT)的优先级高于命令行参数--sort-sections所指定的排序方式。链接脚本当中的非嵌套排序指令加命令行参数所指定的排序方式则相当于它们是嵌套使用,如SORT_BY_NAME(wildcard section pattern)加--sort-sections alignment则等效于SORT_BY_NAME(SORT_BY_ALIGNMENT(wildcard section pattern)),SORT_BY_ALIGNMENT(wildcard section pattern)加--sort-section name等效于SORT_BY_ALIGNMENT(SORT_BY_NAME (wildcard section pattern))。如果链接脚本当中的排序指令已经是嵌套使用则忽略命令行参数所指定的排序方式。SORT_NONE指令可以忽略命令行参数所指定的排序方式,相当于禁止排序。如在实例中,输入文件当中的.init和.fini section都禁止排序,按照默认的方式排列,即先被处理的排前面。使用链接器的命令行参数--print-map可以查看链接过程中的详细情况,包括它们的顺序。
链接器将不属于特定input section的符号视为它们位于名为COMMON的input section中。
当使用链接时垃圾收集功能时,KEEP标记的section不会被删除,如KEEP(*(.init))、KEEP(SORT_BY_NAME(*)(.ctors))。
3)、直接包含的数据值
在一个output section中,可以使用BYTE、SHORT、LONG、QUAD和SQUAD等指令直接包含几个字节的数据。BYTE、SHORT、LONG和QUAD分别表示1字节、2字节、4字节和8字节。SQUAD在64位系统上表示8字节,在32位系统上表示4字节。其中,每个指令都带有一个参数,参数为表达式,表达式的值保存在地址计数器的当前地址上。如下例中的LONG:
SECTIONS { .text : { *(.text) ; LONG(1) } .data : { *(.data) } }
这些指令都被当作output-section-command,所以它们所在的位置也只能出现在output-section-command该出现的位置。在例子中,LONG(1)表示在这个地址保存数值1,占用4个字节,它的字节序跟所在的输出文件的字节序相同。
4)、特殊的output section关键字
还有两个特别的关键字可以作为output-section-command,分别是CREATE_OBJECT_SYMBOLS和CONSTRUCTORS。
CREATE_OBJECT_SYMBOLS表示链接器为每一个输入文件创建一个符号,符号名就是该输入文件名。该关键字用于a.out格式。
CONSTRUCTORS表示将构造函数信息保存在该关键字在链接脚本中所出现的output section当中。该关键字用于a.out、ECOFF和XCOFF等格式,在其他类型的格式中该关键字被忽略。对于COFF和ELF格式,构造函数和析构函数信息会被分别保存在.ctors和.dtors section当中。
3、overlay description语句
overlay description用于描述将在同一内存地址运行的section如何保存到输出文件中。使用OVERLAY指令来表示。
五、链接脚本中还有几个复杂的指令,如MEMORY指令、PHDRS指令和VERSION指令等,关于它们的使用方法可以查阅参考文献。