【问题标题】:C plugin system: dlopen failsC 插件系统:dlopen 失败
【发布时间】:2016-04-22 21:08:31
【问题描述】:

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

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

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

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

插件系统包含在模块中:

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

网络部分包括:

  1. protocol.c 协议的主要部分
  2. message.c 消息处理的主要部分
  3. application.c 用于编写应用程序的主要部分
  4. common.c 包含 ccommon 函数的文件
  5. 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 没有问题,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

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

感谢您的理解。

【问题讨论】:

  • 用 objdump 检查符号表,看符号是否被导出。在 sendappmessage_all 中尝试 __attribute__((used)) 并告诉我会发生什么。
  • 是的,我可以看到,您所说的“尝试 __attribute__((used))”是什么意思?
  • 这是一个用于防止 gcc 删除未使用函数的属性;如果符号在动态符号表中 dlsym 应该能够找到它,所以你不需要它。您能否发布一个重现您行为的最小示例?
  • 我在尝试编写一些代码之前考虑过,我的帖子中没有出现的可能是问题的根源是网络部分中的模块使用全局变量来存储网络信息(例如接收者的地址),然后当主程序尝试加载插件时,他在插件中发现了相同的全局变量,这是不可能的。这只是想法,纯粹是假设性的,因为我是动态链接领域的新手,但这可能是 dlopen 失败的原因吗?
  • 事实上不,我刚刚写了一个模仿这种行为的例子,它可以工作。

标签: c linux networking plugins


【解决方案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 认为我上面写的不正确,这里有一个完全可验证的证明。

首先,一个简单的 ma​​in.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 属性不够用。


如果您希望在最终二进制文件的动态符号表中包含所有且仅非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

【讨论】:

  • 其实可以的,看帖子下关于objdump的评论...不过谢谢你的信息,有关系。
  • 我很抱歉在回答你之前没有尝试你的提议,因为你是对的!它适用于-rdynamic 选项。关于-Wl,-dynamic-list,plugin-system.list,我应该在里面放什么? sendappmessage_all 调用了其他几个函数,我需要把它们都放上去吗?我真的可以放一个* 还是应该用所有函数的名称替换它?
  • -rdynamic 将所有符号添加到动态符号表中。在plugin_system.list 中,您应该包含插件可能访问的所有符号,这些符号不属于项目使用的库的一部分。换句话说,您的插件系统为要使用的插件定义的符号。我将添加一个小sn-p,说明如何使这更容易。
  • 为了澄清前面的评论,-rdynamic 是一个简单的选项; -Wl,-dynamic-list, 是一个更细粒度的版本。我在答案中添加了list_globals.sh 作为示例,您可以如何从一个或多个目标文件中选择所有全局变量(非statics),并将它们列在plugin_system.list 中。
  • 非常感谢,非常简洁!我还有一个问题,如果sendappmessage_all调用了另一个函数,比如sendpacket,我需要把它放在列表中吗?如果是这样,我猜这是一个递归过程,所以它也适用于sendpacket
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2013-05-02
  • 2010-11-07
  • 1970-01-01
  • 1970-01-01
  • 2017-10-12
相关资源
最近更新 更多