嵌入式C语言自我修养 09:链接过程中的强符号和弱符号

9.1 属性声明:weak

GNU C 通过 attribute 声明weak属性,可以将一个强符号转换为弱符号。

使用方法如下。

void  __attribute__((weak))  func(void);
int  num  __attribte__((weak);

编译器在编译源程序时,无论你是变量名、函数名,在它眼里,都是一个符号而已,用来表征一个地址。编译器会将这些符号集中,存放到一个叫符号表的 section 中。

在一个软件工程项目中,可能有多个源文件,由不同工程师开发。有时候可能会遇到这种情况:A 工程师在他负责的 A.c 源文件中定义了一个全局变量 num,而 B 工程师也在他负责的 B.c 源文件中定义了一个同名全局变量 num。那么当我们在程序中打印变量 num 的值时,是该打印哪个值呢?

是时候表演真正的技术了。这时候,就需要用编译链接的原理知识来分析这个问题了。编译链接的基本过程其实很简单,主要分为三个阶段。

  • 编译阶段:编译器以源文件为单位,将每一个源文件编译为一个 .o 后缀的目标文件。每一个目标文件由代码段、数据段、符号表等组成。
  • 链接阶段:链接器将各个目标文件组装成一个大目标文件。链接器将各个目标文件中的代码段组装在一起,组成一个大的代码段;各个数据段组装在一起,组成一个大的数据段;各个符号表也会集中在一起,组成一个大的符号表。最后再将合并后的代码段、数据段、符号表等组合成一个大的目标文件。
  • 重定位:因为各个目标文件重新组装,各个目标文件中的变量、函数的地址都发生了变化,所以要重新修正这些函数、变量的地址,这个过程称为重定位。重定位结束后,就生成了可以在机器上运行的可执行程序。

上面举例的工程项目,在编译过程中的链接阶段,可能就会出现问题:A.c 和 B.c 文件中都定义了一个同名变量 num,那链接器到底该用哪一个呢?

这个时候,就需要引入强符号和弱符号的概念了。

9.2 强符号和弱符号

在一个程序中,无论是变量名,还是函数名,在编译器的眼里,就是一个符号而已。符号可以分为强符号和弱符号。

  • 强符号:函数名、初始化的全局变量名;
  • 弱符号:未初始化的全局变量名。

在一个工程项目中,对于相同的全局变量名、函数名,我们一般可以归结为下面三种场景。

  • 强符号+强符号
  • 强符号+弱符号
  • 弱符号+弱符号

强符号和弱符号在解决程序编译链接过程中,出现的多个同名变量、函数的冲突问题非常有用。一般我们遵循下面三个规则。

  • 一山不容二虎
  • 强弱可以共处
  • 体积大者胜出

为了方便,这是我编的顺口溜。主要意思就是:在一个项目中,不能同时存在两个强符号,比如你在一个多文件的工程中定义两个同名的函数,或初始化的全局变量,那么链接器在链接时就会报重定义的错误。但一个工程中允许强符号和弱符号同时存在。比如你可以同时定义一个初始化的全局变量和一个未初始化的全局变量,这种写法在编译时是可以编译通过的。编译器对于这种同名符号冲突,在作符号决议时,一般会选用强符号,丢掉弱符号。还有一种情况就是,一个工程中,同名的符号都是弱符号,那编译器该选择哪个呢?谁的体积大,即谁在内存中存储空间大,就选谁。

我们接下来写一个简单的程序,来验证上面的理论。定义两个源文件:main.c 和 func.c。

//func.c
int a = 1;
int b;
void func(void)
{
    printf("func:a = %d\n", a);
    printf("func: b = %d\n", b);
}

//main.c
int a;
int b = 2;
void func(void);
int main(void)
{
    printf("main:a = %d\n", a);
    printf("main: b = %d\n", b);
    func();
    return 0;
}

编译程序,可以看到程序运行结果。

$ gcc -o a.out main.c func.c
main: a = 1
main: b = 2
func: a = 1
func: b = 2

我们在 main.c 和 func.c 中分别定义了两个同名全局变量 a 和 b,但是一个是强符号,一个是弱符号。链接器在链接过程中,看到冲突的同名符号,会选择强符号,所以你会看到,无论是 main 函数,还是 func 函数,打印的都是强符号的值。

一般来讲,不建议在一个工程中定义多个不同类型的弱符号,编译的时候可能会出现各种各样的问题,这里就不举例了。在一个工程中,也不能同时定义两个同名的强符号,即初始化的全局变量或函数,否则就会报重定义错误。但是我们可以使用 GNU C 扩展的 weak 属性,将一个强符号转换为弱符号。

//func.c
int a __attribute__((weak)) = 1;
void func(void)
{
    printf("func:a = %d\n", a);
}

//main.c
int a = 4;
void 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
func: a = 4

我们通过 weak 属性声明,将 func.c 中的全局变量 a,转换为一个弱符号,然后在 main.c 里同样定义一个全局变量 a,并初始化 a 为4。链接器在链接时会选择 main.c 中的这个强符号,所以在两个文件中,打印变量 a 的值都是4。

9.3 函数的强符号和弱符号

链接器对于同名变量冲突的处理遵循上面的强弱规则,对于函数同名冲突,同样也遵循相同的规则。函数名本身就是一个强符号,在一个工程中定义两个同名的函数,编译时肯定会报重定义错误。但我们可以通过 weak 属性声明,将其中一个函数转换为弱符号。

//func.c
int a __attribute__((weak)) = 1;
void __attribute__((weak)) func(void)
{
    printf("func:a = %d\n", a);
}

//main.c
int a = 4;
void func(void)
{
    printf("I am a strong symbol!\n");
}
int main(void)
{
    printf("main:a = %d\n", a);
    func();
    return 0;
}

编译程序,可以看到程序运行结果。

$ gcc -o a.out main.c func.c
main: a = 4
func: I am a strong symbol!

在这个程序示例中,我们在 main.c 中重新定义了一个同名的 func 函数,然后将 func.c 文件中的 func() 函数,通过 weak 属性声明转换为一个弱符号。链接器在链接时会选择 main.c 中的强符号,所以我们在 main 函数中调用 func() 时,实际上调用的是 main.c 文件里的 func() 函数。

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 world!

通过运行结果,我们可以看到,虽然我们定义了跟 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() 函数。

本教程根据 C语言嵌入式Linux高级编程视频教程 第05期 改编,电子版书籍可加入QQ群:475504428 下载,更多嵌入式视频教程,可关注:
微信公众号:宅学部落(armlinuxfun)
51CTO学院-王利涛老师:http://edu.51cto.com/sd/d344f

原文地址:http://blog.51cto.com/zhaixue/2348619

时间: 2024-07-29 00:13:20

嵌入式C语言自我修养 09:链接过程中的强符号和弱符号的相关文章

嵌入式C语言自我修养 06:U-boot镜像自拷贝分析:section属性

6.1 GNU C 的扩展关键字:attribute GNU C 增加一个 atttribute 关键字用来声明一个函数.变量或类型的特殊属性.声明这个特殊属性有什么用呢?主要用途就是指导编译器在编译程序时进行特定方面的优化或代码检查.比如,我们可以通过使用属性声明指定某个变量的数据边界对齐方式. attribute 的使用非常简单,当我们定义一个函数.变量或类型时,直接在它们名字旁边添加下面的属性声明即可: __atttribute__((ATTRIBUTE)) 这里需要注意的是:attrib

嵌入式C语言自我修养 10:内联函数探究

10.1 属性声明:noinline & always_inline 这一节,接着讲 attribute 属性声明,attribute可以说是 GNU C 最大的特色.我们接下来继续讲一下跟内联函数相关的两个属性:noinline 和 always_inline.这两个属性的用途是告诉编译器:编译时,对我们指定的函数内联展开或不展开.它们的使用方法如下. static inline __attribute__((noinline)) int func(); static inline __att

嵌入式C语言自我修养 13:总结

13.1 总结 前面12节的课程,主要针对 Linux 内核中 GNU C 扩展的一些常用 C 语言语法进行了分析.GNU C 的这些扩展语法,主要用来完善 C 语言标准和编译优化.而通过 C 标准的发展过程我们又发现,对于一些编译器扩展的一些特性,或者其它编程语言(如:C++)中的好的特性和语法,C 标准也会适时地吸收进来,作为新的 C 语言标准. 在 GNU C 的这些扩展语法中,attribute 和宏定义是两大特色.在嵌入式底层系统中,尤其是 Linux 内核和 U-boot 中,大量使

嵌入式C语言自我修养 11:有一种函数,叫内建函数

11.1 什么是内建函数 内建函数,顾名思义,就是编译器内部实现的函数.这些函数跟关键字一样,可以直接使用,无须像标准库函数那样,要 #include 对应的头文件才能使用. 内建函数的函数命名,通常以 __builtin 开头.这些函数主要在编译器内部使用,主要是为编译器服务的.内建函数的主要用途如下. 用来处理变长参数列表: 用来处理程序运行异常: 程序的编译优化.性能优化: 查看函数运行中的底层信息.堆栈信息等: C 标准库函数的内建版本. 因为内建函数是编译器内部定义,主要由编译器相关的

嵌入式C语言自我修养 03:宏构造利器 - 语句表达式

3.1 基础复习:表达式.语句和代码块 表达式 表达式和语句是 C 语言中的基础概念.什么是表达式呢?表达式就是由一系列操作符和操作数构成的式子.操作符可以是 C 语言标准规定的各种算术运算符.逻辑运算符.赋值运算符.比较运算符等.操作数可以是一个常量,也可以是一个变量.表达式也可以没有操作符,单独的一个常量甚至是一个字符串,也是一个表达式.下面的字符序列都是表达式: 2 + 3 2 i = 2 + 3 i = i++ + 3 "wit" 表达式一般用来数据计算或实现某种功能的算法.表

嵌入式C语言自我修养 04:Linux 内核第一宏:container_of

4.1 typeof 关键字 ANSI C 定义了 sizeof 关键字,用来获取一个变量或数据类型在内存中所占的存储字节数.GNU C 扩展了一个关键字 typeof,用来获取一个变量或表达式的类型.这里使用关键字可能不太合适,因为毕竟 typeof 还没有被写入 C 标准,是 GCC 扩展的一个关键字.为了方便,我们就姑且称之为关键字吧. 通过使用 typeof,我们可以获取一个变量或表达式的类型.所以 typeof 的参数有两种形式:表达式或类型. int i ; typeof(i) j

嵌入式C语言自我修养 05:零长度数组

5.1 什么是零长度数组 顾名思义,零长度数组就是长度为0的数组. ANSI C 标准规定:定义一个数组时,数组的长度必须是一个常数,即数组的长度在编译的时候是确定的.在ANSI C 中定义一个数组的方法如下: int a[10]; C99 新标准规定:可以定义一个变长数组. int len; int a[len]; 也就是说,数组的长度在编译时是未确定的,在程序运行的时候才确定,甚至可以由用户指定大小.比如,我们可以定义一个数组,然后在程序运行时才指定这个数组的大小,还可以通过输入数据来初始化

嵌入式C语言自我修养 07:地址对齐那些事儿

7.1 属性声明:aligned GNU C 通过 attribute 来声明 aligned 和 packed 属性,指定一个变量或类型的对齐方式.这两个属性用来告诉编译器:在给变量分配存储空间时,要按指定的地址对齐方式给变量分配地址.如果你想定义一个变量,在内存中以8字节地址对齐,就可以这样定义. int a __attribute__((aligned(8)); 通过 aligned 属性,我们可以直接显式指定变量 a 在内存中的地址对齐方式.aligned 有一个参数,表示要按几字节对齐

嵌入式C语言自我修养 08:变参函数的格式检查

8.1 属性声明:format GNU 通过 attribute 扩展的 format 属性,用来指定变参函数的参数格式检查. 它的使用方法如下: __attribute__(( format (archetype, string-index, first-to-check))) void LOG(const char *fmt, ...) __attribute__((format(printf,1,2))); 我们经常实现一些自己的打印调试函数.这些打印函数往往是变参函数,那编译器编译程序时