【C语言】强符号和弱符号

2023-10-27

1、强符号、弱符号定义

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

那么对于两个.c文件中存在的同名的变量,编译器该怎么选择呢?这个时候,就需要引入强符号和弱符号的概念了。

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

两种符号的不同组合可能有不同的效果:
强符号 + 强符号 : 编译时会提示“重复定义的错误”
强符号 + 弱符号 : 选择强符号
弱符号 + 弱符号 : 默认的,链接器使用第一个找到的符号。(ps: 有些说是链接器选择占用内存空间最大的那个,个人验证并不是这样)

下面的实验环境为:
gcc version 4.8.5 20150623 (Red Hat 4.8.5-16) (GCC)

2、变量的强符号和弱符号

先通过变量的强符号和弱符号来研究上面的几个组合的特性。

//func.c
int a = 1;

//main.c
#include <stdio.h>

int a = 10;

int main()
{
    printf("a = %d.\n", a);
    return 0;
}

编译时提示:

/tmp/ccJyARMQ.o:(.data+0x0): multiple definition of `a'
/tmp/cccYwric.o:(.data+0x0): first defined here
//func.c
int a = 1;
int b;
int c __attribute__((weak)) = 3;
int d __attribute__((weak)) = 'z';

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

//main.c
#include <stdio.h>

int a;
int b = 9;
int c = 8;
char d __attribute__((weak)) = 'a';

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

gcc -o main main.c func.c输出结果为:

main: a = 1, b = 9, c = 8, d = 97.
func: a = 1, b = 9, c = 8, d = 97.

gcc -o main func.c main.c 输出结果为:

main: a = 1, b = 9, c = 8, d = 122.
func: a = 1, b = 9, c = 8, d = 122.

a,b变量看出,强符号 + 弱符号最后会选择强符号。
c变量说面可以使用__attribute__((weak))声明变量为弱符号。
d变量说明了都是弱符号的情况下,链接器使用第一个找到的符号。
再次我们验证第三个特性,在增加一个文件test.c文件

//test.c
char d char d __attribute__((weak)) = 'h';

先编译不进行链接gcc -c test.c func.c main.c,会分别生成三个.o文件,调整顺序:

$gcc -o main test.o main.o func.o
$ ./main
main: a = 1, b = 9, c = 8, d = 104.
func: a = 1, b = 9, c = 8, d = 104.
$ gcc -o main main.o func.o test.o
$ ./main
main: a = 1, b = 9, c = 8, d = 97.
func: a = 1, b = 9, c = 8, d = 97.
$ gcc -o main func.o test.o main.o
$ ./main
main: a = 1, b = 9, c = 8, d = 122.
func: a = 1, b = 9, c = 8, d = 122.

符合规则,要是这里我们修改test.c文件里面为强符号会怎么样呢?

//test.c
char d = 'h';
得到结果与期望相同,均是选择强符号。
$ gcc -o main test.o main.o func.o // gcc -o main main.o func.o test.o或者gcc -o main func.o test.o main.o
$ ./main
main: a = 1, b = 9, c = 8, d = 104.
func: a = 1, b = 9, c = 8, d = 104.
$ gcc -o main main.o func.o test.o

3、函数的强符号和弱符号

// main.c
#include <stdio.h>

void func()
{
    printf("function from %s.\n", __FILE__);
}

int main()
{
    func();
    return 0;
}

// func.c
#include <stdio.h>

void func()
{
    printf("function from %s.\n", __FILE__);
}

编译报错,重复定义:

/tmp/ccv2g1Fo.o: In function `func':
func.c:(.text+0x0): multiple definition of `func'
/tmp/ccQXcJwI.o:main.c:(.text+0x0): first defined here
collect2: error: ld returned 1 exit status

修改func.c为

//func.c
#include <stdio.h>

void __attribute__((weak)) func()
{
    printf("function from %s.\n", __FILE__);
}

执行结果如下:

$ gcc -o main  main.c func.c
$ ./main
function from main.c.
$ gcc -o main  func.c main.c
$ ./main
function from main.c.
$ gcc -c main.c func.c
$ gcc -o main main.o func.o
$ ./main
function from main.c.
$ gcc -o main func.o main.o
$ ./main
function from main.c.

目前上面发生的都在预料之中,现在我们在看看下面的例子:

// main.c
#include <stdio.h>
int main()
{
    func();
}
//func.c
#include <stdio.h>

int d __attribute__((weak)) = 'z';

void __attribute__((weak)) func()
{
    printf("func: d = %d, %s.\n", d, __FILE__);
}

//test.c
#include <stdio.h>

char d = 'h';

void func()
{
    printf("test: d = %d, %s.\n", d, __FILE__);
}

执行结果如下:

$ gcc -c main.c func.c test.c
$ ar qs libf.a func.o
ar: creating libf.a
$ ar qs libt.a test.o
ar: creating libt.a
$ gcc -o main main.o -L. -lf -lt
$ ./main
func: d = 122, func.c.
$ gcc -o main main.o -L. -lt -lf
$ ./main
test: d = 104, test.c.

看,这里好像就不符合前面的规则所说的了,原因就是GCC(准确地说是链接器)对待库是不一样的 —— 默认的,链接器使用第一个找到的符号,后面的就不搜索了。

4、弱符号的用途

问题:我们不确定外部模块是否提供一个函数func,但是我们不得不用这个函数,即自己模块的代码必须用到func函数:

extern int func(void);
...................
int a = func();

我们不知道func函数是否被定义了;
这会导致2个结果:

  1. 外部存在这个函数func,并且EXPORT_SYMBOL(func),那么在我自己的模块使用这个函数func,正确。
  2. 外部其实不存在这个函数,那么我们使用func,程序直接崩溃。

所以这个时候,attribute((weak)) 派上了用场。我们可以在自己的模块定义一个弱符号函数,当函数被声明为一个弱符号时,会有一个奇特的地方:当链接器找不到这个函数的定义时,也不会报错。编译器会将这个函数名,即弱符号,设置为0或一个特殊的值。只有当程序运行时,调用到这个函数,跳转到0地址或一个特殊的地址才会报错。

int  __attribute__((weak))  func(......)
{
    ..........
}

...................
if(func)
{
    int a = func();
}

将本模块的func转成弱符号类型,如果遇到强符号类型(即外部模块定义了func),那么我们在本模块执行的func将会是外部模块定义的func。

如果外部模块没有定义,那么,将会调用这个弱符号,也就是在本地定义的func,直接返回了一个1(返回值视具体情况而定)

相当于增加了一个默认函数。

原理:连接器发现同时存在弱符号和强符号,有限选择强符号,如果发现不存在强符号,只存在弱符号,则选择弱符号。如果都不存在:静态链接,恭喜,编译时报错,动态链接:对不起,系统无法启动。

weak属性只会在静态库(.o .a )中生效,动态库(.so)中不会生效。

5、弱符号带来的问题

// test.c
#include <stdio.h>
#include <stdlib.h>
#include <string.h>

double a;

void print_char(char *v, int len)
{
    int i = 0;
    for(i = 0; i < len; i++)
        printf("%x", v[i]);
    printf("\n");
}

void func_test()
{
    printf("int test.c: &a = %p, a = %lf, sizeof(a) = %d\n", &a, a, sizeof(a));
    print_char((char*)&a, sizeof(a));
    memset(&a, 0, sizeof(a));
    return;
}

// main.c
#include <stdio.h>

int a = 0xFF00FF00;
int b = 0x00FF00FF;

void func_main()
{
    printf("int main.c: &a = %p, a = %x, sizeof(a) = %d\n", &a, a, sizeof(a));
    printf("int main.c: &b = %p, b = %x, sizeof(b) = %d\n", &b, b, sizeof(b));

}

int main()
{
    func_main();
    func_test();
    func_main();
    return 0;
}

执行结果如下:

$ gcc -o main main.c test.c
/bin/ld: Warning: alignment 4 of symbol `a' in /tmp/ccYA9vPA.o is smaller than 8 in /tmp/ccFEeiTw.o
$ ./main
int main.c: &a = 0x601044, a = ff00ff00, sizeof(a) = 4
int main.c: &b = 0x601048, b = ff00ff, sizeof(b) = 4
int test.c: &a = 0x601044, a = 0.000000, sizeof(a) = 8
0ffffffff0ffffffffffffffff0ffffffff0
int main.c: &a = 0x601044, a = 0, sizeof(a) = 4
int main.c: &b = 0x601048, b = 0, sizeof(b) = 4

