问题在于,当您编译插件系统(即,由插件调用的函数)并将其链接到最终可执行文件时,链接器不会在动态符号表中导出插件使用的符号。
有两种选择:
-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; 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_Hextern void plugin_message(const char *);#endif
它们在 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 := gccCFLAGS := -Wall -Wextra -O2LDFLAGS := -ldl -Wl,-dynamic-list,plugin_system.listPLUGIN_CFLAGS := $(CFLAGS)PLUGIN_LDFLAGS := -fPICPLUGINS := plugin_foo.so plugin_bar.soPROGS:= example.phony: all clean progs pluginsall: clean progs pluginsclean: 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 0export LANG=C LC_ALL=CIFS=: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" fidoneprintf '};n'请记住,使脚本可执行
chmod u+x list_globals.sh。



