1. 强符号和弱符号
1.1 u-boot
和kernel
中的__weak
指令
u-boot
和kernel
比较普遍地使用了__weak
来定义函数。
在include\linux\compiler-gcc.h
中__weak
是这样定义的:
#define __weak __attribute__((weak))
GCC
通过__attribute__((weak))
指令定义的函数或变量称为弱符号(Weak Symbol),实际上这个指令大部分时候都是用来定义函数,很少用于定义变量。
编译器默认函数和初始化了的全局变量为强符号(Strong Symbol),未初始化的全局变量为弱符号(Weak Symbol)。
1.2 强符号和弱符号的链接规则
<<程序员的自我修养>>第三章对这个强弱符号的链接规则做了很好的总结:
针对强弱符号的概念,链接器就会按照如下规则处理与选择被多次定义的全局符号:
- 规则1:不允许强符号被多次定义(即不同的目标文件中不能有同名的强符号);如果有多个强符号定义,则链接器报符号重复定义错误。
- 规则2:如果一个符号在某个目标文件中是强符号,在其他文件中都是弱符号,那么选择强符号。
- 规则3:如果一个符号在所有目标文件中都是弱符号,那么选择其中占用空间最大的一个。比如目标文件A定义全局变量global为int型,占4个字节;目标文件B定义global为doulbe型,占8个字节,那么目标文件A和B链接后,符号global占8个字节(尽量不要使用多个不同类型的弱符号,否则容易导致很难发现的程序错误)。
- 规则1很好理解,如果程序中多次定义了一个初始化过的全局变量(如
int a=0
),则编译器会报告multiple definition
的错误,比较常见。
多次定义的全局函数也属于此类,因为函数名也是一个符号变量,只不过它指向了随后定义的函数体,而且这个变量是只读的,不可改变。这也是为什么函数名可以赋值给一个变量,但是却无法给函数名赋值的原因。
- 规则2就是
u-boot
和kernel
中最常用的技巧了。
首先在平台无关的代码中定义一个__weak
属性的函数,如u-boot
的board_f.c
的文件头部定义了很多led
相关的空函数:
__weak void coloured_LED_init(void) {}
__weak void red_led_on(void) {}
__weak void red_led_off(void) {}
__weak void green_led_on(void) {}
__weak void green_led_off(void) {}
__weak void yellow_led_on(void) {}
__weak void yellow_led_off(void) {}
__weak void blue_led_on(void) {}
__weak void blue_led_off(void) {}
如果需要这些led
相关的功能,再在相关的文件中重新定义并实现这些函数即可,例如文件gpio_led.c
中在宏CONFIG_GPIO_LED_STUBS
打开时就调用__led_set
重新实现了这些函数。
- 规则3指明了同名弱符号的多个定义以占用空间最大的为准,主要是针对不用类型的变量而言;对于函数,其对应的符号相当于一个只读指针变量,而指针类型大小是固定的,不存在这个问题。
2. 强引用和弱引用
<<程序员的自我修养>>第三章对也对强引用和弱引用做了说明:
弱引用和强引用 目前我们所看到的对外部目标文件的符号引用在目标文件被最终链接成可执行文件时,它们必须要被正确决议,如果没有找到该符号的定义,链接器就会报符号未定义错误,这种被称为强应用(Strong Reference)。与之相对应还有一种弱引用(Weak Reference),在处理弱引用时,如果该符号有定义,则链接器将该符号的引用决议;如果该符号未被定义,则链接器对于该引用不报错。链接器处理强引用和弱引用的过程几乎一样,只是对于未定义的弱引用,链接器不认为它是一个错误。一般对于未定义的弱引用,链接器默认其为0,或者是一个特殊的值,以便于程序代码能够识别。
在
GCC
中,我们可以通过使用__attribute__((weakref))
扩展关键字来声明对一个外部函数的引用为弱引用。比如下面这段代码:
__attribute__ ((weakref)) void foo();
int main()
{
foo();
}
我们可以将它编译成一个可执行文件,
GCC
并不会报链接错误。但是当我们运行这个可执行文件时,会发生运行错误。因为当main
函数试图条用foo
函数是,foo
函数的地址为0,于是发生了非法地址访问的错误。一个改进的例子是:
__attribute__ ((weakref)) void foo();
int main()
{
if (foo) foo();
}
这种弱符号和弱引用对于库来说十分有用,比如库中定义的弱符号可以被用户定义的强符号所覆盖,从而使得程序可以使用自定义版本的库函数;或者程序可以对某些扩展功能模块的引用定义为弱引用,当我们将扩展模块与程序连接在一起时,功能模块就可以正常使用;如果我们去掉了某些功能模块,那么程序也可以正常链接,只是缺少了相应的功能,这使得程序的功能更加容易裁剪和组合。
专门检查过u-boot v2016.09
和linux v3.3-3.8
的代码,没有找到采用弱引用来定义函数的情况,可见也不是很常用的做法。
4. 其它
对于弱符号(Weak Symbol)和弱引用,其都仅是GNU
工具链GCC
对C
语言语法的扩展,并不是C
本身的语言特性。
有提到微软的MSVC
就不支持弱符号和弱引用特性。