编译与链接——函数签名与extern C的作用

2021-05-08  本文已影响0人  侠之大者_7d3f

前言

使用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函数, 这个暂时还不清楚。

Linux中提供了一个解析函数签名的工具: c++filt:

$ c++filt _ZN4test3addEii
test::add(int, int)

函数签名/命名修饰引起的问题

如果不同编译器厂商的修饰规则不同,显然不同编译器编译出的目标文件在链接时候会出现找不到符号或者不认识的情况, 因此函数签名不同是造成不同编译器的目标文件不能链接的原因之一, 比如GCC和VC++的签名规则。


extern "C" 的作用

测试, 使用相同的代码,分别命名为 SimpleSection.cpp 和SimpleSection.c, 使用GCC进行编译, 观察符号表:

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
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库中找不到,出现undefined reference.

测试, 准备2个cpp源码文件:

Test1

int add(int a, int b){
    return a + b;
}
#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);
}
上一篇下一篇

猜你喜欢

热点阅读