强符号与弱符号的问题与应用场景
强符号与弱符号是链接中的概念,最早在学习计算机组成原理的时候有所接触。在这里顺道复习复习。
-
弱符号有:
未初始化的全局变量、未初始化的静态变量。弱符号可以有多处定义。
-
强符号有:
初始化的全局变量及静态变量,函数定义。强符号只能有一处定义。如果出现多次定义,在链接时会报重复定义的错误。
对弱符号的引用并不会因为找不到一个定义而报致命错误,在这种情况下编译器会将弱符号赋值为绝对数值 0,这样的处理过程意味这我们可以通过判断弱符号的值是否为 0 来确定弱符号是否有定义。
下面是一个 demo:
#include <stdio.h>
#pragma weak floating_used
extern int floating_used(void);
int floating_used_existing(void)
{
int (*pfunc)(void);
return (pfunc = floating_used) ? 1:0;
}
int main(int argc, char* argv[])
{
if (!floating_used_existing()) {
printf("floating_used is assigned to zero!\n");
} else {
printf("floating_used is not zero!\n");
}
return 0;
}
上述代码中的 #pragma weak floating_used 语句将 floating_used 表示成一个弱符号,将上述代码保存为源文件 main.c 并编译执行,输出如下:
[longyu@debian-10:20:19:13] tmp $ ./a.out
floating_used is assigned to zero!
添加一个 floating_used 函数的实现,内容如下:
int floating_used(void)
{
return 1;
}
将上述代码保存为 lib.c 文件并将该文件与 main.c 同时编译,再次运行程序,输出如下:
[longyu@debian-10:20:19:15] tmp $ gcc main.c lib.c
[longyu@debian-10:20:43:07] tmp $ ./a.out
floating_used is not zero!
这次在链接时找到了一个弱符号的定义,此定义被使用,floating_used 变成了一个非零值。
《链接器与加载器》一书中有如下描述:
解决这个困境的方法就是弱外部符号,就是不会导致加载库成员的外部符号。如果该
符号存在一个有效的定义,无论是从一个显式链接的文件还是普通的外部引用而被链接进来
的库成员中,一个弱外部符号会被解析为一个普通的外部引用。但是如果不存在有效的定义,
弱外部符号就不被定义而实际上解析为 0,这样就不会被认为是一个错误。
demo 的执行结果与上述描述一致。
弱定义
与弱符号相关的另外一个东西是弱定义。对于弱符号的描述,《链接器与加载器》一书中有如下说明:
弱定义定义了一个没有有效的普通定义的全局符号。如果存在有效的普通定义,那么就忽略弱定义。弱定义并不经常使用,但在定义错误伪函数而无须将其分散在独立的模块中的时候是很有用的。
使用弱定义我们可以使用预定的行为来设计程序而无需提供函数的完整实现,只在真正需要的时候才去将完整的函数定义链接进来。
Unity 中弱符号的使用示例
下面是 Unity 中使用弱符号的相关代码:
#ifdef UNITY_INCLUDE_SETUP_STUBS
#ifdef UNITY_WEAK_ATTRIBUTE
UNITY_WEAK_ATTRIBUTE void setUp(void) { }
UNITY_WEAK_ATTRIBUTE void tearDown(void) { }
UNITY_WEAK_ATTRIBUTE void suiteSetUp(void) { }
UNITY_WEAK_ATTRIBUTE int suiteTearDown(int num_failures) { return num_failures; }
#elif defined(UNITY_WEAK_PRAGMA)
#pragma weak setUp
void setUp(void) { }
#pragma weak tearDown
void tearDown(void) { }
#pragma weak suiteSetUp
void suiteSetUp(void) { }
#pragma weak suiteTearDown
int suiteTearDown(int num_failures) { return num_failures; }
#endif
#endif
以上代码将一些初始化与解初始化的函数声明为弱符号。当用户没有定义这些函数时,弱符号提供一个默认的实现,这个实现什么也不做,只起到占位作用。当用户重新定义这些函数后,默认的实现将被覆盖,测试时将调用用户实现的函数。
总结
弱符号、弱定义提供了一种解决特定链接时问题的思路,但是实际的开发过程中并不建议使用这种方式。
Unity 中使用了这种方式,但是也许可以通过函数指针来动态注册初始化与解初始化函数。测试过程的抽象流程中可以通过判断初始化与解初始化函数是否注册来分开处理默认与用户定义这两种不同方式。尽管这些函数的实现有所区别,但是完整的抽象测试流程应该是一致的。
不过,这样的方式是不是降低了一定的易用性呢?
参考书籍:
《链接器与加载器》