如何在 lds中定义符号函数,在源文件中使用

你写了一系列的命令作为一个连接脚本. 每一个命令是一个带有参数的关键字,或者是一个对符号函数的赋值. 你可以用分号分隔命令. 空格一般被忽略.

文件名或格式名之类的字苻串一般可以被直接键入. 如果文件名含有特殊字符,比如一般作为分隔文件名用的逗号, 你可以把文件名放到双引号中. 文件名中间无法使用双引号.

你可以象在C语言中一样,在连接脚本中使用注释, 用'/*'和'*/'隔开. 就像在C中,注释在语法上等同于空格.

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

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

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

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

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

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

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

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

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

可能會需要在'.data'和'.bss'节之间创建一个小的缺口.

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

在本章中,我们会描述一些简单的脚本命令.

在运行一个程序时第┅个被执行到的指令称为"入口点". 你可以使用'ENTRY'连接脚本命令来设置入口点.参数

有多种不同的方法来设置入口点.连接器会通过按顺序尝试以下嘚方法来设置入口点, 如果成功了,就会停止.

* 如果存在,就使用'.text'节的首地址;

有几个处理文件的连接脚本命令.

在当前点包含连接脚本文件mon'这就允許你把不同类型的普通符号函数映射到内存的不同位置。

在一些老的连接脚本上你有时会看到'[COMMON]'。这个符号函数现在已经过时了 它等效於'*(COMMON)'。

当连接时垃圾收集正在使用中时('--gc-sections')这在标识那些不应该被排除在外的节时非常有用。这是通过在输入节的通配符入口外面加上'KEEP()'实现嘚比如'KEEP(*(.init))'或者'KEEP(SORT(*)(.sorts))'。

接下来的例子是一个完整的连接脚本它告诉连接器去读取文件'all.o'中的所有节,并把它们放到输出节'outputa'的开始位置处 该输出節是从位置'0x10000'处开始的。 从文件'foo.o'中来的所有节'.input1'在同一个输出节中紧密排列

你可以通过使用输出节命令'BYTE','SHORT','LONG','QUAD',或者'SQUAD'在输出节中显式包含几个字节的數据每一个关键字后面都跟上一个圆括号中的要存入的值。表达式的值被存在当前的定位计数器的值处

‘BYTE’,‘SHORT’‘LONG’‘QUAD’命令分别存储一个,两个四个,八个字节存入字节后,定位计数器的值加上被存入的字节数

比如,下面的命令会存入一字节的内容1,后面跟上㈣字节其内容是符号函数'addr'的值。

当使用64位系统时‘QUAD’和‘SQUAD’是相同的;它们都会存储8字节,或者说是64位的值而如果软硬件系统都是32位的,一个表达式就会被作为32位计算在这种情况下,‘QUAD’存储一个32位值并把它零扩展到64位, 而‘SQUAD’会把32位值符号函数扩展到64位

如果輸出文件的目标文件格式有一个显式的endianness,它在正常的情况下值就会被以这种endianness存储当一个目标文件格式没有一个显式的endianness时, 值就会被以第一個输入目标文件的endianness存储。

这些命令只在一个节描述内部才有效而不是在它们之间, 所以下面的代码会使连接器产生一个错误信息:

你鈳能使用‘FILL’命令来为当前节设置填充样式。它后面跟有一个括号中的表达式任何未指定的节内内存区域(比如,因为输入节的对齐要求而造成的裂缝)会以这个表达式的值进行填充一个'FILL'语句会覆盖到它本身在节定义中出现的位置后面的所有内存区域;通过引入多个‘FILL’语句,你可以在输出节的不同位置拥有不同的填充样式

这个例子显示如何在未被指定的内存区域填充'0x90':

‘FILL’命令跟输出节的‘=FILLEXP’属性楿似,但它只影响到节内跟在‘FILL’命令后面的部分而不是整个节。如果两个都用到了那‘FILL’命令优先。

有两个关键字作为输出节命令嘚形式出现

`CREATE_OBJECT_SYMBOLS'这个命令告诉连接器为每一个输入文件创建一个符号函数。而符号函数的名字正好就是相关输入文件的名字而每一个符号函数的节就是`CREATE_OBJECT_SYMBOLS'命令出现的那个节。

这个命令一直是a.out目标文件格式特有的 它一般不为其它的目标文件格式所使用。

当使用a.out目标文件格式进荇连接的时候 连接器使用一组不常用的结构以支持C++的全局构造函数和析构函数。当连接不支持专有节的目标文件格式时 比如ECOFF和XCOFF,连接器会自动辩识C++全局构造函数和析构函数的名字对于这些目标文件格式,‘CONSTRUCTORS’命令告诉连接器把构造函数信息放到‘CONSTRUCTORS’命令出现的那个输絀节中对于其它目标文件格式,‘CONSTRUCTORS’命令被忽略

符号函数`__CTOR_LIST__'标识全局构造函数的开始,而符号函数`__DTOR_LIST'标识结束这个列表的第一个WORD是入口嘚数量,紧跟在后面的是每一个构造函数和析构函数的地址再然后是一个零WORD。编译器必须安排如何实际运行代码对于这些目标文件格式,GNU C++通常从一个`__main'子程序中调用构造函数而对`__main'的调用自动被插入到`main'的启动代码中。GNU C++通常使用'atexit'运行析构函数或者直接从函数'exit'中运行。

对于潒‘COFF’或‘ELF’这样支持专有节名的目标文件格式GNU C++通常会把全局构造函数与析构函数的地址值放到'.ctors'和'.dtors'节中。把下面的代码序列放到你的连接脚本中去这样会构建出GNU C++运行时代码希望见到的表类型。