我们可以看到,在main.c和test.c都有一个变量a,在main.c中的为强符号,在test.c中的为弱符号。因为在test.c中a没有初始化,所以根据规则②得知:编译器选择main.c中的a的值初始化那片内存。不要误认为在test.c中使用global_var1时是用的main.c中的global_var1,我之前错误得这样认为。其实是这样的:main.c中的global_var1和test.c中的global_var1引用的时同一块内存区域,只是在两个文件中代表的意义不同 ---- 在main.c中代表一个int型变量,在test.c中代表一个double型变量,它们的起始地址相同,但占用内存空间是不同的, 在main.c中占用4个字节,在test.c中占用8个字节,这点从上图的两个sizeof输出结果中可以得到验证。

解决这种问题的方法:

  1. GCC编译时增加选项-fno-common;
  2. 养成良好的习惯,所有的全局变量都要初始化(变成强符号);
  3. 尽量不使用全局变量。

本文参考链接:
https://blog.csdn.net/astrotycoon/article/details/8008629
https://zhuanlan.zhihu.com/p/55768978
https://cloud.tencent.com/developer/article/1156898

本文内容由网友自发贡献,版权归原作者所有,本站不承担相应法律责任。如您发现有涉嫌抄袭侵权的内容,请联系:hwhale#tublm.com(使用前将#替换为@)

