GNU C 通过 attribute 声明weak属性,可以将一个强符号转换为弱符号。
使用方法如下。
void attribute((weak)) func(void);
int num attribte((weak);
编译器在编译源程序时,无论你是变量名、函数名,在它眼里,都是一个符号而已,用来表征一个地址。编译器会将这些符号集中,存放到一个叫符号表的 section 中。
在一个项目中,我们通常会有数个.c文件,比如在A.c和B.c文件都定义了同名的变量。或者在同一个.c文件中定义同名的全局变量。我们在编译工程,会有什么现象?可能很多人都会说,绝对编译不通过!我们还是通过实际例子来看看:
编译通过
![在这里插入图片描述](https://img-blog.csdnimg.cn/fa9d39c6c2db4473b608f2aaeecf8827.png)
运行结果:
从结果看,并没有报错,运行也正常,打印结果为0(未初始化的变量默认给初值0).
就需要用编译链接的原理知识来分析这个问题了。编译链接的基本过程其实很简单,主要分为三个阶段。
编译阶段:编译器以源文件为单位,将每一个源文件编译为一个 .o 后缀的目标文件。每一个目标文件由代码段、数据段、符号表等组成。
链接阶段:链接器将各个目标文件组装成一个大目标文件。链接器将各个目标文件中的代码段组装在一起,组成一个大的代码段;各个数据段组装在一起,组成一个大的数据段;各个符号表也会集中在一起,组成一个大的符号表。最后再将合并后的代码段、数据段、符号表等组合成一个大的目标文件。
重定位:因为各个目标文件重新组装,各个目标文件中的变量、函数的地址都发生了变化,所以要重新修正这些函数、变量的地址,这个过程称为重定位。重定位结束后,就生成了可以在机器上运行的可执行程序。
一开始举例的一个项目中,不同源文件定义的同名变量,在编译过程中的链接阶段,可能就会出现问题:A.c 和 B.c 文件中都定义了一个同名变量 num,那链接器到底该用哪一个呢?
这个时候,就需要引入强符号和弱符号的概念了。
强符号和弱符号
在一个程序中,无论是变量名,还是函数名,在编译器的眼里,就是一个符号而已。符号可以分为强符号和弱符号。
强符号:函数名、初始化的全局变量名;
弱符号:未初始化的全局变量名。
在一个工程项目中,对于相同的全局变量名、函数名,我们一般可以归结为下面三种场景。
强符号+强符号
强符号+弱符号
弱符号+弱符号
在一个项目中,不能同时存在两个强符号,比如你在一个多文件的工程中定义两个同名的函数,或初始化的全局变量,那么链接器在链接时就会报重定义的错误。但一个工程中允许强符号和弱符号同时存在。比如你可以同时定义一个初始化的全局变量和一个未初始化的全局变量,这种写法在编译时是可以编译通过的。编译器对于这种同名符号冲突,在作符号决议时,一般会选用强符号,丢掉弱符号。还有一种情况就是,一个工程中,同名的符号都是弱符号,那编译器该选择哪个呢?谁的体积大,即谁在内存中存储空间大,就选谁。
下面继续用实际的例子来演示:
可以看到,初始化的变量默认就是强符号的。那疑问来了,初始化后的强符号可以通过weak属性,变成弱符号吗?可以,我们可以使用 GNU C 扩展的 weak 属性,将一个强符号转换为弱符号。
//myfun.c
int a attribute((weak)) = 1;
void func(void)
{
OSI_PRINTFI(“func:a = %d\n”, a);
}
//hello_world.c
int a = 4;
void func(void);
static void prvThreadEntry(void *param)
{
OSI_LOGI(0, “application thread enter, param 0x%x”, param);
for (int n = 0; n < 10; n++)
{
osiThreadSleep(500);
}
OSI_LOGI(0, "a = %d", a);
func();
osiThreadExit();
}
运行结果:
我们通过 weak 属性声明,将 myfunc.c 中的全局变量 a,转换为一个弱符号,然后在 hello_world.c 里同样定义一个全局变量 a,并初始化 a 为4。链接器在链接时会选择 hello_world.c 中的这个强符号,所以在两个文件中,打印变量 a 的值都是4。
函数的强符号和弱符号
链接器对于同名变量冲突的处理遵循上面的强弱规则,对于函数同名冲突,同样也遵循相同的规则。函数名本身就是一个强符号,在一个工程中定义两个同名的函数,编译时肯定会报重定义错误。但我们可以通过 weak 属性声明,将其中一个函数转换为弱符号。读者可以自行验证效果。