C插件系统:dlopen失败

时间:2016-04-22 21:08:32

标签: c linux networking plugins

作为此帖C pluginsystem: symbol lookup error的延续,我仍在编写我的插件系统并遇到新的错误。

为了回顾插件的内容,该程序由一个由shell连接的网络应用程序组成,消息具有类型,因此可用于在网络上创建应用程序。例如,可能的应用程序是聊天或转移应用程序。

所以shell命令可以在网络上发送特定应用程序的消息,当收到消息时,如果它对应于特定的应用程序,则执行动作函数并将消息内容作为参数,它可以是应用程序。 / p>

插件是一个带有init函数的共享库,可以注册它的命令和操作。命令可能只是一个不与网络交互的简单命令,这就是我此刻取得的成就。

插件系统包含模块:

  1. plugin_system.c
  2. 第一个模块用于存储插件的
  3. list.c
  4. 网络部分包括:

    1. protocol.c 协议的主要部分
    2. message.c 邮件处理的主要部分
    3. application.c 用于编写应用程序的主要部分
    4. 带有ccommon功能的
    5. common.c 文件
    6. network.c 有用的网络功能
    7. 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没有问题,init函数加载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

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

      感谢您的理解。

1 个答案:

答案 0 :(得分:1)

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

有两种选择:

  1. 链接最终可执行文件时使用-rdynamic,将所有符号添加到动态符号表中。

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

    文件格式很简单:

     {
         sendappmessage_all;
         plugin_*;
     };
    

    换句话说,您可以列出每个符号名称(函数或数据结构),或列出与所需符号名称匹配的glob模式。记住每个符号后面的分号,以及结束后的分号,或者你在动态列表中得到一个"语法错误"链接时出错。

  3. 请注意,只需标记一个功能"使用" via __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;
    }
    

    为了使所有这些都易于编译,我们需要 Makefile

    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
    

    不止一次运行是安全的;它能做的最糟糕的事情就是弄乱你的人类可读的#34;布局。

    使用例如

    编译上述内容
    make
    

    并通过例如

    运行
    ./example ./plugin_bar.so ./plugin_foo.so
    

    将输出

    Plugin 'bar' is here.
    Plugin 'foo' is here.
    All plugins loaded successfully.
    

    标准错误。

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

    现在,关于__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来确保函数包含在动态符号表中最终的可执行文件<{1}}属性不够。

    如果您希望最终二进制文件中的动态符号表中包含used中的所有且仅有static个函数和符号,则可以是将plugin_system.o的结尾修改为

    Makefile

    list_globals.sh

    example: main.o plugin_system.o
        @rm -f plugin_system.list
        ./list_globals.sh plugin_system.o > plugin_system.list
        $(CC) $(CFLAGS) $^ $(LDFLAGS) -o $@
    

    请记住使脚本可执行,#!/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'