C/C++符号隐藏与依赖管理(二):库的符号隐藏

当程序规模变大之后,人们会对软件进行模块划分,以便分而治之。有了模块之后,就可以将其构建成库(静态库或者动态库)发布给别人使用。

前文所述的符号隐藏手段对于模块内代码的信息隐藏是够的,但是对于库来说是不够的。

当程序规模变大后,我们不可能把所有代码都写到同一个C文件或者CPP文件中。当代码被拆分到多个实现文件中,它们之间需要互相访问就必须通过头文件暴露自己的可访问API给别人。但是当所有文件都被打包在一起编译成库再提供给第三方的时候,这些内部开放的接口却未必都需要被作为库接口暴露出去。

常见的一种做法是将库的内部头文件和外部的头文件分开,对外不发布内部头文件。这是C/C++常用的一种库级别的头文件管理手段,后面我们会专门介绍。遗憾的是,仅通过不发布私有头文件,并没有解决所有问题。

即便不发布内部头文件,内部跨编译单元可被访问的符号默认情况下仍旧会被库全部导出。这样不仅浪费了二进制的空间,增加了库之间符号冲突的概率,而且还让软件包承担了不必要的安全风险。导出的内部符号仍旧可以被外部强制extern,或者是被拿来做一些hack的事情。

现代编程语言会引入module机制来管理软件模块或者库的外部可见性问题,让开发者在发布软件的时候显示的指定需要导出给外部的API,其它的符号都只能被内部访问。但是C和C++语言由于历史包袱重(新的特性需要尽量兼容已经编译过的既有代码),C++语言直到20版本才将module特性标准化,而C语言的module特性至今仍不见踪影。(事实上Java的module特性从2011年提出直到2017年才通过Java9发布,也历时七年之久)。

由于C++20标准刚刚出来不久,编译器对module机制的支持还很不完善,所以该特性离进入实用还有不少距离。感兴趣的同学可以看看我的朋友张超写的这篇文章《C++ Modules 初窥》

回到现实中,在没有语言直接支持的情况下,我们如何隐藏库的内部符号,显示的指定需要导出的API呢?

方法是有的,那就是借助编译器扩展。

GCC4之后支持使用-fvisibility=hidden编译选项,将库的所有符号默认设置为对外不可见。这样编译出的二进制就不会导出可供外部链接的符号。然后再结合GCC的__attribute__ ((visibility ("default")))属性,在代码中明确指定可以暴露给外部的API,于是我们就可以显示的控制库的对外API的可见性。

如下代码示例:

// entry.h

void function1();
__attribute__ ((visibility ("default"))) void entry_point();
// entry.cpp

#include "entry.h"

void function1() {
    // ...
}

void entry_point() {
    function1();
}

当我们采用-fvisibility=hidden将entry.cpp编译成静态库或者动态库后,无论用户是静态链接还是使用dlopen动态库的方式,都只能访问到void entry_point()函数,而不能访问到void funcion1()

通过该方法,我们不仅能显示控制库的导出API,还可以帮助编译器和链接器优化出更好的二进制,并且缩短动态库的加载时间。

Windows下也有类似的机制__declspec(dllexport),它和gcc下的__attribute__ ((visibility ("default")))作用类似。稍微不同的是Windows下还存在__declspec(dllimport)用于API的使用方显示导入外部API,以便编译器对代码进行优化,但gcc下没有对应的扩展。

为了让使用上述编译器扩展的代码能够跨平台,使用该特性的时候可以封装一个宏,根据代码所在的平台和编译器版本,自动转化成不同的实现。

// keywords.h

#if defined _WIN32 || defined __CYGWIN__
  #ifdef BUILDING_MOD
    #ifdef __GNUC__
      #define MOD_PUBLIC __attribute__ ((dllexport))
    #else
      #define MOD_PUBLIC __declspec(dllexport) // Note: actually gcc seems to also supports this syntax.
    #endif
  #else
    #ifdef __GNUC__
      #define MOD_PUBLIC __attribute__ ((dllimport))
    #else
      #define MOD_PUBLIC __declspec(dllimport) // Note: actually gcc seems to also supports this syntax.
    #endif
  #endif
  #define MOD_LOCAL
