Skip to content

LV005-段与链接脚本简介

一、段的概念

1. 基本概念

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

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

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

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

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

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

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

2. 在代码中的体现

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

  • (1)在主函数 main.c 文件中创建不同属性的全局变量
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)创建链接脚本 imx6ull.lds
assembly
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 控制链接过程
makefile
# 使用链接脚本链接
$(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. 刚才的链接脚本?

c
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

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

shell
# 将所有程序的.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

c
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. 解析链接脚本

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

c
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 段。分体式链接脚本实例如下:

c
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

assembly
.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

c
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

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;
}

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

image-20240119005355872