链接器(LD)的工作内容

链接器做些什么

  链接器之所以存在或者产生,基本上是由于程序开发的模块化。这里讲的模块,主要是编译概念上的模块,通常他们按照功能划分,比如一个.c或者.cpp文件就是一个编译单元,就是一个模块,编译后就产生一个.o目标文件。为了最终生成一个可执行文件、静态库或者动态库,就需要把各个编译单元按照特定的约定组合到一起。这里特定的约定指的就是“目标文件格式”,它定义了目标文件、库文件和可执行文件的格式,这里组合这一过程就叫做链接。
  一个编译模块中,通常是函数的定义和全局数据的定义,数据类型的定义通常在头文件中,编译时会被包含在编译模块中。函数和数据由符号来标识,一般符号有全局和静态之分,全局符号可以被其他模块引用,而静态符号只能在本模块中引用。编译各个模块时,编译器会解析该模块。重要的一项工作就是建立符号表,符号表中包含了本模块有哪些符号可以被其他模块引用(导出符号),还包括本模块引用(导入符号,即未定义符号)、但在其他模块中定义的符号。每一个符号都关联一个地址,这个地址指明了该符号在本模块中的偏移地址(通常是一个从0开始的地址)。
  链接器在链接过程中,会扫描各个模块的符号表,得到一个“全局符号表”,链接器由此决定一个符号在哪里被定义,在哪里被引用。并且,将符号引用处替换为定义处的地址,这一过程就叫做符号解析
  链接器的一项终极目标就是生成可执行文件。通常,可执行文件和普通目标文件的重要区别就是地址空间的使用。主流操作系统中,可执行文件都是基于虚拟地址空间的,即每个可执行文件都有相同且独立的地址空间,并且文件中各个段(代码段,数据段,以及进程空间中的堆栈段)都有相似的布局。而普通目标文件却使用从零开始的地址空间,这样一来,模块M中的符号m就可能和模块N中的符号n拥有“相同”的地址。在链接器链接各个模块时,会从各个模块中“提取”类型相同的段进行合并,并将合并后的段写入可执行文件中。这一过程被称为存储空间的分配。值得一提的是,栈、堆以及未初始化的数据这些“运行时”需要的空间不会在可执行文件中占据磁盘空间,但它们占用相应的地址空间。
  由于存在上述“合并”过程,前面提到的符号解析就涉及到另外一个过程:重定位。由于各个模块中的函数/数据地址会被重新排放,那么对这些符号的引用也必须被相应地调整。这一调整过程被称作重定位。
  符号解析,存储空间分配,还有重定位,这三个过程是一个有机的整体,是“同时”进行的,且这三个过程也是模块化所带来的必须要解决的问题。

 

链接器脚本

 

许多脚本是相当的简单的.

可能的最简单的脚本只含有一个命令: ‘SECTIONS’. 你可以使用’SECTIONS’来描述输出文件的内存布局.

‘SECTIONS’是一个功能很强大的命令. 这里这们会描述一个很简单的使用. 让我们假设你的程序只有代码节, 初始化过的数据节, 和未初始化过的数据节. 这些会存在于’.text’,’.data’和’.bss’节, 另外, 让我们进一步假设在你的输入文件中只有这些节.

对于这个例子, 我们说代码应当被载入到地址’0x10000’处, 而数据应当从0x8000000处开始(虚拟地址空间). 下面是一个实现这个功能的脚本:

SECTIONS
{
    . = 0x10000;
    .text : { *(.text) }
    . = 0x8000000;
    .data : { *(.data) }
    .bss : { *(.bss) }
}

你使用关键字’SECTIONS’写了这个SECTIONS命令, 后面跟有一串放在花括号中的符号赋值和输出节描述的内容.

上例中, 在’SECTIONS’命令中的第一行是对一个特殊的符号’.’赋值, 这是一个定位计数器. 如果你没有以其它的方式指定输出节的地址(其他方式在后面会描述), 那地址值就会被设为定位计数器的现有值. 定位计数器然后被加上输出节的尺寸. 在’SECTIONS’命令的开始处, 定位计数器拥有值’0’.

第二行定义一个输出节,’.text’. 冒号是语法需要,现在可以被忽略. 节名后面的花括号中,你列出所有应当被放入到这个输出节中的输入节的名字. ‘‘是一个通配符,匹配任何文件名. 表达式’(.text)’意思是所有的输入文件中的’.text’输入节.

因为当输出节’.text’定义的时候, 定位计数器的值是’0x10000’,连接器会把输出文件中的’.text’节的地址设为’0x10000’.

余下的内容定义了输出文件中的’.data’节和’.bss’节. 连接器会把’.data’输出节放到地址’0x8000000’处. 连接器放好’.data’输出节之后, 定位计数器的值是’0x8000000’加上’.data’输出节的长度. 得到的结果是连接器会把’.bss’输出节放到紧接’.data’节后面的位置.

连接器会通过在必要时增加定位计数器的值来保证每一个输出节具有它所需的对齐. 在这个例子中, 为’.text’和’.data’节指定的地址会满足对齐约束, 但是连接器可能会需要在’.data’和’.bss’节之间创建一个小的缺口。

就这样,这是一个简单但完整的连接脚本。

每个连接都被一个’连接脚本’所控制. 这个脚本是用连接命令语言书写的。

 

一个链接脚本的例子:

ENTRY(start)
SECTIONS
{
    . = 0x100000;

	.text :
	{
		*(.text)
		. = ALIGN(4096);
	}
	.data :
	{
		*(.data)
		*(.rodata)
		. = ALIGN(4096);
	}
	.bss :
	{
		*(.bss)
		. = ALIGN(4096);
	}
	.stab :
	{
		*(.stab)
		. = ALIGN(4096);
	}
	.stabstr :
	{
		*(.stabstr)
	 	. = ALIGN(4096);
	}
	
	/DISCARD/ : { *(.comment) *(.eh_frame) }
}

这个脚本告诉ld程序如何构造我们所需的内核映像文件
首先,脚本声明了内核程序的入口地址是符号 "start" 。然后声明了段起始位置
0x100000(1MB),接着是第一个段.text段(代码段)、已初始化数据段.data、未初始
化数据段.bss以及它们采用的4096的页对齐方式
。Linux GCC 增加了额外的数据段.rodata,这是一个只读的已初始化数据段,放置常量什么的。另外为了简单起见,我们
把.rodata段和.data段放在了一起。最后的stab和stabstr段暂时无需关注,等到后面讲
到调试信息的时候就会明白。
 

你可能感兴趣的:(内核设计)