美文网首页
编译与链接——函数签名与extern C的作用

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

作者: 侠之大者_7d3f | 来源:发表于2021-05-08 11:25 被阅读0次

    前言

    使用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);
    }
    

    相关文章

      网友评论

          本文标题:编译与链接——函数签名与extern C的作用

          本文链接:https://www.haomeiwen.com/subject/uptudltx.html