冰山之下:从extern C到程序链接


阅读C++代码的时候,或者看一些教程的时候,我们经常会看到这么一个关键字 extern "C",例如我们在 Python调用C++之PYBIND11源码分析这篇文章中对举的例子进行宏展开后看到,被用作模块初始化的函数 initexample()前面就有 extern "C"所修饰。那么extern “C”`是干什么的呢?
解释它很简单,一句话就可以:它是链接指示符,用来告诉编译器不要更改它所修饰的函数或者变量等的名字,确保链接器能正确将目标文件链接在一起。
但是要解释为什么不使用就链接不通过,就说来话长了。

改名(name mangling)

改名,翻译成重命名可能更符合习惯,就是对C++中定义的函数等的名字按照一定规则去更改,至于是什么规则,有于C++标准没有说明,所以C++编译器们各显神通,每个编译器重命名的方法自己定,只要确保名字是唯一的就行。改名的原因也很简单,因为C++支持重载,这导致同一个作用域中很可能出现很多函数名字都一样,这就给编译器生成代码造成了困扰。大家都叫张三,其中一个张三打架了,你总不能让所有张三的家长都来学校吧。绝顶聪明的编译器工程师总会有办法解决,既然都叫张三,可以加前缀后缀后缀嘛。张三A、张三B、张三C……别的班可以根据需要改成张三甲、张三乙、张三丙……
改名字虽然解决重载的问题,但是却给链接带来的麻烦。如果大家都是C++编译器还好,只要两个编译器的改名规则一样,当链接外部库的时候就能链接的上。但是如果是库使用C++编译的,当前编译程序使用C编译器呢?问题就来了,C中是没有改名这种说法的,C不支持重载没必要多此一举,所以C编译器不会对函数名字做修改,这样就导致链接器做链接的时候,拿着一个头文件中声明的正常的名字去二进制库文件里面找一个改过名字的函数,怎么叫它都不会答应,就会导致出现下面这种错误。

undefined reference to `balabala'
ld returned 1 exit status

问题一个一个来,那就一个一个解决,既然改名了找不到,那就允许指定某些函数不改名,因此引入了extern "C"这个指示符。被这个指示符限定的函数,或者在其中的代码块中的函数变量等,不做改名处理,它们原本叫什么就是什么,命名冲突的问题让程序员自己解决。这样链接器就能顺利进行链接。
需要注意的是extern "C"应该被看作一个整体,实际上"C"用来表示函数使用什么语言编写,所有C++编译器都被要求支持"C",当然如果使用的编译器支持,你也可以使用extern "Ada", extern "FORTRAN"等,让编译器知道使用何种方式去调用其中定义的函数。它表明了这些函数应该按照对应函数的调用方式去调用。这样做的好处不仅是别的语言可以调用你,你也可以调用别的语言,因为大家已经遵循了同一个语言层面的ABI。关于ABI的内容,我在交叉编译和ABI简介中介绍过,感兴趣的朋友可以去看一下。

下面用个小例子做简单演示:

// defined in library.h
#ifdef __cplusplus
extern "C" {
#endif

int my_function(int);

#ifdef __cplusplus
}
#endif

// defined in library.cpp
#include <iostream>
# include "library.h"

int my_function(int arg) {

    std::cout << "arg is " << arg << std::endl;
}

// defined in main.c
#include "library.h"
int main(int argc, char* args[]) {
    my_function(9);
    return 0;
}

将上面的代码保存为三个对应的文件,使用下面两条命令去编译:

g++ -shared -fPIC library.cpp library.h -o libmylib.so 
gcc main.c -o main -lmylib -L .

如果头文件library.h中函数的声明没有加extern "C"是编译不过去的。在这个例子中有两个小知识点,第一就是extern "C"可以单独用在一行,以可以使用花括号将代码包起来;第二就是#ifdef __cplusplus这个预处理语句,它使得一份代码在C和C++中都能编译。__cplusplus这个宏定义也是不需要我们自己定义的,编译器会根据自身类型决定这个宏定义有没有。如果我是炎黄子孙,那么我肯定带有龙的传人这个标签,不需要别人专门在你胸前别一个。

如果为了解释extern "C",到这里差不多也就够了。但好奇心驱使我,继续寻找问题的答案。比如,链接器是怎么工作的,它为什么需要确切的函数名字,以及拿到名字后如何在一个二进制文件中找到它?

在说链接器如何工作之前,我们还需要先简单说说ELF格式。

Executable and Linkable Format