如果你正使用GNU C++支持来进行优先初始化那它提供一些可以控制全局构造函数运荇顺序的功能,你必须在连接时给构造函数排好序以保证它们以正确的顺序被执行当使用'CONSTRUCTORS'命令时,替代为`SORT(CONSTRUCTORS)'当使用'.ctors'和'dtors'节时,使用`*(SORT(.ctors))'和`*(SORT(.dtors))'

通常编译器和连接器会自动处理这些事情,并且你不必亲自关心这些事情但是,当你正在使用C++并自己编写连接脚本时,你可能就要考虑這些事情了

连接器不会创建那些不含有任何内容的输出节。这是为了引用那些可能出现或不出现在任何输入文件中的输入节时方便比洳:

如果至少在一个输入文件中有'.foo'节,它才会在输出文件中创建一个'.foo'节

如果你使用了其它的而不是一个输入节描述作为一个输出节命令仳如一个符号函数赋值,那这个输出节总是被创建即使没有匹配的输入节也会被创建。

一个特殊的输出节名`/DISCARD/'可以被用来丢弃输入节任哬被分配到名为`/DISCARD/'的输出节中的输入节不包含在输出文件中。

上面我们已经展示了一个完整的输出节描述,看下去就象这样:

每一个输出節可以有一个类型类型是一个放在括号中的关键字,已定义的类型如下所示:

这个节应当被标式讵不可载入所以当程序运行时,它不會被载入到内存中

支持这些类型名只是为了向下兼容,它们很少使用它们都具有相同的效果:这个节应当被标式讵不可分配,所以当程序运行时没有内存为这个节分配。

连接器通常基于映射到输出节的输入节来设置输出节的属性你可以通过使用节类型来重设这个属性,比如在下面的脚本例子中,‘ROM’节被定址在内存地址零处并且在程序运行时不需要被载入。‘ROM’节的内容会正常出现在连接输出攵件中

每一个节有一个虚地址(VMA)和一个载入地址(LMA);出现在输出节描述中的地址表达式设置VMS

连接器通常把LMA跟VMA设成相等。你可以通过使用‘AT’关键字改变这个跟在关键字‘AT’后面的表达式LMA指定节的载入地址。或者通过`AT&gtLMA_REGION'表达式, 你可以为节的载入地址指定一个内存区域

这个特性是为了便于建立ROM映像而设计的。比如下面的连接脚本创建了三个输出节:一个叫做‘.text’从地址‘0x1000’处开始,一个叫‘.mdata’盡管它的VMA是'0x2000',它会被载入到'.text'节的后面最后一个叫做‘.bss’是用来放置未初始化的数据的,其地址从'0x3000'处开始符号函数'_data'被定义为值'0x2000', 它表示定位计数器的值是VMA的值,而不是LMA

这个连接脚本产生的程序使用的运行时初始化代码会包含象下面所示的一些东西,以把初始化后的数据从ROM映像中拷贝到它的运行时地址中去注意这节代码是如何利用好连接脚本定义的符号函数的。

你可以通过使用`&gtREGION'把一个节赋给前面已经定义嘚一个内存区域

这里有一个简单的例子:

你可以通过使用`:PHDR'把一个节赋给前面已定义的一个程序段。如果一个节被赋给一个或多个段那后來分配的节都会被赋给这些段,除非它们显式使用了':PHDR'修饰符你可以使用':NONE'来告诉连接器不要把节放到任何一个段中。

这儿有一个简单的例孓:

你可以通过使用'=FILLEXP'为整个节设置填充样式FILLEXP是一个表达式。任何没有指定的输出段内的内存区域(比如因为输入段的对齐要求而产生嘚裂缝)会被填入这个值。如果填充表达式是一个简单的十六进制值比如,一个以'0x'开始的十六进制数字组成的字符串并且尾部不是'k'或'M',那一个任意的十六进制数字长序列可以被用来指定填充样式;前导零也变

为样式的一部分对于所有其他的情况,包含一个附加的括号戓一元操作符'+'那填充样式是表达式的最低四字节的值。在所有的情况下数值是big-endian.

你还可以通过在输出节命令中使用'FILL'命令来改变填充值。

這里是一个简单的例子:

一个覆盖描述提供一个简单的描述办法以描述那些要被作为一个单独内存映像的一部分载入内存,但是却要在同┅内存地址运行的节在运行时,一些覆盖管理机制会把要被覆盖的节按需要拷入或拷出运行时内存地址并且多半是通过简单地处理内存位。 这个方法可能非常有用比如在一个特定的内存区域比另一个快时。

覆盖是通过‘OVERLAY’命令进行描述‘OVERLAY’命令在‘SECTIONS’命令中使用,僦像输出段描述一样‘OVERLAY’命令的完整语法如下:

除了‘OVERLAY’关键字,所有的都是可选的每一个节必须有一个名字(上面的SECNAME1和SECNAME2)。在‘OVERLAY’結构中的节定义跟通常的‘SECTIONS’结构中的节定义是完全相同的除了一点,就是在‘OVERLAY’中没有地址跟内存区域的定义

节都被定义为同一个開始地址。所有节的载入地址都被排布使它们在内存中从整个'OVERLAY'的载入地址开始都是连续的(就像普通的节定义,载入地址是可选的缺渻的就是开始地址;开始地址也是可选的,缺省的是当前的定位计数器的值)

如果使用了关键字`NOCROSSREFS', 并且在节之间存在引用连接器就会報告一个错误。因为节都运行在同一个地址上所以一个节直接引用另一个节中的内容是错误的。

对于'OVERLAY'中的每一个节连接器自动定义两個符号函数。符号函数`__load_start_SECNAME'被定义为节的开始载入地址符号函数`__load_stop_SECNAME'被定义为节的最后载入地址。SECNAME中的不符合C规定的任何字符都将被删除C(或鍺汇编语言)代码可能使用这些符号函数在必要的时间搬移覆盖代码。

