《ld reference manual》

转载 2013年12月02日 17:09:01

原文转载自:http://blog.csdn.net/huiyuyang_fish/article/details/16884593



ld reference manual》

ld链接器参考手册(中文翻译版


作者微博:@MTK_蛙蛙鱼

翻译时间:2013年11月22日

完成时间:2013年12月04日

更新时间:2013年1223


GNU-ld地址:<<ld reference manual>>


:这篇文章目前仅翻译链接脚本的部分,本人只有英语四级的水平,如有拗口,还请谅解。


LD

  1. Overview
  2. Invocation
  3. Linker Scripts
  4. Machine Dependent Features
  5. BFD
  6. Reporting Bugs
  7. Appendix A MRI Compatible Script Files
  8. Appendix B GNU Free Documention License
  9. LD Index


3 Linker Scripts

每次链接都会由链接脚本控制,它是由链接命令语言写成的

链接脚本的主要目的是描述输入文件的段应该如何被映射到输出文件中以及控制输出文件的内存布局,链接脚本所做的的大部分事情不会超出它。但是如果必要的话,链接脚本可以直接使用下面描述的命令让链接器做许多另外的操作

连接器一定会使用链接脚本的,如果你没有提供,连接器会使用默认的链接脚本,不过它已经集成在了链接器的可执行文件中(其实是在.rodata段中)。可以使用“--verbose”选项来显示默认的链接脚本信息。'-r' or -N'会影响默认的链接脚本

可以使用“-T”命令选项来替换默认的链接脚本

输入文件(即目标文件)也能暗地里作为链接脚本,可以看看这个小节 Implicit Linker Scripts

  • Basic Script Concepts: Basic Linker Script Concepts
  • Script Format: Linker Script Format
  • Simple Example: Simple Linker Script Example
  • Simple Commands: Simple Linker Script Commands
  • Assignments: Assigning Values to Symbols
  • SECTIONS: SECTIONS Command
  • MEMORY: MEMORY Command
  • PHDRS: PHDRS Command
  • VERSION: VERSION Command
  • Expressions: Expressions in Linker Scripts
  • Implicit Linker Scripts: Implicit Linker Scirpts
3.1 Basic Linker Script Concepts

在描述链接脚本语言前,先需要了解一些基本的概念和词汇

每个文件被称为object file,输出文件经常叫executable,不过这里也把它叫做object file。每个object file里面都有许多section,输入文件叫input section,输出文件叫output section

每个section都有名称和大小,大部分的section还有数据,我们称作section contents。如果一个section被标记为loadable(比如.text or .data段),代表了它的内容会被加载到内存中执行,如果一个section没有内容,那可能是allocatable的(比如.bss段),代表了它会被分配一块初始化为0的内存。如果一个section以上两种都不是,那典型的就是调试信息了

每个loadable or allocatable output section都有两个地址,一个是VMA or virtual memory address,是section执行的时候的地址。另一个是LMA or load memory address,是sectioin被加载的地址,当然,大部分情况下它们是相同的。但是举个例子,当一个带数据的section被加载进ROM,程序执行的时候又被拷贝到RAM的情况,它们就是不同的(这种技术经常使用在初始化的全局变量放在ROM的系统当中),像这样的情况,这个ROM的地址就是LMA,RAM的地址就会是VMA

使用objdump程序的‘-h’选项可以查看object file中的section

每个object file有许多symbol,称作symbol table。一个symbol可能被定义或没定义。每个symbol都有一个名称,每个定义的symbol都有地址和其他信息。如果你编译一个C or C++程序,那么每个定义的函数和全局或者静态变量都会产生一个定义的symbol(定义),反之每个input file中没有被定义的函数或全局变量就会产生一个没有定义的symbol(引用)

使用objdump程序的‘-t’选项可以查看object file中的symbol

3.2 Linker Script Format

链接脚本是一种文本文件

链接脚本由一系列命令写成,每个命令要么是一个关键字可能会跟一些参数要么是一个分配的symbol。每个命令用分号分隔,空格是会被忽略的

想文件或格式名的字符串是可以直接正常使用的。如果文件名包含了逗号,那么你需要加双引号,否则就是分开的文件名了,文件名不能使用双引号的哦(no way)

你可以像C语言一样使用'/*'和'*/'来注释,这些会被当做空格处理的

3.3 Simple Linker Script Example

其实许多链接脚本十分简单的

最简单的脚本可能仅有一句‘SECTIONS’的命令,你可以使用它来描述输出文件的内存布局

这个‘SECTIONS’命令非常强大,现在我们来简单的使用它。假设你的程序仅有代码、初始化数据和未初始化的数据,它们分别在'.text'、'.data'、‘.bss’ 段(section)里面,假设在输入文件有且仅有这些段

这个例子是,代码加载到地址0x10000处,数据放到0x8000000处,那么链接脚本可以这样写:

[plain] view plaincopy在CODE上查看代码片派生到我的代码片
  1. SECTIONS  
  2. {  
  3.   . = 0x10000;  
  4.   .text : { *(.text) }  
  5.   . = 0x8000000;  
  6.   .data : { *(.data) }  
  7.   .bss : { *(.bss) }  
  8. }  

‘SECTIONS’命令的关键字就是‘SECTIONS’,然后有一堆被分配的符号和输出段的描述都被放在了大括号里面

在‘SECTIONS’命令第一行是设置‘.’符号的值,它代表位置计数(location counter)。如果没有用其他的方式(其他方式后面会说到)来指定输出段的地址,那么这个地址就是当前位置计数的值。位置计数会随着输出段的大小而增加,在‘SECTIONS’命令的开始它的值是0

第二行定义了一个输出段‘.text’,接着是一个分号的语法(和段名要保持至少一个空格哦),然后会接一个大括号,里面放着输入段的名称,它们最后都会被放到这个输出段里。这个‘*’符号是一个通配符,它用来匹配任意一个文件名,比如‘*(.text)’的意思是所有输入文件中叫‘.text’的输入段

因为当输出段'.text'被定义的时候这个位置计数被设置成了0x10000,因此链接器会把输出文件中的'.text'段地址设置成0x10000

剩下的就是‘.data’和'.bss'段的定义。链接器会把‘.data'输出段的地址设置成0x8000000,当把'.data'输出段放置好后,这个位置计数的值就是0x8000000加上‘.data’输出段的大小。最后,链接器会把‘.bss’输出段立即放到‘.data’输出段的后面

链接器需要确认一个输出段所要求的对齐,然后有必要增加位置计数。在这个例子中,‘.text’和'.data'段的地址可以满足任何对齐方式的限制,但是‘.data’和'.bss'段可能需要一个小的间隙来满足对齐

That's it!一个完整的链接脚本就是这么简单

3.4 Simple Linker Script Commands

在这个小节我们会描述简单的链接脚本命令

  • Entry Point: Setting the entry point
  • File Commands: Commands dealing with files
  • Format Commands: Commands dealing with object file formats
  • REGION ALIAS: Assign alias names to memory regions
  • Miscellaneous Commands: Other linker script commands
3.4.1 Setting the Entry Point

程序执行的第一条指令我们叫做entry point。你可以使用ENTRY命令来设置它,参数就是符号的名称:

[plain] view plaincopy在CODE上查看代码片派生到我的代码片
  1. ENTRY(symbol)  

其实有多种方式去设置entry point。以下列出了很多方式,链接器会按照顺序去try,直到有一直方式成功才停止:

  • 使用‘-e’选项
  • 使用ENTRY(symbol)链接脚本命令
  • 一个目标符号(target specific symbol)的值,通常就是start,但是像基于PE和BeOS的系统会去检查许多entry符号,通常匹配第一个被发现的
  • ‘.text’段第一个字节的地址
  • 0
3.4.2 Commands Dealing with Files

有几个和文件处理相关的命令

INCLUDE filename

把叫filename名字的链接脚本文件包含到当前点。链接器会在当前目录和‘-L’选项的目录去搜索它。INCLUDE嵌套数做多可达10次

你可以把INCLUDE指令放到最顶上,放到MEMORY or SECTIONS命令里面,也可以放到输出段的描述里面

INPUT(file, file, ...

INPUT(file file ...

这个INPUT命令可以直接包含需要被链接文件的名字,就像它们在命令行中的文件名字一样

比如,你想在任何时候都链接sbur.o,但是又不想每次都要在命令行中写一次,就可以在脚本中使用‘INPUT(subr.o)’

实际上,如果你喜欢,可以把所有的输入文件放入链接脚本中,然后仅需一个链接选项‘-T’就行

这个case里面涉及一个sysroot prefix的配置,如果文件名是以‘/’开头,那么执行脚本就会去sysroot prefix中搜索文件,否则链接器会去试着在当前目录中打开。如果没有发现,那么就去搜索归档库的搜索路径,你可以去查看Command Line Options 这个小节中的‘-L’选项

如果你使用‘INPUT(-lfile)’,链接器会把它转换为libfile.a,就行命令行中使用‘-l’选项一样

当你使用暗示在脚本中的INPUT命令时,那么这些文件被链接的位置就是脚本文件被包含进的位置。这样可能会影响归档的搜索。

GROUP(file, file, ...

GROUP(file file ...

这个GROUP命令有点像INPUT,但是它包含的文件必须都是归档文件,这些文件会被重复搜索直到没有新的未定义的引用出现。你也可以去查看 Command Line Options 这个小节中的‘-(’选项

AS_NEEDED(file, file, ...)

AS_NEEDED(file file ...)

略.

OUTPUT(filename

这个OUTPUT命令定义了输出文件,实际上就是命令选项‘-o filename’。如果都使用了,命令行选项会有限处理

你可以使用OUTPUT命令定义一个默认的输出文件名而不用常见的a.out作为默认值

SEARCH_DIR(path

这个SEARCH_DIR命令会加入一个归档库的搜索路径,实际上就是命令选项'-Lpath'。如果都使用了,这些路径都会被搜索,不过命令行指定的路径会被有限搜索

STARTUP(filename

STARTUP命令非常像INPUT命令,不过这个文件会首先作为输入文件被链接,就像放在命令行最开始被链接一样。这个命令非常有用,因为系统中的第一个文件经常是放entry point的

3.4.3 Commands Dealing with Object File Formats

现在介绍一下一对处理目标文件(object file)格式的命令

OUTPUT_FORMAT(bfdname

OUTPUT_FORMAT(default, big, little

第一个命令可以使用BFD格式来定义输出文件格式,实际上就是命令行选项‘--oformatbfdname’,如果都使用了,命令行有限处理

你也可以使用第二个命令,带有3个参数来使用命令行选项‘-EB’和'-EL'指定的不同格式,这回设定输出的期望字节序

如果‘-EB’和‘-EL’都没使用,那输出格式将会是第一个参数default,‘-EB’代表第2个参数big,‘-EL’代表第3个参数little

举个例子,使用MIPS ELF目标的脚本命令:

[plain] view plaincopy在CODE上查看代码片派生到我的代码片
  1. OUTPUT_FORMAT(elf32-bigmips, elf32-bigmips, elf32-littlemips)  

默认格式是‘elf32-bigmips’,但使用‘-EL’命令选项后,输出文件格式为‘elf32-littlemips’

TARGET(bfdname

略.

3.4.4 Assign alias names to memory regions

略.

3.4.5 Other Linker Script Commands

下面介绍另外几个脚本命令

ASSERT(exp, message

exp一定要是非0,如果是0的话,链接器就会退出然后打印message

EXTERN(symbol symbol ...

强制symbol作为输出文件的外部引用。举个列子,这样可以触发对标准库的链接。你可以使用几个symbol作为EXTERN,这个命令其实和'-u'选项是一样的

...

OUTPUT_ARCH(bfdarch

指定一个特别的输出机器结构,参数用的是BFD库的名字。你可以使用objdump程序的‘-f’选项查看目标文件的机器结构

LD_FEATURE(string

略.

3.5 Assigning Values to Symbols

你可以给一个符号(symbol)赋值,它会把这些定义的符号放入全局符号表(symbols table)中

  • Simple Assignments: Simple Assignments
  • HIDDEN: HIDDEN
  • PROVIDE: PROVIDE
  • PROVIDE_HIDDEN: PROVIDE_HIDDEN
  • Source Code Reference: How to use a linker script defined symbol in source code
3.5.1 Simple Assignments

你可以使用下面C语言的赋值操作:

[plain] view plaincopy在CODE上查看代码片派生到我的代码片
  1. symbol = expression ;  
  2. symbol += expression ;  
  3. symbol -= expression ;  
  4. symbol *= expression ;  
  5. symbol /= expression ;  
  6. symbol <<= expression ;  
  7. symbol >>= expression ;  
  8. symbol &= expression ;  
  9. symbol |= expression ;  

第一种是给符号定义一个expression的值,另外的情况,符号一定是已经定义了的然后这个值会被改变

‘.’是一个特殊的符号,它表示位置计数,你可能仅仅是在SECTIONS命令里面使用

表达式(expressions)的定义,请看 Expressions 小节

你可以把符号赋值写在它自己正确的位置或者SECTIONS命令或者作为输出段描述的一部分

段里面的符号也可以用表达式来赋值,更多的信息请看 Expression Section 小节

下面有个例子展示了符号赋值的3个不同地方:

[plain] view plaincopy在CODE上查看代码片派生到我的代码片
  1. floating_point = 0;  
  2. SECTIONS  
  3. {  
  4.   .text :  
  5.     {  
  6.       *(.text)  
  7.       _etext = .;  
  8.     }  
  9.   _bdata = (. + 3) & ~ 3;  
  10.   .data : { *(.data) }  
  11. }  

在这个例子中,‘floating_point’符合被定义为0。‘_etext’符号被定义为‘.text’输入段最后的地址。‘_bata’符号被定义为‘.text’输出段最后按4字节对齐的地址

3.5.2 HIDDEN

对于ELF目标文件,定义的符号会被隐藏不会被导出,语法是HIDDEN(symbol = expression)

下面把例子用HIDDEN进行重写:

[plain] view plaincopy在CODE上查看代码片派生到我的代码片
  1. HIDDEN(floating_point = 0);  
  2. SECTIONS  
  3. {  
  4.   .text :  
  5.     {  
  6.       *(.text)  
  7.       HIDDEN(_etext = .);  
  8.     }  
  9.   HIDDEN(_bdata = (. + 3) & ~ 3);  
  10.   .data : { *(.data) }  
  11. }  

在这个例子中,3个符号都不会被外部模块看到

3.5.3 PROVIDE

有许多例子证实,脚本中定义的符号任何对象只能对其引用而不能定义它,比如传统链接器定义的'etext'符号。然而,ANSI C要求用户使用‘etext’符号作为函数名而不能报错,这个RPOVIDE关键字就能定义这样一个符号,比如‘etext’,仅仅是一个引用而不是定义,语法是PROVIDE(symbol = expression)

下面是一个‘etext’使用的例子:

[plain] view plaincopy在CODE上查看代码片派生到我的代码片
  1. SECTIONS  
  2. {  
  3.   .text :  
  4.     {  
  5.       *(.text)  
  6.       _etext = .;  
  7.       PROVIDE(etext = .);  
  8.     }  
  9. }  

在这个例子中,如果程序定义‘_etext’(带有前导下划线),链接器会报很多重定义错误。另一方面,如果程序定义‘etext’(没有前导下划线),那么链接器会默默地使用程序中的定义,如果程序中只是引用了‘etext’而没有定义它,链接器则使用脚本中的定义

补充:如果你查看链接后的文件的符号表(readelf -s),会发现它的符号绑定信息是GLOBAL,即外部可见的

3.5.4 PROVIDE_HIDDEN

类似PROVIDE的用法。对于ELF目标文件,定义的符号会被隐藏不会被导出

补充:如果你查看链接后的文件的符号表,会发现它的符号绑定信息是LOCAL,即内部可见的

3.5.5 Source Code Reference

从源代码来访问链接脚本中定义的变量其实并不直观,脚本中的符号和高级语言中定义的变量不能等同,它是一个符号而不是一个值

在进一步讨论之前,需要注意的是当源代码中的符号名字被放入符号表(symbol table)中时,编译器通常会把它们进行转换。举例来说,Fortran语言编译器通常会加前导或后接一个下划线,C++编译器会进行扩展的‘name mangling’。因此在源代码中的使用的变量和在链接脚本中定义的变量是可能有差异的。举例来说,C语言中引用脚本中的变量像这样:

[plain] view plaincopy在CODE上查看代码片派生到我的代码片
  1. extern int foo;  

但是在链接脚本中它被定义为这样:

[plain] view plaincopy在CODE上查看代码片派生到我的代码片
  1. _foo = 1000;  

不过,在下面的例子中我们假设没有名称转换

当一个符号被声明,比如用C语言,那么会有两件事情发生,第一件事情是编译器会保留一块内存空间来存储这个符号的值,第二件事情是编译器会在符号表中创建一个符号并存储好该符号的地址,符号表中包含的是存储该符号值的地址,比如在文件中这样一个C语言的声明:

[plain] view plaincopy在CODE上查看代码片派生到我的代码片
  1. int foo = 1000;  

在符号表中创建一个‘foo’的符号,符号的地址就是把整数型数字1000存储在内存中的地址

当一个程序要引用一个符号时,编译器首先会生成一段去符号表中找到该符号内存块地址的代码,然后在生成一段读取内存块值的代码,比如:

[plain] view plaincopy在CODE上查看代码片派生到我的代码片
  1. foo = 1;  

去符号表中查询‘foo’符号的地址值,然后往这个地址里写值1,然而:

[plain] view plaincopy在CODE上查看代码片派生到我的代码片
  1. int * a = & foo;  

去符号表中查询‘foo’符号的地址值,然后将地址直接拷贝到变量‘a’的内存块里面

对比一下链接脚本中的符号声明,它会在符号表中创建一个符号,但是不会给它分配任何内存,所以它们仅仅是一个地址没有值,比如脚本中这样定义:

[plain] view plaincopy在CODE上查看代码片派生到我的代码片
  1. foo = 1000;  

在符号表中创建一个‘foo’的符号,然后存储它的地址值为1000,但地址1000指向的存储位置没有任何意义。这就意味着你不能访问链接脚本中定义的符号的值(它没有值),你唯一能做的就是访问符号的地址

因此,当你在源代码中使用一个链接脚本定义的符号时,你应该使用它的地址,而不去尝试使用它的值。举例,假设你想拷贝一个叫.ROM段的内容到.FLASH段中,链接脚本这样声明的:

[plain] view plaincopy在CODE上查看代码片派生到我的代码片
  1. start_of_ROM   = .ROM;  
  2. end_of_ROM     = .ROM + sizeof (.ROM) - 1;  
  3. start_of_FLASH = .FLASH;  

你的C语言代码应该这样写:

[plain] view plaincopy在CODE上查看代码片派生到我的代码片
  1. extern char start_of_ROM, end_of_ROM, start_of_FLASH;  
  2.   
  3. memcpy (& start_of_FLASH, & start_of_ROM, & end_of_ROM - & start_of_ROM);  

注意,使用‘&’操作也是完全正确的

3.6 SECTIONS Command

这个SECTIONS命令告诉链接器如何映射输入段到输出段以及在内存中如何放置输出段

SECTIONS命令的格式如下:

[plain] view plaincopy在CODE上查看代码片派生到我的代码片
  1. SECTIONS  
  2. {  
  3.   sections-command  
  4.   sections-command  
  5.   ...  
  6. }  

sections-command可能是如下的一种:

  • 一个ENTRY命令(看 Entry Command
  • 一个符号赋值(看 Assignments
  • 一个输出段描述
  • 一个叠加描述
为了方便使用位置计数(location counter)ENTRY命令和符号赋值命令被允许放在SECTIONS命令中,这样可以使链接脚本更加容易理解,因为你可以在输出文件中有意义的位置使用它们

输出段描述和叠加描述会在下面介绍

如果你在链接脚本中没有使用SECTIONS命令,那么链接器会将输入段按照顺序放到一个有实际名字的输出段中,这个输出段就是被解析到的第一个输入文件。如果第一个文件能呈现所有输出段,那么输出文件中段的顺序就和第一个输入文件的一样,第一个段的地址会被设置成0

  • Output Section Description: Output section description
  • Output Section Name: Output section name
  • Output Section Address: Output section address
  • Input Section: Input section description
  • Output Section Data: Output section data
  • Output Section Keywords: Output section keywords
  • Output Section Discarding: Output section discarding
  • Output Section Attributes: Output section attributes
  • Overlay Description: Overlay description
3.6.1 Output Section Description

一个完整的输出段描述像这个样子:

[plain] view plaincopy在CODE上查看代码片派生到我的代码片
  1. section [address] [(type)] :  
  2.   [AT(lma)]  
  3.   [ALIGN(section_align)]  
  4.   [SUBALIGN(subsection_align)]  
  5.   [constraint]  
  6.   {  
  7.     output-section-command  
  8.     output-section-command  
  9.     ...  
  10.   } [>region] [AT>lma_region] [:phdr :phdr ...] [=fillexp]  

大部分输出段都不会使用到这里面绝大多数的段选项属性

section旁边的空格是必须的,这样段名才不会混淆。分号和大括号也是必须的。换行和另外的空格就无所谓了

output-section-command可能是如下的一种:

  • 一个符号赋值(看 Assignments
  • 一个输入段描述(看 Input Section
  • 一个直接包含的数据值(看 Output Section Data
  • 一个特殊的输出段关键字(看 Output Section Keywords
3.6.2 Output Section Name

输出段的名字就是section,它是有限制的。比如a.out的格式只限制了几个段名的使用(仅仅允许使用‘.text’、‘.data’、‘.bss’)。如果输出格式支持任何段名,但是想数字或者不是名字的名称,那么必须要加上引号。段名可以包含了一些有序字符,但是如果包含了一些比如像逗号这样的不常规字符那么久一定要加引号

‘/DISCARD/’是一个特殊的输出段名,看 Output Section Discarding

3.6.3 Output Section Address

这个address是输出段的虚拟地址,是一个表达式,是可选项,但如果有的话,可以精确指定输出段的地址

如果这个地址没有被指定,那么它将会被探索出来。然后这个地址将会被调整到输出段要求的对齐方式,这个要求的对齐方式严格针对每个被包含在输出段里的输入段

输出段地址按照以下方式来探索:

  • 如果一个output memory region被设置,那么输出地址就是加上这个region后的下一个空闲地址
  • 如果一个MEMORY命令被使用,它创建了一列memory region,那么输出地址就是选择加上第一个和段属性兼容的region后的下一个空闲地址
  • 如果没有memory region被指定,也没有能匹配的段,那么输出地址就是位置计数的值
举个例子:

[plain] view plaincopy在CODE上查看代码片派生到我的代码片
  1. .text . : { *(.text) }  

[plain] view plaincopy在CODE上查看代码片派生到我的代码片
  1. .text : { *(.text) }  

是十分不一样的。第一个‘.text’的地址是当前的位置计数值,第二个它的地址是当前位置计数值经过输入段的严格对齐后的值

这个address可以是任意表达式,看 Expressions。比如,你想设定段以0x10个字节来对齐,可以像这样写:

[plain] view plaincopy在CODE上查看代码片派生到我的代码片
  1. .text ALIGN(0x10) : { *(.text) }  

ALIGN会将当前的位置计数值向上扩展到一个合适的值

指定一个段的address会改变当前位置计数的值,当然了,空段会被忽略

3.6.4 Input Section Description

输出段命令(output-section-command)使用最多的是输入段的描述

输入段描述是使用最多的基本的链接脚本操作,输出段可以告诉链接器你的程序在内存中如何布局,输入段描述告诉链接器如何映射输入文件到内存中

  • Input Section Basics: Input section basics
  • Input Section Wildcards: Input section wildcard patterns
  • Input Section Common: Input section for common symbols
  • Input Section Keep: Input section and garbage collection
  • Input Section Example: Input section example

3.6.4.1 Input Section Basics

输入段描述由一个文件名和一列可选的段名包含在括号里面组成

文件名和段名可以是通配符模式,更详细的内容可以查看 Input Section Wildcards

大部分输入段描述都是包含所有该段名的输入段到输出段中,举例来说,包含所有的‘.text’输入段,你可以这样写:

[plain] view plaincopy在CODE上查看代码片派生到我的代码片
  1. *(.text)  

这里的‘*’是一个通配符,它匹配了所有的文件名。为了排除一些被匹配到的通配符文件名,你可以使用EXCLUDE_FILE来排除用它指定的一列文件,例如:

[plain] view plaincopy在CODE上查看代码片派生到我的代码片
  1. *(EXCLUDE_FILE (*crtend.o *otherfile.o) .ctors)  

这个描述会包含所有文件的‘.ctors’段,但是除了crtend.o和otherfile.o

下面有两种方式来包含多于1个段:

[plain] view plaincopy在CODE上查看代码片派生到我的代码片
  1. *(.text .rdata)  
  2. *(.text) *(.rdata)  

它们的不同点是‘.text’和‘rdata’输入段在输出段中的顺序。第一个例子,它们先把两个段混在一起然后按照链接输入的顺序放置,第二个例子,首先会放置所有文件的‘.text’输入段然后在跟着放所有的‘.rdata'输入段

你也可以用一个文件名指定包含一个文件中的段,如果一个或多个文件中包含了特殊数据,你可以就会这样做,例如:

[plain] view plaincopy在CODE上查看代码片派生到我的代码片
  1. data.o(.data)  

为了重新定义出输出段标志(section header flag),INPUT_SECTION_FLAGS可以被使用

下面一个简单的例子,它使用了ELF格式的段标志:

[plain] view plaincopy在CODE上查看代码片派生到我的代码片
  1. SECTIONS {  
  2.   .text : { INPUT_SECTION_FLAGS (SHF_MERGE & SHF_STRINGS) *(.text) }  
  3.   .text2 :  { INPUT_SECTION_FLAGS (!SHF_WRITE) *(.text) }  
  4. }  

在上面的例子中,输出段‘.text’由所有文件的‘.text’输入段组成且它会设置上SHF_MERGE和SHF_STRINGS段标志,输出段‘.text2’也是又所有文件的‘.text’组成但它会去掉SHF_WRITE段标志

你也可以指定在归档包中的文件,可以这样写,一个归档,一个冒号,然后是被匹配的文件,,冒号旁边不能有空格

[plain] view plaincopy在CODE上查看代码片派生到我的代码片
  1. `archive:file'  
  2.           matches file within archive   
  3. `archive:'  
  4.           matches the whole archive   
  5. `:file'  
  6.           matches file but not one in an archive  

‘archive’和‘file’都可以包含通配符。在DOS文件系统中,通过一个简单的字母后面跟一个冒号来指定一个驱动器,所以‘c:myfile.o’就是指向了一个驱动器上的文件而不是归档名叫‘c’中的‘myfile.o’文件。‘archive:file’这样的文件格式可以在EXCLUDE_FILE列表中使用但不能在其他链接脚本上下文中使用,比如你不能在INPUT命令中使用‘archive:file’从归档包中解压这个文件

如果你的文件名后面没有跟一列段,那么这个输入文件中的所有段都会被包含到输出段中。这不是一个很常见的做法,但它可能在某些情况下非常有用,例如:

[plain] view plaincopy在CODE上查看代码片派生到我的代码片
  1. data.o  

如果你使用的文件名不是‘archive:file’也没有任何通配符字符,那么链接器会首先去链接命令行和INPUT命令找该文件,否则,链接器会尝试去像在命令行中的输入文件一样去打开。注意,这种方式和INPUT命令不同,因为链接器并不会去归档库路径搜索该文件

3.6.4.2 Input Section Wildcard Patterns

在输入段描述中,文件名和段名都可以使用通配符模式

在许多例子中文件名使用的*’是一个简单的通配符模式

像下面的通配符经常在Unix shell中使用

[plain] view plaincopy在CODE上查看代码片派生到我的代码片
  1. `*'  
  2.         matches any number of characters   
  3. `?'  
  4.         matches any single character   
  5. `[chars]'  
  6.         matches a single instance of any of the chars; the `-' character may be used to specify a range of characters, as in `[a-z]' to match any lower case letter  
  7. `\'  
  8.         quotes the following character  

当一个文件名用通配符去匹配的时候,通配符不会去匹配‘/’字符(在Unix中用来进行目录划分)。通配符中包含'*'字符是个例外,它可以匹配任何文件名不管是否包含了‘/’字符。在段名中,通配符是可以去匹配‘/’字符的

文件名通配符是去匹配明确出现在命令行和INPUT命令中的文件。链接器不会去搜索扩展通配符的目录

如果一个文件名有多个匹配或者说一个文件名被显示的匹配到了,那么链接器将会使用链接脚本中的第一个匹配。下面的这个例子中,输入段描述的顺序是错误的,因为data.o的规则将不会被使用:

补充:每个输入段只能被使用一次

[plain] view plaincopy在CODE上查看代码片派生到我的代码片
  1. .data : { *(.data) }  
  2. .data1 : { data.o(.data) }  

正常情况下,链接器会将匹配的文件和段按照链接的顺序放置,不过你可以使用SORT_BY_NAME关键字改变它的顺序,把它放在通配符括号的前面(如SORT_BY_NAME(.text*))。当使用SORT_BY_NAME关键字时,链接器会按照名字升序排序来放置到输出文件中

SORT_BY_ALIGNMENT类似SORT_BY_NAME,不同点是按照对齐的方式排序段后把它们放置到输出文件中

SORT_BY_INIT_PRIORITY类似SORT_BY_NAME,不同点是它是按照一个GCC的init_priority属性将段名编码的值进行排序后将它们放置为输出文件中

SORT是SORT_BY_NAME的别名

排序命令可以嵌套使用,但是最多只能嵌套一次

  1. SORT_BY_NAME (SORT_BY_ALIGNMENT (wildcard section pattern)),先按照名字排序,如果名字相同在按照对齐方式排序
  2. SORT_BY_ALIGNMENT (SORT_BY_NAME (wildcard section pattern)),先按照对齐方式排序,如果对齐方式一样在按照名字排序
  3. SORT_BY_NAME (SORT_BY_NAME (wildcard section pattern)),同SORT_BY_NAME (wildcard section pattern)
  4. SORT_BY_ALIGNMENT (SORT_BY_ALIGNMENT (wildcard section pattern)),同SORT_BY_ALIGNMENT (wildcard section pattern)
  5. 另外的嵌套排序命令都是无效的

如果在链接脚本和命令行中都有段排序命令,命令行会被有限处理

如果在脚本中的排序命令不是嵌套的,那么命令行的排序选项会被处理成嵌套命令

  1. SORT_BY_NAME (wildcard section pattern) 和 --sort-sections alignment同SORT_BY_NAME (SORT_BY_ALIGNMENT (wildcard section pattern))一样
  2. SORT_BY_ALIGNMENT (wildcard section pattern)和 --sort-sections name同SORT_BY_ALIGNMENT (SORT_BY_NAME (wildcard section pattern))一样
如果在脚本中的排序命令是嵌套的,那么命令行选项会被忽略掉

SORT_NONE可以忽略掉命令中的排序选项,禁止段的排序

如果你对输入段到底被放哪儿去了比较疑惑的话,可以使用‘-M’链接选项产生一个映射文件,这个文件可以精确的显示输入段是如何映射到输出段中的

下面这个例子展示了通配符如何被利用到部分文件。链接脚本直接将所有文件的‘.text’段放入了'.text'段,所有文件的‘.bss’段放入了‘.bss’段。链接脚本把所有以大写字母开头的文件的‘.data’段放入‘.DATA’段,其他文件的‘.data’段放入'.data'段中

[plain] view plaincopy在CODE上查看代码片派生到我的代码片
  1. SECTIONS {  
  2.   .text : { *(.text) }  
  3.   .DATA : { [A-Z]*(.data) }  
  4.   .data : { *(.data) }  
  5.   .bss : { *(.bss) }  
  6. }  

3.6.4.3 Input Section for Common Symbols

一个特殊的符号是通用符号(common symbol),因为在许多目标文件(object file)格式中,它都没有一个特殊的输入段。链接器对待它们就像它们在自己输入段一样命名为‘COMMON’

你可以同其他输入段一样使用‘COMMON’段这个名字,你可以将一个特别输入文件的通用符号放入一个段中而其他输入文件的通用符号放入另一个段中

大多数情况,输入文件的通用符号都会被放入‘.bss’段中,例如:

[plain] view plaincopy在CODE上查看代码片派生到我的代码片
  1. .bss { *(.bss) *(COMMON) }  

不过有些目标文件格式不止一种通用符号。比如,MIPS ELF目标文件格式为了区分标准的通用符号和小的通用符号,链接器会使用不同的特殊段名。对于像MIPS ELF目标文件格式,链接器使用‘COMMON’作为标准的通用符号,使用‘.scommon’作为小的通用符号,这样就允许你将不同的通用符号放到内存的不同位置

有时候在老的链接脚本中会看到‘[COMMON]’,这个符号现在已经被废弃了,它和‘*(COMMON)’是一样的

3.6.4.4 Input Section and Garbage Collection

如果链接器启用了垃圾回收('--gc-sections'),那为了保留一个段就非常有必要了。我们可以在输入段前加上KEEP(),就像KEEP (*(.init))或者KEEP (SORT_BY_NAME (*)(.ctors))

3.6.4.5 Input Section Example

下面是一个完整的链接脚本的例子。它让链接器把文件all.o的所有输入段放入一个以0x10000地址开始的‘outputa’输出段中,紧接着把foo.o文件的'.input1'输入段放入相同的输出段中,foo.o的输入段‘input2’放入‘outputb’输出段中,紧接着把foo1.o的输入段‘.input1’放入,最后将所以文件剩下的'.input1'和'.input2'输入段放入'outputc'输出段中

[plain] view plaincopy在CODE上查看代码片派生到我的代码片
  1. SECTIONS {  
  2.   outputa 0x10000 :  
  3.     {  
  4.     all.o  
  5.     foo.o (.input1)  
  6.     }  
  7.   outputb :  
  8.     {  
  9.     foo.o (.input2)  
  10.     foo1.o (.input1)  
  11.     }  
  12.   outputc :  
  13.     {  
  14.     *(.input1)  
  15.     *(.input2)  
  16.     }  
  17. }  

3.6.5 Output Section Data

你可以在输出段命令中直接使用BYTE, SHORT, LONG, QUAD, SQUAD关键字来包含字节数据,每个关键字都会用括号包含一个表达式,它存储了值(看 Expressions),这个表达式的值被存放在当前的位置计数的位置

BYTE, SHORT, LONG, QUAD, SQUAD命令分别存储1,2,4,8个字节的值,在把字节值存储后,位置计数会增加相应的字节数

举个例子,下面存储了一个字节的值后紧接着又存储了‘addr’符号的值:

[plain] view plaincopy在CODE上查看代码片派生到我的代码片
  1. BYTE(1)  
  2. LONG(addr)  

对于使用64位的值,QUAD和SQUAD是一样的,它们都使用8个字节或64位的值。当使用32位的值,表达式就是用32位来计算。如果QUAD存储的是一个32位的值,那它会用0扩展为64位,而SQUAD则符号扩展到64位

如果输出文件的目标格式显示的指出了字节序,那么这个值就会按照指定的字节序存储,反之,这个值会按照第一个输入目标文件的字节序来存储

注意,这些命令只能写在输出段描述的里面,如果你按照下面的写法就错了:

[plain] view plaincopy在CODE上查看代码片派生到我的代码片
  1. SECTIONS { .text : { *(.text) } LONG(1) .data : { *(.data) } }  

这样才是正确的:

[plain] view plaincopy在CODE上查看代码片派生到我的代码片
  1. SECTIONS { .text : { *(.text) ; LONG(1) } .data : { *(.data) } }  

你可以使用FILL命令来设定当前段的填充模式(fill pattern),它会用一个括号包含一个表达式

这样的话,任何一个在这个段的没有指定的内存区域(比如,由于输入段必须的对齐导致留下的间隙)都会用这个表达式的值重复的填充。它会填充FILL命令定义之后的内存位置,如果你在一个输出段中包含了几个FILL命令,那么还可以有不同填充模式

下面的例子展示了如何给未指定的内存区域填充值‘0x90’:

[plain] view plaincopy在CODE上查看代码片派生到我的代码片
  1. FILL(0x90909090)  

这个FILL命令和输出段属性'=fillexp'类似,但它仅仅影响的是该段内FILL命令之后的部分,而不是整个段。如果都使用的话,FILL命令优先考虑,更详细的信息,去看看Output Section Fill小节

3.6.6 Output Section Keywords

有一对关键字它们会出现在输出段命令(output-section-command)中

CREATE_OBJECT_SYMBOLS

这个命令告诉链接器为每个输入文件创建一个符号,符号的名字就是输入文件的名字,符号的段就是命令所在的输出段

这个命令常用在a.out的目标文件格式中,在其他目标文件不常用

CONSTRUCTORS

当链接a.out目标文件格式时,链接器会使用一种不寻常的构造设定方式来支持C++的全局构造和全局析构。当链接一种不支持任意段的目标格式时,比如ECOFF和XCOFF,那么链接器会通过名字自动去识别C++的全局构造和全局析构。对于这些目标文件格式,这个CONSTRUCTORS命令可以告诉连机器将构造信息放入该命令所在的输出段中,而对于其他的目标文件,该命令会被忽略

‘__CTOR_LIST__’标识全局构造的开始,'__CTOR_END__'标识全局构造的结束,类似的,‘__DTOR_LIST__’标识全局析构的开始,‘__DTOR_END__’标识了全局析构的结束。第一个字是入口地址的个数,接着是每个构造或析构的地址,最后是一个0字。编译器会安排好实际要跑的代码。对于这些目标文件格式,GNU C++会在__main中去呼叫构造,__main又会被自动的插入main的开始,GNU C++会使用atexit去跑析构,或者直接从exit函数(进程结束)析构

对于COFF和ELF支持任意段名的目标文件格式,GNU C++会将全局构造和全局析构安排到.ctors和.dtors段中,按照顺序把它放到链接脚本中将会建立GNU C++运行时代码的顺序,脚本如下:

[plain] view plaincopy在CODE上查看代码片派生到我的代码片
  1. __CTOR_LIST__ = .;  
  2. LONG((__CTOR_END__ - __CTOR_LIST__) / 4 - 2)  
  3. *(.ctors)  
  4. LONG(0)  
  5. __CTOR_END__ = .;  
  6. __DTOR_LIST__ = .;  
  7. LONG((__DTOR_END__ - __DTOR_LIST__) / 4 - 2)  
  8. *(.dtors)  
  9. LONG(0)  
  10. __DTOR_END__ = .;  

如果你使用的GNU C++支持优先级初始化,它可以在全局构造运行时提供顺序控制,那么一定要在链接时排序构造,这样能确保它们的执行顺序正确。当使用CONSTRUCTORS命令时,用‘SORT_BY_NAME (CONSTRUCTORS)’代替,当使用.ctors和.dtors段时,应该用'*(SORT_BY_NAME (.ctors))'和‘*(SORT_BY_NAME (.dtors))’代替

通常,编译器和链接器会自动处理这些问题,你不需要关心它们。然而,如果你使用C++还要自己写链接脚本时,则需考虑该问题

3.6.7 Output Section Discarding

链接器不会去创建一个没有内容的输出段。如果引用的输入段没有在任何一个输入文件中出现,那么这么做是方便的,例如:

[plain] view plaincopy在CODE上查看代码片派生到我的代码片
  1. .foo : { *(.foo) }  

如果这个‘.foo’输入段至少有一个输入文件存在就会创建一个‘.foo’输出段,如果这个输入段为空,那么链接脚本也会指明,对于需要分配空间时也会去创建一个输出段

对于一个被抛弃的的输出段,链接器将会忽略给该段指定的地址,除非在输出段中有符号的定义,那样的话,即使这个输出段被抛弃,链接器依然会遵守该段地址的指定

一个特殊的输出段名‘/DSICARD/’可以用来抛弃输入段,任何被放在名字为‘/DISCARD/’输出段中的输入段都不会被包含到输出文件中

3.6.8 Output Section Attributes

对于一个完整的输出段描述:

[plain] view plaincopy在CODE上查看代码片派生到我的代码片
  1. section [address] [(type)] :  
  2.   [AT(lma)]  
  3.   [ALIGN(section_align)]  
  4.   [SUBALIGN(subsection_align)]  
  5.   [constraint]  
  6.   {  
  7.     output-section-command  
  8.     output-section-command  
  9.     ...  
  10.   } [>region] [AT>lma_region] [:phdr :phdr ...] [=fillexp]  

到目前,我们已经介绍了sectionaddressoutput-section-command。在这个小节中,我们将介绍剩下的段属性

  • Output Section Type: Output section type
  • Output Section LMA: Output section LMA
  • Forced Output Alignment: Forced Output Alignment
  • Forced Input Alignment: Forced Input Alignment
  • Output Section Constraint: Output section constraint
  • Output Section Region: Output section region
  • Output Section Phdr: Output section phdr
  • Output Section Fill: Output section fill
3.6.8.1 Output Section Type

每个输出段都可以有一个类型,它被放在括号里面。有如下定义:

NOLOAD

这个段被标记为不能被加载,也就是说在程序运行的时候,它不会被加载到内存中

DSECT

COPY

INFO

OVERLAY

这些类型名是向后兼容的,不过很少使用。它们都是一个作用:这个段被标记为不能分配,也就是说当程序运行的时候,它不会被分配内存

通常,链接器会给一个输出段设定属性,你可以重新定义它的段类型。举例来说,下面的脚本中,这个‘ROM’输出段在内存中的地址是0,但是在程序运行的时候,它不会被加载进内存

3.6.8.2 Output Section LMA

每个段都有一个虚拟地址(VMA)和一个加载地址(LMA),看一下 Basic Script Concepts 小节。虚拟地址的介绍可以看看早前的 Output Section Address 小节。加载地址的指定是用 AT 和 AT>关键字,它们是可选项

AT关键字把参数包含在括号里面,它明确指定了段的加载地址。AT>关键字把内存区域(memory region)作为参数,可以看看 MEMORY 小节,段的加载地址就是这个区域内的下一个空闲地址,当然这个地址是要段对齐的

如果对于一个可分配的(allocatable )段没有AT和AT>关键字,那么链接器会通过以下方式推算出来:

  • 如果段指定了一个VMA,那么LMA也是它
  • 如果段是不可分配的,那么LMA设定为VMA
  • 否则,如果发现一个与当前段兼容的内存区域,且内存区域至少有一个段,那么LMA被设定后,当前段的VMA和LMA的差与那个内存区域的最后一个段的VMA与LMA相同
  • 如果没有发现内存区域,那么就把整个地址空间当做一个默认的内存区域,重复前一个的步骤
  • 如果没有发现内存区域,该段之前又没有段(补充:即使把整个地址空间当做一个内存区域,但是对于第3个条件还要满足至少存在一个段),则LMA和VMA相等

这种特征很容易被设计来构建一个ROM映像。举例来说,下面的脚本创建了3个输出段:‘.text’段它的起始地址为0x1000,‘.mdata’段它被加载到‘.text’段之后但它的虚拟地址为0x2000,‘.bss’存储着没有初始化的数据起始地址为0x3000。符号_data的值是0x2000,它显示的是位置计数即VMA的值而不是LMA

[plain] view plaincopy在CODE上查看代码片派生到我的代码片
  1. SECTIONS  
  2.   {  
  3.   .text 0x1000 : { *(.text) _etext = . ; }  
  4.   .mdata 0x2000 :  
  5.     AT ( ADDR (.text) + SIZEOF (.text) )  
  6.     { _data = . ; *(.data); _edata = . ;  }  
  7.   .bss 0x3000 :  
  8.     { _bstart = . ;  *(.bss) *(COMMON) ; _bend = . ;}  
  9. }  

运行初始化可以使用一段脚本产生的程序将ROM映像中的初始化数据拷贝到运行时的地址空间,可以看到在脚本中定义符号的好处

[plain] view plaincopy在CODE上查看代码片派生到我的代码片
  1. extern char _etext, _data, _edata, _bstart, _bend;  
  2. char *src = &_etext;  
  3. char *dst = &_data;  
  4.   
  5. /* ROM has data at end of text; copy it.  */  
  6. while (dst < &_edata)  
  7.   *dst++ = *src++;  
  8.   
  9. /* Zero bss.  */  
  10. for (dst = &_bstart; dst< &_bend; dst++)  
  11.   *dst = 0;  

3.6.8.3 Forced Output Alignment

你可以使用ALIGN来增加输出段的对齐方式

3.6.8.4 Forced Input Alignment

你可以使用SUBALIGN来强制输出段中的输入段对齐方式,这个值会覆盖掉原来输入段的对齐方式不管是大还是小

3.6.8.5 Output Section Constraint

你可以使用关键字ONLY_IF_RO和ONLY_IF_RW指定,如果所有输入段是只读或者所有输入段是读写,则输出段应该要被创建

3.6.8.6 Output Section Region

你可以使用‘>region’将一个段分配到之前定义好的一个内存区域中,see MEMORY.

下面是一个简单的例子:

[plain] view plaincopy在CODE上查看代码片派生到我的代码片
  1. MEMORY { rom : ORIGIN = 0x1000, LENGTH = 0x1000 }  
  2. SECTIONS { ROM : { *(.text) } >rom }  

3.6.8.7 Output Section Phdr

你可以使用‘:phdr’给一个段分配一个之前定义过的程序段(segment),具体看PHDRS。如果一个段会被分配给多个segment,那么通过显示的指明‘:phdr’可以改变可分配段被分配给segment的顺序。使用:NONE告诉链接器不要将该段分配给任何segment

下面一个简单的例子:

[plain] view plaincopy在CODE上查看代码片派生到我的代码片
  1. PHDRS { text PT_LOAD ; }  
  2. SECTIONS { .text : { *(.text) } :text }  

3.6.8.8 Output Section Fill

你可以使用‘=fillexp’来设定填充模式,fillexp是一个表达式(see Expressions)。对于在输出段中没有被定义的内存空隙(补充:如果使用内存区域则和memory region发生歧义)(例如,输入段对齐后残留下来的空隙)可以用这个值来重复填充。如果表达式是一个简单的十六进制数,那就是一串以'0x'开头和一长串十六进制数字组成的不以'k'或者‘M’结尾的字符,前面的0也是表达式的一部分。其他的表达式,需要在外面加上括号或者+且至少有4个有意思的字节。对于所有的表达式,数字都用大端字节序

你也可以使用FILL命令来改变输出段的填充值,(see Output Section Data)

下面是一个例子:

[plain] view plaincopy在CODE上查看代码片派生到我的代码片
  1. SECTIONS { .text : { *(.text) } =0x90909090 }  

3.6.9 Overlay Description

略.

3.7 MEMORY Command

链接器的默认配置允许所有有效内存的分配,你可以使用MEMORY命令来重新定义它

MEMORY命令描述了一个内存块的位置和大小。你可以用它来描述哪块内存区域可以被链接器使用,哪块内存区域一定要避开。你可以将段分配特殊的内存区域内,链接器会给段在内存区域中分配地址,如果满了就会有警告,链接器是不会压缩段使之适合放到区域中

一个链接脚本最多只能拥有一个MEMORY命令,然而,你可以定义很多内存块,语法如下:

[plain] view plaincopy在CODE上查看代码片派生到我的代码片
  1. MEMORY  
  2.   {  
  3.     name [(attr)] : ORIGIN = origin, LENGTH = len  
  4.     ...  
  5.   }  

这个name定义了区域的名字。区域的名字对于脚本外部没有意义,它会存储在一个隔离的名字空间里,不会和符号名、文件名或者段名冲突。在MEMORY命令里的每个内存区域名字不能冲突,不过,你可以给已经存在的内存区域定义别名,see REGION ALIAS (3.4.4 Assign alias names to memory regions)

这个attr字符串是可选项,它指明那些在链接脚本中没有被映射的输入段会被放到哪个特殊的内存区域。如 SECTIONS (3.6 SECTIONS Command)小节,对于没有指定输出段的那些输入段,链接器会创建一个同名的输出段。如果定义了区域属性,链接器可以使用它们为那些创建的输出段指定内存区域

这个attr字符串可以有一下字符组成:

[plain] view plaincopy在CODE上查看代码片派生到我的代码片
  1. `R'  
  2.        Read-only section   
  3. `W'  
  4.        Read/write section   
  5. `X'  
  6.        Executable section   
  7. `A'  
  8.        Allocatable section   
  9. `I'  
  10.        Initialized section   
  11. `L'  
  12.        Same as `I'   
  13. `!'  
  14.         Invert the sense of any of the attributes that follow  

如果一个没有映射的段符合这些属性,除了‘!’属性,它就会被放入此内存区域中。这个‘!’属性保留来给那些任何属性都不符合的那些没有映射的段

origin是一个数字表达式,表示内存区域的起始地址。这个表达式必须由常数来计算得到不能包含任何符号。关键字ORIGIN还可以缩写为org或者o(但是不能是ORG)

len表示了内存区域的大小。和origin表达式一样,只能使用常数计算。关键字LENGTH可以缩写为len或者l

下面有个例子,我们定义两个可以有效分配的内存区域:一个是起始地址为0大小为256K,另一个的起始地址为‘0x40000000’大小为4M。链接器会将每个没有映射的只读或者可执行的段放入‘rom’内存区域中,将其余没有映射的段放入‘ram’的内存区域

一旦你定义了一个内存区域,你可以使用‘>region’输出段属性直接给输出段指明一个内存区域。比如,有一个‘mem’的内存区域,那么你可以在输出段中使用‘>mem’,see Output Section Region (3.6.8.6 Output Section Region)。如果没有给这个输出段指明地址,链接器会将它放到该内存区域内下一个有效地址,如果把输入段放入内存区域中太大了,链接器就会报错

通过ORIGIN(memory)和LENGTH(memory)可以访问内存区域的其实地址和大小:

[plain] view plaincopy在CODE上查看代码片派生到我的代码片
  1. _fstack = ORIGIN(ram) + LENGTH(ram) - 4;  

3.8 PHDRS Command

ELF文件格式使用了程序头(program header),也可以称作段(segment)。程序头的作用是描述一个程序如何被加载进内存。你可以通过使用objdump的命令行参数‘-p’选项打印出程序头

当你在本地跑一个ELF格式的程序时,系统为了知道怎样加载程序会依次读取程序头,必须保证程序头的设置完全正确才能进行。这份文档不会详细描述系统如何解析程序头,更详细的信息可以看看ELF ABI的相关内容

链接器在默认情况下会创建一个可用的程序头。然而,在很多情况下,你可能需要更加精确和详细的程序头设定,可以是用PHDRS命令来达到此目的。当链接器在链接脚本中看到PHDRS命令时,那么它就不会在创建任何程序头了

链接器仅仅在需要产生一个ELF输出文件时关注PHDRS命令,其他情况下,就会忽略掉PHDRS命令

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

[plain] view plaincopy在CODE上查看代码片派生到我的代码片
  1. PHDRS  
  2. {  
  3.   name type [ FILEHDR ] [ PHDRS ] [ AT ( address ) ]  
  4.         [ FLAGS ( flags ) ] ;  
  5. }  

name仅仅是用来被SECTIONS命令引用的,它不会被放到输出文件中。程序头的名字会被存储在一个隔离的名字空间,不会和符号名,文件名或段(section)名冲突的。每个程序头的名字必须不同。这个头是按照顺序处理的,段的映射地址是按照升序进行加载的

确定的程序头类型描述了一个内存的段,这个段是要被系统从文件中加载进内存的段。在脚本中,你可以指定可分配段(section)放置的段(segment),输出段属性中使用‘:phdr’来指定该段(section)放入一个特殊的段(segment),具体看Output Section Phdr

通常,可以将一个特定的section放入多个segment中,很少暗示一个segment的内存里面可以包含其他。你可以重复使用‘:phdr’,但一个segment仅能使用一次

使用‘:phdr’可以将一个section放入一个或多个segment中,而没有指定‘:phdr’的可以分配section链接器会将它们放入相同的segment中。为了方便起见,会将一整个连续section放入一个单独的segment中。你可以使用:NONE告诉链接器不要将该section放到任何segment中

你可能会在程序头类型的后面使用FILEHDR和PHDRS关键字作为segment内容的扩展。FILEHDR关键字的意思是segment会包含ELF文件头,PHDRS关键字的意思是segment会包含ELF程序头自己本身。如果使用了PT_LOAD,那么这两个关键字就是第一优先级

type类型有以下几种,关键字的值都用数字来表示的

PT_NULL (0)

表示一个无用的程序头

PT_LOAD (1)

表示这个程序头描述的段是会被从文件中加载进内存

PT_DYNAMIC(2)

表示这个段存放的是动态链接的信息

PT_INTERP(3)

表示这个段存放了程序的注释信息

PT_NOTE(4)

表示这个段为提示信息

PT_SHLIB(5)

一个保留的程序头类型,在ELF ABI中定义了但没有使用

PT_PHDR(6)

表示这个段是一个程序头

expression

一个数值的表达式代表了程序头类型,它的类型可能没有被定义

你可以使用AT表达式给segment指定一个内存地址,AT命令也是被输出段属性使用(具体看Output Section LMA),程序头中的AT命令可以覆盖掉输出段中的属性

通常,链接器设置的segment标志是各个section的标志的组合。你可以使用FLAGS关键字显示的指明segment的标志,flags的值一定要是整数,就是设定了程序头中p_flags的值

下面是一个PHDRS的例子,展示一个典型的程序头集合

[plain] view plaincopy在CODE上查看代码片派生到我的代码片
  1. PHDRS  
  2. {  
  3.   headers PT_PHDR PHDRS ;  
  4.   interp PT_INTERP ;  
  5.   text PT_LOAD FILEHDR PHDRS ;  
  6.   data PT_LOAD ;  
  7.   dynamic PT_DYNAMIC ;  
  8. }  
  9.   
  10. SECTIONS  
  11. {  
  12.   . = SIZEOF_HEADERS;  
  13.   .interp : { *(.interp) } :text :interp  
  14.   .text : { *(.text) } :text  
  15.   .rodata : { *(.rodata) } /* defaults to :text */  
  16.   ...  
  17.   . = . + 0x1000; /* move to a new page in memory */  
  18.   .data : { *(.data) } :data  
  19.   .dynamic : { *(.dynamic) } :data :dynamic  
  20.   ...  
  21. }  

3.9 VERSION Command

略.

3.10 Expressions in Linker Scripts

表达式的语法和C语言完全相同。所有的表达式值都是整数且大小相同,如果你的本地主机和目标主机都是32位那大小就是32位,否则就是64位

你可以在表达式中使用和设定符号的值

为了表达式的使用链接器定义了几个有用的内建函数

  • Constants: Constants
  • Symbolic Constants: Symbolic constants
  • Symbols: Symbol Names
  • Orphan Sections: Orphan Sections
  • Location Counter: The Location Counter
  • Operators: Operators
  • Evaluation: Evaluation
  • Expression Section: The Section of an Expression
  • Builtin Functions: Builtin Functions
3.10.1 Constants

所有的常数都是整数

像C语言一样,链接器也是将以‘0’开头的整数当成8进制,‘0x’或'0X'开头的当成16进制。有改变的是,链接器接受后缀,‘h’ or 'H'表示16进制,‘o’ or 'O'表示8进制,‘b’ or 'B'表示二进制,'d' or 'D'表示10进制。如果一个整数既没有前缀也没有后缀那它就是10进制

还有,可以添加后缀K和M来放大常数的倍数到1024倍和1024*1024倍。举例来说,下面给出的值都是相同的:

[plain] view plaincopy在CODE上查看代码片派生到我的代码片
  1. _fourk_1 = 4K;  
  2. _fourk_2 = 4096;  
  3. _fourk_3 = 0x1000;  
  4. _fourk_4 = 10000o;  

注意,这个K和M可不能连续使用

3.10.2 Symbolic Constants

可以通过CONSTANT (name)来指定一个常数,这里的name是如下一种:

MAXPAGESIZE

目标主机最大页大小

COMMONPAGESIZE

目标主机的默认页大小

举例来说:

[plain] view plaincopy在CODE上查看代码片派生到我的代码片
  1. .text ALIGN (CONSTANT (MAXPAGESIZE)) : { *(.text) }  

它会将创建的段按照目标主机的最大页大小对齐

3.10.3 Symbol Names

除了引号,符号名都是以字母、下划线或者句点开头,可能包含了字母、数字、下划线、句点、连接符。没有引号的符号名不能和关键字冲突。如果你将符号名用双引号包起来,那符号名可以包含一些奇怪的符号或者同关键字一样:

[plain] view plaincopy在CODE上查看代码片派生到我的代码片
  1. "SECTION" = 9;  
  2. "with a space" = "also with a space" + 10;  

因为符号可以包含非字母的字符,因此对于符号和空格的界限是很好区分的。比如,‘A-B’是一个符号,‘A - B’是一个包含减法的表达式

3.10.4 Orphan Sections

孤儿段就是那些在链接脚本中没有被显式地映射到输出文件的输入文件中的输入段。链接器有一个简单的规则将它们放到输出文件中。它会将它们放到相同属性的非孤儿段后面,比如代码段和数据段,可加载段和非可加载段等等,如果没有那样的地方就把它们放到文件的末尾

对于ELF目标文件来说,段的属性包含了段类型和段标志

孤儿段的名字可以作为C语言的标识,链接器会自动的去看 PROVIDE命令的两个符号:__start_SECNAME和__stop_SECNAME,其中SECNAME是段名。它们代表了孤儿段的起始地址和末尾地址。注意,大多数的段名是无法用C语言标识的,因为它们包含了‘.’字符

3.10.5 The Location Counter

变量dot ‘.’表示当前输出位置的计数。因为它指向的是一个输出段的的一个位置,因此仅可能出现在SECTIONS命令的表达式中。它像一个普通符号一样存在于表达式里

给dot.赋值会引起位置计数移动,这样可以在输出段中创建一个大的空闲(holes)。位置计数不能在输出段里向后移动,也不能在输出段外向后移动。创建出的那个区域会被覆盖

[plain] view plaincopy在CODE上查看代码片派生到我的代码片
  1. SECTIONS  
  2. {  
  3.   output :  
  4.     {  
  5.       file1(.text)  
  6.       . = . + 1000;  
  7.       file2(.text)  
  8.       . += 1000;  
  9.       file3(.text)  
  10.     } = 0x12345678;  
  11. }  

在上面这个例子中,来自file1的'.text'段放在了'output'输出段的开始位置,紧接着是1000个字节的空隙,然后跟着来自file2的‘.text’段,也接了1000个字节的空隙,最后放了来自file3的‘.text’段。符号‘= 0x12345678’这个数据会去填充那些空隙(see Output Section Fill)

注意:dot.实际上表示的是相对于当前对象起始位置的偏移。如果它在SECTIONS命令中,那么它的起始位置就是0,因此dot.就是一个绝对地址。如果dot.在一个输出段中,那么它指向的就是该段起始位置的偏移,不是一个绝对地址。一个这样的脚本如下:

[plain] view plaincopy在CODE上查看代码片派生到我的代码片
  1. SECTIONS  
  2. {  
  3.     . = 0x100  
  4.     .text: {  
  5.       *(.text)  
  6.       . = 0x200  
  7.     }  
  8.     . = 0x500  
  9.     .data: {  
  10.       *(.data)  
  11.       . += 0x600  
  12.     }  
  13. }  

这个'.text'的其实地址被设定为了0x100,大小实际上是0x200的字节,如果'.text'输入段不足0x200字节大小,则去填充它(但是,如果数据太大的话,就会产生错误,因为这样dot.实际上是在向后移动)。这个‘.data’段其实位置是0x500,拥有一个额外的0x600字节大小的区域在'.data'输入段和'.data'输出段末尾之间

如果链接器需要放置孤儿段的话,那么将位置计数的值赋给一些符号可能导致一个不是预期的值。比如:

[plain] view plaincopy在CODE上查看代码片派生到我的代码片
  1. SECTIONS  
  2. {  
  3.     start_of_text = . ;  
  4.     .text: { *(.text) }  
  5.     end_of_text = . ;  
  6.   
  7.     start_of_data = . ;  
  8.     .data: { *(.data) }  
  9.     end_of_data = . ;  
  10. }  

如果链接器需要放置某些没有在脚本中提到的输入段,比如.rodata段,那么它可能将这样的段放入'.text'和'.data'之间。你可能认为,链接器会把.rodata段如到空白处,不过对于链接器来说,空白没有特别的含义。同样的,链接器不会联想出符号名和它们的段有关系。链接器会认为所有的赋值操作或者其他的声明都是在输出段之上的,除非有对dot.赋值这样特别的方式。于是乎,链接器会这样放置孤儿段:

[plain] view plaincopy在CODE上查看代码片派生到我的代码片
  1. SECTIONS  
  2. {  
  3.     start_of_text = . ;  
  4.     .text: { *(.text) }  
  5.     end_of_text = . ;  
  6.   
  7.     start_of_data = . ;  
  8.     .rodata: { *(.rodata) }  
  9.     .data: { *(.data) }  
  10.     end_of_data = . ;  
  11. }  

这种方式根本不是作者写脚本想得到start_of_data值的意图。给位置计数自己赋值可以影响到孤儿段的放置方式,如果给位置计数赋值会让链接器认为赋值操作下面紧跟着就是一个输出段,因此你可以这样写:

[plain] view plaincopy在CODE上查看代码片派生到我的代码片
  1. SECTIONS  
  2. {  
  3.     start_of_text = . ;  
  4.     .text: { *(.text) }  
  5.     end_of_text = . ;  
  6.   
  7.     . = . ;  
  8.     start_of_data = . ;  
  9.     .data: { *(.data) }  
  10.     end_of_data = . ;  
  11. }  

现在,孤儿段‘.rodata’会被放置到end_of_text和start_of_data之间

3.10.6 Operators

链接器同样可以处理类似C语言的操作符算法和绑定的优先处理级别:

[plain] view plaincopy在CODE上查看代码片派生到我的代码片
  1. precedence      associativity   Operators                Notes  
  2. (highest)  
  3. 1               left            !  -  ~                  (1)  
  4. 2               left            *  /  %  
  5. 3               left            +  -  
  6. 4               left            >>  <<  
  7. 5               left            ==  !=  >  <  <=  >=  
  8. 6               left            &  
  9. 7               left            |  
  10. 8               left            &&  
  11. 9               left            ||  
  12. 10              right           ? :  
  13. 11              right           &=  +=  -=  *=  /=       (2)  
  14. (lowest)  

注意:(1)前缀操作(2)see Assignments

3.10.7 Evaluation

链接器对表达式的推导是懒惰的,它仅仅在必须要的时候才会去计算一个表达式的值

为了按照顺序的把所有段链接在一起链接器需要许多信息,比如第一个段的起始地址和内存区域的起始地址和长度。在链接器阅读脚本时,这些值会被尽快地计算出来

然而在存储分配之前其他的值(比如符号值)是不知道或者不需要的。这些值计算出来后,其他的信息(比如输出段的大小)就可以被符号赋值表达式使用

这个段的大小也必须等到存储分配后才知道,因此依赖这些的赋值操作都无法在存储分配前完成

有些依赖位置计数dot.的表达式一定要在段分配期间计算出来

如果一个表达式的值被需要,但是这个值是无效的,那么会报错。比如,一个这样写的脚本:

[plain] view plaincopy在CODE上查看代码片派生到我的代码片
  1. SECTIONS  
  2.   {  
  3.     .text 9+this_isnt_constant :  
  4.       { *(.text) }  
  5.   }  

将会报错‘non constant expression for initial address’

3.10.8 The Section of an Expression

地址和符号可能是相对于段或绝对于段的,一个段的相对符号是可从重定位的。如果你使用‘-r’链接选项来要求重定位输出,那么链接器会改变段里的相对符号的值。另一方面来说,绝对符号在任何链接形式中至始至终都保持相同的值

脚本中很多表达式都是地址,像段相对符号和那些返回值是地址的内建函数,比如ADDR,LOADADDR,ORIGIN和SEGMENT_START都是这样的。其他的表达式就是一个简单的数字或者是一个返回非地址值的内建函数,比如LENGTH。有个复杂点的情况是,如果你使用LD_FEATURE ("SANE_EXPR") 命令(see Miscellaneous Commands),那么数字和绝对符号在它们依赖的段里会有不同的对待,这种方式和老版本的ld兼容。

输出段外的所有数字都会被认为是地址,输出段里的所有绝对符号都会被认为是数字,如果LD_FEATURE ("SANE_EXPR")命令被声明,那么所有的绝对符号和数字都被简单的认为是数字

下面是个简单的例子

[plain] view plaincopy在CODE上查看代码片派生到我的代码片
  1. SECTIONS  
  2.   {  
  3.     . = 0x100;  
  4.     __executable_start = 0x100;  
  5.     .data :  
  6.     {  
  7.       . = 0x10;  
  8.       __data_start = 0x10;  
  9.       *(.data)  
  10.     }  
  11.     ...  
  12.   }  

dot.和__executable_start都会被设定为绝对地址0x100,而后面的两个复制语句中的dot.和__data_start都会被设定为相对于.data段0x10的数字

对于表达式,可以包含数字,相对地址和绝对地址,那么链接器会按照如下的规则来处理:

  • 一个相对地址的一元操作和两个在同一个段中的相对地址或者一个相对地址和一个数字的二元操作,那么操作它们为偏移地址
  • 一个绝对地址的一元操作和一个或多个绝对地址或两个不在同一个段的相对地址的二元操作,那么先非绝对地址转换为绝对地址在操作
每个子表达式的按照如下规则:

  • 一个操作仅有数字,那么它的结果也是数字
  • 比较表达式‘&&’和‘||’的结果也是数字
  • 同一个段中的两个相对地址或者两个绝对地址的二元算法和逻辑操作的结果也是数字
  • 其他相对地址的操作或一个相对地址和一个数字的结果是在同一个段中的相对地址
  • 其他绝对地址的操作的结果是一个绝对地址
你可以使用内建函数ABSOLUTE来强制一个表达式为一个绝对表达式否则是相对的。举例来说,创建一个绝对的符号,它的值是输出段'.data'末尾的地址:

[plain] view plaincopy在CODE上查看代码片派生到我的代码片
  1. SECTIONS  
  2.   {  
  3.     .data : { *(.data) _edata = ABSOLUTE(.); }  
  4.   }  

如果‘ABSOLUTE’没有使用,那么'_edata'就是相对于'.data'段的值

使用LOADADDR也可以强制一个表达式为绝对表达式,因为这个特殊的内建函数返回的是一个绝对的地址

3.10.9 Builtin Functions

链接脚本语言中包含了一些内建函数可以给表达式使用

ABSOLUTE (exp)

返回这个exp表达式的绝对值(非重定位值,负数取反)。最有用的地方是,可以给一个段内定义的相对符号分配一个绝对值

ADDR (section)

返回一个命名段的VMA地址,你的脚本文件中一定要在前面定义了这个段。在下面的例子中,start_of_output_1,symbol_1和symbol_2都会被分配相同的值,除了symbol_1分配的.output1段的相对地址值,其他两个都是绝对地址值:

[plain] view plaincopy在CODE上查看代码片派生到我的代码片
  1. SECTIONS { ...  
  2.   .output1 :  
  3.     {  
  4.     start_of_output_1 = ABSOLUTE(.);  
  5.     ...  
  6.     }  
  7.   .output :  
  8.     {  
  9.     symbol_1 = ADDR(.output1);  
  10.     symbol_2 = start_of_output_1;  
  11.     }  
  12. ... }  

ALIGN (align)

ALIGN (exp, align)

返回当前位置计数或者一个表达式按照align边界对齐的值。单个操作数的ALIGN不会改变位置计数的值——它仅仅是一个算法。两个操作数的ALIGN允许一个表达式向上对齐(ALIGN (align)同等ALIGN (. , align))

下面例子中,将输出段.data对齐到下一个0x2000字节的边界,将一个变量为输入段的下一个0x8000的边界:

[plain] view plaincopy在CODE上查看代码片派生到我的代码片
  1. SECTIONS { ...  
  2.   .data ALIGN(0x2000): {  
  3.     *(.data)  
  4.     variable = ALIGN(0x8000);  
  5.   }  
  6. ... }  

例子中的第一个ALIGN操作就是指定了段的位置,因为它使用的是段属性 (see Output Section Address),第二个使用ALIGN操作用来定义一个符号

NEXT内建函数的功能很接近ALIGN

ALIGNOF (section)

返回一个命名段的对齐字节数,这个段需要是已经分配过的。如果在计算的时候一个段还没有被分配,那么链接器会报错。下面的例子中,.outut段的对齐字节数作为了该段第一个值存储

[plain] view plaincopy在CODE上查看代码片派生到我的代码片
  1. SECTIONS{ ...  
  2.   .output {  
  3.     LONG (ALIGNOF (.output))  
  4.     ...  
  5.     }  
  6. ... }  

BLOCK (exp)

这个是ALIGN的同样意思,为了和老版本的链接脚本兼容。它经常使用来设置一个输出段的地址

DATA_SEGMENT_ALIGN (maxpagesize, commonpagesize)

略.

DATA_SEGMENT_END (exp)

略.

DATA_SEGMENT_RELRO_END (offset, exp)

略.

DEFINED (symbol)

如果symbol在符号表中定义了那么返回1,否则返回0。你可以使用这个功能给符号提供一个默认值。举例来说,下面的脚本片段中设置全局符号‘begin’为段‘.text’的起始位置——但是如果一个被叫做‘begin’的全局符号已经存在了,那么它的值就被保留

LENGTH (memory)

返回命名内存区域的长度

LOADADDR (section)

返回命名段的绝对LMA地址 (see Output Section LMA)

MAX (exp1, exp2)

返回exp1exp2中的最大值

MIN (exp1, exp2)

返回exp1exp2中的最小值

NEXT (exp)

返回下一个没有被分配的地址,它是exp的倍数。这个功能和ALIGN (exp)几乎一样,除非你用MEMORY命令在输出文件中定义了一个不连续的内存,否则它们就是一样的

ORIGN (memory)

返回命名内存区域的起始地址

SEGMENT_START (segment, default)

设定名为segment段的地址,如果这个段已经显示的被指定(比如在命令行中使用‘-T’选项),那么这个值就会被返回,否则它的值就是value。目前,‘-T’命令行选项只能设定“text”,“data”和“bss”段,不过SEGMENT_START可以设定任意段

SIZEOF (section)

返回一个命名段的大小字节数,这个一定要是已经分配了的。如果在计算的时候这个段没有被分配,链接器会报错。下面的例子中,symbol_1和symbol_2都指明了它:

[plain] view plaincopy在CODE上查看代码片派生到我的代码片
  1. SECTIONS{ ...  
  2.   .output {  
  3.     .start = . ;  
  4.     ...  
  5.     .end = . ;  
  6.     }  
  7.   symbol_1 = .end - .start ;  
  8.   symbol_2 = SIZEOF(.output);  
  9. ... }  

SIZEOF_HEADERS

sizeof_headers

返回输出文件头的大小字节数。这些信息在输出文件的开始位置。你可以使用这个数来设定第一个段的起始地址。

当产生ELF输出文件时,如果链接器使用了SIZEOF_HEADERS内建函数,那么链接器一定会在所有的段地址和大小被决定前计算出程序头(program headers)的大小。如果链接器发现,需要增加程序头时,就会报错“not enough room for program headers”。为了避免这样的错误,你一定要避免使用SIZEOF_HEADERS功能,或者重新写你的链接脚本来强制避免链接器产生新增的程序头,或者你用PHDRS命令去定义你自己的程序头

3.11 Implicit Linker Scripts

如果你指定的链接输入文件不能被链接器识别为目标文件或者归档文件,那么它会被当做链接脚本来读取。如果这个文件不能被解析为链接脚本文件,链接器就会报错

一个暗含的链接器脚本不会取代默认的链接脚本

一个典型的暗含的链接脚本通常仅包含符号赋值或者INPUT,GROUP或VERSION命令

任何一个由暗含的链接脚本指定的输入文件的位置是由链接脚本在命令行中的位置决定的,这会影响到归档文件的搜索


2013年12月04日  10:05

update:

2013年12月23日:补充翻译了《3.8 PHDRS Command》小节和《3.6.8.7 Output Section Phdr》小节。


【MySQL 5.7 Reference Manual】15.4.3 Adaptive Hash Index(自适应哈希索引)

自适应哈希索引(AHI)使InnoDB平台看起来更像一个内存数据库(在系统负载适当并且分配给缓存池的内存充裕的情况下),且不牺牲任何事务特性或可靠性。这个特性可以在服务启动时通过innodb_adap...

GNU LD Manual

  • 2016年01月01日 20:10
  • 2.07MB
  • 下载

【资源共享】完整版RK3399芯片手册《RK3399 Technical Reference Manual 1.4》

【资源共享】完整版RK3288芯片手册 《RK3399 Technical Reference Manual 1.4》   完整资料总共824页   详细的寄存器与操作资料   ...

Expect and TCL mini reference manual

Expect and TCL mini reference manual Expect is an extension of the TCL language. This means t...

【MySQL 5.7 Reference Manual】15.4.2 Change Buffer(变更缓冲)

变更缓冲是一个特殊的数据结构,当目标页不在缓冲池中时,变更缓冲负责缓存对二级索引页的变更。被缓冲的变更内容可能是INSERT,UPDATE,或DELETE操作(DML)的结果。在下一次读操作时这些页会...

【MySQL 5.7 Reference Manual】15.4.12.1 InnoDB Temporary Table Undo Logs(InnoDB临时表Undo日志)

临时表undo日志,在MySQL 5.7.2中被引入,用于存放临时表和相关对象。这种类型的undo日志不是一个redo日志,因为临时表在崩溃恢复期间不会被恢复并且不需要redo日志。然而,临时表und...

【MySQL 5.7 Reference Manual】15.4.13 Redo Log(Redo日志)

redo日志基于磁盘的数据结构,在崩溃恢复期间用于纠正不完整事务所写入的数据。在正常操作情况下,redo日志编码请求以改变InnoDB表数据,这些数据来自于SQL语句或低级API调用的结果。如果在意外...

MySQL 5.7 Reference Manual Chapter 13 Functions and Operators 参考手册第十三章函数与操作符内容总结

MySQL 5.7 Reference Manual Chapter 13 Functions and Operators 参考手册第十三章函数与操作符内容总结...

GTK+ Reference Manual

GTK+ Reference Manual for GTK+ 2.6.2 ---------------------------------------------------------------...
  • uunubt
  • uunubt
  • 2011年01月09日 15:46
  • 382

MySQL 5.7 Reference Manual Chapter 10 Language Structure 参考手册第十章语言结构内容总结

MySQL 5.7 Reference Manual Chapter 10 Language Structure 参考手册第十章语言结构内容总结...
内容举报
返回顶部
收藏助手
不良信息举报
您举报文章:《ld reference manual》
举报原因:
原因补充:

(最多只允许输入30个字)