简图记录学习~
C语言在编译过程函数和变量都按符号的方式记录,常在编译链接时发生符号重复定义、符号无定义等错误,一不小心还会出现强弱符号覆盖这类不易察觉的隐含问题。以下部分内容来自《程序员的自我修改》,非常好的一本关于编译链接和库的书籍。
一、概念
符号:编译器在链接不同目标文件时,需要处理各个文件之间的定义、调用和访问关系,编译器将函数和变量等都统称为符号,每个elf文件都有一个符号段.symtab存放所有符号,其实就是一个ELF32_Sym结构体类型的数组。
typedef struct{
Elf32_Word st_name;//符号名,实为字符串表下标。对应函数和变量一般为函数名和变量名
Elf32_Addr st_value;//符号值,函数或者变量为其所在地址
Elf32_Word st_size;//符号长度,变量为类型长度如int为4
unsigned char st_info;//符号信息,包括符号类型(局部/全局/弱引用)、绑定信息(未知/数据/函数/段名/文件名)
unsigned char st_other;//未使用
Elf32_Half st_shndx;//符号所在段
}ELF32_Sym;
强弱符号:函数或者已初始化全局变量为强符号,未初始化全局变量为弱符号(有命令可以转化强符号为弱符号)
常见弱符号转换命令:
__attrible__((week)) void fun();
//或者:
extern void fun();
#pragma weak fun
强弱符号编译规则:1、强符号不能重复定义,但强弱可共存 2、强弱共存时,强符号覆盖弱符号 3、都是弱符号时,选择第一个size最大的弱符号定义(默认值为0)
//举例:
//a.c文件:
int i=2;
//b.c文件:
int i;
int main()
{
printf("i=%d\n",i);
return 0;
}
/*当链接a.c b.c后打印i=2,b文件被覆盖*/
强弱引用:对强符号访问为强引用,若链接完发现未定义编译失败;对弱符号访问为弱引用,链接完成是发现未定义不会编译失败,但直调用弱符号函数会导致运行时报错(可以先判断函数地址是否非0再调用)
//a.c内容
__attrible__((week)) void fun();
int main()
{
if (fun)fun();
return 0;
}
/*a.c单独编译能成功,运行时也不会出错,如果链接一个定义了fun的文件,则可调用过去*/
二、查看符号方法
查看符号表方法:readelf -S xxx.o --->num序号也是所在字符串表下标;value符号值;size符号大小;Type符号类型;bind符号绑定信息;Vis无用;Ndx所在段;Name符号名;
查看目标文件符号方法:nm -n(按地址排序默认按函数名)L(打印文件位置) xxx.o --->地址(如0x12345678) 类型(A绝对 地址/B bss段/C 未初始化符号/D 数据段/R 只读段/T 代码段/U 未定义,小写表示内部符号 大写外部符号) 符号名(如main)
三、常用技巧及注意事项
1、如何防止多个全局变量强弱覆盖问题?
(1)最优方案:优化掉全局变量 (2)一般方案:定义为static,就无强弱符号概念,外部无法访问 (3)最差方案:要求全部全局变量必须初始化,只要有人未初始化该变量就有被覆盖风险;
GCC提供编译命令:-fno-common识别覆盖问题
2、弱符号使用技巧,在以下场景可以很好解决问题
(1)希望有编译时可带可不带的扩展功能模块时,扩展功能访问都用弱引用方式;(2)希望支持如多线程和单线程多个版本时,可按弱引用调用线程库接口,判断为0时走单线程流程。通过编译时是否带-lpthread控制;(3)避免开源组件开源传染,开源组件要求发布时能单独编译且保持功能正常,如果你在SDK下适配了开源组件,必须以弱引用访问SDK,否则SDK会被要求开源;