在覆盖区域的最后定位计数器的值被设为覆盖区域的开始地址加仩最大的节的长度。

这里是一个例子记住这只会出现在‘SECTIONS’结构的内部。

拷贝'.text1'到覆盖区域的C代码看上去可能会像下面这样:

注意'OVERLAY'命令只昰为了语法上的便利因为它所做的所有事情都可以用更加基本的命令加以代替。上面的例子可以用下面的完全特效的写法:

连接器在缺渻状态下被配置为允许分配所有可用的内存块你可以使用‘MEMORY’命令重新配置这个设置。

‘MEMORY’命令描述目标平台上内存块的位置与长度伱可以用它来描述哪些内存区域可以被连接器使用,哪些内存区域是要避免使用的然后你就可以把节分配到特定的内存区域中。连接器會基于内存区域设置节的地址对于太满的区域,会提示警告信息连接器不会为了适应可用的区域而搅乱节。

一个连接脚本最多可以包含一次'MEMORY'命令但是,你可以在命令中随心所欲定义任意多的内存块语法如下:

NAME是用在连接脚本中引用内存区域的名字。出了连接脚本區域名就没有任何实际意义。区域名存储在一个单独的名字空间中它不会和符号函数名,文件名节名产生冲突,每一块内存区域必须囿一个唯一的名字

ATTR字符串是一个可选的属性列表,它指出是否为一个没有在连接脚本中进行显式映射地输入段使用一个特定的内存区域如果你没有为某些输入段指定一个输出段,连接器会创建一个跟输入段同名的输出段如果你定义了区域属性,连接器会使用它们来为咜创建的输出段选择内存区域

ATTR字符串必须包含下面字符中的一个,且必须只包含一个:

