冰山之下:从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头(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并关注,获取更多最新文章 , 学习使我快乐!

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

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 215,384评论 6 497
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 91,845评论 3 391
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 161,148评论 0 351
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 57,640评论 1 290
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 66,731评论 6 388
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 50,712评论 1 294
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,703评论 3 415
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 38,473评论 0 270
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,915评论 1 307
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,227评论 2 331
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,384评论 1 345
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 35,063评论 5 340
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,706评论 3 324
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,302评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,531评论 1 268
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 47,321评论 2 368
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,248评论 2 352

推荐阅读更多精彩内容