为什么在外部模块外部实现的方法仍可由模块访问

时间:2014-09-06 23:59:24

标签: c++ c api dynamic-loading

拥有./library.so(rary.cpp)和./main(main.cpp)可执行文件,两者共享相同的api.h

唯一的方法(void method (void))只有api.h中的签名,实际的实现位于main.cpp

在编译时rary.cpp不包括方法的实际定义:在创建main.cpp文件时从不提及library.so。如果我通过dlopendlsym访问共享文件,那么从library.so到方法的任何调用都会实际引用仅在主程序中实现的方法。

我没想到会发生这种情况,据我所知library.so从未出现在方法的实现中,所以编译器(我认为)应该抱怨未实现的方法正在调用。

所以我的问题是:

  1. 这是正常的吗?在主二进制文件中实现的方法应该只是通过名称直接从动态加载的模块中访问? (我认为无法保证编译后如何在二进制文件中实际调用符号)
  2. 有办法防止这种情况发生吗?假设我正在为某人为我的程序编写插件提供API,这样他们可以猜出主二进制文件中的方法名称并做一些... hack?
  3. 我可以期望这种行为在编译器和操作系统之间保持一致吗?
  4. 依靠这种行为我或者是一个好习惯吗?
  5. <小时/>

    源代码

    的main.cpp

    #include "api.hpp"
    #include <iostream>
    #include <dlfcn.h>
    
    using std::cout;
    using std::endl;
    
    void method (void)
    {
        cout << "method happened" << endl;
    }
    
    void method (int num)
    {
        cout << "method happened with num " << num << endl;
    }
    
    int main (void)
    {
        cout << "started main" << endl;
    
        typedef void(* initfunc)();
    
        void * handle = dlopen("./library.so", RTLD_LAZY);
    
        initfunc func = reinterpret_cast<initfunc> (dlsym(handle, "init"));
    
        func();
    
        return 0;
    }
    

    rary.cpp

    #include "api.hpp"
    #include <iostream>
    
    using std::cout;
    using std::endl;
    
    extern "C" void init (void)
    {
        method();
        method(69);
        cout << "library ran" << endl;
    }
    

    api.h

    void method (void);
    void method (int);
    

    的CMakeLists.txt

    project(apitest CXX)
    
    add_executable(apitest "main.cpp")
    add_library(rary MODULE "rary.cpp")
    target_link_libraries(apitest dl)
    

    编译

    cmake .
    make
    

    结果

    $ ./apitest
    started core
    method happened
    method happened with num 69
    library ran
    

2 个答案:

答案 0 :(得分:2)

  
    

Q1。这是正常的吗?是主二进制文件中实现的方法,应该直接从中访问     动态加载模块只是他们的名字? (我认为不能保证如何     编译后可以在二进制文件中实际调用符号

  

是。默认情况下,符号在外部可见,除非另有特别标记(至少使用GCC)。

  
    

Q2。有办法防止这种情况吗?假设我正在为某人为我的插件编写插件提供API     程序,这样他们可以猜出主二进制文件中的方法名称并做一些...黑客攻击?

  

是的,您可以通过使用选项-fvisibility=hidden编译程序来防止这种情况。我通过将其添加到CMakeLists.txt:

来修改您的示例
set (CMAKE_CXX_FLAGS "-fvisibility=hidden")

我更改了init函数的定义,使其标记为可见,如下所示:

extern "C" __attribute__ ((visibility ("default"))) void init (void)

我还修改了你的main.cpp以检查来自dlsym的错误(因为你的原始版本没有):

initfunc func = reinterpret_cast<initfunc> (dlsym(handle, "init"));

if (func == NULL) {
    cout << dlerror() << endl;
    return 1;
}

执行此操作后,您的程序将输出以下内容:

$ ./apitest 
started main
./apitest: symbol lookup error: ./library.so: undefined symbol: _Z6methodv

由于init函数标记为可见,因此可以从main函数调用它。但是method函数不是,所以你得到一个未定义的符号错误。

  
    

Q3。我可以期望这种行为在编译器和操作系统之间保持一致吗?

  

没有。似乎在Windows上,行为或多或少地反转 - 默认情况下,除非明确标记,否则不会导出符号。

  
    

Q4。依靠这种行为我或者是一个好习惯吗?

  

制作库时的最佳做法是导出构成库公共API的符号。这有很多好处:

  • 链接时间将缩短
  • 减少符号冲突的可能性
  • 更好的编译器优化

GCC Wiki Visibility Page(我找到了此答案的大部分信息)包含大量有关此问题的信息,包括最佳做法提示。

答案 1 :(得分:1)

Linux中的共享库是正常的。

共享库与windows dll不同。在这个主题中它更像是静态库。

编译lib时,它缺少method。如果你的主要人员没有提供它,它只是无法加载库而dlopen将失败,你应该使用dlerror来检查原因。

你可以尝试一下:

handle = dlopen("./library.so", RTLD_LAZY);
if (!handle) {
    cerr<< dlerror()<<endl;
    exit(-1);
}