前言
使用nm
查看目标文件的符号表发现, 函数名进行了包装修饰:
nm SimpleSectipn.o
0000000000000000 D g_index
0000000000000000 D global_init_var
U _GLOBAL_OFFSET_TABLE_
0000000000000000 B global_uninit_var
0000000000000028 T main
0000000000000000 T _Z5func1i
0000000000000000 T _Z5func2v
U _Z6printfPKcz
0000000000000004 d _ZZ4mainE10static_var
0000000000000004 b _ZZ4mainE11static_var2
为什么一个简单的func1
要包装为_Z5func1i
如此奇怪的名称, 包装肯定是有目的的,为了防止符号冲突, 比如定义了一个 hello()的函数,一个 int hello=1的变量, 2者如果都用hello作为符号则势必引起冲突,因此为了解决这些符号冲突,有必要使用一定的修饰规则对变量和函数名进行修饰防止冲突。
C++函数签名
测试代码:
// 函数重载, overload
int add(int a, int b){return a + b; }
float add(float a, float b){return a + b; }
// class嵌套
class C {
int add(int a, int b){return a + b;}
class C2 {
int add(int a, int b){
return a + b;
}
};
};
// namepace
namespace test
{
int add(int a, int b){return a + b;}
class C {
int add(int a, int b){return a + b;}
};
} // namespace test
使用GCC进行编译, 采用readelf -s xxx.o
查看符号表:
Symbol table '.symtab' contains 12 entries:
Num: Value Size Type Bind Vis Ndx Name
0: 0000000000000000 0 NOTYPE LOCAL DEFAULT UND
1: 0000000000000000 0 FILE LOCAL DEFAULT ABS simpleSymbol.cpp
2: 0000000000000000 0 SECTION LOCAL DEFAULT 1
3: 0000000000000000 0 SECTION LOCAL DEFAULT 2
4: 0000000000000000 0 SECTION LOCAL DEFAULT 3
5: 0000000000000000 0 SECTION LOCAL DEFAULT 5
6: 0000000000000000 0 SECTION LOCAL DEFAULT 6
7: 0000000000000000 0 SECTION LOCAL DEFAULT 7
8: 0000000000000000 0 SECTION LOCAL DEFAULT 4
9: 0000000000000000 24 FUNC GLOBAL DEFAULT 1 _Z3addii
10: 0000000000000018 30 FUNC GLOBAL DEFAULT 1 _Z3addff
11: 0000000000000036 24 FUNC GLOBAL DEFAULT 1 _ZN4test3addEii
上面符号表中没有包含 class中的add函数, 这个暂时还不清楚。
-
_Z3addii ---- 全局的函数 int add(int a, int b): GCC中规则采用_Z修饰前缀, 3代表函数名的长度,add为3个字符, i, i代表输出参数的类型, i代表int类型。
-
_Z3addff --- 全局的函数 float add(float a, float b) 与int add类似,只是输入参数用f表示, f为float
-
_ZN4test3addEii --- 命名空间中的函数, test为命名空间的名称, E不清楚, ii代表2个int
Linux中提供了一个解析函数签名的工具: c++filt
:
$ c++filt _ZN4test3addEii
test::add(int, int)
函数签名/命名修饰引起的问题
如果不同编译器厂商的修饰规则不同,显然不同编译器编译出的目标文件在链接时候会出现找不到符号或者不认识的情况, 因此函数签名不同是造成不同编译器的目标文件不能链接的原因之一, 比如GCC和VC++的签名规则。
extern "C" 的作用
测试, 使用相同的代码,分别命名为 SimpleSection.cpp 和SimpleSection.c, 使用GCC进行编译, 观察符号表:
- SimpleSection.cpp
readelf -s SimpleSectipn.o
Symbol table '.symtab' contains 22 entries:
Num: Value Size Type Bind Vis Ndx Name
0: 0000000000000000 0 NOTYPE LOCAL DEFAULT UND
1: 0000000000000000 0 FILE LOCAL DEFAULT ABS SimpleSectipn.cpp
2: 0000000000000000 0 SECTION LOCAL DEFAULT 1
3: 0000000000000000 0 SECTION LOCAL DEFAULT 3
4: 0000000000000000 0 SECTION LOCAL DEFAULT 4
5: 0000000000000000 0 SECTION LOCAL DEFAULT 5
6: 0000000000000000 0 SECTION LOCAL DEFAULT 6
7: 0000000000000000 0 SECTION LOCAL DEFAULT 8
8: 0000000000000004 4 OBJECT LOCAL DEFAULT 3 _ZZ4mainE10static_var
9: 0000000000000004 4 OBJECT LOCAL DEFAULT 4 _ZZ4mainE11static_var2
10: 0000000000000000 0 SECTION LOCAL DEFAULT 10
11: 0000000000000000 0 SECTION LOCAL DEFAULT 11
12: 0000000000000000 0 SECTION LOCAL DEFAULT 12
13: 0000000000000000 0 SECTION LOCAL DEFAULT 9
14: 0000000000000000 4 OBJECT GLOBAL DEFAULT 3 global_init_var
15: 0000000000000000 4 OBJECT GLOBAL DEFAULT 4 global_uninit_var
16: 0000000000000000 40 FUNC GLOBAL DEFAULT 1 _Z5func1i
17: 0000000000000000 0 NOTYPE GLOBAL DEFAULT UND _GLOBAL_OFFSET_TABLE_
18: 0000000000000000 0 NOTYPE GLOBAL DEFAULT UND _Z6printfPKcz
19: 0000000000000000 28 FUNC GLOBAL DEFAULT 6 _Z5func2v
20: 0000000000000000 4 OBJECT GLOBAL DEFAULT 8 g_index
21: 0000000000000028 55 FUNC GLOBAL DEFAULT 1 main
- SimpleSection.c
readelf -s SimpleSectipn.o
Symbol table '.symtab' contains 23 entries:
Num: Value Size Type Bind Vis Ndx Name
0: 0000000000000000 0 NOTYPE LOCAL DEFAULT UND
1: 0000000000000000 0 FILE LOCAL DEFAULT ABS SimpleSectipn.c
2: 0000000000000000 0 SECTION LOCAL DEFAULT 1
3: 0000000000000000 0 SECTION LOCAL DEFAULT 3
4: 0000000000000000 0 SECTION LOCAL DEFAULT 4
5: 0000000000000000 0 SECTION LOCAL DEFAULT 5
6: 0000000000000000 0 SECTION LOCAL DEFAULT 6
7: 0000000000000000 0 SECTION LOCAL DEFAULT 8
8: 0000000000000004 4 OBJECT LOCAL DEFAULT 3 static_var.1922
9: 0000000000000000 4 OBJECT LOCAL DEFAULT 4 static_var2.1923
10: 0000000000000000 0 SECTION LOCAL DEFAULT 10
11: 0000000000000000 0 SECTION LOCAL DEFAULT 11
12: 0000000000000000 0 SECTION LOCAL DEFAULT 12
13: 0000000000000000 0 SECTION LOCAL DEFAULT 9
14: 0000000000000000 4 OBJECT GLOBAL DEFAULT 3 global_init_var
15: 0000000000000004 4 OBJECT GLOBAL DEFAULT COM global_uninit_var
16: 0000000000000000 40 FUNC GLOBAL DEFAULT 1 func1
17: 0000000000000000 0 NOTYPE GLOBAL DEFAULT UND _GLOBAL_OFFSET_TABLE_
18: 0000000000000000 0 NOTYPE GLOBAL DEFAULT UND printf
19: 0000000000000000 23 FUNC GLOBAL DEFAULT 6 func2
20: 0000000000000000 0 NOTYPE GLOBAL DEFAULT UND puts
21: 0000000000000000 4 OBJECT GLOBAL DEFAULT 8 g_index
22: 0000000000000028 55 FUNC GLOBAL DEFAULT 1 main
同样的代码, C++ , C编译器生成的符号表不同,说明C++, C的符号修饰规则不同:
函数/变量 | C++ | C |
---|---|---|
global_init_var | global_init_var | global_init_var |
global_uninit_var | global_uninit_var | global_uninit_var |
func1 | _Z5func1i | func1 |
func2 | _Z5func2v | func2 |
printf | _Z6printfPKcz | printf |
通过简单的比较可以发现:
- C编译器/C代码 不进行符号修饰
- C++编译器/C++代码 存在符号修饰
- C和C++采用GCC编译, 变量符号相同, 函数符号不同,主要原因是C++为了支持重载和命名空间机制,进行了符号修饰
存在的问题 : C++ 代码如果直接调用C语言的库函数,由于C++的函数签名机制使得调用的函数在C库中找不到,出现undefined reference
.
测试, 准备2个cpp源码文件:
Test1
- c_utils.cpp
int add(int a, int b){
return a + b;
}
- main.cpp
#include<stdio.h>
extern int add(int a, int b);
int main(){
int ret = add(5, 10);
printf("ret: %d\n", ret);
}
直接使用gcc进行编译: gcc main.cpp c_utils.cpp
可以正确编译链接。 使用nm
查看符号, add
函数被C++编译器修饰为 _Z3addii
, 由于c_utils.cpp也是采用C++编译的,因此c_utils.cpp中的add和main.cpp中的add引用 具有相同的函数命名修饰,所以链接没有问题。
nm a.out | grep add
0000000000001184 T _Z3addii
(base)
Test2
将c_utils.cpp改为 c_utils.c, 然后用gcc进行编译, 产生了链接错误: undefined reference to `add(int, int)'
gcc main.cpp c_utils.c
/usr/bin/ld: /tmp/ccGw3nSE.o: in function `main':
main.cpp:(.text+0x17): undefined reference to `add(int, int)'
collect2: error: ld returned 1 exit status
原因分析: c_utils.c 采用C编译,因此add函数不进行命名修饰, 但是main.cpp采用c++编译器编译,引用的add函数会按照C++命名修饰规则修饰, 这样导致2者函数符号不匹配,当然链接器找不到 add函数了。
C++ 兼容C代码的解决方案 —— extern "C"
C语言不支持extern C,只有C++才支持extern C, 使用方法: 使用 extern "C" 将C代码包含, 被包含的代码当做C代码进行处理,不使用C++的命名修饰机制。
测试:加上extern "C", GCC编译正确:
#include<stdio.h>
extern "C"{
extern int add(int a, int b);
}
int main(){
int ret = add(5, 10);
printf("ret: %d\n", ret);
}
使用nm
查看符号表, 果然 add函数没有使用C++的函数签名:
$ nm a.out | grep add
0000000000001184 T add
通用的解决方案: 使用 __cplusplus
宏进行判断,如果当前编译单元为C++, 则extern C起作用:
#include<stdio.h>
#ifdef __cplusplus
extern "C"{
#endif
extern int add(int a, int b);
#ifdef __cplusplus
}
#endif
int main(){
int ret = add(5, 10);
printf("ret: %d\n", ret);
}
网友评论