【C语言】强符号和弱符号 的相关文章

  • [黑科技] WPS通过VB宏函数实现自编号功能

    这篇文章主要是作为李老师 算法设计与分析 助教课程中 与她交流 学到的一些基础知识 它主要是讲述Word通过宏函数设置一些操作 比如在Word全文中替换一些符号 再如对Word上角表进行编号 如果删除中间某个值 运行宏函数自动编号 对Wor
  • 小程序-页面生成为图片,点击保存和分享

    前言 在实际项目中 我们可能会遇到类似的场景 点击后生成一张banner 该banner是图片类型 格式不限 用户在该页面单击之后会生成一个有遮罩的预览 长按之后有保存和分享等操作 一 海报制作 这里我们需要使用canvas来制作banne
  • 已经有int了,为什么要用integer?

    int是JAVA八大基本数据类型 byte shor int long char boolean float double 之一 JAVA语言为八大基本数据提供了包装类 Integer对应是int类型的包装类 就是把int类型包装成Obje
  • ffmpeg基础四:RTP协议

    参考 零声学院 协议学习方法 1 协议是什么 双方约定好如何传输消息 比如视频传输协议 要告诉你这个包是h264包 还是aac音频包 这个信息一般放在协议头 对方收到网络包 可以直接在协议头部获取出这些信息 所以协议的组成一般都是 协议头