`!' 对前一个属性值取反

如果一个未映射节匹配了仩面除'!'之外的一个属性,它就会被放入该内存区域'!'属性对

该测试取反,所以只有当它不匹配上面列出的行何属性时一个未映射节才会被放入到内存区域。

ORIGIN是一个关于内存区域地始地址的表达式在内存分配执行之前,这个表达式必须被求值产生一个常数这意味着你不鈳以使用任何节相关的符号函数。关键字'ORIGIN'可以被缩写为'org'或'o'(但是不可以写为,比如‘ORG’)

LEN是一个关于内存区域长充(以字节为单位)的表達式就像ORIGIN表达式,这个表达式在分配执行前也必须被求得为一个常数值关键字'LENGTH'可以被简写为‘len'或'l'。

在下面的例子中我们指定两个可鼡于分配的内存区域:一个从0开始,有256kb长度另一个从0x4000000开始,有4mb长度连接器会把那些没有进行显式映射且是只读或可执行的节放到'rom'内存區域。并会把另外的没有被显式映射地节放入到'ram'内存区域

一旦你定义了一个内存区域,你也可以指示连接器把指定的输出段放入到这个內存区域中这可以通过使用'&gtREGION'输出段属性。比如如果你有一个名为'mem'的内存区域,你可以在输出段定义中使用'&gtmem'如果没有为输出段指定地址,连接器就会把地址设置为内存区域中的下一个可用的地址如果总共的映射到一个内存区域的输出段对于区域来说太大了,连接器会提示一条错误信息

ELF目标文件格式使用“程序头”,它也就是人们熟知的“节”程序头描述了程序应当如何被载入到内存中。你可以通過使用带有'-p'选项的‘objdump’命令来打印出这个程序头

当你在一个纯ELF系统上运行ELF程序时,系统的载入程序通过读取文件头来计算得到如何来载叺这个文件这只在程序头被正确设置的情况下才会正常工作。本手册并不打算介绍系统载入程序如何解释文件头的相关细节问题;关于哽多信息请参阅ELF ABI。

连接顺在缺省状态下会自己创建一个可用的程序头但是,在某些情况下你可能需要更为精确地指定程序头。你可鉯使用命令‘PHDRS’达到这个目的当连接器在连接脚本中看到‘PHDRS’命令时,它只会创建被指定了的程序头

连接器只在产生ELF输出文件时关心‘PHDRS’命令。在其它情况下连接器只是简单地忽略‘PHDRS’。

下面是‘PHDRS’命令的语法单词‘PHDRS’,‘FILEHDR’‘AT’和‘FLAGS’都是关键字。

NAME只在连接脚夲的‘SECTIONS’命令中引用时用到它不会被放到输出文

件中。程序头的名字会被存储到单独的名字空间中每一个程序头都必须有一个唯一的洺字。

某些特定类型的程序头描述系统载入程序要从文件中载入到内存的节在连接脚本中,你通过把可载入的输出节放到段中来指定这些段的内容你可以使用‘:PHDR’输出节属性把一个节放到一个特定的段中。

把某些节放到多个段中也是正常的这仅仅暗示了一个内存段中含有另一个段。你可以重复使用‘:PHDR’在每一个应当含有这个节的段中使用它一次。

如果你使用‘:PHDR’把一个节放到多个段中那连接器把隨后的所有没有指定‘:PHDR’的可分配节都放到同一个段中。这是为了方便因为通常一串连续的节会被放到一个单独的段中。你可以使用‘:NONE’来覆盖缺省的段告诉连接器不要把节放到任何一个段中。

你可能在程序头类型后面使用‘FILEHDR’和‘PHDRS’关键字来进一步描述段的内容‘FILEHDR’关键字表示段应当包含ELF文件头。‘PHDRS’关键字表示段应当包含ELF程序头本身

TYPE可以是如下的一个。数字表示关键字的值

表示一个不用的程序头。

表示这个程序头描述了一个被从文件中载入的段

表示一个可以从中找到动态链接信息的段。

表示一个可以从中找到关于程序名解釋的段

表示一个存有备注信息的段。

一个保留的程序头类型被定义了,但没有被ELF ABI指定

表示一个可以从中找到程序头的段。

一个给出程序头的数值类型的表达式这可以在使用上面未定义的类型时使用。

你可以通过使用‘AT’表达式指定一个段应当被载入到内存中的一个特定的地址这跟在输出节属性中使用‘AT’命令是完全一样的。程序头中的‘AT’命令会覆盖输出节属性中的

连接器通常会基于组成段的節来设置段属性。你可以通过使用‘FLAGS’关键字来显式指定段标志FLAGS的值必须是一个整型值。它被用来设置程序头的‘p_flags'域

这里是一个关于‘PHDRS’的例子。它展示一个在纯ELF系统上的一个标准的程序头设置

在使用ELF时,连接器支持符号函数版本符号函数版本只在使用共享库时有鼡。动态连接器在运行一个可能跟一个更早版本的共享库链接程序时可以使用符号函数版本来选择一个函数的特定版本。

你可以直接在主连接脚本中包含一个版本脚本或者你可以以一个隐式连接脚本的形式提供这个版本脚本。你也可以使用‘--version-script'连接器选项

‘VERSION’命令的语法很简单:

版本脚本命令的格式跟Sun在Solaris 2.5中的连接器的格式是完全一样的。版本脚本定义一个版本节点树你可以在版本脚本中指定节点名和依赖关系。你可以指定哪些符号函数被绑定到哪些版本节点上你还可以把一组指定的符号函数限定到本地范围,这样在共享库的外面它們就不是全局可见的了

最简单的演示版本脚本语言的方法是出示几个小例子:

这个示例版本脚本定义了三个版本节点。第一个版本节点萣义为‘VERS_1.1’它没有其它的依赖脚本把符号函数‘foo1’绑定给‘VERS_1.1’。它把一些数量的符号函数限定到本地范围这样它们在共享库的外面就鈈可见了;这是通过通配符来完成的,所以任何名字以‘old’‘original’或‘new’开头的符号函数都会被匹配。可用的通配符跟在shell中匹配文件名时┅样

下面,版本脚本定义一个节点‘VER_1.2’这个节点依赖‘VER_1.1’。脚本把符号函数‘foo2’绑定给节点‘VERS_1.2’

最后,版本脚本定义节点‘VERS_2.0’这個节点依赖‘VERS_1.2’。脚本把符号函数‘bar1’和‘bar2 ’绑定给版本节点‘VERS_2.0’

当连接器发现一个定义在库中的符号函数没有被指定绑定到一个版本節点,它会把它绑定到一个未指定基础版本的库你可以通过使用‘global: *;’把所有未指定的符号函数绑定到一个给定的版本节点上。

版本节点嘚名字没有任何特殊的含义只是为了方便人们阅读版本‘2.0’可以出现在‘1.1’和‘1.2’之间。但是在书写版本脚本时,这会是一个引起混亂的办法

如果在版本脚本中,这是一个唯一的版本节点节点名可以被省略。这样的版本脚本不给符号函数赋任何版本只是选择哪些苻号函数会被全局可见而哪些不会。

当你把一个程序跟一个带有版本符号函数的共享库连接时程序自身知道每个符号函数的哪个版本是咜需要的,而且它还知道它连接的每一个节享库中哪些版本的节点是它需要的这样,在运行时

动态载入程序可以做一个快速的确认,鉯保证你连接的库确实提供了所有的程序需要用来解析所有动态符号函数的版本节点用这种方法,就有可能让每一个动态连接器知道所囿的外部符号函数不需要通过搜索每一个符号函数引用就能解析

符号函数版本在SunOS上做次版本确认是一种很成熟的方法。一个被提出来的基本的问题是对于外部函数的标准引用会在需要时被绑定到正确的版本但不是在程序启动的时候全部被绑定。如果一个共享库过期了┅个需要的界面可能就不存在了;当程序需要使用这个界面的时候,它可能会突然地意外失败有了符号函数版本后,当用户启动他们的程序时如果要使用的共享库太老了的话,用户会得到一条警告信息

GNU对Sun的版本确认办法有一些扩展。首先就是能在符号函数定义的源文件中把一个符号函数绑定到一个版本节点而不是在一个版本脚本中这主要是为了减轻库维护的工作量。你可以通过类似下面的代码实现這一点:

第二个GNU的扩展是在一个给定的共享库中允许同一个函数的多个版本通过这种办法,你可以不增加共享库的主版本号而对界面做唍全不相容的修改

要实现这个,你必须在一个源文件中多次使用'.symver'操作符这里是一个例子:

当你有一个给定符号函数的多个定义后,有必要有一个方法可以指定一个缺省的版本对于这个符号函数的外部引用就可以找到这个版本。用这种方法你可以只声明一个符号函数嘚一个版本作为缺省版本,否则你会拥有同一个符号函数的多个定义。

如果你想要绑定一个引用到共享库中的符号函数的一个指定的版夲你可以很方便地使用别名(比如,old_foo),或者你可以使用'.symver'操作符来指定绑定到一个外部函数的特定版本

你也可以在版本脚本中指定语言。

連接脚本语言中的表达式的语法跟C的表达式是完全是致的所有的表达式都以整型

值被求值。所有的表达式也被以相同的宽度求值在32位系统是它是32位,否则是64位

你可以在表达式中使用和设置符号函数值。

连接器为了使用表达式定义了几个具有特殊途的内建函数。

所有嘚常数都是整型值

就像在C中,连接器把以'0'开头的整型数视为八进制数把以'0x'或'0X'开头的视为十六进制。连接器把其它的整型数视为十进制

另外,你可以使用'K'和'M'后缀作为常数的度量单位分别为'1024'和''。比如下面的三个常数表示同一个值。

除了引用符号函数名都是以一个字毋,下划线或者句号开始可以包含字母,数字下划线,句点和连接号不是被引用的符号函数名必须不和任何关键字冲突。你可以指萣一个含有不固定它符数或具有跟关键字相同名字但符号函数名必须在双引号内:

因为符号函数可以含有很多非文字字符所以以空格分隔符号函数是很安全的。比如'A-B'是一个符号函数,而'A - B'是一个执行减法运算的表达式

一个特殊的连接器变量"dot"'.'总是含有当前的输出定位计数器。因为'.'总引用输出段中的一个位置它只可以出现在'SECTIONS'命令中的表达式中。'.'符号函数可以出现在表达式中一个普能符号函数允许出现的任哬位置

把一个值赋给'.'会让定位计数器产生移动。这会在输出段中产生空洞定位计数器从不向前移动。

在前面的例子中来自'file1'的'.text'节被定位在输出节'output'的起始位置。它后面跟有1000byte的空隙然后是来自'file2'的'.text'节,同样是后面跟有1000byte的空隙最后是来自'file3'的'.text'节。符号函数'=0x'指定在空隙中填入什麼样的数据

注意:'.'实际上引用的是当前包含目标的从开始处的字节偏移。通常它就是'SECTIONS'语句,其起始地址是0因为'.'可以被用作绝对地址。但是如果'.'被用在一个节描述中它引用的是从这个节起始处开始的偏移,而不是一个绝对地址这样,在下面这样一个脚本中:

'.text'节被赋於起始地址0x100尽管在'.text'输入节中没有足够的数据来填充这个区域,但其长度还是0x200bytes(如果

数据太多,那会产生一条错误信息因为这会试图紦'.'向前移)。'.data'节会从0x500处开始并且它在结尾处还会有0x600的额外空间。

连接器可以识别标准的C的算术运算符集, 以及它们的优先集.

优先集 结合性 運算符 备注

连接器是懒惰求表达式的值它只在确实需要的时候去求一个表达式的值。

连接器需要一些信息比如第一个节的起始地址的徝,还有内存区域的起点与长度在做任何连接的时候这都需要。在连接器读取连接脚本的时候这些值在可能的时候被计算出来。

但是其它的值(比如符号函数的值)直到内存被分配之后才会知道或需要。这样的值直到其它信息(比如输出节的长度)可以被用来进行符號函数赋值的时候才被计算出来

直到内存分配之后,节的长度才会被知道所以依赖于节长度的赋值只能到内存分配之后才会被执行。

囿些表达式比如那些依赖于定位计数器'.'的表达式,必须在节分配的过程中被计算出来

如果一个表达式的结果现在被需要,但是目前得鈈到这个值这样会导致一个错误。比如象下面这样一个脚本:

当一个连接器计算一个表达式时,得到的结果可能是一个绝对值也可能跟某个节相关。一个节相关的表达式是从一个节的基地址开始的固定的偏称值

表达式在连接脚本中的位置决定了它是绝对的或节相关嘚。一个出现在输出节定义中的表达式是跟输出节的基地址相关的一个出现在其它地方的表达式则是绝对的。

如果你通过'-r'选项指定需要鈳重位输出那一个被赋为节相关的表达式的符号函数就会是可重定位的。意思是下一步的连接操作会改变这个符号函数的值符号函数嘚节就是节相关的表达式所在的节。

一个被赋为绝对表达式的符号函数在后面进一步的连接操作中会始终保持它的值不变符号函数会是絕对的,并不

会有任何的特定的相关节

如果一个表达式有可能会是节相关的,你可以使用内建函数'ABSOLUTE'强制一个表达式为绝对的比如,要創建一个被赋为输出节'.data'的末尾地址的绝对符号函数:

为了使用连接脚本表达式连接脚本语言含有一些内建函数。

`ABSOLUTE(EXP)'返回表达式EXP的绝对值(鈈可重定位而不是非负)。主要在把一个绝对值赋给一个节定义内的符号函数时有用

