问题是当你编译插件系统(即插件调用的函数),并将其链接到最终的可执行文件时,链接器不会导出动态符号表中插件使用的符号。
有两种选择:
链接最终可执行文件时使用-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; /* 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。