链接脚本(1) --- 在默认的链接脚本中插入段

在看u-boot源码中,看到了arch/sandbox/cpu/u-boot-spl.lds文件,其中最后一行写着INSERT BEFORE .data;,这事很有意思,研究一下。

1. 默认的链接脚本

使用Linux gcc编译主机程序时我们是不需要指定链接脚本的,比如下面:

main.c

#include 

int main(void)
{
	printf("hello world!!!\n");
	return 0;
}

编译执行。

$ gcc main.c
$ ./a.out
hello world!!!
$ ld --verbose
...

但这里其实是有链接脚本的,也即ld工具有一套自己的链接脚本,可以使用ld --verbose查看。

2. 自定义段

有时候软件设计时我们需要标记一些特殊的段用于特殊用途,比如下面:

#include 

// 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. 给默认的链接脚本打补丁

  1. 使用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`等关键字看链接的过程
...
  1. 编写个新的链接脚本,使用-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`编译的
  1. 需注意执行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的数组不占空间

你可能感兴趣的:(C,linux)