`ADDR(SECTION)'返回节SECTION的绝对地址(VMA)。你的脚本之前必须已经萣义了这个节的地址在接下来的例子中,'symbol_1'和'symbol_2'被赋以相同的值

返回定位计数器'.'对齐到下一个EXP指定的边界后的值。‘ALIGN’不改变定位计数器嘚值它只是在定位计数器上面作了一个算术运算。这里有一个例子它在前面的节之后,把输出节'.data'对齐到下一个'0x2000'字节的边界并在输入節之后把节内的一个变量对齐到下一个'0x8000'字节的边界。

这个例子中前一个'ALIGN'指定一个节的位置因为它是作为节定义的可选项ADDRESS属性出现的。第②个‘ALIGN’被用来定义一个符号函数的值

内建函数'NEXT'跟‘ALIGN’非常相似。

这是'ALIGN'的同义词是为了与其它的连接器保持兼容。这在设置输出节的哋址时非常有用

这跟下面的两个表达同义:

如果你指定了一个连接器输出文件,而连接器不能识别它是一个目标文件还是档案文件它會试图把它读作一个连接脚本。如果这个文件不能作为一个连接脚本被分析连接器就会报告一个错误。

一个隐式的连接器脚本不会替代缺省的连接器脚本

一般,一个隐式的连接器脚本只包含符号函数赋值或者'INPUT','GROUP'或'VERSION'命令。

连接器通过BFD库来对目标文件和档案文件进行操作這些库允许连接器忽略目标文件的格式而使用相关的例程来操作目标文件。只

要简单地创建一个新的BFD后台并把它加到库中一个不同的目標文件格式就会被支持。但是为了节约运行时内存连接器和相关的工具一般被配置为只支持可用的目标文件格式的一个子集,你可以使鼡'objdump -i'来列出你配置的所有支持的格式

就像大多数的案例,BFD是一个在多种相互有冲突的需求之间的一个折中影响BFD设计的一个最主要的因素昰效率。因为BFD简化了程序和后台更多的时间和精力被放在了优化算法以追求更快的速度。

BFD解决方案的一个副产品是你必须记住有信息丢夨的潜在可能在使用BFD机制时,有两处地方有用信息可能丢失:在转化时和在输出时

它如何工作: BFD概要。

当一个目标文件被打开时BFD子程序自动确定输入目标文件的格式。然后它们在内存中用指向子程序的指针构建一个描述符这个描述符被用作存取目标文件的数据结构元素。

因为需要来自目标文件的不同信息BFD从文件的不同节中读取它们,并处理比如,连接器的一个非常普遍的操作是处理符号函数表烸一个BFD后台提供一个在目标文件的符号函数表达形式跟内部规范格式之间的转化的函数,当一个连接器需要一个目标文件的符号函数表时它通过一个内存指针调用一个来自相应的BFD后台的子程序,这个子程序读取表并把它转化为规范表然后,连接器写输出文件的符号函数表另一个BFD后台子程序被调用,以创建新的符号函数表并把它转化为选定的输出格式

在输出的过程中,信息可能会被丢失BFD支持的输出格式并不提供一致的特性,并且在某一种格式中可以被描述的信息可能在另一种格式中没有地方可放一个例子是在'b.out'中的对齐信息,在一個'a.out'格式的文件中没有地方可以存储对齐信息,所以当一个文件是从'b.out'连接而成的并产生的是一个'a.out'的文件,对齐信息就不会被传入到输出攵件中(连接器还是在内部使用对齐信息所以连接器的执行还是正确的)

另一个例子是COFF节名字。COFF文件中可以含有不限数量的节每一个嘟有一个文字的节名。如果连接的目标是一种不支持过多节的格式(比如'a.out')或者是一种不含有节名的格式(比如,Oasys格式)连接器不能潒通常那样简单地处理它。你可以通过把所需的输入输出节通过连接脚本语言进行详细映射来解决这下问题

在规范化的过程中信息也会丟失。BFD内部的对应于外部格式的规范形式并不是完全详尽的;有些在输入格式中的结构在内部并没有对应的表示方法这意味着BFD后台在从外部到内部或从内部到外部的转

化过程中不能维护所有可能的数据。

这个限制只在一个程序读取一种格式并写成另一种格式的时候会是一個问题每一个BFD后台有责任维护尽可能多的数据,内部的BFD规范格式具有对BFD内核不透明的结构体只导出给后台。当一个文件以一种格式读取后规范格式就会为之产生。同时后台把所有可能丢失的信息进行存储。如果这些数据随后会写以相同的格式写回后台程序就可以使用BFD内核提供的跟选前准备的相同的规范格式。因为在后台之间有大量相同的东西在把big endianCOFF拷贝成littile endian COFF时,或者'a.out'到'b.out'时不会有信息丢失。当一些混合格式被连接到一起时只有那些格式跟目标格式不同的文件会丢失信息。

最近在看Linux内核时总是遇到一些囷连接脚本相关的东东,搞得人一头雾水终于下定决心把它搞明白,写下一点心得希望对和我一样的人有所帮助!

你写了一系列的命囹作为一个连接脚本. 每一个命令是一个带有参数的关键字,或者是一个对符号函数的赋值. 你可


以用分号分隔命令. 空格一般被忽略.

文件名或格式名之类的字符串一般可以被直接键入. 如果文件名含有特殊字符,比如一般作为分隔文件名用的逗


号, 你可以把文件名放到双引号中. 文件名中間无法使用双引号.

你可以象在C语言中一样,在连接脚本中使用注释, 用'/*'和'*/'隔开. 就像在C中,注释在语法上等同于空格.

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

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

'SECTIONS'是一个功能很强大的命令. 这里这们会描述一个很简单的使用. 让我们假设你的程序只有代码节,


初始化过的数据节, 和未初始化过的数据节. 这些会存在于'.text','.data'和'.bss'节, 另外, 让我们进一
步假设在你的输入文件中只有这些节.

對于这个例子, 我们说代码应当被载入到地址'0x10000'处, 而数据应当从0x8000000处开始. 下面是一个实现

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

上例中, 在'SECTIONS'命令中的第一行是对一个特殊的符号函数'.'赋值, 这是一个定位计数器. 如果你没有以其


它的方式指定输出节的地址(其他方式在后面会描述), 那地址值就会被设为定位计数器的现有值. 定位计数器
然后被加上输出节的尺寸. 在'SECTIONS'命令的开始处, 定位计数器拥有值'0'.

第二行定义一个输出节,'.text'. 冒号是语法需要,现在可以被忽略. 节名后面的花括号中,你列出所有应当被


放入到这个输出节中的输入節的名字. '*'是一个通配符,匹配任何文件名. 表达式'*(.text)'意思是所有的输

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


器放好'.data'输出节之后, 定位计数器的值是'0x8000000'加上'.data'输出节的长度. 得到的结果是连接器会

连接器会通过在必要时增加定位计数器的值来保证每一個输出节具有它所需的对齐. 在这个例子中, 为'.text'


和'.data'节指定的地址会满足对齐约束, 但是连接器可能会需要在'.data'和'.bss'节之间创建一个小的缺口.

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

在本章中,我们会描述一些简单的脚本命令.

在运行一个程序时第一个被执行到的指令称为"入口点". 你可以使用'ENTRY'连接脚本命令来设置入口点.参数

有多种不同的方法来设置入口点.连接器会通过按顺序尝试以下的方法来设置入口点, 如果成功了,就会停止.

* 如果存在,就使用'.text'节的首地址;

有几个处理文件的连接脚本命令.


在当前点包含连接脚本文件mon'。这就允许你把不同类型的普通符号函数映射到

在一些咾的连接脚本上你有时会看到'[COMMON]'。这个符号函数现在已经过时了 它等效于'*(COMMON)'。

当连接时垃圾收集正在使用中时('--gc-sections')这在标识那些不应该被排除在外的节时非常有用。这

接下来的例子是一个完整的连接脚本它告诉连接器去读取文件'all.o'中的所有节,并把它们放到输出节


每一个关鍵字后面都跟上一个圆括号中的要存入的值表达式的值被存在当前的定位计数器的值处。

‘BYTE’‘SHORT’,‘LONG’‘QUAD’命令分别存储一个两個,四个八个字节。存入字节后定位计


数器的值加上被存入的字节数。

比如下面的命令会存入一字节的内容1,后面跟上四字节,其内嫆是符号函数'addr'的值

当使用64位系统时,‘QUAD’和‘SQUAD’是相同的;它们都会存储8字节或者说是64位的值。而如果软硬件


系统都是32位的一个表達式就会被作为32位计算。在这种情况下‘QUAD’存储一个32位值,并把它零扩展
到64位 而‘SQUAD’会把32位值符号函数扩展到64位。

如果输出文件的目標文件格式有一个显式的endianness它在正常的情况下,值就会被以这种endianness存储


当一个目标文件格式没有一个显式的endianness时, 值就会被以第一个输入目标文件的endianness存储

注意, 这些命令只在一个节描述内部才有效而不是在它们之间, 所以下面的代码会使连接器产生一个错

你可能使用‘FILL’命囹来为当前节设置填充样式。它后面跟有一个括号中的表达式任何未指定的节内内存


区域(比如,因为输入节的对齐要求而造成的裂缝)会以这个表达式的值进行填充一个'FILL'语句会覆盖到
它本身在节定义中出现的位置后面的所有内存区域;通过引入多个‘FILL’语句,你可以茬输出节的不同位置

这个例子显示如何在未被指定的内存区域填充'0x90':

‘FILL’命令跟输出节的‘=FILLEXP’属性相似但它只影响到节内跟在‘FILL’命令後面的部分,而不是


整个节如果两个都用到了,那‘FILL’命令优先

有两个关键字作为输出节命令的形式出现。


这个命令告诉连接器为每┅个输入文件创建一个符号函数而符号函数的名字正好就是相关输入文件的名字。

这个命令一直是a.out目标文件格式特有的 它一般不为其咜的目标文件格式所使用。


当使用a.out目标文件格式进行连接的时候 连接器使用一组不常用的结构以支持C++的全局构造函
数和析构函数。当连接不支持专有节的目标文件格式时 比如ECOFF和XCOFF,连接器会自动辩识C++
全局构造函数和析构函数的名字对于这些目标文件格式,‘CONSTRUCTORS’命令告诉連接器把构造
函数信息放到‘CONSTRUCTORS’命令出现的那个输出节中对于其它目标文件格式,‘CONSTRUCTORS’

符号函数`__CTOR_LIST__'标识全局构造函数的开始而符号函数`__DTOR_LIST'標识结束。这个列表的第一个


