• 【转】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++符号隐藏与依赖管理(三):头文件管理



    作者:MagicBowen
    链接:https://www.jianshu.com/p/97d28e4613a7
    来源:简书
    著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。

  • 相关阅读:
    Mybatis的传值(四种方式)
    用现有表往另一张表插数据
    eclipse 项目发布到tomcat中(转)
    git 基本使用
    (转) Sping的事务传播
    <mvc:annotation-driven/>浅析
    Python函数参数的五种类型
    使用alembic进行数据库版本管理
    Center OS 7 安装 $$
    Python3.x 配置原生虚拟环境
  • 原文地址:https://www.cnblogs.com/nuoforever/p/16096151.html
Copyright © 2020-2023  润新知