美文网首页
编译与链接——函数签名与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的作用

    前言 使用nm 查看目标文件的符号表发现, 函数名进行了包装修饰: 为什么一个简单的func1 要包装为_Z5fu...

  • c和c++混编注意事项

    1、c的编译器与c++编译器生成函数签名机制不同,所以在cpp文件中引用c语言的函数需要加 extern C{ ...

  • 面试题之关键字

    C语言中extern 的作用,extern “c”的作用? ①extern 可以置于变量或者函数前,以标示变量或函...

  • C++编程思想第二章对象的创建与使用——读书笔记

    创建程序 编译——链接——装载——生成可执行文件 声明与定义 变量声明,函数声明 变量定义,函数定义 extern...

  • c++之extern "c"

    extern "c" 被extern "c"修饰的代码将用c语言方式编译。例如对重载函数进行修饰,将编译不通过,因...

  • C++常见基础知识

    1.在C++ 程序中调用被C 编译器编译后的函数,为什么要加extern “C”? 答:首先,extern是C/C...

  • C++工程师常见的面试题总结

    1.在C++ 程序中调用被C 编译器编译后的函数,为什么要加extern “C”? 答:首先,extern是C/C...

  • C++面试常见问题上(含答案)

    1.在C++ 程序中调用被C 编译器编译后的函数,为什么要加extern “C”? 答:首先,extern是C/C...

  • 2018-04-25

    总结 签名导出:使用extern "C"导出函数对函数名有约束,也可以使用struct包装所有函数签名 版本控制:...

  • extern 标识符的作用

    extern 标识符的作用 1.在C语言当中,extern符号的作用主要是声明变量和函数 比如 在A.c文件中...

网友评论

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

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