WORD是入口的数量紧跟在后面的是每一个构造函数和析构函数的地址,再然后是一个零WORD编译
器必须安排如何實际运行代码。对于这些目标文件格式GNU C++通常从一个`__main'子程序中调用
析构函数,或者直接从函数'exit'中运行

对于像‘COFF’或‘ELF’这样支持专有节洺的目标文件格式,GNU C++通常会把全局构造函数与析构


函数的地址值放到'.ctors'和'.dtors'节中把下面的代码序列放到你的连接脚本中去,这样会构建
出GNU C++运荇时代码希望见到的表类型

如果你正使用GNU C++支持来进行优先初始化,那它提供一些可以控制全局构造函数运行顺序的功能

通常,编译器囷连接器会自动处理这些事情并且你不必亲自关心这些事情。但是当你正在使用


C++,并自己编写连接脚本时你可能就要考虑这些事情叻。

连接器不会创建那些不含有任何内容的输出节这是为了引用那些可能出现或不出现在任何输入文件中的输入

如果至少在一个输入文件中有'.foo'节,它才会在输出文件中创建一个'.foo'节

如果你使用了其它的而不是一个输入节描述作为一个输出节命令比如一个符号函数赋值,那這个输出节总是被


创建即使没有匹配的输入节也会被创建。

一个特殊的输出节名`/DISCARD/'可以被用来丢弃输入节任何被分配到名为`/DISCARD/'的输出节中嘚输入


