在编程时经常会碰到一种情况叫符号重复定义,一般意味着多个目标文件中含有相同名字全局符号的定义,而有时又不会报错,为什么?
在 glibc 中定义了很多类似 read()
open()
的函数,但是又可以自己定义相同的函数?
这就涉及到了强弱符号以及强弱引用的概念了,这里详细介绍。
强弱符号
在编程时经常会碰到一种情况叫符号重复定义,一般意味着多个目标文件中含有相同名字全局符号 (变量或者函数) 的定义,这种符号的定义被称为强符号 (Strong Symbol),有些符号的定义被称为弱符号 (Weak Symbol)。
例如,如下的示例,两个 C 代码文件 main.c
以及 other.c
。
/* main.c */
#include <stdio.h>
int foobar(void)
{
return 0;
}
int data = 100;
int main(void)
{
printf("Data is %d\n", data);
return 0;
}
/* other.c */
int data = 1;
int foobar(void)
{
return 0;
}
然后通过如下方式进行编译。
$ gcc main.c other.c
/tmp/ccJ2vhHv.o:(.data+0x0): multiple definition of `data'
/tmp/cch1fzSM.o:(.data+0x0): first defined here
/tmp/ccJ2vhHv.o: In function `foobar':
other.c:(.text+0x0): multiple definition of `foobar'
/tmp/cch1fzSM.o:main.c:(.text+0x0): first defined here
collect2: error: ld returned 1 exit status
上述会发现两个地方出现了重复定义问题,分别是一个全局变量 int data
以及全局函数 int foobar(void)
。
对于 C/C++ 来说,编译器默认会将函数和已经初始化的全局变量作为强符号,而未初始化的全局变量则默认为弱符号。所以,如果将 other.c
文件中的第一行修改为 int data
实际上就不会再报错了,但是对于函数来说,目前还没有太好的默认修改办法,需要显示声明。
weak
实际上 GCC 提供了一个 __attribute__((weak))
来将任何一个强符号定义为弱符号。注意,强符号和弱符号都是针对定义来说的,如上的示例可以替换为。
/* other.c */
int __attribute__((weak)) data = 1;
int __attribute__((weak)) foobar(void)
{
return 0;
}
总结
针对强弱符号的概念,链接器就会按照如下规则处理与选择被多次定义的全局符号:
- 不允许强符号被多次定义 (即不同的目标文件中不能有同名的强符号),如果有多个强符号定义,则链接器报符号重复定义错误。
- 如果一个符号在某个目标文件中是强符号,在其他文件中都是弱符号,那么选择强符号。
- 如果一个符号在所有目标文件中都是弱符号,那么选择其中占用空间最大的一个。
对于最后一点,如果两个文件同时定义了 global_var
变量,类型分别为 int
和 double
,也就是分别占用 4 和 8 字节,那么 global_var
符号最终占用的是 8 字节。
注意,尽量不要使用多个不同类型的弱符号,否则容易导致很难发现的程序错误。
强弱引用
一些对外部文件的符号引用,如果在被链接成可执行文件时,没有找到相关的符号,那么链接器就会报符号未定义错误,这种被称为强应用 (Strong Reference)。
与之相对应还有一种弱引用 (Weak Reference),如果可以找到符号则连接,否则连接器会默认设置为 0 或者一个特殊值。
weakref
在 GCC 中,可以使用 __attribute__((weakref))
来声明对一个外部函数的引用为弱引用。
/* main.c */
#include <stdio.h>
static void foo() __attribute__((weakref("bar")));
int main(void)
{
if (foo)
foo();
return 0;
}
/* other.c */
#include <stdio.h>
void bar()
{
printf("Hello World!!!\n");
}
注意,在声明弱引用时需要使用 static
关键字。
如果通过 gcc main.c
进行编译则不会打印任何东西,而通过 gcc main.c other.c
编译会看到相关的 Hello World!!!
输出。
weak_alias
在 glibc 代码中,可以看到很多对函数的属性修饰,包括了各种 alias 的定义,这里简单看下 weak_alias 的相关内容。
函数的属性定义在 include/libc-symbols.h
头文件中,其中 weak_alias
的定义如下。
/* Define ALIASNAME as a weak alias for NAME.
If weak aliases are not available, this defines a strong alias. */
# define weak_alias(name, aliasname) _weak_alias (name, aliasname)
# define _weak_alias(name, aliasname) \
extern __typeof (name) aliasname __attribute__ ((weak, alias (#name)));
可以参考 glibc 中的一个实际应用,也就是 gettimeofday()
。
# include <sysdep.h>
# include <errno.h>
int __gettimeofday (struct timeval *tv, struct timezone *tz)
{
return INLINE_SYSCALL (gettimeofday, 2, tv, tz);
}
libc_hidden_def (__gettimeofday)
weak_alias (__gettimeofday, gettimeofday)
libc_hidden_weak (gettimeofday)
示例
#include <stdio.h>
void foobar() __attribute__ ((weak));
void foobar(void)
{
printf("libfoobar test\n");
}
#include <stdio.h>
void foobar(void)
{
printf("app test\n");
}
#include <stdio.h>
void foobar(void);
int main(void)
{
foobar();
return 0;
}
all:
gcc -c foobar.c
ar crv libfoobar.a foobar.o
#gcc main.c app.c libfoobar.a -o foobar
gcc main.c libfoobar.a -o foobar
首先生成一个静态库,可以通过 nm libfoobar.a
看到定义了一个 Weak
类型的 foobar()
函数。
----- 直接使用app.c中的代码
$ gcc libfoobar.a main.c app.c -o foobar
$ ./foobar
app test
----- 如果不使用app.c中的代码
$ gcc libfoobar.a main.c -o foobar
$ ./foobar
libfoobar test
在使用时需要注意顺序,如果 libfoobar.a
中包含了 weak
函数,那么应该放在最后,否则会报错 undefined reference
。
总结
这种弱符号和弱引用对于库来说十分有用,比如库中定义的弱符号可以被用户定义的强符号所覆盖,从而使得程序可以使用自定义版本的库函数。
或者程序可以对某些扩展功能模块的引用定义为弱引用,当用户将扩展模块与程序链接在一起时,功能模块就可以正常使用;如果我们去掉了某些功能模块,那么程序也可以正常链接,只是缺少了相应的功能,这使得程序的功能更加容易裁剪和组合。