一、前情提要
上周在使用第三方库的时候,出现链接提示符号未定义的情况。但是使用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不止,程序猿,道阻且长啊~