共享库中未解决符号的简易检查方法?

93

我正在编写一个相当大的C++共享对象库,遇到了一个小问题,使得调试很麻烦:

如果我在头文件中定义一个函数/方法,并且忘记为它创建存根(在开发过程中),因为我正在构建共享对象库而不是可执行文件,没有错误出现在编译时告诉我已经忘记实现该函数。唯一的方法是在运行时找出有问题,当最终链接到这个库的应用程序崩溃并显示“未定义的符号”错误时。

我正在寻找一种简单的方法来检查我是否在编译时拥有所有需要的符号,也许是我可以添加到我的Makefile中的东西。

我想到的一个解决方案是通过nm -C -U运行编译后的库以获取所有未定义引用的解码列表。但问题是,这也会出现其他库(例如GLibC)中的所有引用列表,当最终应用程序组装时,当然也会与该库链接在一起。可以使用nm的输出来grep我的所有头文件,并查看是否有任何相应名称..但这似乎很疯狂。毫无疑问,这不是一个罕见的问题,肯定有更好的解决办法。


4
“nm -C -u” 命令在我的电脑上使用小写的“-u”参数救了我好几次!我留下这个评论是为了下次需要时能够找到它。 - dpritch
4个回答

101

查看链接器选项 -z defs / --no-undefined。创建共享对象时,如果有未解析的符号,将导致链接失败。

如果您正在使用gcc调用链接器,则可以使用编译器的-Wl选项将该选项传递给链接器:

gcc -shared ... -Wl,-z,defs

举个例子,考虑以下文件:

#include <stdio.h>

void forgot_to_define(FILE *fp);

void doit(const char *filename)
{
    FILE *fp = fopen(filename, "r");
    if (fp != NULL)
    {
        forgot_to_define(fp);
        fclose(fp);
    }
}

现在,如果你将其构建为共享对象,它将成功:

> gcc -shared -fPIC -o libsilly.so silly.c && echo succeeded || echo failed
succeeded

但是如果您添加-z defs,链接将会失败,并告诉您缺少的符号:

> gcc -shared -fPIC -o libsilly.so silly.c -Wl,-z,defs && echo succeeded || echo failed
/tmp/cccIwwbn.o: In function `doit':
silly.c:(.text+0x2c): undefined reference to `forgot_to_define'
collect2: ld returned 1 exit status
failed

4
这个答案可以帮助那些来自Windows背景的人,因为在那里,DLLs(动态链接库)的外部符号必须在编译时解析。非常好的代码片段! - Shmil The Cat
@ShmilTheCat:嗨, 我已经尝试在我的 make 文件中添加 -Wl,-z,defs,但在运行时仍然出现未定义的符号错误,而在编译时没有。 我该怎么办?在我的场景中,我有两个文件夹一个在另一个文件夹中(比如,A/B,即B在A中),我可以看到“libA.so”中有一些 B 的符号。 我不确定是否每个 B 中的符号都在“libA.so”中可用。 我该如何确保这一点? - Vinay
@ShmilTheCat 为了使其更像 Windows 的 DLL,您可以在链接器命令行中添加“-Bsymbolic”。即使 forgot_to_define 现在由 -z 检查存在于库中,可执行文件仍然可以用自己的定义覆盖它,并且库自己的定义将进入该覆盖。 -Bsymbolic 可以强制使库自己的函数定义进入其函数。 - Kaz
仅适用于实际使用的函数。如果我执行 // forgot_to_define(fp); 它不会报错。 - agentsmith
在gcc 6.3中,它显示:未识别的命令行选项“-W1,-z,-defs”。 - Sandeep
1
@mk.. 标志是 -Wl(小写字母 L),而不是 -W1(数字 1)。 - R Samuel Klatchko

26
在Linux(您似乎正在使用的)上,ldd -r a.out 可以给出您要查找的确切答案。
更新:创建用于检查的 a.out 的一种简单方法:
 echo "int main() { return 0; }" | g++ -xc++ - ./libMySharedLib.so
 ldd -r ./a.out

4
这几乎和我在原帖中建议的"nm -C -U"做的一样。问题是根本没有 'a.out' 应用程序,它只是一个共享库,所以执行 "ldd -r mylibrary.so" 会输出大量内容,因为它使用了许多其他动态库的符号。我特别关注的是在 我的 头文件中定义而缺失的符号,而不是外部库中的符号。 - David Claridge
3
可以接受这个建议,问题是如何轻松检查未定义的符号,而不是如何避免未定义的符号。 - http8086

10

那测试套件呢?你可以创建模拟可执行文件,链接到所需的符号。如果链接失败,则意味着你的库接口不完整。


4
我曾经遇到过同样的问题。我在使用C++开发组件模型时,当然需要让组件在运行时动态加载。以下是我应用过的三种解决方案:
  1. 花些时间定义一个能够编译静态文件的构建系统。虽然这会花费你一些工程时间,但它将节省你大量时间避免这些恼人的运行时错误。
  2. 将你的函数分组为众所周知和易于理解的部分,以便你可以将函数/存根分组,确保每个相应的函数都有其存根。如果你花时间充分记录它,可以编写一个脚本来检查定义(例如通过其doxygen注释)并检查相应的.cpp文件。
  3. 创建几个测试可执行文件,加载相同的库,并指定RTLD_NOW标志以dlopen(如果您在*NIX下)。它们将信号缺少的符号。
希望这些能帮到你。

类似于RTLD_NOW,是否存在一种环境变量来强制立即(非惰性)绑定? - Stefano Borini
1
yup. 这就是 LD_BIND_NOW (libc5; glibc since 2.1.1)。如果设置为非空字符串,则会导致动态链接器在程序启动时解析所有符号,而不是将函数调用解析推迟到首次引用它们的时候。这在使用调试器时非常有用。 - Stefano Borini
这对 OP 目的也可能有用处:LD_WARN (仅限 ELF)(自 glibc 2.1.3 以来)如果设置为非空字符串,则会发出有关未解析符号的警告。 - Stefano Borini
Stefano:是的,没错。这些变量是存在的。然而,如果你将动态加载推迟到以后(比如当用户加载一个模块时),除非你实际编写测试程序来加载预期的(未来的)库,否则这些变量是没有用处的。 - Diego Sevilla

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接