LV04-06-重定位-01-段与链接脚本基础

本文主要是重定位——段与链接脚本相关笔记,若笔记中有错误或者不合适的地方,欢迎批评指正😃。

点击查看使用工具及版本
Windows版本 windows11
Ubuntu版本 Ubuntu16.04的64位版本
VMware® Workstation 16 Pro 16.2.3 build-19376536
终端软件 MobaXterm(Professional Edition v23.0 Build 5042 (license))
Linux开发板 正点原子 i.MX6ULL Linux 阿尔法开发板
uboot NXP官方提供的uboot,NXP提供的版本为uboot-imx-rel_imx_4.1.15_2.1.0_ga(使用的uboot版本为U-Boot 2016.03)
linux内核 linux-4.15(NXP官方提供)
Win32DiskImager Win32DiskImager v1.0
点击查看本文参考资料
分类 网址 说明
官方网站 https://www.arm.com/ ARM官方网站,在这里我们可以找到Cotex-Mx以及ARMVx的一些文档
https://www.nxp.com.cn/ NXP官方网站
https://www.nxpic.org.cn/NXP 官方社区
https://u-boot.readthedocs.io/en/latest/u-boot官网
https://www.kernel.org/linux内核官网
点击查看相关文件下载
分类 网址 说明
NXP https://github.com/nxp-imx NXP imx开发资源GitHub组织,里边会有u-boot和linux内核的仓库
https://elixir.bootlin.com/linux/latest/source 在线阅读linux kernel源码
nxp-imx/linux-imx/releases/tag/rel_imx_4.1.15_2.1.0_ga NXP linux内核仓库tags中的rel_imx_4.1.15_2.1.0_ga
nxp-imx/uboot-imx/releases/tag/rel_imx_4.1.15_2.1.0_ga NXP u-boot仓库tags中的rel_imx_4.1.15_2.1.0_ga
I.MX6ULL i.MX 6ULL Applications Processors for Industrial Products I.MX6ULL 芯片手册(datasheet,可以在线查看)
i.MX 6ULL Applications ProcessorReference Manual I.MX6ULL 参考手册(下载后才能查看,需要登录NXP官网)
ARM Cortex-A7 MPCore Technical Reference Manual Cortex-A7 MPCore技术参考手册
ARM Architecture Reference Manual ARMv7-A and ARMv7-R edition ARM架构参考手册ARMv7-A和ARMv7-R版
Arm Generic Interrupt Controller Architecture Specification- version 3 and version 4 Arm通用中断控制器架构规范-版本3和版本4
ARM Generic Interrupt Controller Architecture Specification - Version 2.0 Arm通用中断控制器架构规范-版本2.0
ARM Cortex-A Series Programmer's Guide for ARMv7-A Cortex-A系列ARMv7-A编程指南

一、段的概念

1. 基本概念

段是程序的组成元素。将整个程序分成一个一个段,并且给每个段起一个名字,然后在链接时就可以用这个名字来指示这些段,使得这些段排布在合适的位置。程序的段包括以下几部分:

(1)代码段(.text):存放代码指令

(2)只读数据段(.rodata):存放有初始值并且 const 修饰的全局类变量(全局变量或 static 修饰的局部变量)

(3)数据段(.data):存放有初始值的全局类变量

(4)零初始化段(.bss):存放没有初始值或初始值为 0 的全局类变量

(5)注释段(.comment):存放注释

需要注意的是:bss 段和注释段不保存在 bin或者elf 文件中。注释段里面的机器码是用来表示文字的。

2. 在代码中的体现

这里用到之前的工程代码,具体修改了哪些内容可以看这里:。

  • (1)在主函数 main.c 文件中创建不同属性的全局变量
1
2
3
4
5
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)创建链接脚本 imx6ull.lds
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
SECTIONS {
. = 0x80100000;

. = ALIGN(4);
.text :
{
*(.text)
}

. = ALIGN(4);
.rodata : { *(.rodata) }

. = ALIGN(4);
.data 0x80200000 : { *(.data) }

. = ALIGN(4);
__bss_start = .;
.bss : { *(.bss) *(.COMMON) }
__bss_end = .;
}
  • (3)在 Makefile 文件中指明使用链接脚本 imx6ull.lds 控制链接过程
