内容简介:在一个源文件中引用一个变量或函数,当我们只声明,而没有定义时,一般编译是可以通过的。这是因为编译是以文件为单位的,编译器会将一个个源文件首先编译为 .o 目标文件。编译器只要能看到函数或变量的声明,会认为这个变量或函数的定义可能会在其它的文件中,所以不会报错。甚至如果你没有包含头文件,连个声明也没有,编译器也不会报错,顶多就是给你一个警告信息。但链接阶段是要报错的,链接器在各个目标文件、库中都找不到这个变量或函数的定义,一般就会报未定义错误。当函数被声明为一个弱符号时,会有一个奇特的地方:当链接器找不到这个
9.4 弱符号的用途
在一个源文件中引用一个变量或函数,当我们只声明,而没有定义时,一般编译是可以通过的。这是因为编译是以文件为单位的,编译器会将一个个源文件首先编译为 .o 目标文件。编译器只要能看到函数或变量的声明,会认为这个变量或函数的定义可能会在其它的文件中,所以不会报错。甚至如果你没有包含头文件,连个声明也没有,编译器也不会报错,顶多就是给你一个警告信息。但链接阶段是要报错的,链接器在各个目标文件、库中都找不到这个变量或函数的定义,一般就会报未定义错误。
当函数被声明为一个弱符号时,会有一个奇特的地方:当链接器找不到这个函数的定义时,也不会报错。编译器会将这个函数名,即弱符号,设置为0或一个特殊的值。只有当程序运行时,调用到这个函数,跳转到0地址或一个特殊的地址才会报错。
- / / func . c
- int a __attribute__ ( ( weak ) ) = 1 ;
- / / main . c
- int a = 4 ;
- void __attribute__ ( ( weak ) ) func ( void ) ;
- int main ( void )
- {
- printf ( "main:a = %d\n" , a ) ;
- func ( ) ;
- return 0 ;
- }
- 编译程序,可以看到程序运行结果。
- $ gcc - o a . out main . c func . c
- main : a = 4
- Segmentation fault ( core dumped )
在这个示例程序中,我们没有定义 func() 函数,仅仅是在 main.c 里作了一个声明,并将其声明为一个弱符号。编译这个工程,你会发现是可以编译通过的,只是到了程序运行时才会出错。
为了防止函数运行出错,我们可以在运行这个函数之前,先做一个判断,即看这个函数名的地址是不是0,然后再决定是否调用、运行。这样就可以避免段错误了,示例代码如下。
- / / func . c
- int a __attribute__ ( ( weak ) ) = 1 ;
- / / main . c
- int a = 4 ;
- void __attribute__ ( ( weak ) ) func ( void ) ;
- int main ( void )
- {
- printf ( "main:a = %d\n" , a ) ;
- if ( func )
- func ( ) ;
- return 0 ;
- }
- 编译程序,可以看到程序运行结果。
- $ gcc - o a . out main . c func . c
- main : a = 4
函数名的本质就是一个地址,在调用 func 之前,我们先判断其是否为0,为0的话就不调用了,直接跳过。你会发现,通过这样的设计,即使这个 func() 函数没有定义,我们整个工程也能正常的编译、链接和运行!
弱符号的这个特性,在库函数中应用很广泛。比如你在开发一个库,基础的功能已经实现,有些高级的功能还没实现,那你可以将这些函数通过 weak 属性声明,转换为一个弱符号。通过这样设置,即使函数还没有定义,我们在应用程序中只要做一个非0的判断就可以了,并不影响我们程序的运行。等以后你发布新的库版本,实现了这些高级功能,应用程序也不需要任何修改,直接运行就可以调用这些高级功能。
弱符号还有一个好处,如果我们对库函数的实现不满意,我们可以自定义与库函数同名的函数,实现更好的功能。比如我们 C 标准库中定义的 gets() 函数,就存在漏洞,常常成为黑客堆栈溢出攻击的靶子。
- int main ( void )
- {
- char a [ 10 ] ;
- gets ( a ) ;
- puts ( a ) ;
- return 0 ;
- }
C 标准定义的库函数 gets() 主要用于输入字符串,它的一个 Bug 就是使用回车符来判断用户输入结束标志。这样的设计很容易造成堆栈溢出。比如上面的程序,我们定义一个长度为10的字符数组用来存储用户输入的字符串,当我们输入一个长度大于10的字符串时,就会发生内存错误。
接着我们定义一个跟 gets() 相同类型的同名函数,并在 main 函数中直接调用,代码如下。
- #include < stdio . h >
- char * gets ( char * str )
- {
- printf ( "hello world!\n" ) ;
- return ( char * ) 0 ;
- }
- int main ( void )
- {
- char a [ 10 ] ;
- gets ( a ) ;
- return 0 ;
- }
- 程序运行结果如下。
- hello
通过运行结果,我们可以看到,虽然我们定义了跟 C 标准库函数同名的 gets() 函数,但编译是可以通过的。程序运行时调用 gets() 函数时,就会跳转到我们自定义的 gets() 函数中运行。
9.5 属性声明:alias
GNU C 扩展了一个 alias 属性,这个属性很简单,主要用来给函数定义一个别名。
- void __f ( void )
- {
- printf ( "__f\n" ) ;
- }
- void f ( ) __attribute__ ( ( alias ( "__f" ) ) ) ;
- int main ( void )
- {
- f ( ) ;
- return 0 ;
- }
- 程序运行结果如下。
- __f
通过 alias 属性声明,我们就可以给 f() 函数定义一个别名 f(),以后我们想调用 f() 函数,可以直接通过 f() 调用即可。
在 Linux 内核中,你会发现 alias 有时会和 weak 属性一起使用。比如有些函数随着内核版本升级,函数接口发生了变化,我们可以通过 alias 属性给这个旧接口名字做下封装,起一个新接口的名字。
- / / f . c
- void __f ( void )
- {
- printf ( "__f()\n" ) ;
- }
- void f ( ) __attribute__ ( ( weak , alias ( "__f" ) ) ) ;
- / / main . c
- void __attribute__ ( ( weak ) ) f ( void ) ;
- void f ( void )
- {
- printf ( "f()\n" ) ;
- }
- int main ( void )
- {
- f ( ) ;
- return 0 ;
- }
当我们在 main.c 中新定义了 f() 函数时,在 main 函数中调用 f() 函数,会直接调用 main.c 中新定义的函数;当 f() 函数没有新定义时,就会调用 __f() 函数。
更多嵌入式教程:QQ群/微信公众号:宅学部落
本教程电子书籍下载地址: https://pan.baidu.com/s/1a6L0cyIQKKLlmIfRw7U6Dg
以上就是本文的全部内容,希望本文的内容对大家的学习或者工作能带来一定的帮助,也希望大家多多支持 码农网
猜你喜欢:本站部分资源来源于网络,本站转载出于传递更多信息之目的,版权归原作者或者来源机构所有,如转载稿涉及版权问题,请联系我们。