GNU-ld链接脚本浅析

from:http://blog.chinaunix.net/uid/20499746.html

1. 什么是链接脚本

链接器主要有两个作用,一是将若干输入文件(.o文件)根据一定规则合并为一个输出文件(例如ELF格式的可执行文件);一是将符号与地址绑定(当然加载器也要完成这一部分工作)。关于链接器的工作机制可以参考《Linker and Loader》一书,本文只关心它的第一个功能,即如何根据一定规则将一个或多个输入文件合并成输出文件。这里的一定规则是通过链接脚本描述的。链接器有一个编译到其二进制代码中的默认链接脚本,大多数情况下使用它链接输入文件并生成目标文件。当然,我们也可以提供自定义的脚本以精确控制目标文件的格式,如同Linux内核做得那样,链接器“- T”参数用于指定自定义的脚本文件。

链接脚本有自己的一套语法,本文无意对它进行过多论述,后文描述vmlinux_32.lds.S内容时会对内核用到的语法进行解释。如果你希望了解完整的脚本语法,可以阅读参考文献1

2. 一些准备知识

说起链接器,ELF文件格式通常是绕不开的,介绍它的文档多不胜数。实际上,对于了解链接脚本,我们完全没必要去学习ELF的具体格式,有一个全局的视图就足够了(当然,了解ELF格式会让事情变得轻松,你可以很轻易的将脚本中的某些元素和ELF格式中的一些字段联系起来,例如后面看到的PHDRS关键字就很容易和ELF的程序头部表关联)。

 

1. 链接器视图overview(摘自《ELF文件格式分析》,滕启明)

1展示了从链接器的角度,如何看待输入文件和输出文件的视图。左边的链接视图对应输入文件,它为链接器提供的主要内容是section(节区)。随便找一个.o文件,通过objdump后可以找到类似下面的内容:

10 .init         00000030  080482b4  080482b4  000002b4  2**2

                  CONTENTS, ALLOC, LOAD, READONLY, CODE

 11 .plt          00000050  080482e4  080482e4  000002e4  2**2

                  CONTENTS, ALLOC, LOAD, READONLY, CODE

 12 .text         0000019c  08048340  08048340  00000340  2**4

                  CONTENTS, ALLOC, LOAD, READONLY, CODE

这里的.init.plt.text就是section名。不同的.o文件可以有相同的section,例如.text。编译器在生成.o文件时会根据所生成二进制的不同性质把它们放入相应的section中。例如函数编译后的二进制代码通常放到.text,而const关键字修饰的全局数组会放到.rodata中。GCC有除了默认的section,例如.text.data.bss.debug.dynsym等,也支持用户自定义section,在后面的内容中我们可以看到Linux大量使用GCC的扩展__attribute__ ((section(“section_name”))生成自定义section

链接器在进行链接时,会根据链接脚本从输入的.o文件中挑选出感兴趣的section,把它们合并生成新的section,这些新产生的section归属于目标文件的某个segment(段),并出现在目标文件中。例如file1.ofile2.o分别有两个.text,它们在链接后生产的目标文件也会有一个.text,而这个.text既是由file1.ofile2.o.text合并而来的。这里提到了segment的概念,见图1的右部执行视图Segment可以看作一组具有相同属性(或部分相同属性)的section的集合,属性是指读、写、执行(通常用rwxrwe表示)。例如.text通常存放的是代码编译后的二进制,它具有r-x权限;.rodata存放是的只读数据,如常量字符串,它通常具有r―权限(实际上也可以具有x权限,例如用一个全局const数组存放可执行的机器码);那么在生成目标文件时,.text.rodata就可以通过一个具有r-x属性的text segment来包含它们,这就是我们通常说的文本段。经常看到有朋友在C版问常量字符串的地址为什么在文本段?常量字符串放哪儿?之类的问题,其实写一个简单的程序,例如:

int main()

{

    printf(“%s
”, “hello world”);

}

gcc -S编译后可以看到:

.section    .rodata

.LC0:

.string ”hello, world”

这里常量字符串”hello, world”放到了.rodata section,链接后该section通常会和.text section一起放到目标文件的text segment中,这就是为什么字符串地址和main()函数的地址如此相近的原因。

SegmentELF术语中称为program headers,用来描述整个目标文件以什么样的方式加载到内存中,方式是指加载的地址、segment的长度和属性等等。用objdump -p命令可以查看目标文件的segment,当然你也可以在通过objdump -Dx得到内容中找到它们。其内容如下所示(类似):

Program Header:

    PHDR off    0×00000034 vaddr 0×08048034 paddr 0×08048034 align 2**2

         filesz 0×00000100 memsz 0×00000100 flags r-x

  INTERP off    0×00000134 vaddr 0×08048134 paddr 0×08048134 align 2**0

         filesz 0×00000013 memsz 0×00000013 flags r–

    LOAD off    0×00000000 vaddr 0×08048000 paddr 0×08048000 align 2**12

         filesz 0x0000055c memsz 0x0000055c flags r-x

    LOAD off    0x0000055c vaddr 0x0804955c paddr 0x0804955c align 2**12

         filesz 0x000000fc memsz 0×00000104 flags rw-

 DYNAMIC off    0×00000570 vaddr 0×08049570 paddr 0×08049570 align 2**2

         filesz 0x000000c8 memsz 0x000000c8 flags rw-

    NOTE off    0×00000148 vaddr 0×08048148 paddr 0×08048148 align 2**2

         filesz 0×00000044 memsz 0×00000044 flags r–

EH_FRAME off    0x000004e8 vaddr 0x080484e8 paddr 0x080484e8 align 2**2

         filesz 0x0000001c memsz 0x0000001c flags r–

到这里,我们可以简单的对链接器的工作做一个概括。链接器从输入的.o文件中挑选出感兴趣的section(注意,我们再次提到了感兴趣的section”。是的,并不是所有出现在.o文件中的section都会出现在最后的目标文件中。在后面我们会看到Linux如何把它不感兴趣的section排除在外),根据链接脚本提供的规则生成新的section,再根据新section的属性把它们分为不同的segment

目标文件加载到内存的过程实际上就是若干不同segment被加载到内存的过程,下一节我们会看到Linux内核image是如何划分segment的。

参考文章:

http://blog.chinaunix.net/uid-26404697-id-3183005.html

发表评论

电子邮件地址不会被公开。 必填项已用 * 标注

您可以使用这些 HTML 标签和属性: <a href="" title=""> <abbr title=""> <acronym title=""> <b> <blockquote cite=""> <cite> <code> <del datetime=""> <em> <i> <q cite=""> <strike> <strong>