在看u-boot
源码中,看到了arch/sandbox/cpu/u-boot-spl.lds
文件,其中最后一行写着INSERT BEFORE .data;
,这事很有意思,研究一下。
1. 默认的链接脚本
使用Linux gcc编译主机程序时我们是不需要指定链接脚本的,比如下面:
main.c
#include <stdio.h> int main(void) { printf("hello world!!!\n"); return 0; }
编译执行。
$ gcc main.c $ ./a.out hello world!!! $ ld --verbose ...
但这里其实是有链接脚本的,也即ld
工具有一套自己的链接脚本,可以使用ld --verbose
查看。
2. 自定义段
有时候软件设计时我们需要标记一些特殊的段用于特殊用途,比如下面:
#include <stdio.h>
// static --- 定义局部符号,仅当前文件可见,避免污染全局符号表
// attribute --- 修改属性,指导编译器的行为等
// used --- 由于是局部的符号,且暂无人使用,所以编译器在开优化的情况下可能会丢弃该符号,使用`used`告诉编译器这个符号有人用,不要丢
// section --- 修改段名
// #level --- 将参数转换为字符串
// const --- 定义一个常量符号
// ##func --- 符号名称拼接
// _user##func --- 拼接后的符号名即为最终定义的符号
// func --- 右值,函数地址
#define FUNC_USER(func, level) static \
__attribute__((used, section(".func_user." #level))) \
void (* const _user_##func)(void) = func;
static void aa(void) { printf("%s\n", __func__); } // 定义函数
FUNC_USER(aa, 5); // 定义了一个常量函数指针`_user_aa`,它的值是函数aa()的地址,它的段名为`.func_user.5`
static void bb(void) { printf("%s\n", __func__); }
FUNC_USER(bb, 4);
static void cc(void) { printf("%s\n", __func__); }
FUNC_USER(cc, 6);
int main(void)
{
// C中引用链接脚本中的符号,给这两个符号赋予常量函数指针的类型,因为它标记的位置存放的是函数的地址
extern void (* const _func_user_start)(void);
extern void (* const _func_user_end)(void);
void (* const *p)(void); // 定义一个指向常量函数指针的指针,即二级指针
for (p = &_func_user_start; p!= &_func_user_end; p++) { // 循环遍历这个函数地址表
(*p)(); // 解引用二级指针,执行对应的函数
}
return 0;
}
这份代码实现的目的是通过宏FUNC_USER()
将某一类数据自动构建成一个数组,而且这类数据和数组是解耦的,然后就可以使用for
循环遍历了。这个梦想很好,但是默认的链接脚本不支持我们这么干啊。
3. 给默认的链接脚本打补丁
- 使用
ld --verbose
将默认的链接脚本输出到一个文件,然后修改这个链接脚本,再使用-T
指定修改后的链接脚本,这样做ld链接时将不再使用默认的链接脚本。
$ ld --verbose > 0.lds
$ $ vim 0.lds # 删除文件中多余的行,即文件头部`====`行之前的内容,以及文件尾部`====`行之后的内容
# 找个合适的位置添加以下内容
.user ALIGN(8) : {
PROVIDE(_func_user_start = .); /* PROVID()E表示这个符号会被外部使用,也可以不加 */
KEEP(*(SORT(.func_user.*))); /* KEEP()表示这里的匹配项全部无条件保留,避免被垃圾回收 */
/* SORT()表示对匹配的段名进行排序,按字母顺序从低到高,不使用SORT()的话按照链接器发现段的先后顺序排序 */
PROVIDE(_func_user_end = .);
}
$ gcc main.c -T0.lds -Wl,Map=main.map # `-Wl,Map=main.map`告诉gcc将`-Map=main.map`参数传递给ld,这个参数用于生成链接过程文件,以方便我们查看,也可以不加
$ ./a.out
bb
aa
cc
$ vi main.map # 查看map文件,可以搜索`user`等关键字看链接的过程
...
- 编写个新的链接脚本,使用
-T
追加到默认链接脚本中。
$ vi 1.lds # 新建链接脚本填入一下内容
SECTIONS {
.user ALIGN(8) :{
PROVIDE(_func_user_start = .);
KEEP(*(SORT(.func_user.*)));
PROVIDE(_func_user_end = .);
}
}
INSERT BEFORE .rodata; /* 表示该链接脚本适用于补充到默认链接脚本中的,也可以用`AFTER` */
$ gcc main.c -T1.lds -Wl,Map=main.map
# 若此时还有个2.lds,也是可以使用`-T1.lds -T2.lds`编译的
- 需注意执行
gcc main.c -T0.lds -T1.lds
会报错..ld: .rodata not found for insert
,也即只有使用默认的链接脚本时INSERT
有效。
4. 关于标签
以上提供的链接脚本中是在链接脚本里定义了上下边界的标签,当然在借助SORT()
的情况下也有其他方法,比如内联汇编或C。
// 内嵌汇编
asm(
".global _func_user_start_asm\n" /* 将符号导出到全局 */
".section .func_user.0\n" /* 修改段名,这个名字将被排序在前面,发现`.func_user.`这个段名编译时汇编报错,使用attribute时可以用 */
"_func_user_start_asm:\n" /* 定义符号 */
);
// C
void (* __attribute__((section(".func_user.0"))) _func_user_start_c[0])(void); // 长度为0的数组不占空间