链接脚本linker script的妙用
- 1.概述
- 2.静态链接和动态链接
- 2.1 静态链接
- 2.2 动态链接
- 2.3 两种链接方式的对比
- 3.链接脚本
- 3.1 链接脚本实例分析
- 3.2 内存的分段链接
- 3.3 指定第一个文件的链接
- 3.4 自己定义代码段名字
- 4.总结
1.概述
编译器将编写的C程序代码进行翻译,变成机器可以执行的程序,这个大致上可以分为四个步骤:预编译、编译、汇编、链接。
其中编译和链接这两个过程比较重要。编译过程就是将源代码通过程序翻译后生成机器可以认识的机器语言。而链接就是将目标文件进行组合,最后生成在特定平台上可以正常运行的可执行程序。
本文主要描述链接这个过程。由于汇编器生成的目标代码(.o)文件不能被立即执行,因为里面一般都会包含其他的源文件中的符号、变量或者函数调用等等,要想处理好这些问题,就必须将程序进行链接。
2.静态链接和动态链接
根据开发人员指定库函数的链接方式,链接又分为动态链接和静态链接两种。
2.1 静态链接
我们在进行嵌入式开发过程中时,往往接触到比较多的就是静态链接。前面说过,编译器将源代码编译成一个一个的.o文件的目标文件,这些文件又会存在各种依赖关系,所以将各种.o文件汇集到一起。
这种方式编译出来的程序,可以直接运行,不依赖于外部库文件。
2.2 动态链接
当涉及到程序比较多的时候,如果每个程序都依赖于同样的一个库里面的函数,那么这个库就是共享的。
2.3 两种链接方式的对比
静态链接方式,适合单应用程序,比如嵌入式rtos等等。这种将所有的目标文件都链接到一个可执行的文件中,所以执行效率很高。但是文件内存占用大。动态链接时,如果app1运行将libc加载到内存中,下次app2直接可以从内存中使用。这种方式可以让每个程序的文件大小比较小,但是相对于的,执行效率相对比较低。
3.链接脚本
一般在进行gcc进行链接的时候,都会考虑到链接脚本(linker script),该文件一般以lds文件作为后缀名。该文件规定了将特定的section放到文件内,并且控制着输出文件的布局。一般来说,自己编写的链接脚本可以指定传递参数-T xxx.lds
,其中xxx.lds
则是自己编写的链接脚本。
xxx.lds基本格式如下:
SECTIONS
{
sections-command
sections-command
......
}
那么什么是sections?每个目标文件都有一些列的段,比如代码段、数据段、bss段等等。
3.1 链接脚本实例分析
如果没有实际的东西,那么说起理论来将索然无味。下面就具体来看下面的一个链接脚本的布局。
一个最简单的linker脚本文件如下:
SECTIONS
{
.=0x10000; /*(1)*/
.text:{*(.text)} /*(2)*/
.=0x800000; /*(3)*/
.data:{*(.data)} /*(4)*/
.bss:{*(.bss)} /*(5)*/
}
下面来解释一下上述的程序
(1).
的定义是location counter,也就是把当前的程序指向0x10000,如果没有这个地址,默认该符号的值为0。或者在gcc的链接选项中-Ttext 0x10000
也是一样的效果。
(2).text
指向代码段,其中*这个符号代表所有的输入文件的.text section合并成的一个
(3).=0x800000
将定位器的符号设置成0x800000
(4).data
指向所有输入文件的数据段,并且这个地址的起始为0x800000
(5).bss
表示所有输入文件的bss段
上述从一个最简单的链接脚本分析了链接脚本的语法格式。
3.2 内存的分段链接
如果一块内存在sram中,一块内存在sdram中,这两块地址并不连续,那么需求是将代码段(.text)段放在sram区,数据段(.data)与bss段放在ddr区,这时链接脚本该如何进行设计。
首先假设sram的空间地址为0x1000处开始的,可用空间为1M。ddr的地址空间为0x40000000,目前只用到2M。
首先可用在lds文件中做一个声明
MEMORY
{
ram : org = 0x00001000, len = 1M
ddr : org = 0x40000000, len = 2M
}
然后链接脚本可用以如下的方式进行编写
SECTIONS
{
. = 0x00001000;
. = ALIGN(4096);
.text:{*(.text)}>ram
.data:{*(.data)}>ddr
.bss:{*(.bss)}>ddr
}
只需要指定对应的链接段即可。
3.3 指定第一个文件的链接
有的时候,需要考虑到链接顺序的问题,比如在有些处理器中,系统从一个固定的地址启动,但这个地址一定最开始的时候会存放一个异常向量表。从异常向量表中跳转到实际的入口函数处去执行。那么这该如何进行设计?
一般来说我们链接代码段的时候,都是链接的.text section。但是,我们也可用指定该文件的代码段。比如可以在第一个需要编译的文件头部加上
.section ".text.entrypoint"
这样就会指定
SECTIONS
{
. = 0x00001000;
. = ALIGN(4096);
.text:
{
KEEP(*(.text.entrypoint))
*(.text)
}>ram
.data:{*(.data)}>ddr
.bss:{*(.bss)}>ddr
}
其中keep相当于告诉编译器,这部分不要被垃圾回收。
3.4 自己定义代码段名字
有些时候,需要将特定的符号指定到特定的地址,这样的好处就是可用通过地址访问对应的函数。这个应用在rt-thread rtos操作系统应用的比较经典。
在很多时候,需要指定初始化的执行顺序。比如驱动的初始化顺序等等。实现这种功能有很多种实现方式,上中下策都可以,下策就是直接通过函数调用关系进行调用。中策就是采用回调函数的方式进行设计。上策就是利用linker script进行函数扩展。
直接调用的方式实现起来比较简单,也比较好理解,直接调用对应的函数即可。
回调函数就是利用函数指针,当回调函数绑定了指针时,执行该回调函数检查该函数是否绑定,然后选择执行。这样可用降低耦合性。
采用linker script方式时,相当于把函数的指针集合到一个.text
的空间中。这样执行的时候,只需要找到linker中对应的地址,转换成函数即可,这种方式就很好扩展。
在rt-thread中,函数导出命令使用了这种技巧
/* board init routines will be called in board_init() function */
#define INIT_BOARD_EXPORT(fn) INIT_EXPORT(fn, "1")
/* pre/device/component/env/app init routines will be called in init_thread */
/* components pre-initialization (pure software initilization) */
#define INIT_PREV_EXPORT(fn) INIT_EXPORT(fn, "2")
/* device initialization */
#define INIT_DEVICE_EXPORT(fn) INIT_EXPORT(fn, "3")
/* components initialization (dfs, lwip, ...) */
#define INIT_COMPONENT_EXPORT(fn) INIT_EXPORT(fn, "4")
/* environment initialization (mount disk, ...) */
#define INIT_ENV_EXPORT(fn) INIT_EXPORT(fn, "5")
/* appliation initialization (rtgui application etc ...) */
#define INIT_APP_EXPORT(fn) INIT_EXPORT(fn, "6")
而INIT_EXPORT
的实现如下:
#define INIT_EXPORT(fn, level) \
RT_USED const init_fn_t __rt_init_##fn SECTION(".rti_fn." level) = fn
而在链接脚本中编写如下:
. = ALIGN(4);
__rt_init_start = .;
KEEP(*(SORT(.rti_fn*)))
__rt_init_end = .;
. = ALIGN(4);
最后可用查看map文件,查看地址
*(SORT(.rti_fn*))
.rti_fn.0 0xffffffff802bd418 0x8 build\kernel\src\components.o
0xffffffff802bd418 __rt_init_rti_start
.rti_fn.0.end 0xffffffff802bd420 0x8 build\kernel\src\components.o
0xffffffff802bd420 __rt_init_rti_board_start
.rti_fn.1 0xffffffff802bd428 0x8 build\drivers\drv_gpio.o
0xffffffff802bd428 __rt_init_loongson_pin_init
.rti_fn.1.end 0xffffffff802bd430 0x8 build\kernel\src\components.o
0xffffffff802bd430 __rt_init_rti_board_end
.rti_fn.2 0xffffffff802bd438 0x8 build\kernel\components\dfs\src\dfs.o
0xffffffff802bd438 __rt_init_dfs_init
.rti_fn.2 0xffffffff802bd440 0x8 build\kernel\components\net\lwip-2.0.2\src\arch\sys_arch.o
0xffffffff802bd440 __rt_init_lwip_system_init
.rti_fn.3 0xffffffff802bd448 0x8 build\drivers\drv_rtc.o
0xffffffff802bd448 __rt_init_rt_hw_rtc_init
.rti_fn.3 0xffffffff802bd450 0x8 build\kernel\components\drivers\src\workqueue.o
0xffffffff802bd450 __rt_init_rt_work_sys_workqueue_init
.rti_fn.4 0xffffffff802bd458 0x8 build\drivers\net\synopGMAC.o
0xffffffff802bd458 __rt_init_rt_hw_eth_init
.rti_fn.4 0xffffffff802bd460 0x8 build\kernel\components\dfs\filesystems\elmfat\dfs_elm.o
0xffffffff802bd460 __rt_init_elm_init
.rti_fn.4 0xffffffff802bd468 0x8 build\kernel\components\libc\compilers\newlib\libc.o
0xffffffff802bd468 __rt_init_libc_system_init
.rti_fn.4 0xffffffff802bd470 0x8 build\kernel\components\net\sal_socket\src\sal_socket.o
0xffffffff802bd470 __rt_init_sal_init
.rti_fn.6 0xffffffff802bd478 0x8 build\kernel\components\finsh\shell.o
0xffffffff802bd478 __rt_init_finsh_system_init
.rti_fn.6.end 0xffffffff802bd480 0x8 build\kernel\src\components.o
0xffffffff802bd480 __rt_init_rti_end
0xffffffff802bd488 __rt_init_end = .
0xffffffff802bd488 . = ALIGN (0x4)
0xffffffff802bd488 . = ALIGN (0x4)
实际上在执行的时候,实现如下
/**
* RT-Thread Components Initialization for board
*/
void rt_components_board_init(void)
{
#if RT_DEBUG_INIT
int result;
const struct rt_init_desc *desc;
for (desc = &__rt_init_desc_rti_board_start; desc < &__rt_init_desc_rti_board_end; desc ++)
{
rt_kprintf("initialize %s", desc->fn_name);
result = desc->fn();
rt_kprintf(":%d done\n", result);
}
#else
volatile const init_fn_t *fn_ptr;
for (fn_ptr = &__rt_init_rti_board_start; fn_ptr < &__rt_init_rti_board_end; fn_ptr++)
{
(*fn_ptr)();
}
#endif
}
并不是访问的具体的函数,而是从__rt_init_rti_board_start
指向的指针开始,不停向下执行,直到__rt_init_rti_board_end
结尾。这样就不依赖于具体的函数的实现了。所以函数的扩展性非常好。
4.总结
以上介绍了linker script的原理,以及在实际使用过程中的几个使用的技巧。这些都是在实际的项目中总结的来的,其实理解了linker script将可用完成很多有趣的使用技巧。只是平时我们并没有特别关注这个文件的使用,也并没有实际去编写一个linker script完成一个工程的构建。关于linker script的语法和使用,还有很多可以自由发挥的地方。