作为嵌入式软件工程师,应该要清楚程序的每一条指令在哪里,什么时候会被加载到内存,什么时候会被执行。链接脚本会明确告诉你程序的代码和数据在内存中的分布。精确控制代码和数据在内存中的分布是高效利用内存资源的前提。自定义链接脚本是资深嵌入式软件工程师的必备技能,更是嵌入式架构师的最基本要求。此外,灵活定制链接脚本在编程方面有更高级的应用。
一、编译链接原理
简单讲述编译链接的基本原理有助于后面内容的理解。
a. 简单点说,一个可执行程序包括文件头、代码段(.text)、数据段(.bss)、符号段等信息,在Linux GCC工具链中,该执行程序文件的格式是elf,而在elf格式中,段称为section。现加上某个程序有file1.c和file2.c两个文件。
b. 一个file1.c文件中有代码(函数),也有数据(全局变量,假设都没有初始化的),因此在编译(arm-linux-gcc)之后就会产生一个对应的file1.o,在该文件中会有产生.text section,其会将file.c中所有的函数代码编译后的指令放到该区域,假设长度是0x100字节;同样会产生.bss section,会将所有的全局变量定义到该区域,假设长度是0x20字节。这时,file1.o是可重定位的文件,即.text段是从地址0开始的,其真正的虚拟运行地址还需要在链接阶段重定位完成。
c. 对于另一个file2.c文件,同样会产生对应的file2.o,而该文件中会有对应的.text section(假设长度是0x200字节)和.bss section(假设长度是0x40字节)。其.text段同样是从地址0开始。
d. 链接的最终结果就是产生唯一的一个可执行文件result,而该文件只有一个.text段和一个.bss段,而且.text段会按照指定的链接地址重定位好,即.text段不再是从0开始。那么从这个结果来看,链接的过程应该包括这两个步骤:
d1. 合并file1.o的.text和file2.o的.text到result的.text,其长度为0x100+0x200=0x300字节;合并file1.o的.bss和file2.o的.bss到result的.bss,长度是0x20+0x40=0x60字节
d2. 根据指定的链接地址重定位result的.text段和.bss段,即.text段从指定的地址开始,各个函数的起始地址也会根据该地址进行重新定位。
二、链接脚本基本语法
这里以最基础的链接脚本语法作为示例,GCC工具链下的链接脚本后缀一般是.lds。
ENTRY(_start)//指定_start为程序的第一条指令
SECTIONS//指定内存分布
{
//顿号是地址标识符,这里指定为0x40000000是虚拟运行地址
. = 0x40000000;
//即.text的地址是从0x40000000开始,.text的内容包括file1.o和file2.o的.text
.text :
{
file1.o (.text)//先合并file1.o的.text
file2.o (.text) //再合并file2.o的.text
}
//此时地址是:0x40000000+0x300=0x40000300
//地址标识符可以进行表达式计算,
//则.bss的起始地址是0x40000300+0x400=0x40000700
. = . + 0x400;
.bss:
{
file1.o(.bss)
file2.o(.bss)
}
}
三、关键的链接命令参数
链接命令程序是:arm-linux-ld,通过在命令行传入-T参数来指定自定义的链接脚本。如–Tfile.lds 代表链接时使用自定义的file.lds脚本,而不是系统默认的链接脚本。
四、可变长数组的实现
介绍完编译链接原理,我们开始讲述链接脚本在编程中的第一个高级运用--可变长数组。可变数组我们一般见于C++或者JAVA等高级语言,其是指在运行过程中动态地改变数组的大小,往往是通过链表队列的方式来实现变长需求。我们这里所阐述的可变长数组严格意义上是指静态可变长数组,或者更严格一点讲,在静态链接时数组大小也已经固定,只是在编程形式上看起来是可变长的。
我们在下面将要介绍的uboot启动引导模块中有一个命令模式,即uboot会响应用户输入的命令以达到修改系统参数、显示板级系统信息、加载指定操作系统等目的。Uboot原则上可以支持任意多的命令,只要编程人员愿意添加。我们很容易想到,uboot是根据用户在命令行中输入的命令字符串在事先设置好的命令字符串数组中匹配目标命令的,也很容易想到这个匹配的过程就是for循环中strcmp的过程。既然是for循环,那肯定有一个大小的问题,就是这个数组的大小到底是多少。我们很有可能这样实现:
Struct cmd_type cmd[]={cmd1,cmd2,cmd3,…};
命令个数=sizeof(cmd)/sizeof(cmd_type)
这样的做法是可行的,但是它不够灵活,至少每次添加一条命令都要到该数组中去签到,如果真支持100个命令,那估计得用一个专门的文件来定义这些命令的初始化了。有没有一种比较优雅的方法去灵活支持命令的扩展呢?软件其实也是一种艺术,我们来看看uboot是怎么做的!
1. uboot命令的格式
-include/command.h
struct cmd_tbl_s {
char *name; //命令名称
int maxargs; //最多有几个参数
int repeatable;//是否支持回车即重复命令
int (*cmd)(struct cmd_tbl_s *, int, int, char *[]);//执行命令
char *usage;//命令使用示例
char *help; //命令使用帮助信息
};
typedef struct cmd_tbl_s cmd_tbl_t;
2. 非常重要的宏定义
-include/command.h
#define Struct_Section __attribute__ ((section (".u_boot_cmd")))
#define U_BOOT_CMD(name,maxargs,rep,cmd,usage,help) \
cmd_tbl_t __u_boot_cmd_##name Struct_Section = {#name, maxargs, rep, cmd, usage, help}
3. 如何定义一条命令
-Common/command.c
U_BOOT_CMD(
version, 1, 1, do_version,
"version - print monitor version\n",
NULL
);
用户在命令行输入version,那uboot就会调用do_version这个函数来打印uboot的版本。看看它宏展开之后是啥样的:
struct cmd_tbl_s __u_boot_cmd_version __attribute__ ((section(".u_boot_cmd"))) = \
{ version, 1, 1, do_version, "version - print monitor version\n", NULL};
这里的__attribute__ ((section(".u_boot_cmd")))是啥来的,嗯嗯,说了这么多,终于跟链接脚本扯上关系了。__attribute__属性是GCC工具链支持的特性语法,其表示该结构变量在编译后会存放到.u_boot_cmd这个section,这个section是自定义的,而像.text存放代码,.bss存在数据是默认产生的section.
好,咱们的链接脚本该出场了吧。
4. uboot链接脚本
SECTIONS
{…
__u_boot_cmd_start = .;
.u_boot_cmd : { *(.u_boot_cmd) }
__u_boot_cmd_end = .;
…}
-include/command.h中有以下定义:
extern cmd_tbl_t __u_boot_cmd_start;
extern cmd_tbl_t __u_boot_cmd_end;
即__u_boot_cmd_start代表命令数组的开始,而__u_boot_cmd_end代表命令数组的结束。.u_boot_cmd : { *(.u_boot_cmd) }中*代表所有.o文件的.u_boot_cmd内容都合并到可执行文件的.u_boot_cmd中,即所有文件中通过U_BOOT_CMD宏定义的命令都会被合并到该section.
5. 重新看看for循环怎么找到命令的
-common/command.c
就不解释了吧。只说一点,cmdtp是cmd_tbl_t型指针,cmdtp++代表指向下一个cmd_tbl_t结构体,即下一条uboot命令。
六、Linux呢?
有人会问,Linux是否也应用了该特性呢?废话,Linux应该是将C语言的编程技巧运用到登峰造极的地步了,怎么会放过这个武器呢。自己学着总结一下。
还是随便点一下,每个Linux驱动模块的入口是什么啊,记起来不?是
module_init(XXX_init)
看看人家的宏展开,没晕吧~~
既然有链接脚本在编程中的高级运用之一,就会有之二、之三。我们接下来会讲述怎么利用链接脚本来做内存的分时复用的,还会讲讲C++编译器怎么支持类对象的构造和析构函数的。期待吧~~
嵌入式交流群:149294942(QQ)
blog: http://blog.csdn.net/yueqian_scut
我们追求:
1.忠于Linux源码,百分百原创。
2.从上电第一行代码、系统第一行代码、模块第一行代码、应用第一行代码,深入讲解嵌入式软件生命周期。
3 深刻理解硬件体系, 聚焦软件层次设计、模块设计和框架设计。
请关注我们,谢谢!