记一次gcc链接提示符号未定义错误

一、前情提要

上周在使用第三方库的时候,出现链接提示符号未定义的情况。但是使用readelf查看了第三方so内部符号的属性时,可以看到实际是有定义的,但是so内部的符号与我报错的符号存在一定的差异。这就涉及到了extern "C"的作用和g++跟gcc之间对符号处理上的一些差异,拖了很久,今天完成一下这个问题的输出吧。

二、符号差异

1、现象

我们这里简单模拟复现一下当时的情景。简单准备了一个so的源码“func.cpp”,包括其头文件"func.h"定义:

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

int func(int a, int b);

// func.cpp
#include "func.h"

int func(int a, int b) {
  return a + b;
}

使用g++ -shared -fPIC -o libfunc.so func.cpp命令编译生成对应的so文件。
在这里插入图片描述

然后我们在简单写个main函数,并在main函数中去调用该so实现的func函数,源码“main.c”如下:

// main.c
#include "func.h"

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

我们尝试编译一下main.c和对应的libfunc.so。用以下命令gcc main.c -o exec -L./ -lfunc
在这里插入图片描述

此时就会遇到和我在工作中链接第三方库时出现的问题了,提示main.c中的存在一个未定义却引用的符号undefined reference to 'func’

2、初步探究

当时还挺懵的,我明明定义了int func(int a, int b)啊,就在func.cpp里面,我也链接上了对应的so库,编译命令中的"-lfunc"就是很好的证明。那为什么还会提示符号未定义呢?于是我使用了readelf命令,查看了func在libfunc.so中的情况。readelf -a ./libfunc.so | grep "func",结果如下:
在这里插入图片描述
可以看到在libfunc.so中存在定义,但是这个符号看着和上面链接时报错提示的符号长得不太一样啊,应该就是这个问题导致链接时找不到符号定义了。根据符号的形式,基本确定是由于so使用g++编译导致符号被编译器修改,而gcc在编译main.c的时候却依旧使用的原符号“func”,导致了符号未定义。

3、g++和gcc在编译时对符号处理的差异

出现了这样的问题,赶紧查一波资料巩固一下g++对符号处理的原理。

大概可以归纳为以下几点:
1)除了全局变量不用做改编之外,其它所需要改编符号的时候,都是以_Z开始;
2)若想表示某个符号是在命名空间或类中的,要以“N”开始,以“E”结束;
3)所有的名字空间名、类名、函数名或变量名,改编的时候都是名字所包含的字符数加上真正的名字;
4)所有的名字按照从外层到里层的顺序进行改编;
5)如果是函数的话,所有的参数按照前后出现的顺序进行改编。
(此处参考博文:https://blog.csdn.net/roland_sun/article/details/43233565)

我们参照上面的规则,手动修饰下func函数:
1)func为全局函数符号,因此以"_Z"开头;
2)由于是全局符号,不需要以“N”开头,和以“E”结尾;
3)变量名“func”一共四个字符,所以是“4func”
4)参数为“int a, int b”,因此按顺序为“ii”

综上所述,“func”在经过g++编译修饰后,应该为“_Z4funcii”,与我们使用readelf查看到的到的情况完全一致。

该修饰过程,全程由编译器“g++”自主完成,对使用者无感知。而“gcc”则不会对符号进行修饰。这是为什么呢?为什么要差别对待呢?

这是因为C++区别于C新增了命名空间、类、函数重载等新特性,因此会存在不同命名空间,或者不同类,或者同类中等,存在同名函数的情况。为了保证在编译后,目标文件中符号的唯一性,因此g++对c++的原文件的符号进行了修饰,因此我们在了解了修饰规则后,我们可以根据修饰后的符号找到对应的具体符号位置。

三、问题解决

既然找到了问题的根因,那么我们就要想办法解决了。由于第三方库是由第三方直接编译后发布给我,我缺少源码文件也无法重新编译,因此只能从我这一侧先想办法解决了。

1、g++编译main.c

既然库内部的符号我们无法改变,那我们只能将main.c编译时也同样进行符号修饰。我尝试使用g++对main.c进行编译。

我们使用命令g++ -c -o main.o main.c编译源文件并生成目标文件“main.o”。在编译完成后,我们再使用readelf命令查看目标文件中func符号的存在形式,结果如下图所示:
在这里插入图片描述
可以看到目标文件“main.o”中,符号"func"已经变成了修饰后的形式“_Z4funcii”,那接下来的链接过程应该就不再会提示符号找不到的问题了。我们来试一下:
在这里插入图片描述
可以看到我们已经能正常进行so的链接了。生成的可执行文件“exec”运行也是没有什么问题。那这个问题到目前为止,已经基本解决了。

但是我们还是想按照规范来,g++编译C++文件,gcc编译C文件,这时候应该要怎么办呢。对于这种C接口,还是希望可以保留原有的符号名,而非修饰后的符号名,那么我们需要使用extern “C”来对接口进行修饰。

2、extern “C”

我们常常能在头文件中可以看到以下的结构:

#ifdef __cplusplus
extern "C" {
#endif

/*
 ...符号声明...
 */

#ifdef __cplusplus
}
#endif

这一段代码有什么用呢?我们先在之前的测试代码中尝试使用一下,看看有什么效果。我们改造一下头文件func.h,改造后如下所示:

// func.h
#ifndef _FUNC_H_
#define _FUNC_H_

#ifdef __cplusplus
extern "C" {
#endif

#include <stdio.h>

int func(int a, int b);

#ifdef __cplusplus
}
#endif
#endif

然后尝试重新编译“libfunc.so”:
在这里插入图片描述

我按照修改后的头文件重新编译了so库,我们再用readelf查看一下新的“libfunc.so”中的“func”符号的情况:

在这里插入图片描述
我们发现,在头文件中加入上面那段代码后,so库内的符号名变成了未修饰的样子。那是不是我们的main.c文件可以直接使用gcc进行编译连接了呢?尝试一下:
在这里插入图片描述
确实可以直接链接生成可执行文件了。根据上文中对g++和gcc对符号处理的差异性介绍,我们可以得出结论,extern “C”“告诉”编译器,接下来的这段代码,需要按照C语言标准进行编译,那么符号名将不会被编译器修饰

那么对于C类型接口,我们应当使用extern “C”来进行声明,这样我们在C类型源文件中引用该符号时,可以使用“gcc”编译器对源文件进行编译,不需要为了保持符号一致而使用“g++”对“C”文件进行编译了。

三、总结

至此,我们已经完全解决了此次遇到的符号未定义的问题,并且深究了其相关原理和相应的解决方案。这次的问题,在事后看来,其实很简单,也很好解决,但是在事发当时却让我一头雾水,也有可能是因为当时加班挺晚了,导致大脑不够灵活了,哈哈哈。

问题虽然简单,但是我们还是需要去深究其根因是什么,了解和学习相关原理,然后尝试从多角度多方案去解决问题,这样才能有所成长!

也欢迎各路大神来我的个人网站(www.ccccxy.top/coding)留言和指导,本文也会同步更新至个人网站,多谢!

生命不息,bug不止,程序猿,道阻且长啊~

已标记关键词 清除标记
©️2020 CSDN 皮肤主题: 数字20 设计师:CSDN官方博客 返回首页