在C语言中创建一个模块系统(动态加载)

44

如何在运行时加载已编译的C代码,并调用其中的函数?不是像简单地调用exec()那样。

编辑:加载模块的程序是用C编写的。


3
很好的问题。许多人知道如何做到这一点,但那些不知道的人最好学习这个有价值的技巧。 - Mike Dunlavey
9个回答

49

dlopen 是正确的方法。下面是一些示例:

使用 dlopen 加载插件:

#include <dlfcn.h>
...
int
main (const int argc, const char *argv[])
{

  char *plugin_name;
  char file_name[80];
  void *plugin;
  ...
  plugin = dlopen(file_name, RTLD_NOW);
  if (!plugin)
  {
     fatal("Cannot load %s: %s", plugin_name, dlerror ());
  }

编译上述代码:

% cc  -ldl -o program program.o 

那么,假设这是插件的API:

/* The functions we will find in the plugin */
typedef void (*init_f) ();
init_f init;
typedef int (*query_f) ();
query_f query;
在插件中查找init()的地址:
init = dlsym(plugin, "init");
result = dlerror();
if (result)
{
   fatal("Cannot find init in %s: %s", plugin_name, result);
}
init();

使用另一个返回值的函数query():

query = dlsym (plugin, "query");
result = dlerror();
if (result)
{
    fatal("Cannot find query in %s: %s", plugin_name, result);
}
printf("Result of plugin %s is %d\n", plugin_name, query ());

您可以从在线获取完整的示例。


2
你介意把完整的示例放到Github上吗?那里阅读会更容易。 - Stefan Profanter
如果使用C++编译器,当您使用dlsym时,使用带有mangled字符串的函数名是标准的吗?还是在函数上使用extern "c"只需使用正常的函数名在dlsym上使用? - searchengine27

39

在 Linux/UNIX 中,您可以使用 POSIX dlopen / dlsym / dlerror /dlclose 函数来动态打开共享库并访问它们提供的符号(包括函数)。详情请参见手册页面


POCO库的原则是这样的吗? - daohu527

11

针对GNU/Linux用户

动态加载库是一种机制,我们可以在运行时决定要使用/调用哪个函数来运行我们的程序。我认为在某些情况下也可能使用静态变量。

首先查看man 3 dlopen在线查看

所需的头文件是:dlfcn,由于这不是标准的一部分,因此您应该将其与此库链接到您的对象文件中:libdl。(so / a),因此您需要类似于:

gcc yours.c -ldl

然后您有一个名为a.out的文件,可以运行它但是它无法正常工作,我将解释原因。

一个完整的示例:

首先创建两个文件func1.cfunc2.c。我们想要在运行时调用这些函数。

func.c

int func1(){
    return 1;
}

func2.c

const char* func2(){
    return "upgrading to version 2";
}

现在我们有两个函数,让我们创建我们的模块:
ALP ❱ gcc -c -fPIC func1.c
ALP ❱ gcc -c -fPIC func2.c
ALP ❱ gcc -o libfunc.so -shared -fPIC func1.o func2.o 

对于对 -fPIC 感兴趣的人 => PIC

现在你有一个名为 libfunc.so 的动态库。

让我们创建主程序(= temp.c),它想要使用这些函数。

头文件

#include <stdio.h>
#include <stdlib.h>
#include <dlfcn.h> 

和主程序

int main()
{
    // pointer function to func1 and func2
    int         ( *f1ptr )();
    const char* ( *f2ptr )();

    // for pointing to the library
    void* handle = NULL;

    // for saving the error messages
    const char* error_message = NULL;

    // on error dlopen returns NULL
    handle = dlopen( "libfunc.so", RTLD_LAZY );

    // check for error, if it is NULL
    if( !handle )
    {
        fprintf( stderr, "dlopen() %s\n", dlerror() );
        exit( 1 );
    }

    /*
        according to the header file:

        When any of the above functions fails, call this function
        to return a string describing the error.  Each call resets
        the error string so that a following call returns null.

        extern char *dlerror (void) __THROW;
    */

    // So, reset the error string, of course we no need to do it just for sure
    dlerror();

    // point to func1
    f1ptr = (int (*)()) dlsym( handle, "func1" );

    // store the error message to error_message
    // because it is reseted if we use it directly
    error_message = dlerror();
    if( error_message ) //   it means if it is not null
    {
        fprintf( stderr, "dlsym() for func1 %s\n", error_message );
        dlclose( handle );
        exit( 1 );
    }

    // point the func2
    f2ptr = (const char* (*)()) dlsym( handle, "func2" );

    // store the error message to error_message
    // because it is reseted if we use it directly
    error_message = dlerror();
    if( error_message ) //   it means if it is not null
    {
        fprintf( stderr, "dlsym() for func2 %s\n", error_message );
        dlclose( handle );
        exit( 1 );
    }

    printf( "func1: %d\n", ( *f1ptr )() );
    printf( "func2: %s\n", ( *f2ptr )() );

    // unload the library
    dlclose( handle );

    // the main return value
    return 0;
}

现在我们只需要编译这段代码(即temp.c),尝试执行以下命令:
ALP ❱ gcc temp.c -ldl
ALP ❱ ./a.out
libfunc.so: cannot open shared object file: No such file or directory

它不起作用!为什么很容易理解,因为我们的 a.out 程序不知道在哪里找到相关的库:libfunc.so,因此它告诉我们“无法打开...”。如何告诉程序(即 a.out)找到它的库?
1. 使用 ld 链接器 2. 使用环境变量 LD_LIBRARY_PATH 3. 使用标准路径
第一种方法,借助 ld
使用 -Wl,-rpath 和 pwd,并将路径作为参数放入其中。
ALP ❱ gcc temp.c -ldl
ALP ❱ ./a.out
libfunc.so: cannot open shared object file: No such file or directory
ALP ❱ pwd
/home/shu/codeblock/ALP
ALP ❱ gcc temp.c -ldl -Wl,-rpath,/home/shu/codeblock/ALP
ALP ❱ ./a.out
func1: 1
func2: upgrading to version 2

第二种方式
ALP ❱ gcc temp.c -ldl
ALP ❱ ./a.out
libfunc.so: cannot open shared object file: No such file or direc
ALP ❱ export LD_LIBRARY_PATH=$PWD
ALP ❱ echo $LD_LIBRARY_PATH
/home/shu/codeblock/ALP
ALP ❱ ./a.out
func1: 1
func2: upgrading to version 2
ALP ❱ export LD_LIBRARY_PATH=
ALP ❱ ./a.out
libfunc.so: cannot open shared object file: No such file or 

第三种方式

您当前路径中有 libfunc.so,因此您可以将其复制到标准库路径中。

ALP $ sudo cp libfunc.so /usr/lib
ALP ❱ gcc temp.c -ldl
ALP ❱ ./a.out
func1: 1
func2: upgrading to version 2

你可以将其从/usr/lib中删除并使用它。由你决定。
注意:
如何确定我们的a.out知道它的路径? 很简单:
ALP ❱ gcc temp.c -ldl -Wl,-rpath,/home/shu/codeblock/ALP
ALP ❱ strings a.out  | grep \/
/lib/ld-linux.so.2
/home/shu/codeblock/ALP

我们如何在中使用它?
据我所知,你不能这样做,因为g++会混淆函数名称,而gcc则不会,因此你应该使用:extern "C" int func1(); 例如。

有关更多详细信息,请参阅man页面和Linux编程书籍。


2
不错!还有第四种方法,根据dlopen手册的描述,“如果文件名包含斜杠(“/”),则被解释为相对或绝对路径名。” 因此,'handle = dlopen("./libfunc.so",RTLD_LAZY);' 允许按照所述进行编译,并且只需执行“./a.out”即可成功,无需进行其他任何操作。 - HermannSW

9

看到这个问题已经得到解答,但认为其他对此感兴趣的人可能会欣赏一下来自旧插件应用程序的跨平台示例。该示例适用于win32或linux,并在指定文件参数中搜索并调用名为“constructor”的函数,该函数位于动态加载的.so或.dll中。该示例是用c++编写的,但对于c语言,过程应该是相同的。

//firstly the includes
#if !defined WIN32
   #include <dlfcn.h>
   #include <sys/types.h>
#else
   #include <windows.h>
#endif

//define the plugin's constructor function type named PConst
typedef tcnplugin* (*PConst)(tcnplugin*,tcnplugin*,HANDLE);

//loads a single specified tcnplugin,allmychildren[0] = null plugin
int tcnplugin::loadplugin(char *file) {
    tcnplugin *hpi;
#if defined WIN32               //Load library windows style
    HINSTANCE hplugin=LoadLibrary(file);
    if (hplugin != NULL) {
            PConst pinconstruct = (PConst)GetProcAddress(hplugin,"construct");
#else                                   //Load it nix style
    void * hplugin=dlopen(file,RTLD_NOW);
    if (hplugin != NULL) {
            PConst pinconstruct = (PConst)dlsym(hplugin,"construct");
#endif   
            if (pinconstruct != NULL) { //Try to call constructor function in dynamically loaded file, which returns a pointer to an instance of the plugin's class
                    hpi = pinconstruct(this, this, hstdout);
            } else {
                    piprintf("Cannot find constructor export in plugin!\n");
                    return 0;
            }
    } else {
            piprintf("Cannot open plugin!\n");
#if !defined WIN32
            perror(dlerror());
#endif
            return 0;
    }
    return addchild(hpi); //add pointer to plugin's class to our list of plugins
}

也需要提到,如果你想调用模块函数的模块是用c++编写的,那么你必须使用extern "C"声明该函数,如下所示:
extern "C" pcparport * construct(tcnplugin *tcnptr,tcnplugin *parent) {
    return new pcparport(tcnptr,parent,"PCPARPORT",0,1);
}

需要哪些头文件才能在Linux上运行?'::'表示它是C++,而不是C,对吗? - Jonathan Leffler

3
此外,您还可以查看cpluff。它是一个纯C的插件管理库。

2

像Perl这样的动态语言经常这样做。 Perl解释器是用C编写的,许多Perl模块部分是用C编写的。 当需要这些模块时,编译后的C组件会动态地加载。 如另一个答案中所述,存储这些模块的机制是Windows上的DLL,在UNIX上是共享库(.so文件)。 我相信在UNIX上加载共享库的调用是dlopen()。 您可以通过查看该调用的文档来找到在UNIX上如何完成此操作的指针。 对于Windows,您需要研究DLL并学习如何在运行时动态加载它们。 [或者可能通过Cygwin UNIX仿真层,这可能允许您在Windows上使用与在UNIX上相同的调用,但我不建议除非您已经在使用和编译Cygwin。]

请注意,这与仅链接共享库不同。如果您事先知道将要调用的确切代码,可以对共享库进行构建,并且构建将“动态链接”到该库;没有任何特殊处理,从库中获取例程仅在程序实际调用它们时才会加载到内存中。但是,如果您计划编写能够加载任意任意对象代码的内容,即无法在构建时标识的代码,而是等待在运行时通过某种方式选择,则必须使用dlopen()及其Windows兄弟。
您可能会查看Perl或其他动态语言的实现方式以了解一些真实的示例。负责此类动态加载的Perl库是DynaLoader;我相信它既有Perl组件也有C组件。我确定像Python之类的其他动态语言也有类似的机制,您可能更喜欢查看;未发布的Perl 6虚拟机Parrot肯定也有这样的机制(或将来会有)。
说起来,Java通过其JNI(Java Native Interface)接口完成此操作,因此您可以查看OpenJDK的源代码,以了解Java在UNIX和Windows上是如何实现此操作的。

2
如果您考虑使用框架,Qt提供了QPluginLoader: Qt 5文档(或者查看旧的Qt 4.8文档,请点击这里
如果您需要/想要更细粒度的控制,Qt还提供了一种动态加载库的方式,即QLibrary: Qt 5文档(或者查看旧的Qt 4.8文档,请点击这里
更好的是,它们可以跨平台使用。

0

有一种自己动手的方法。虽然这种方法(和可能性)因系统而异,但总体思路是打开文件,将文件内容读入内存,将该内存变为可执行状态,初始化函数指针到内存中的有效位置,就可以使用了。

当然,这是建立在假设代码是可执行的情况下 - 这种情况很不可能发生。代码可能需要将数据加载到RAM中,并且可能需要全局/静态变量的空间。您可以自己加载所有这些,但您需要进入可执行代码并调整其中的所有内存引用。

大多数操作系统都允许动态链接,这样可以为您完成所有这些工作。


1
将可执行文件读入内存,正确获取所有保护设置并找到正确的符号是很困难的。既然有标准操作系统函数可以更好地完成这项工作,为什么要重复造轮子呢? - Jay Conrod
1
关于“将文件内容读入内存,使该内存可执行”的部分涵盖了很多内容,因为通常在加载时需要进行大量的重定位和代码调整。我曾经尝试过,这不是给懦弱者的任务。 - Mike Dunlavey

0

在 Windows 下,我是这样做的:

  • 生成代码(用 C 语言编写,因为易于找到编译器,且库要求最小)
  • 启动任务将其编译 / 链接成 DLL 文件
  • 用 LoadLibrary 函数加载 DLL
  • 使用 GetProcAddress 函数获取函数指针

通常,生成 / 编译 / 链接步骤不会超过一秒钟。


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接