一、段的概念
段是程序的组成元素。将整个程序分成一个一个段,并且给每个段起一个名字,然后在链接时就可以用这个名字来指示这些段,使得这些段排布在合适的位置。
一个程序通常包含以下五个段:
代码段(.text) | 存放代码指令 |
只读数据段(.rodata) | 存放有初始值并且const修饰的全局类变量(全局变量或static修饰的局部变量) |
数据段(.data) | 存放有初始值的全局类变量 |
零初始化段(.bss) | 存放没有初始值或初始值为0的全局类变量 |
注释段(.comment) | 存放注释 |
bss段和注释段不保存在bin/elf文件中
注释段里面的机器码是用来表示文字的
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段
二、链接脚本
顾名思义,链接脚本控制程序的链接过程,它规定如何把输入文件内的段放入输出文件, 并控制输出文件内的各部分在程序地址空间内的布局。
为了在链接时使用链接脚本,需要在 Makefile 用-T filename.lds 指定。否则在编译时将使用默认的链接脚本。
需要注意,对于结构较为简单的程序,也可以使用默认的链接脚本,并手动指定不同段在输出文件中的位置。
#将所有程序的.text段放在一起,起始地址设置为0x80100000#将所有程序的.data段放在一起,起始地址设置为0x80102000$ (LD) -Ttext 0x80100000 -Tdata 0x80102000 ...
默认的链接脚本无法进行一些段的复杂操作,这时候就需要用到链接脚本。
链接脚本的语法:
SECTIONS { ... secname start BLOCK(align) (NOLOAD) : AT ( ldadr ) { contents } >region :phdr =fill ...}
secname: 段的名称
start: 段的运行地址(runtime addr),也称为重定位地址(relocation addr)
AT(ldadr): ldadr 是段的加载地址(loadaddr);AT是链接脚本函数,用于将该段的加载地址
设定为 ldadr;如果不添加这个选项,默认的加载地址等于运行地址。
{ contents }: { }用来表示段的起始结束;content 为该段包含的内容,可以由用户自己指定。
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}
注意:我们需要依次排列代码段、只读数据段、数据段、.bss段、.common。
上面写的链接脚本称为一体式链接脚本,与之相对的是分体式链接脚本,区别在于代码段(.text) 和数据段(.data)的存放位置是否是分开的。
一体式链接脚本的代码段后面依次是只读数据段、数据段、bss 段,都是连续在一起的;
分体式链接脚本则是代码段、只读数据段,中间间隔很远之后才是数据段、bss 段;
分体式链接脚本实例:
SECTIONS { . = 0x80100000; . = ALIGN(4); .text : { *(.text) } . = ALIGN(4); .rodata : { *(.rodata) } . = ALIGN(4); .data 0x80800000 : { *(.data) } ......(省略)
很多代码采用了一体式链接脚本,原因如下:
分体式链接脚本适合单片机,因为单片机自带有flash,不需要将代码复制到内存占用空间。而嵌入式系统内存非常大,没必要节省这点空间,并且有些嵌入式系统没有可以直接运行代码的Flash,就需要从存储设备如 Nand Flash 或者 SD 卡复制整个代码到内存;
JTAG等调试器一般只支持一体式链接脚本;
三、清除bss段
最开始就说过了,bin 文件中并不会保存 bss 段的值,因为这些值都是 0,保存这些值没有意义并会使得 bin 文件臃肿。当程序运行涉及到 bss 段上的数据时,CPU 会从 bss 段对应的内存地址去读取对应的值,为了确保从这段内存地址上读取到的 bss 段数值为 0,在程序运行前需要将这一段内存地址上的数据清零,即清除 bss 段。
具体思路就是将 bss 段对应的地址读取,并将地址上的数据依次清零。
如下为在汇编文件中实现清除bss段功能:
.text //.text表示代码段,汇编系统预定义段名,说明下面的汇编是代码段.global _start //.global表示_start是一个全局符号/* 标签_start,说明汇编程序的默认入口是_start,也可以在链接脚本中使用ENTRY来指明其它的入口点,类似 C 语言 main()函数,_start 是整个程序的入口,即程序执行的第一条指令*/_start: /* 设置栈 */ ldr sp,=0x80200000 //将0x80200000赋值给寄存器sp,即设置栈地址,因为C语言函数调用时,保存现场/上下文和传递参数需要用到栈 bl clean_bss //跳转到标签clean_bss,相当于调用clean_bss函数,并将bl main指令地址存储到寄存器lr中 bl main //进入C语言的main()函数,并将b halt指令地址存储到寄存器lr中halt: //标签halt b halt //跳转到标签halt,循环执行b halt指令执行,这就是一个死循环。如果main函数返回,就在这里死循环。clean_bss: //相当于一个函数,clean_bss 是函数名,下面汇编指令是函数内容 /* 清除bss段 */ ldr r1, =__bss_start //将链接脚本定义的bss起始地址赋值给寄存器r1 ldr r2, =__bss_end //将链接脚本定义的bss结束地址赋值给寄存器r2 mov r3, #0 //将0赋值给寄存器r3,即r3=0clean: //下面汇编指令相当于循环体,直到 R1 与 R2 相等 str r3, [r1] //将寄存器r3的值存储到寄存器r1的值对应地址中 add r1, r1, #4 //将寄存器 r1 的值加上 4,赋值给寄存器 r1,即 r1 = r1+4 cmp r1, r2 //比较寄存器r1的值与寄存器r2的值 bne clean //如果寄存器r1的值与寄存器r2的值不相等,跳转到标签clean mov pc, lr //如果寄存器r1的值与寄存器r2的值相等,就执行此行,返回到 bl main 处,继续执行
四、重定位
程序是按顺序,从起始地址依次往下运行的;当需要破坏这种顺序执行跳转到某一具体目标地址执行时,需要告知CPU下一条要执行的指令位于这一目标地址处,而这一目标地址需要通过计算目标位于程序中的位置来获得;计算目标地址的过程称为重定位。简单的理解重定位,就是当a.c中调用b.c中的函数relfun()时,需要通过计算获得relfun位于可执行程序中的具体位置,这里计算得到relfun地址的过程即可理解为重定位。重定位所做的事就是使每个引用 (目标)的位置,都能找到对应的目标:
/* a.c 文件 */int main(void){ relfun() /* 该函数位于b.c文件中 */ return 0;}/* b.c 文件 */void relfun(void){ printf(“this relfun\n”);}/* a.c和b.c编译后生成可执行文件ab.exe */地址 | 伪代码0:main1: call relfun….200: relcun /* 程序链接时计算得到的地址 */
重定位实际就是在运行地址处执行一段位置无关码PIC(position-independent code),让这段PIC(也就是重定位代码)从运行地址处把整个程序镜像拷贝一份到链接地址处,完了之后使用一句长跳转指令从运行地址处直接跳转到链接地址处去执行同一个函数,这样就实现了重定位之后的无缝连接。
这里要注意,位置无关码需要使用相对跳转命令 b 或 bl。
重定位之前,不可使用绝对地址:不可访问全局类变量(全局变量或static修饰的局部变量);不可访问有初始值的数组(初始值放在rodata里,需要绝对地址来访问)。
重定位之后,使用 ldr pc = xxx,跳转到绝对地址(runtime address)