C插件系统: dlopen fails

标签 c linux networking plugins

作为这篇文章的延续 C pluginsystem: symbol lookup error ,我还在写我的插件系统,遇到新的bug。

回顾一下插件是什么,该程序由一个由外壳接口(interface)的网络应用程序组成,消息具有一种类型,因此可用于在网络上创建应用程序。例如,可能的应用程序是聊天或传输应用程序。

因此shell命令可以在网络上发送特定应用程序的消息,当接收到消息时,如果它对应于特定应用程序,则以消息内容作为参数执行 Action 函数,它可能是应用程序。

插件是一个共享库,带有一个注册它的命令和操作的 init 函数。命令可能只是一个不与网络交互的简单命令,这就是我目前实现的原因。

插件系统包含在模块中:

  • plugin_system.c
  • list.c 被第一个模块用来存储插件

  • 网络部分包括:
  • protocol.c 协议(protocol)的主要部分
  • message.c 消息处理的主要部分
  • application.c 用于编写应用程序的主要部分
  • 带有 ccommon 函数的 common.c 文件
  • network.c 有用的网络函数

  • protocole 中的模块都是相互依赖的,为了方便,我已经拆分了文件。
    所有模块都使用 -fPIC 选项编译。

    要编译一个不与网络交互的名为 plug.c 的插件,我使用:
    gcc -Wall -O2 -std=gnu99  -D DEBUG -g -fPIC -c -o plug.o plug.c
    gcc -Wall -O2 -std=gnu99  -D DEBUG -g -o plug.so plug.o plugin_system.o list.o -shared
    

    它运行良好,库中加载了 dlopen没问题,初始化函数加载了dlsym并正确执行,以便注册插件,然后我执行了命令,我可以看到它工作。

    现在我不想为插件添加对网络通信的支持,所以我修改了我使用的相同测试插件,它只有一个打印消息的命令。我已调用 sendappmessage_all一个通过网络向每个人发送消息的函数,在 message.c 中定义。

    我可以在不添加网络模块对象的情况下编译新插件,它编译,插件正确加载,但是当它调用 sendappmessage_all显然它失败了消息
     symbol lookup error: ./plugins/zyva.so: undefined symbol: sendappmessage_all
    

    所以为了让它工作,我应该喜欢带有网络模块的插件,这就是我所做的
    gcc -Wall -O2 -std=gnu99  -D DEBUG -g -o plug.so plug.o plugin_system.o list.o protocol.o message.o thread.o common.o application.o network.o -shared
    

    它可以编译,但是当我尝试加载插件时,dlopen返回 NULL .
    我也尝试过只添加一个模块,最坏的情况下它只会导致 undefined symbol错误,但我 dlopen仍然返回 NULL .

    我知道这是很多信息,另一方面你可能不想看到代码,但我试图以最简洁的方式变得更清晰,因为它比帖子更复杂和更大。

    感谢您的理解。

    最佳答案

    问题是当您编译插件系统(即插件调用的函数)并将其链接到最终的可执行文件时,链接器不会导出动态符号表中插件使用的符号。

    有两种选择:

  • 使用-rdynamic链接最终可执行文件时,将所有符号添加到动态符号表中。
  • 使用-Wl,-dynamic-list,plugin-system.list链接最终可执行文件时,添加文件 plugin-system.list 中列出的符号到动态符号表。

    文件格式很简单:
     {
         sendappmessage_all;
         plugin_*;
     };
    

    换句话说,您可以列出每个符号名称(函数或数据结构),或与所需符号名称匹配的全局模式。记住每个符号后和右大括号后的分号,否则在链接时会出现“动态列表中的语法错误”错误。

  • 请注意,仅通过 __attribute__((used)) 将函数标记为“已使用”不足以使链接器将其包含在动态符号表中(至少使用 GCC 4.8.4 和 GNU ld 2.24)。

    由于 OP 认为我上面写的内容不正确,因此这里有一个完全可验证的证明。

    一、简单的 main.c 加载命令行命名的插件文件,并执行它们的const char *register_plugin(void);功能。因为函数名在所有插件之间共享,我们需要在本地链接它们(RTLD_LOCAL)。
    #include <stdlib.h>
    #include <string.h>
    #include <dlfcn.h>
    #include <stdio.h>
    
    static const char *load_plugin(const char *pathname)
    {
        const char    *errmsg;
        void          *handle; /* We deliberately leak the handle */
        const char * (*initfunc)(void);
    
        if (!pathname || !*pathname)
            return "No path specified";
    
        dlerror();
        handle = dlopen(pathname, RTLD_NOW | RTLD_LOCAL);
        errmsg = dlerror();
        if (errmsg)
            return errmsg;
    
        initfunc = dlsym(handle, "register_plugin");
        errmsg = dlerror();
        if (errmsg)
            return errmsg;
    
        return initfunc();
    }
    
    int main(int argc, char *argv[])
    {
        const char *errmsg;
        int         arg;
    
        if (argc < 1 || !strcmp(argv[1], "-h") || !strcmp(argv[1], "--help")) {
            fprintf(stderr, "\n");
            fprintf(stderr, "Usage: %s [ -h | --help ]\n", argv[0]);
            fprintf(stderr, "       %s plugin [ plugin ... ]\n", argv[0]);
            fprintf(stderr, "\n");
            return EXIT_SUCCESS;
        }
    
        for (arg = 1; arg < argc; arg++) {
            errmsg = load_plugin(argv[arg]);
            if (errmsg) {
                fflush(stdout);
                fprintf(stderr, "%s: %s.\n", argv[arg], errmsg);
                return EXIT_FAILURE;
            }
        }
    
        fflush(stdout);
        fprintf(stderr, "All plugins loaded successfully.\n");
        return EXIT_SUCCESS;
    }
    

    插件可以通过在 中声明的某些函数(和/或变量)进行访问。 plugin_system.h :
    #ifndef   PLUGIN_SYSTEM_H
    #define   PLUGIN_SYSTEM_H
    
    extern void plugin_message(const char *);
    
    #endif /* PLUGIN_SYSTEM_H */
    

    它们在 中实现plugin_system.c :
    #include <stdio.h>
    
    void plugin_message(const char *msg)
    {
        fputs(msg, stderr);
    }
    

    并在 中列为动态符号plugin_system.list :
    {
        plugin_message;
    };
    

    我们还需要一个插件, plugin_foo.c :
    #include <stdlib.h>
    #include "plugin_system.h"
    
    const char *register_plugin(void) __attribute__((used));
    const char *register_plugin(void)
    {
        plugin_message("Plugin 'foo' is here.\n");
        return NULL;
    }
    

    并且只是为了消除对每个插件具有相同名称的注册函数的影响的任何混淆,另一个名为 的插件plugin_bar.c :
    #include <stdlib.h>
    #include "plugin_system.h"
    
    const char *register_plugin(void) __attribute__((used));
    const char *register_plugin(void)
    {
        plugin_message("Plugin 'bar' is here.\n");
        return NULL;
    }
    

    为了使所有这些易于编译,我们需要一个 生成文件 :
    CC              := gcc
    CFLAGS          := -Wall -Wextra -O2
    LDFLAGS         := -ldl -Wl,-dynamic-list,plugin_system.list
    PLUGIN_CFLAGS   := $(CFLAGS)
    PLUGIN_LDFLAGS  := -fPIC
    PLUGINS         := plugin_foo.so plugin_bar.so
    PROGS           := example
    
    .phony: all clean progs plugins
    
    all: clean progs plugins
    
    clean:
        rm -f *.o $(PLUGINS) $(PROGS)
    
    %.so: %.c
        $(CC) $(PLUGIN_CFLAGS) $^ $(PLUGIN_LDFLAGS) -shared -Wl,-soname,$@ -o $@
    
    %.o: %.c
        $(CC) $(CFLAGS) -c $^
    
    plugins: $(PLUGINS)
    
    progs: $(PROGS)
    
    example: main.o plugin_system.o
        $(CC) $(CFLAGS) $^ $(LDFLAGS) -o $@
    

    请注意,Makefile 需要制表符而不是空格。在此处列出文件总是会将它们转换为空格。因此,如果您将上述内容粘贴到文件中,则需要通过例如修复缩进。
    sed -e 's|^  *|\t|' -i Makefile
    

    多次运行它是安全的;它可以做的最糟糕的事情是弄乱您的“人类可读”布局。

    使用例如编译上述内容
    make
    

    并通过例如运行它
    ./example ./plugin_bar.so ./plugin_foo.so
    

    应该输出
    Plugin 'bar' is here.
    Plugin 'foo' is here.
    All plugins loaded successfully.
    

    到标准误。

    就个人而言,我更喜欢通过一个结构注册我的插件,带有一个版本号,以及至少一个函数指针(指向初始化函数)。这让我在初始化它们之前加载所有插件,并解决例如插件间冲突或依赖关系。 (换句话说,我使用具有固定名称的结构,而不是具有固定名称的函数来识别插件。)

    现在,至__attribute__((used)) .如果修改plugin_system.c进入
    #include <stdio.h>
    
    void plugin_message(const char *msg) __attribute__((used));
    
    void plugin_message(const char *msg)
    {
        fputs(msg, stderr);
    }
    

    并将 Makefile 修改为具有 LDFLAGS := -ldl只是,示例程序和插件可以正常编译,但运行它会产生
    ./plugin_bar.so: ./plugin_bar.so: undefined symbol: plugin_message.
    

    换句话说,如果导出到插件的 API 在单独的编译单元中编译,您将需要使用 -rdynamic-Wl,-dynamic-list,plugin-system.list确保函数包含在最终可执行文件的动态符号表中; used属性不够。

    如果您想要所有且仅非- static plugin_system.o 中的函数和符号包含在最终二进制文件的动态符号表中,例如修改Makefile结尾进入
    example: main.o plugin_system.o
        @rm -f plugin_system.list
        ./list_globals.sh plugin_system.o > plugin_system.list
        $(CC) $(CFLAGS) $^ $(LDFLAGS) -o $@
    

    list_globals.sh :
    #!/bin/sh
    [ $# -ge 1 ] || exit 0
    export LANG=C LC_ALL=C
    IFS=:
    IFS="$(printf '\t ')"
    
    printf '{\n'
    readelf -s "$@" | while read Num Value Size Type Bind Vis Ndx Name Dummy ; do
        [ -n "$Name" ] || continue
        if [ "$Bind:$Type" = "GLOBAL:FUNC" ]; then
            printf '    %s;\n' "$Name"
        elif [ "$Bind:$Type:$Ndx" = "GLOBAL:OBJECT:COM" ]; then
            printf '    %s;\n' "$Name"
        fi
    done
    printf '};\n'
    

    记得使脚本可执行,chmod u+x list_globals.sh .

    关于C插件系统: dlopen fails,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/36803328/

    相关文章:

    c - 为什么重定向到 stdout 和 stdin 在 PIPE 编程中如此普遍

    linux - 如何配置 nginx 为公众服务

    c - getsockname() c 没有设置值

    linux - 状态 FIN_WAIT_1 出现问题

    c++ - 如何使用Wireshark帮助创建协议(protocol)模糊测试框架?

    c - 使用一个结构重新散列链式哈希表并调整其大小

    c++ - 如何处理笔记本电脑上的电源关闭按钮

    linux - 有没有我可以调用的命令来打印出 malloc 数据结构?

    linux - AWK 中的多重拆分

    c++ - 在 C++ 中附加数据的最佳方法是什么