节不包含在输出文件中。

上面我们已经展示了一个完整的输出节描述,看下去就象这样:

每一个输出节可以有一个类型类型是┅个放在括号中的关键字,已定义的类型如下所示:


这个节应当被标式讵不可载入所以当程序运行时,它不会被载入到内存中
支持这些类型名只是为了向下兼容,它们很少使用它们都具有相同的效果:这个节应当被标式讵不
可分配,所以当程序运行时没有内存为这個节分配。

连接器通常基于映射到输出节的输入节来设置输出节的属性你可以通过使用节类型来重设这个属性,


比如在下面的脚本例孓中,‘ROM’节被定址在内存地址零处并且在程序运行时不需要被载入。
‘ROM’节的内容会正常出现在连接输出文件中

每一个节有一个虚哋址(VMA)和一个载入地址(LMA);出现在输出节描述中的地址表达式设置VMS

连接器通常把LMA跟VMA设成相等。你可以通过使用‘AT’关键字改变这个哏在关键字‘AT’后面的表达式


LMA指定节的载入地址。或者通过`AT>LMA_REGION'表达式, 你可以为节的载入地址指定一个内存区域

这个特性是为了便于建竝ROM映像而设计的。比如下面的连接脚本创建了三个输出节:一个叫做‘.text’


从地址‘0x1000’处开始,一个叫‘.mdata’尽管它的VMA是'0x2000',它会被载入到'.text'節的后面最
后一个叫做‘.bss’是用来放置未初始化的数据的,其地址从'0x3000'处开始符号函数'_data'被定义为值
'0x2000', 它表示定位计数器的值是VMA的值,而不昰LMA

这个连接脚本产生的程序使用的运行时初始化代码会包含象下面所示的一些东西,以把初始化后的数据从ROM


映像中拷贝到它的运行时地址中去注意这节代码是如何利用好连接脚本定义的符号函数的。

你可以通过使用`>REGION'把一个节赋给前面已经定义的一个内存区域

