一尘不染

C插件系统:dlopen失败

linux

作为此后C插件系统的继续:符号查找错误,我仍在编写我的插件系统,并遇到了新的错误。

概括一下插件是什么,该程序包含一个由外壳接口的网络应用程序,消息具有类型,因此可用于在网络上创建应用程序。例如,可能的应用程序是聊天或转移应用程序。

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

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

插件系统包含以下模块:

  1. plugin_system.c
  2. __第一个模块用于存储插件的 list.c

网络部分包括:

  1. protocol.c协议的 主要部分
  2. message.c 消息处理的主要部分
  3. application.c 用于编程应用 程序的 主要部分
  4. __具有ccommon功能的 common.c 文件
  5. network.c 有用的网络功能

protocole中的模块都是相互依赖的,为方便起见,我使用了分割文件。所有模块都使用-fPIC选项进行编译。

要编译不与网络交互的名为 plug.c wich 的插件,我使用:

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

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

感谢您的理解。


阅读 272

收藏
2020-06-07

共1个答案

一尘不染

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

有两种选择:

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

  2. -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;
}

为了使所有这些都易于编译,我们需要一个 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

运行一次以上是安全的;最糟糕的是,搞砸了您的“人类可读”布局。

使用例如编译以上

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属性不足。


如果要在最终二进制文件的动态符号表staticplugin_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

2020-06-07