LV005-段与链接脚本简介
一、段的概念
1. 基本概念
段是程序的组成元素。将整个程序分成一个一个段,并且给每个段起一个名字,然后在链接时就可以用这个名字来指示这些段,使得这些段排布在合适的位置。程序的段包括以下几部分:
(1)代码段(.text):存放代码指令
(2)只读数据段(.rodata):存放有初始值并且 const 修饰的全局类变量(全局变量或 static 修饰的局部变量)
(3)数据段(.data):存放有初始值的全局类变量
(4)零初始化段(.bss):存放没有初始值或初始值为 0 的全局类变量
(5)注释段(.comment):存放注释
需要注意的是:bss 段和注释段不保存在 bin 或者 elf 文件中。注释段里面的机器码是用来表示文字的。
2. 在代码中的体现
这里用到之前的工程代码,具体修改了哪些内容可以看这里:。
- (1)在主函数 main.c 文件中创建不同属性的全局变量
char g_charA = 'A'; // 存储在 .data 段
const char g_charB = 'B'; // 存储在 .rodata 段
const char g_charC; // 存储在 .bss 段
int g_intA = 0; // 存储在 .bss 段
int g_intB; // 存储在 .bss 段2
3
4
5
- (2)创建链接脚本 imx6ull.lds
SECTIONS {
. = 0x80100000;
. = ALIGN(4);
.text :
{
*(.text)
}
. = ALIGN(4);
.rodata : { *(.rodata) }
. = ALIGN(4);
.data 0x80200000 : { *(.data) }
. = ALIGN(4);
__bss_start = .;
.bss : { *(.bss) *(.COMMON) }
__bss_end = .;
}2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
- (3)在 Makefile 文件中指明使用链接脚本 imx6ull.lds 控制链接过程
# 使用链接脚本链接
$(LD) -T imx6ull.lds -g start.o uart.o main.o my_printf.o -o relocate.elf -lgcc -L/home/hk/2software/gcc-linaro-4.9.4/lib/gcc/arm-linux-gnueabihf/4.9.42
- (4)然后我们编译程序,会得到一个 dis 文件,我们打开这个文件会发现以下内容:
在反汇编文件中程序的地址从 0x80100000 开始

整个程序被分为不同的段,每个段以 Disassembly of section 作为开始