随机推荐

  • [Python] 过程型程序设计进阶(三):Python函数装饰器

    概念 修饰器也是一个函数 接受一个函数或方法作为其唯一的参数 并返回一个修饰后的函数或方法 作用 对函数或方法进行一次修饰和包裹 之前学习过 property修饰器 接下来学习如何自定义一个修饰器 自定义函数修饰器 自定义一个装饰器 一般是
  • Jmeter集合点技术(同步定时器)

    一 集合点简介 我们怎么实现真正的并发 并发 指的是系统中正在操作业务的用户 在Jmeter中 成为线程数 Jmeter中的各线程 用户 在进行业务操作中的顺序存储存在一定的随机性 集合点的目的 让各个线程 用户 步调一致 对系统进行加压
  • MySQL8.0连接问题总结

    MySQL8 0连接问题总结 1 驱动包版本 2 驱动类 3 连接属性 1 驱动包版本 对于8 0版本的MySQL数据库 驱动包版本也要跟上 一般使用mysql connector java 8 0 11 否则会报如下错误 Caused b
  • vue代码片段

    Place your snippets for vue here Each snippet is defined under a snippet name and has a prefix body and description The
  • new Object() 和Object.create(null)

    new Object 和Object create null const obj1 a 10 b 20 const obj2 a 10 b 20 obj1 obj2 gt gt gt false 引用类型 因为内存地址不同 const ob
  • Spring之ApplicationContext快速入门

    目录 一 概述 二 代码演示 三 BeanFactory与ApplicationContext的关系 四 BeanFactory的继承体系 五 ApplicationContext的继承体系 一 概述 ApplicationContext称
  • mfc 程序闪退_VC6 在Window10 上操作打开文件时闪退或直接退出的解决方法

    1 下载FileTool exe 并解压 2 打开VC6 0 点击File Open Workspace 选择刚解压出来的FileTool dsw 并确定 3 点击Bulid Build FileTool dll 生成FileTool dl
  • 编程求1平方+2平方+...+n平方

    题目描述 编程求1平方 2平方 n平方 输入 输入一行 只有一个整数n 1 lt n lt 200 输出 输出只有一行 这意味着末尾有一个回车符号 包括1个整数 样例 输入 5 输出 55 提示 循环语句 include
  • 浅谈Visitor访问者模式

    一 前言 什么叫访问 如果大家学过数据结构 对于这点就很清晰了 遍历就是访问的一般形式 单独读取一个元素进行相应的处理也叫作访问 读取到想要查看的内容 对其进行处理就叫作访问 那么我们平常是怎么访问的呢 基本上就是直接拿着需要访问的地址来读
  • [USACO Open08]农场周围的道路

    题目描述 约翰的 N 1 N 10 9 只奶牛要出发去探索牧场四周的土地 她们将沿着一条路走 一直走到三岔路口 可以认为所有的路口都是这样的 这时候 这一群奶牛可能会分成两群 分别沿着接下来的两条路继续走 如果她们再次走到三岔路口 那么仍有
  • linux ./ 执行run文件,如何在Ubuntu中执行.bin和.run文件

    在解释如何在Ubuntu上执行 bin和 run文件之前 让我们首先定义这些文件扩展名到底是什么 Bin档 Ubuntu中的Binary或BIN文件指的是安装软件包 其中大多数是self extracting可执行文件 用于在系统上安装软件
  • BF算法 KMP算法

    BF算法 又叫朴素算法 时间复杂度为O mn 相比KMP算法比较简单 举个例子 对于给定的主字符串 ababbcabcdabcde 和子串 abcd 我们用i和j来分别遍历两个字符串 比较两个i j 对应字符串位置的元素是否相等 如果相等则
  • 应用软件的层次划分

    谈到应用程序的层次 我们平时所说的层次有两种 逻辑的层次 layer 和部署的层次 tier 这两种层次划分的目的是不同的 因此划分方式也有一些差异 能够为应用程序带来的好处也是不同的 逻辑层次逻辑层次 layer 划分的最重要的目的在于调
  • JavaScript设计模式(五)——发布订阅模式、桥接模式、组合模式

    个人简介 个人主页 前端杂货铺 学习方向 主攻前端方向 正逐渐往全干发展 个人状态 研发工程师 现效力于中国工业软件事业 人生格言 积跬步至千里 积小流成江海 推荐学习 前端面试宝典 Vue2 Vue3 Vue2 3项目实战 Node js
  • java父类_java 子类与父类

    子类是由继承得到的类 被继承的类就是父类 子类与父类是 is a 关系 一 子类与父类 1 子类 1 子类定义 class 子类名 extends 父类名 2 子类继承性 子类继承了父类的所有属性和除了构造方法的其余方法 子类与父类在同个包
  • Python实现保留三位有效数字

    网上查找了较多的四舍五入的方法 发现不是自己想要的 于是自己按数目级别写了一段 后面又做了更改 做了简单的整合 整体思路就是取第三位数作判断 如果是第三位是5 再判断第四位的奇偶性 作为小白 代码整体逻辑比较呆板 希望有大神做下修改 定义保
  • ssm框架ajax登录页面,ssm框架登录注册demo

    实例简介 ssm框架登录注册demo html页面 ajax实现登录注册 实例截图 核心代码 ssm ssm pom xml src main java controller TestController java UserControll
  • 海思(MPP)媒体处理软件平台(1)-----功能简介

    概述 HI3531D 海思提供的媒体处理软件平台 Media Process Platform 简称 MPP 可支持应用软件快速 开发 该平台对应用软件屏蔽了芯片相关的复杂的底层处理 并对应用软件直接提供 MPI MPP Programe
  • React渲染顺序及useEffect执行顺序探究(含并发模式)

    前言 在不借助任何演示的情况下 你能清楚地说出 React 组件的渲染顺序以及 useEffect 的执行顺序吗 你知道 React18 并发模式 下执行情况是不同的吗 下面就让我们一起来看一看吧 React 16 先来看目前大部分人还在用
  • 【C语言】强符号和弱符号

    1 强符号 弱符号定义 编译器在编译源程序时 无论你是变量名 函数名 在它眼里 都是一个符号而已 用来表征一个地址 编译器会将这些符号集中 存放到一个叫符号表的 section 中 那么对于两个 c文件中存在的同名的变量 编译器该怎么选择呢