我们在用RISC-V GCC做嵌入式开发的时候,免不了要和启动文件和链接文件等打交道,本篇文章记录了一些链接脚本相关的学习笔记。
1.基础概念
链接脚本的主要作用是描述输入文件中的段应当如何映射到输出文件中,并控制输出文件的内存布局。多数链接脚本都执行类似功能。但是,如果需要,链接脚本也可以使用下面所描述的命令指挥链接器进行很多其他操作。
链接器通常使用一个链接脚本。如果没有为其提供一个,链接器将会使用默认的编译在链接器执行文件内部的脚本。可以使用命令’–verbose’显示默认的链接脚本。
为了描述链接脚本语言,我们需要定义一些基本概念和词汇。
链接器将许多输入文件组合成一个输出文件。输出文件和每个输入文件都有一个特定的已知格式成为目标文件格式。每个文件都被称为目标文件。输出文件通常叫做可执行文件,但我们仍将其称为目标文件。每个目标文件在其他东西之间,都有一个段列表。有时把输入文件的段称作输入段,类似的,输出文件的段称作输出段。
每个目标文件中的段都有名字和大小。多数段还有一个相关的数据块,称为 段内容。一个段可能被标记为可加载,表示当输出文件运行时,段内容需要先加载到内存中。一个没有内容的段可能是可分配段,即在内存中留出一段空间(有时还需要清零)。一个即不是加载又不是可分配的段,通常含有一些调试信息。
每个加载或可分配输出段有两个地址。第一个地址为VMA,或者叫做虚地址。这是当输出文件运行时段所拥有的地址。第二个地址是LMA,或者叫加载内存地址。这是段将会被加载的地址。一个它们会产生区别的例子是,当一个数据段加载到ROM, 此后在程序启动时被复制到RAM中(这个技术通常被用来初始化全局变量)。此种情况下,ROM使用LMA地址,RAM使用VMA地址。
如果想查看目标文件中的段,可以用objdump程序的’-h’选项。
每个目标文件还有一个符号列表,称为符号列表。一个符号可能是被定义的或者未定义的。每个符号都有一个名字,且所有已定义的符号在其他信息中间都有一个地址。如果将一个c或者c++程序编译成目标文件,会将所有定义过的函数和全局变量以及静态变量作为已定义符号。所有输入文件引用的未定义的函数或者全局变量会成为未定义符号。
2.常用关键词与用法
ENTRY(symbol) 用来指定程序执行的入口点
MEMORY 内存分配命令
SECTIONS 段命令 描述输出文件的内存和布局
.text 程序代码段
.rodata 只读数据
.data 可读写且需要初始化的数据
.bss 可读写的清零初始化数据
ASSERT 断言
PROVIDE(symbol=expression) 定义一个符号
AT 后跟MEMORY定义的内存区域或者地址
ALIGN 字节对齐
3 . MEMORY
链接器默认的设置允许分配所有可用的内存。你通过MEMORY命令可以重载这些。
MEMORY命令描述了一个内存块在目标中的位置和大小。你可以使用它描述一个可能会在链接器中使用的内存区域,以及那些必须避免使用的内存区域。此后你可以把段放到特定的内存区域里。链接器将会基于内存区域设置段地址,如果区域趋于饱和将会产生警告信息。链接器不会为了把段更好的放入内存区域而打乱段的顺序。
一个链接脚本可能含有许多MEMORY命令,但是,所有定义的内存块都被当作他们是在一个MEMORY命令中定义的一样。MEMORY的语法是:
MEMORY { name [(attr)] : ORIGIN = origin, LENGTH = len ... }
name是链接脚本用来引用内存区域的名字。区域名在链接脚本外部没有任何意义。区域名被存储在一个独立的名字空间,且不会与符号名,文件名,或者段名起冲突。每个内存区域必须在MEMORY命令中有一个不同的名字。但是你此后可以使用REGION_ALIAS命令为已存在的内存区域添加别名。
attr字符是一个可选的属性列表,用来决定是否让一个脚本中没有显式指定映射的输入段使用一个特定的内存区域。就像SECTIONS中进行过的说明,如果你不为一个输入段指定一个输出段,链接器将会创建一个与输入段名字相同的输出段。如果你定义了区域属性,链接器会使用他们来决定创建的输出段存放的内存区域。
attr字符串只能使用下面的字符组成:
‘R’只读段
‘W’读写段
‘X’可执行段
‘A’可分配段
‘I’已初始化段
‘L’类似于’I’
‘!’反转其后面的所有属性
如果一个未映射段匹配了上面除’!’之外的一个属性,它就会被放入该内存区域。’!’属性对该测试取反,所以只有当它不匹配上面列出的行何属性时,一个未映射段才会被放入到内存区域。
origin是一个数字表达式,代表了内存区域的起始地址。表达式必须等价于一个常数并且不能含有任何符号。关键字ORIGIN缩短为org或者o(但不能写成ORG)。
len是一个表达式用来给出内存区域中的字节数大小。类似于origin表达式,表达式必须只能为数字的切必须求值为常数。关键字LENGTH可以被缩写为len或者l。
下面的例子里,我们制定了有两个可分配的内存区域:一个从’0’开始有256k字节,另一个从’0x40000000’开始,由4兆字节。链接器把所有没有显式映射到一个内存区域的段放到’rom’内存区域内,段可以是只读的或者可执行的。链接器将把其它没显式指定内存区域映射的段放到’ram’内存区域。
MEMORY { rom (rx) : ORIGIN = 0, LENGTH = 256K ram (!rx) : org = 0x40000000, l = 4M }
一旦你定义了一个内存区域,你可以使用’>region’输出段属性指引链接器把特殊输出段放到该内存区域。例如,如果你拥有一个内存区域名为’mem’,你可以在输出段定义中使用’>mem’。参考Output Section Region。如果没有给输出段指出地址,链接器将会把地址放到最先符合要求的内存区域中的可用地址。如果指引给一个内存区域的组合输出段比区域还大,链接器将会提交错误。
可以通过ORIGIN(memory)和LENGTH(memory)函数获得内存区域的起始地址以及长度:
_fstack = ORIGIN(ram) + LENGTH(ram) - 4;
4. 段描述
4.1输出段
完整的输出段描述如下
section [address] [(type)] : [AT(lma)] [ALIGN(section_align) | ALIGN_WITH_INPUT] [SUBALIGN(subsection_align)] [constraint] { output-section-command output-section-command ... } [>region] [AT>lma_region] [:phdr :phdr ...] [=fillexp] [,]
地址(address)是一个输出段VMA(虚地址)的表达式。此地址为可选参数,但如果给出了地址,则输出地址就会被精确的设置到给定值。
如果输出的地址没有给定,则依照下面的尝试选择一个地址。此地址将会被调整到符合输出端要求的对齐地址。输出段的对齐要求是所有输入节中含有的对齐要求中最严格的一个。
输出段地址探索如下:
如果为段设置了内存区域,则段被放如该区域,并且段地址为区域中的下一个空闲位置。
如果使用MEMORY命令创建了一个内存区域列表,此时第一个属性匹配段的区域被选择来加载段,段地址为区域中的下一个空闲位置。参见MEMORY。
如果没有指定的内存区域,或者没有匹配段的,则输出地址将会基于当前位置计数器的值
4.2输入段
输入段存在于输出段的内容中,用来指定不同输入段在输出段中的位置,常见的有.text .data .rodat .bss COMMOM等,一个输入段描述由跟随在段名称后面括号包含的一个可选的文件名称列表构成。也可以使用通配符,例如
*main.o(.text)或者直接*(.text)
前一个代表main.o 文件中所有.text段,后一个代表所有参与链接文件中的.text段,当然也可以排除一些文件
EXCLUDE_FILE (*文件名.o) *(.text)
5. 一些内建函数
ABSOLUTE(exp)
返回表达式exp的绝对(非可重分配的,而不是非负)值。主要用来在段定义内为符号分配一个绝对值,通常段定义内的符号值都是相对段地址的。
ADDR(section)
返回名为’section’的段的地址(VMA)。你的脚本必须事先未该段定义了位置。在下面的例子里,start_of_output_1, symbol_1, symbol_2分配了同样的值,除了symbol_1为与段.output1相关的值而其他两个为绝对值:
SECTIONS { ... .output1 : { start_of_output_1 = ABSOLUTE(.); ... } .output : { symbol_1 = ADDR(.output1); symbol_2 = start_of_output_1; } ... }
LENGTH(memory)
返回名为memory的内存的长度。
MAX(exp1, exp2)
返回exp1和exp2最大的
MIN(exp1, exp2)
返回exp1和exp2最小的。
ORIGIN(memory)
返回名为memory的内存区域的起始地址。
SIZEOF(section)
返回名为section段的字节数。如果段还没被分配就是用函数求值,将会产生错误。