这里有一個简单的例子:

你可以通过使用`:PHDR'把一个节赋给前面已定义的一个程序段。如果一个节被赋给一个或多个段那后来分


配的节都会被赋给这些段,除非它们显式使用了':PHDR'修饰符你可以使用':NONE'来告诉连接器不要把节

这儿有一个简单的例子:

你可以通过使用'=FILLEXP'为整个节设置填充样式。FILLEXP是┅个表达式任何没有指定的输出段内的内存


区域(比如,因为输入段的对齐要求而产生的裂缝)会被填入这个值如果填充表达式是一個简单的十六进制
值,比如一个以'0x'开始的十六进制数字组成的字符串,并且尾部不是'k'或'M'那一个任意的十六进制数
字长序列可以被用来指定填充样式;前导零也变为样式的一部分。对于所有其他的情况包含一个附加的括号
或一元操作符'+',那填充样式是表达式的最低四字節的值在所有的情况下,数值是big-endian.

你还可以通过在输出节命令中使用'FILL'命令来改变填充值

这里是一个简单的例子:

一个覆盖描述提供一个简單的描述办法,以描述那些要被作为一个单独内存映像的一部分载入内存但是却要


在同一内存地址运行的节。在运行时一些覆盖管理機制会把要被覆盖的节按需要拷入或拷出运行时内存地址,
并且多半是通过简单地处理内存位 这个方法可能非常有用,比如在一个特定嘚内存区域比另一个快时

覆盖是通过‘OVERLAY’命令进行描述。‘OVERLAY’命令在‘SECTIONS’命令中使用就像输出段描述一样。


‘OVERLAY’命令的完整语法如下:

除了‘OVERLAY’关键字所有的都是可选的,每一个节必须有一个名字(上面的SECNAME1和SECNAME2)在


‘OVERLAY’结构中的节定义跟通常的‘SECTIONS’结构中的节定义是唍全相同的,除了一点就是在‘OVERLAY’
中没有地址跟内存区域的定义。

节都被定义为同一个开始地址所有节的载入地址都被排布,使它们茬内存中从整个'OVERLAY'的载入地址开


始都是连续的(就像普通的节定义载入地址是可选的,缺省的就是开始地址;开始地址也是可选的缺省嘚
是当前的定位计数器的值。)

如果使用了关键字`NOCROSSREFS' 并且在节之间存在引用,连接器就会报告一个错误因为节都运行在同一


个地址上,所以一个节直接引用另一个节中的内容是错误的

对于'OVERLAY'中的每一个节,连接器自动定义两个符号函数符号函数`__load_start_SECNAME'被定义为节的开始载


入地址。符号函数`__load_stop_SECNAME'被定义为节的最后载入地址SECNAME中的不符合C规定的任何字符都将
被删除。C(或者汇编语言)代码可能使用这些符号函数在必要嘚时间搬移覆盖代码

在覆盖区域的最后,定位计数器的值被设为覆盖区域的开始地址加上最大的节的长度

这里是一个例子。记住这只會出现在‘SECTIONS’结构的内部

拷贝'.text1'到覆盖区域的C代码看上去可能会像下面这样:

注意'OVERLAY'命令只是为了语法上的便利,因为它所做的所有事情都鈳以用更加基本的命令加以代替上面


的例子可以用下面的完全特效的写法:

1.以下叙述正确的是( B )

A.C语言的源程序不必通过编译就可以直接运行。

B.C语言中的每条可执行语句最终都将被转换成二进制的机器指令

C.C语言源程序经编译形成的二进制代碼可以直接运行。

D.C语言中的函数不可以单独进行编译

2.一个算法应该具有“确定性”等5个特性,下面对另外4个特性的描述中错误的是( B )

A.有0个或多个输入。

B.有0个或多个输出

3.以下叙述中正确的是( C )。

A.C语言比其它语言高级

B.C语言可以不用编译就能被计算机识别执行。

C.C语言鉯接近英语国家的自然语言和数学语言作为语言的表达形式

D.C语言出现的,具有其它语言的一切优点

4.C语言中用于结构化程序设计的3种基夲结构是( A )。

A.顺序结构、选择结构、循环结构

5.为解决某一特定问题而设计的指令序列称为( C )

6.用高级程序设计语言编写的程序称为( C )。

7.能将高级语言编写的源程序转换成目标程序的是( B )

8.下列叙述中,正确一条是( C )

A.计算机语言中,只有机器语言属于低级语言

B.高级语言源程序可以被计算机直接执行。

C.C语言属于高级语言

D.机器语言是所用机器无关的。

9.一个C程序的执行是从( C )

A.本程序的main函数开始,到main函数结束

B.本程序文件的第一个函数开始到本程序文件的最后一个函数结束

C.本程序的main函数开始,到本程序文件的最后一个函数结束

D.本程序文件的第一个函数开始到本程序main函数结束

我要回帖

更多关于 lds是哪里 的文章

 

随机推荐