1
2
# 使用链接脚本链接
$(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.4
  • (4)然后我们编译程序,会得到一个dis文件,我们打开这个文件会发现以下内容:

在反汇编文件中程序的地址从 0x80100000 开始

image-20240118225232225

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

image-20240118225506206

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

image-20240118225929924

二、链接脚本

链接脚本控制程序的链接过程,它规定如何把输入文件内的段放入输出文件, 并控制输出文件内的各部分在程序地址空间内的布局。

1. 刚才的链接脚本?

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
SECTIONS {
. = 0x80100000;
. = ALIGN(4);
.text :
{
*(.text)
}
. = ALIGN(4);
.rodata : { *(.rodata) }
. = ALIGN(4);
.data : { *(.data) }
. = ALIGN(4);
__bss_start = .;
.bss : { *(.bss) *(.COMMON) }
__bss_end = .;
}

这就是刚才的链接脚本,具体使用的时候我们需要再编译的过程中使用-T filename.lds 指定,否则在编译时将使用默认的链接脚本(默认的链接脚本无法进行一些段的复杂操作):

image-20240118230103293

需要注意,对于结构较为简单的程序,也可以使用默认的链接脚本,并手动指定不同段在输出文件中的位置,例如:

1
2
3
4
# 将所有程序的.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.4

2. 链接脚本语法

相关的语法,我们其实可以看GNU的官方文档:Using LD, the GNU linker

1
2
3
4
5
6
SECTIONS {
...
secname start BLOCK(align) (NOLOAD) : AT ( ldadr )
{ contents } >region :phdr =fill
...
}
  • secname:段的名称
  • start:段的运行地址( runtime addr),也称为重定位地址( relocation addr)
  • AT ( ldadr ): ldadr 是段的加载地址( load addr); AT 是链接脚本函数,用于将该段的加载地址设定为 ldadr;如果不添加这个选项,默认的加载地址等于运行地址。
  • 其他的链接脚本函数我们之后用到了再学习,想进一步了解可以参考上面的官方文档。
  • { contents }: { } 用来表示段的起始结束; content 为该段包含的内容,可以由用户自己指定。
  • BLOCK(align) (NOLOAD), >region :phdr =fill:很少用到,可以不做深入学习。

3. 解析链接脚本

我们来分析一下上边用测试的链接脚本:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
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
}

根据上述链接脚本的配置, .bin 文件中的数据结构如下图所示:

image-20230915222306966

上面我们写的链接脚本称为一体式链接脚本,与之相对的是分体式链接脚本,区别在于代码段(.text)和数据段(.data)的存放位置是否是分开的。例如现在的一体式链接脚本的代码段后面依次就是只读数据段、数据段、bss 段,都是连续在一起的。 分体式链接脚本则是代码段、只读数据段,中间间隔很远之后才是数据段、 bss 段。分体式链接脚本实例如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
SECTIONS {
. = 0x80100000; //设置链接地址为 0x80100000,这也是.text 段的起始地址
. = ALIGN(4);
.text :
{
*(.text)
}
. = ALIGN(4);
.rodata : { *(.rodata) } //假设 rodata 段的结束地址为 0x8010xxxx
. = ALIGN(4);
.data 0x80800000 : { *(.data) } //指定 data 段的起始地址为 0x80200000,和 rodata 段之间有较大间隔
……(省略)
}

之后的代码更多的采用一体式链接脚本,原因如下:

(1)分体式链接脚本适合单片机,因为单片机自带有 flash,不需要将代码复制到内存占用空间。而我们的嵌入式系统内存非常大,没必要节省这点空间,并且有些嵌入式系统没有可以直接运行代码的 Flash,就需要从存储设备如 Nand Flash 或者 SD 卡复制整个代码到内存;

(2)JTAG 等调试器一般只支持一体式链接脚本;

4. 清除bss段

之前提到过 bin 文件中并不会保存 bss 段的值,因为这些值都是 0,保存这些值没有意义并会使得 bin 文件臃肿。当程序运行涉及到 bss 段上的数据时, CPU 会从 bss 段对应的内存地址去读取对应的值,为了确保从这段内存地址上读取到的 bss 段数值为 0,在程序运行前需要将这一段内存地址上的数据清零,即清除 bss 段。

4.1 start.S

1
2
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
.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, lr

4.2 imx6ull.lds

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
SECTIONS {
. = 0x80100000;

. = ALIGN(4);
.text :
{
*(.text)
}

. = ALIGN(4);
.rodata : { *(.rodata) }

. = ALIGN(4);
.data : { *(.data) }

. = ALIGN(4);
__bss_start = .;
.bss : { *(.bss) *(.COMMON) }
__bss_end = .;
}

4.3 main.c

1
2
3
4
5
6
7
8
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;
}

编译过后,烧写到板子执行,保存在 bss 段中的变量 g_intA, g_intB 的值都为 0,表明清除 bss 段成功(但是吧,我没清的话)。

image-20240119005355872