段落之间的地址是连续的,并且从低地址到高地址,段依次为:代码段、只读数据段、数据段、 bss 段、注释段(注意 bss 段和注释段不包含在 elf/bin 文件中。那我们定义的几个变量都在哪?这个就进去搜一搜吧。

二、链接脚本
链接脚本控制程序的链接过程,它规定如何把输入文件内的段放入输出文件, 并控制输出文件内的各部分在程序地址空间内的布局。
1. 刚才的链接脚本?
SECTIONS {
. = 0x80100000;
. = ALIGN(4);
.text :
{
*(.text)
}
. = ALIGN(4);
.rodata : { *(.rodata) }
. = ALIGN(4);
.data : { *(.data) }
. = ALIGN(4);
__bss_start = .;
.bss : { *(.bss) *(.COMMON) }
__bss_end = .;
}2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
这就是刚才的链接脚本,具体使用的时候我们需要再编译的过程中使用 -T filename.lds 指定,否则在编译时将使用默认的链接脚本(默认的链接脚本无法进行一些段的复杂操作):

需要注意,对于结构较为简单的程序,也可以使用默认的链接脚本,并手动指定不同段在输出文件中的位置,例如:
# 将所有程序的.text 段放在一起, 起始地址设置为 0x80100000
# 将所有程序的.data 段放在一起, 起始地址设置为 0x80102000
#$(LD) -Ttext 0x80100000 -Tdata 0x80102000 -g start.o uart.o main.o my_printf.o -o relocate.elf -lgcc -L/home/hk/2software/gcc-linaro-4.9.4/lib/gcc/arm-linux-gnueabihf/4.9.42
3
2. 链接脚本语法
相关的语法,我们其实可以看 GNU 的官方文档:Using LD, the GNU linker
SECTIONS {
...
secname start BLOCK(align) (NOLOAD) : AT ( ldadr )
{ contents } >region :phdr =fill
...
}2
3
4
5
6
- secname:段的名称
- start:段的运行地址( runtime addr),也称为重定位地址( relocation addr)
- AT ( ldadr ): ldadr 是段的加载地址( load addr); AT 是链接脚本函数,用于将该段的加载地址设定为 ldadr;如果不添加这个选项,默认的加载地址等于运行地址。
- 其他的链接脚本函数我们之后用到了再学习,想进一步了解可以参考上面的官方文档。
- { contents }: { } 用来表示段的起始结束; content 为该段包含的内容,可以由用户自己指定。
- BLOCK(align) (NOLOAD), > region : phdr = fill:很少用到,可以不做深入学习。
3. 解析链接脚本
我们来分析一下上边用测试的链接脚本:
SECTIONS {
. = 0x80100000; //设定链接地址为 0x80100000
. = ALIGN(4); //将当前地址以 4 字节为标准对齐
.text : //创建段,其名称为 .text
{ //.text 包含的内容为所有链接文件的数据段
*(.text) // *:表示所有文件
}
. = ALIGN(4); //将当前地址以 4 字节为标准对齐
.rodata : { *(.rodata) } //.rodata 存放在.text 之后, 包含所有链接文件的只读数据段
. = ALIGN(4);
.data : { *(.data) } //.data 存放在.rodata 之后,包含所有链接文件的只读数据段
. = ALIGN(4);
__bss_start = .; //将当前地址的值存储为变量__bss_start
.bss : { *(.bss) *(.COMMON) } //.bss 存放在.data 段之后, 包含所有文件的 bss 段和注释段
__bss_end = .; //将当前地址的值存储为变量__bss_end
}2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
根据上述链接脚本的配置, .bin 文件中的数据结构如下图所示:
上面我们写的链接脚本称为 一体式链接脚本,与之相对的是分体式链接脚本,区别在于代码段(.text)和数据段(.data)的存放位置是否是分开的。例如现在的一体式链接脚本的代码段后面依次就是只读数据段、数据段、bss 段,都是连续在一起的。 分体式链接脚本则是代码段、只读数据段,中间间隔很远之后才是数据段、 bss 段。分体式链接脚本实例如下:
SECTIONS {
. = 0x80100000; //设置链接地址为 0x80100000,这也是.text 段的起始地址
. = ALIGN(4);
.text :
{
*(.text)
}
. = ALIGN(4);
.rodata : { *(.rodata) } //假设 rodata 段的结束地址为 0x8010xxxx
. = ALIGN(4);
.data 0x80800000 : { *(.data) } //指定 data 段的起始地址为 0x80200000, 和 rodata 段之间有较大间隔
……(省略)
}2
3
4
5
6
7
8
9
10
11
12
13
之后的代码更多的采用一体式链接脚本,原因如下:
(1)分体式链接脚本适合单片机,因为单片机自带有 flash,不需要将代码复制到内存占用空间。而我们的嵌入式系统内存非常大,没必要节省这点空间,并且有些嵌入式系统没有可以直接运行代码的 Flash,就需要从存储设备如 Nand Flash 或者 SD 卡复制整个代码到内存;
(2)JTAG 等调试器一般只支持一体式链接脚本;
4. 清除 bss 段
之前提到过 bin 文件中并不会保存 bss 段的值,因为这些值都是 0,保存这些值没有意义并会使得 bin 文件臃肿。当程序运行涉及到 bss 段上的数据时, CPU 会从 bss 段对应的内存地址去读取对应的值,为了确保从这段内存地址上读取到的 bss 段数值为 0,在程序运行前需要将这一段内存地址上的数据清零,即清除 bss 段。
4.1 start.S
.text
.global _start
_start:
/* 设置栈 */
ldr sp,=0x80200000
/* 清除bss段 */
bl clean_bss
/* 跳转到主函数 */
bl main
halt:
b halt
clean_bss:
ldr r1, =__bss_start //将链接脚本变量__bss_start变量保存于r1
ldr r2, =__bss_end //将链接脚本变量__bss_end变量保存于r2
mov r3, #0
clean:
strb r3, [r1] //将当前地址下的数据清零
add r1, r1, #1 //将r1内存储的地址+1
cmp r1, r2 //相等:清零操作结束;否则继续执行clean函数清零bss段
bne clean
mov pc, lr2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
4.2 imx6ull.lds
SECTIONS {
. = 0x80100000;
. = ALIGN(4);
.text :
{
*(.text)
}
. = ALIGN(4);
.rodata : { *(.rodata) }
. = ALIGN(4);
.data : { *(.data) }
. = ALIGN(4);
__bss_start = .;
.bss : { *(.bss) *(.COMMON) }
__bss_end = .;
}2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
4.3 main.c
int main(int argc, const char * argv[])
{
Uart_Init(); //初始化 uart 串口
printf("g_intA = 0x%08x &g_intA = 0x%x\n\r", g_intA, &g_intA); //打印 g_intA 的值
printf("g_intB = 0x%08x &g_intB = 0x%x\n\r", g_intB, &g_intB); //打印 g_intB 的值
return 0;
}2
3
4
5
6
7
8
编译过后,烧写到板子执行,保存在 bss 段中的变量 g_intA, g_intB 的值都为 0,表明清除 bss 段成功(但是吧,我没清的话)。