08年搞了一个可以在linux下使用GNU GCC编译的NXP工程,学到了不少东西。从linux kernel、u-boot借鉴了不少东西,下面对链接文件做一个简单分析。
名词解释
- The Location Count(.) – 地址计数器
- Load Memory Address(LMA) – 加载内存地址
- Virtual Memory Address(VMA) – 虚拟内存地址
- section contents – 段内容
- Input Section – 输入段
- Output Section – 输出段
链接脚本的基本概念
连接器合并多个输入文件到一个单独的目标文件。输出文件和每个输入文件都是特殊的数据格式,被称为目标文件格式。每个文件被称为目标文件。输出文件通常被称为可执行文件,但是我们仍称其为目标文件。每个目标文件有一系列段。我们通常认为在输入文件中的段是输入段;同样,在输出文件的段是输出段。
目标文件的每个段都有名字和大小。大多数段也关联了大量的数据,被称为段内容。一个段可以被标记为可加载的,意思是它的内容在这个输出文件运行的时候被加载进内存。一个没有可分配内容的段,意思是需要在内存留出一块空间,但是没有特别的需要加载到那里(在一些情况下这些内存必须为0)。既没有可加载也没有可申请的段通常多少包含些调试信息。
每一个可加载或可分配的输出段都有俩个地址。第一个是VMA。这个是输出文件运行的地址。第二个是LMA。这个是段加载的地址。大多数情况下这俩个地址是一样的。一个它们不一样的例子是当数据段被装填到ROM,然后在程序运行的时候被拷贝到RAM(这个技巧通常用于在一个基于ROM的系统下初始化全局变量)。在这个情况下,ROM地址是LMA,RAM地址是VMA。
你可以使用带参数’-h’的objdump程序看一个目标文件的各个段信息。
每个目标文件也包含一系列符号,被称为符号表。一个符号可以是定义或者未定义的。每个符号都有一个名字,并且每一个定义的符号都有一个地址。如果你编译C或者C++程序到目标文件,你会从每一个定义的函数和全局或静态变量得到一个符号。指向输入文件的每一个未定义的符号或者全局变量会成为一个未定义符号。
你可以通过nm程序看一个目标的符号表,或者使用带’-t’参数的objdump程序。
一个简单的链接脚本例子
许多链接脚本是相当简单的。
最简单的链接脚本只有一个命令:”SECTIONS“。你会使用”SECTIONS“命令来描述输出文件的地址分配。
”SECTIONS“命令是非常有用的。下面我们简单介绍以下如何使用它。假设程序只包含代码、初始化数据和未初始化数据。相对应的,它们会分别在’.text’、’.data’和’.bss’段。
- .text(代码段) – 存放的是代码段和const修饰的只读变量
- .data(数据段) – 段存放的是赋初值的全局变量
- .bss(堆栈段) – 存放的是函数中的变量和未赋初值的全局变量
对这个例子,加入代码段在地址0x10000,数据段在地址0x8000000。下面就是链接脚本:
1 2 3 4 5 6 7 8 9 10 |
SECTIONS { . = 0x10000; .text : { *(.text) } . = 0x8000000; .data : { *(.data) } .bss : { *(.bss) } } |
上面例子中第三行设置了一个特殊的符号’.’ ,这是一个位置计数器。如果你不用其他方式指定输出段的地址,地址被设置为位置计数器的地址。位置计数器的地址会根据输出段的大小增长。’SECTIONS’命令的起始位置计数器为0.
第四行定义了一个输出段,’.text’。冒号是语法要求的。在输出段名字的花括号中,列出了应该放入这个输出段的输入段的名字。符号’*’是通配符,匹配任何文件名。表达式’*(.text)’意思是在所有输入文件的所有’.text’输入段。
因为定义输出段’.text’时位置计数器是’0x10000’,linker会在输出文件中设置’.text’段的地址为’0x10000’。
剩下几行在输出文件定义了’.data’和’.bss’。linker把’.data’输出段放到地址’0x8000000’。在linker放置’.data’输出段后,当前的位置计数器是’0x8000000’加上’.data’输出段的大小。从而linker把’.bss’段放在’.data’段的后面。
如果需要,在位置计数器增加过程中,linker会保证每个输出段的要求的对齐大小。在这个例子中,指定地址的’.text’h和’.data’段可以满足任意对齐限制,但是linker会在’.data’和’.bss’段中间加入一个小空隙。
这就是一个简单完整的链接文件。
链接文件参数
OUTPUT_ARCH
指定输出平台
OUTPUT_FORMAT
指定目标文件格式
ENTRY
程序中第一条运行的指令称为entry point 。有下面几种方法设置entry point。
- 使用’-e’命令行选项;
- 在链接文件下使用ENTRY(symbol)命令;
- 一些平台下定义的特殊符号。对大多数平台是start ;
- ‘.text’段第一个字节的地址,如果存在的话;
- 地址0。
MEMORY
linker的默认配置允许使用所有可用内存,我们使用MEMORY命令覆盖它。
MEMORY命令描述了目标的内存地址和大小。我们使用它描述linker使用哪些地址空间以及必须禁用哪些地址空间。然后我们可以将sections分配给特殊的内存空间。linker会基于内存空间设置section地址,并在空间变满后给出警告。The linker will not shuffle sections around to fit into the available regions.
链接脚本可以包含至少1个MEMORY命令。然而,你可以在里面定义多个内存空间,语法是:
1 2 3 4 5 6 7 |
MEMORY { name [(attr)] : ORIGIN = origin, LENGTH = len ... } |
name在链接脚本中指示空间,这个空间名字在链接脚本外没有任何意思。空间名字存储在单独的命名空间,并且不会和符号名字、文件名字或者段名字冲突。每一个内存空间必须有一个不同的名字。然而,你可以在之后使用REGION_ALIAS命令对存在的内存空间起一个别名。
attr字符串是一个可选的属性列表用来指明如何使用这个内存空间。
attr必须包含以下字符:
- ‘R’ – 只读段
- ‘W’ – 读/写段
- ‘X’ – 可执行段
- ‘A’ - 可申请段
- ‘I’ – 初始化段
- ‘L’ – 和’I’一样
- ‘!’ – 反转后续的属性
origin是一个数学表达式指示内存空间的起始地址。这个表达式必须能计算出一个定值并且不能包含任何符号。关键字ORIGIN可以缩写成org或o。
len是一个表明memory region的表达式,单位为字节。和origin一样,这个表达式也必须计算出一个定值。关键字LENGTH可以缩写为len或l。
上面的例子定义了2个空间。一个起始于0、共128K,一个起始于0x40000000、共16K。linker会把满足下面条件的段放到’FLASH’内存空间,1. 没有明确指明map到内存空间的段;2. 可执行或者只读。linker会把其他没有指明map的段放置到’RAM’内存空间。
一旦定义了内存空间,你可以使用’>region’输出段属性把特定的输出段放置到这个内存空间。例如,如果你有一个命名为’RAM’的内存空间,你可以使用’>RAM’在定义的输出段。如果output section没有指定地址,linker会设置为内存空间内的下一个可用的地址。如果指向组合的output sections对于region过大,linker会发出一个错误信息。
ALIGN
返回当前计数(.)或者任意表达式对齐到下一个对齐边界。
下面是一个例子,作用是将输出段.data在之前的段后0x2000字节处对齐并且在输入段之后的0x8000边界处设置了一个变量:
1 2 3 4 5 6 7 8 9 10 11 12 |
SECTIONS { ... . data ALIGN(0x2000): { *(.data) variable = ALIGN(0x8000); } ... } |
在这个例子中ALIGN的第一个用法是定义了段的位置因为它被用作段定义的一个可选地址选项。第二个用法用来定义一个符号的值。
Output Section LMA
每一个section都有一个虚拟地址(VMA)和一个加载地址(LMA)。指定虚拟地址见Output Section Address。加载地址用AT或者AT>关键字指定。指定加载地址是可选的。
程序的初始化代码会做以下事:从ROM image拷贝初始化数据到它的运行地址。
输出段地址
地址是输出段的VMA的表示方式。这个地址是可选的,但是如果提供这个地址那么就一样要按照说明设置输出地址。
输出段地址遵循一下循序:
- 如果一个段被设置了输出内存空间,那么它被加入到这个空间并且它的地址在这个空间的下一个空地址。
- 如果使用MEMORY命令建立了一个内存空间列表,那么第一个属性兼容这个段的空间被选中。这个段的输出地址是空间的下一个空地址。
- 如果没有指定内存空间或者没有匹配的段那么输出地址基于当前位置计数器的值
举一个例子:
1 2 3 |
.text . : { *(.text) } |
和
1 2 3 |
.text : { *(.text) } |
是不同的。第一个设置’.text’输出段地址为当前位置计数器的值。第二十被设置为当前位置计数器的值并对齐到’.text’内的所有输入段中的最严密的对齐方式。
地址可以是任意数字表达式。比如,如果你想把段对齐在0x10边界,使得段地址的最低4位为0,那么你需要像这样做:
1 2 3 |
.text ALIGN(0x10) : { *(.text) } |
这