为什么在外部模块之外实现的方法仍然可以访问该模块

why are methods implemented outside an external module still accessible to the module

本文关键字:模块 访问 方法 外部 实现 为什么      更新时间:2023-10-16

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

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

在编译时,rary.cpp不包括该方法的实际定义:在制作library.so文件时从未提及main.cpp。尽管如此,如果我通过dlopendlsym访问共享文件,那么从library.so内部对该方法的任何调用实际上都引用了仅在主程序中实现的方法。

据我所知,library.so从未出现在该方法的实现中,所以编译器(我认为)应该抱怨调用了一个未实现的方法。

所以我的问题是:

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


源代码

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

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函数被标记为可见,因此可以从主函数调用它。但是method函数不是,所以您会得到一个未定义的符号错误。

Q3.我能指望编译器和操作系统之间的这种行为是一致的吗

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

Q4.我应该依赖这种行为吗

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

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

GCC Wiki可见性页面(我在其中找到了这个答案的大部分信息)包含了很多关于这方面的信息,包括最佳实践技巧。

这对于Linux中的共享库来说是正常的。

共享库与windowsdll不同。在这个主题中,它更类似于静态库。

编译lib时,它缺少method。如果您的main不提供它,它将无法加载库,dlopen将失败,您应该使用dlerror来检查原因。

你可以试试:

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