最近在学习《MIT6.828》,因此顺便简单学习一下ld脚本。主要参考gnu的官方文档,对其用法做一个简单的概括。LD,同时参考了一些网上的资料, ld - 链接脚本学习笔记与实践过程。
本文的布局以及内容基本按照LD进行描述,强烈建议读者自行阅读官方文档LD。
链接脚本概述
当链接器将可重定位文件通过重定位等操作生成可执行程序的时候,链接脚本可以控制输入文件中的各个段在生成文件中的位置,并且可以规定生成的可执行文件加载入内存之后的空间布局。
链接器ld
默认使用其内部的连接脚本,因此在一般情况下,我们都不需要对其进行修改。在使用ld
的时候,通过-T
选项,可以使用自己写的链接脚本完成链接过程,否则会使用默认的链接脚本。
一些基本概念
ELF
文件是由一些section
组成的,最重要的是3个:.text
段,即代码段,保存着运行时的指令的二进制代码;.data
段,即数据段,保存着已经初始化的全局变量以及局部静态变量;.bss
段,即未初始化的数据段,保存着未指定初始值或初始值为0的全局变量以及局部静态变量,将.bss
和.data
区段分开的原因:.bss
在磁盘上可以只保留标记,而不必真正的分配那么大的磁盘空间给这部分区域,只需在加载入内存之后,分配这些区域,这样可以有效地节省磁盘空间。
section
可以被标记为loadable
,意味着当输出文件运行起来的时候,这个section
的content
信息应当被加载到memory
中。
section
没有content
信息,并且被标记为allocatable
,意味着会有一块内存会被创建,但是却没有任何内容放在上面,程序运行起来的时候无需要加载任何信息到memory
中。
如果section
既不是loadable
也不是allocatable
,那么意味着它包含了一些debug
的信息。
链接脚本的输出文件是一个elf
文件,里面包含了多个section
,包括loadable
的section,也包括allocatable section
。每一个section
包含两个地址,分别是VMA
以及LMA
。
VMA
是virtual memory address
,LMA是load memory address
。大多数两类地址是相同的,但是在嵌入式开发中不大相同,LMA
是flash
地址,而VMA
是将flash
加载到ram
里面运行的ram
地址。
一个简单例子
##
SECTIONS
{
. = 0x10000;
.text : { *(.text) }
. = 0x8000000;
.data : { *(.data) }
.bss : { *(.bss) }
}
-
SECTIONS
字段描述了输出的ELF
文件的内存布局。 .
符号是一个定位符,描述了当前位置的地址。如果不使用"."
来指定开始地址,那么将会从0开始分配地址。.text
表示输出文件中将会生成一个.text
段。*
号是一个通配符,匹配所有的输入elf
文件,*(.text)
表示所有的输入elf
文件中的.text
段。由于定位符.
被设置为0x10000
,因此在输出的文件中,.text
段的起始地址将被设置为0x10000
,(每个sectiond都有一个Addr字段,用来表明在加载到内存中的地址)可以通过readelf
命令查看。- 同样的,
.data
段的地址被设置为0x8000000
,.bss
段的地址被设置为0x8000000 + sizoef(.data)
,即直接被放置到了.data
的后面。
代码举例
- 我们定义两个文件,main.c和add.c,其中main.c调用了add.c中的代码,我们将分别查看使用了上诉链接脚本和默认链接脚本时,其地址的变化。
//main.c int initial_data = 100; extern int uinitial_data; int add(int lhs, int rhs);; void main() { uinitial_data = 20; int c = add(initial_data, uinitial_data); }
//add.c int uinitial_data; int add(int lhs, int rhs) { return lhs + rhs; }
- 先编译出可重定位文件
main.o
和add.o
,并分别查看其segment
:gcc -c main.c
和gcc -c add.c
。
从节头中可以看出,由于未进行链接,所有的Address全为0。
- 使用默认链接脚本进行链接,
ld -o main main.o add.o
,并显示其节头。(这个并不是gcc中使用的用法,此处只是进行举例)
发现其.text
地址Address为0x4000e8。
- 使用自定义的连接脚本,
ld -T my.ld -o main main.o add.o
,并显示其节头
从图中可以看出.text
和.data
,.bss
的节的地址和链接脚本中的配置是一致的。
我们还可以改变.bss
和.data
的相对地址,并将.data
改名为.dataTest
。
连接脚本的一些基本语法
设置程序执行入口
- 在程序中执行的第一条指令称为程序入口。可以用下述命令指定程序的入口。
ENTRY(symbol)
- 程序入口的设置规则,优先级由高到低
ld -e
选项指定ENTRY(symbol)
设置- 特定的符号的值。例如很多程序使用符号
start
的值作为入口地址。 - 代码段的第一个地址
- 地址0
设置变量
PROVIDE
provide
在脚本中定义了一个变量,之后,C代码中可以通过extern
来使用这个变量。
SECTIONS
{
.text :
{
*(.text)
_etext = .;
PROVIDE(etext = .);
}
}
- 如果程序中定义了变量
_etext
,那么链接的时候会报重复定义的错误。如果程序中定义了etext
,程序不会报错,而是使用程序自己定义的那个变量。如果程序未定义etext
而使用了它,那么程序就会使用链接脚本中定义的变量。