作为此后C插件系统的继续:符号查找错误,我仍在编写我的插件系统,并遇到了新的错误。
概括一下插件是什么,该程序包含一个由外壳接口的网络应用程序,消息具有类型,因此可用于在网络上创建应用程序。例如,可能的应用程序是聊天或转移应用程序。
因此,shell命令可以在网络上发送特定应用程序的消息,当接收到一条消息时,如果它对应于特定应用程序,则将消息内容作为参数执行一个动作功能,它可能就是该应用程序。
插件是一个共享库,带有一个初始化函数,用于注册其命令和动作。命令可能只是一个不与网络交互的简单命令,这就是我目前实现的原因。
插件系统包含以下模块:
网络部分包括:
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并正确执行,因此插件已注册,然后我执行了命令,可以看到它正常工作。
dlopen
dlsym
现在,我不想为插件添加对网络通信的支持,因此,我修改了与我使用的相同的测试插件,该测试插件仅具有打印消息的命令。我已经调用了sendappmessage_all一个函数,该函数通过 message.c中 定义的网络上的每个人发送 消息 。
sendappmessage_all
我可以在不添加网络模块对象的情况下编译新插件,它可以编译,插件可以正确加载,但是当它调用时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。
NULL
undefined symbol
我知道这是很多信息,另一方面,您可能不想看代码,但我试图以最简洁的方式使代码更清晰,因为它比帖子更复杂,更大。
感谢您的理解。
问题在于,当您编译插件系统(即,由插件调用的函数)并将其链接到最终可执行文件时,链接器不会在动态符号表中导出插件使用的符号。
有两种选择:
-rdynamic链接最终可执行文件时使用,将所有符号添加到动态符号表中。
-rdynamic
-Wl,-dynamic-list,plugin-system.list在链接最终的可执行文件时使用,将文件中列出的符号添加plugin-system.list到动态符号表中。
-Wl,-dynamic-list,plugin-system.list
plugin-system.list
文件格式很简单:
{ sendappmessage_all; plugin_*; };
换句话说,您可以列出每个符号名称(函数或数据结构),也可以列出与所需符号名称匹配的全局模式。请记住,每个符号后和右括号后都使用分号,否则在链接时会收到“动态列表中的语法错误”错误。
请注意,仅标记功能为“使用”的通道__attribute__((used))不足以使链接器将其包括在动态符号表中(至少在GCC 4.8.4和GNU ld 2.24中)。
__attribute__((used))
由于OP认为我在上面写的是不正确的,所以这里是上述的完全可验证的证明。
首先,一个简单的 main.c 加载在命令行上命名的插件文件,并执行其const char *register_plugin(void);功能。由于函数名称在所有插件之间共享,因此我们需要在本地将它们链接(RTLD_LOCAL)。
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成
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仅包含,示例程序和插件将编译得很好,但是运行它将产生
LDFLAGS := -ldl
./plugin_bar.so: ./plugin_bar.so: undefined symbol: plugin_message.
换句话说,如果导出到插件的API是在单独的编译单元中编译的,则您将需要使用-rdynamic或-Wl,-dynamic-list,plugin- system.list确保功能已包含在最终可执行文件的动态符号表中;该used属性不足。
-Wl,-dynamic-list,plugin- system.list
used
如果要在最终二进制文件的动态符号表static中plugin_system.o包括所有非函数和符号,则可以例如将的结尾修改Makefile为
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。
chmod u+x list_globals.sh