#else
  #if __GNUC__ >= 4
    #define MOD_PUBLIC __attribute__ ((visibility ("default")))
    #define MOD_LOCAL  __attribute__ ((visibility ("hidden")))
  #else
    #define MOD_PUBLIC
    #define MOD_LOCAL
  #endif
#endif

如上参考了"https://gcc.gnu.org/wiki/Visibility"中给出的宏定义。它根据不同的平台和编译器版本,定义了MOD_PUBLICMOD_LOCAL的不同实现。

#include "keywords.h"

MOD_PUBLIC void function(int a);

class MOD_PUBLIC SomeClass
{
   int c;
   // Only for use within this DSO(Dynamic Shared Object)
   MOD_LOCAL void privateMethod();
public:
   Person(int _c) : c(_c) { }
   static void foo(int a);
};

如上的例子中,void function(int a)class SomeClass在库的内部和外部都可访问,但是类的void privateMethod()接口只能在库的内部使用,外部是无法使用的。

至此,我们给出当前现状下C/C++库级别API的管理建议:可以使用编译选项默认隐藏库的符号,然后使用编译器属性显示指定库需要导出的API

最后我们补充一点对动态库的要求。

不同平台对于静态库和动态库的使用大部分时候是相似的,但在某些细节上仍然会有区别。

所有平台下的静态库(.a或者.lib)都是可以缺符号的,即在生成时可以存在待链接的外部符号。然而对于动态库,OSX下要求不能缺符号(OSX下动态库是dylib格式,生成时是需要链接成功的,如果缺符号链接器会报错)。而在Linux系统下动态库(.so)生成的时候却是可以缺符号的。

在Linux下,如果是在链接期使用缺符号的so,需要构建目标通过指定其它的动态库或者静态库为缺失符号的so把符号补全,否则就会链接失败。而如果是采用dlopen的方式打开so的话,那么该so必须自身符号是完备的,否则在动态加载的时候会出错。

因此,这里我们给出另一个C/C++库符号管理的建议:保证动态库不要缺符号,是自满足的。如果违反了这条原则,那么这个动态库就无法用于动态加载;即使只是链接期使用,因为把符号缺失的细节泄露给了使用者,造成使用方的麻烦,所以也是不推荐的。

动态库可以和静态库进行链接,以获取自己需要的符号。但是有些时候我们只想要和静态库进行链接,却不想在动态库中将静态库中的符号间接暴露出去。这时可以采用-fvisibility=hidden选项重新编译该静态库。但遗憾的是我们不总是能够控制第三方静态库的编译过程,这时可以借助链接器提供的显示指定符号表的方法。该方法需要按照链接器的规范写一个导出符号表,在链接期通过参数传递给链接器,这样就可以精细的控制动态库需要暴露的符号了。该方法并不常用,因此我们不多做介绍,具体用法可以参考https://www.gnu.org/software/gnulib/manual/html_node/LD-Version-Scripts.html

而动态库和动态库的链接,其实并不需要把对方的二进制真实链接进来。目标的动态库会记住它所依赖的动态库(通过目标动态库中的rpath)。这种情况下也算该动态库是自满足的,因为用户在使用该动态库的时候,并不需要再为其寻找依赖。

最后我们总结一下对于库符号管理的一些建议:

1)推荐使用编译选项默认隐藏库的所有符号,然后使用编译器属性显示指定库需要导出的API;
(建议对该方法进行封装,以保证代码兼容各种平台和编译器版本)

2)保证动态库不要缺符号,是自满足的;

C/C++符号隐藏与依赖管理(三):头文件管理

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

推荐阅读更多精彩内容