ELF(Executable and Linkable Format) 是一种为动态链接库、可执行文件、目标文件和core dumps文件设计的一种通用格式标准。ELF的设计目标是实现一种灵活的、可扩展和跨平台格式。在Linux平台下可以通过readelf这个工具去读取它的内容。
由于ELF既可以是一个库文件,也可以是一个可执行程序,因此它可以有以下两种视角:
在这里插入图片描述

其中,每个ELF文件都有一个ELF头(ELF header),它位于文件的开始,用于描述整个文件的内容概貌,它包含ELF魔数、文件类型、目标机器的架构、程序入口、程序信息表、段信息表的地址等信息。

程序信息表(program header table)用来告诉系统如何创建这个程序的一个进程,它在可执行文件中必须存在,在可重定位文件(relocatable files )中可以没有。段信息表(section header table)中有录着文件中每个段的名字、大小、位置等元信息,每个段在这个表里面都会有一条记录与之对应。可链接文件必须有段信息表,其他类型文件可以没有。另外,在ELF中除了ELF头位置固定以外,其他段的信息位置和顺序都可以是不固定的。

ELF中有很多的段,分门别类的记录了关于这个文件的信息,比如:记录机器指令的段(.text);记录程序初始化数据的段(.data);记录版本信息的字段(.comment);记录字符串的段(.strtab)等等。其中有几个字段和程序链接关系密切,链接器就是根据它们的信息去链接程序的。它们就是符号表段(.symtab)、依赖段(.dynamic)、重定位段(.rel, .rela)。由于本文主要主要讲解extern "C"这个命令禁止函数重命名来防止链接程序的时候出错,因此我们只着重将符号表依赖。

可执行文件、库文件或者目标文件都会包含一个符号表,符号表的每一条记录表示一个符号(symbol,为了不混淆,下面把符号表中的一个符号称为一条记录),每一条记录和程序中的出现的函数、变量等一一对应。简单的看(实际上不是,只是为了方便说明),每条记录包含三个字段:名字、类型和值。名字字段保存了指向了前面提到的字符串段(.strtab)中记录记录的名字的地址;类型保存这条记录是一个函数或者是变量;值一般保存这条记录实际的地址偏移。例如我们之前的例子中,函数my_function在符号表里面就有一条记录和它对应,其中记录的名字字段指向了字符串段中my_funcion这个字符串,类型字段存储了它是一个函数类型,值字段存储它世纪的内容的地址。

符号(symbol)一般可以分为两种:一种是它的值记录地址的表示的内容能在本文件中找到,这种符号称为已定义符号;另一种是符号的值表示的值不在本文件中,这种符号称为未定义符号。

依赖段中记录本文件对其他文件的依赖,它里面保存了依赖的名字等内容。

铺垫就这么多,接下来可以简单说说链接器怎么工作了。

动态链接

程序的运行,通常主要步骤有以下几步:

  1. 装载可执行文件;
  2. 装载可执行文件的依赖,依赖的内容在依赖表(.dynamic)中;
  3. 链接和重定位;
  4. 将控制权交给程序。

第三步主要是动态链接器来完成,它的工作就是为已定义的符号一个确切的地址,然后通过解析未定义的符号的地址。怎么解析的呢?就是根据符号的名字来解析,它会拿着符号的名字去所有依赖中一个一个找,如果依赖中有依赖继续递归式的找,直到在某个依赖中找到一个和这个符号名字一样的已定义符号或者以找不到而告终。找到就链接成功找不到就报错终止程序执行。真相大白,如果可执行文件依赖的某个动态库改了名字,就会导致最终找不到这个在可执行文件中未定义的符号的定义,这就是为什么要阻止编译器改名。编译的时候的链接器就相当于为动态链接器做了一次预演。

豁然开朗。

总结

extern "C"的目的是为告诉C++编译器不要对它修饰的函数等进行改名,主要依据就是有可能别的编译器是不进行改名操作或者改名规则不同,这会使得链接器按着未定义的符号的名字却找不到找另一个同名字的已定义的符号,导致链接失败。

公众号二维码

本文首发于个人微信公众号TensorBoy。微信扫描上方二维码或者微信搜索TensorBoy并关注,及时获取更多最新文章!

References

[1] Name mangling (C++ only)
[2] Questions about extern “C” linkage directive - C / C++
[3] How does C++ linking work in practice?
[4] Linkers part 2
[5] Static, Shared Dynamic and Loadable Linux Libraries
[6] http://www.skyfree.org/linux/references/ELF_Format.pdf

发布了45 篇原创文章 · 获赞 4 · 访问量 1万+

猜你喜欢

转载自blog.csdn.net/ZM_Yang/article/details/104712889
今日推荐