什么是未定义的参考/未解析的外部符号错误?常见原因是什么?如何解决/预防?


当前回答

在链接共享库时,请确保未隐藏使用的符号。

gcc的默认行为是所有符号都可见。但是,当使用选项-fvisibility=hidden构建转换单元时,只有标记为__attribute__((可见性(“默认”))的函数/符号在生成的共享对象中是外部的。

您可以通过调用以下命令来检查要查找的符号是否为外部符号:

# -D shows (global) dynamic symbols that can be used from the outside of XXX.so
nm -D XXX.so | grep MY_SYMBOL 

隐藏/本地符号用小写符号类型的nm表示,例如t而不是代码段的“t”:

nm XXX.so
00000000000005a7 t HIDDEN_SYMBOL
00000000000005f8 T VISIBLE_SYMBOL

您还可以使用nm和选项-C来定义名称(如果使用了C++)。

与Windows DLL类似,可以使用define标记公共函数,例如DLL_public定义为:

#define DLL_PUBLIC __attribute__ ((visibility ("default")))

DLL_PUBLIC int my_public_function(){
  ...
}

大致对应于Windows的/MSVC版本:

#ifdef BUILDING_DLL
    #define DLL_PUBLIC __declspec(dllexport) 
#else
    #define DLL_PUBLIC __declspec(dllimport) 
#endif

有关可见性的更多信息可以在gcc wiki上找到。


当使用-fvisibility=hidden编译翻译单元时,生成的符号仍然具有外部链接(以大写符号类型显示,单位为nm),如果对象文件成为静态库的一部分,则可以毫无问题地用于外部链接。只有当对象文件链接到共享库中时,链接才会变为本地链接。

要查找对象文件中隐藏的符号,请运行:

>>> objdump -t XXXX.o | grep hidden
0000000000000000 g     F .text  000000000000000b .hidden HIDDEN_SYMBOL1
000000000000000b g     F .text  000000000000000b .hidden HIDDEN_SYMBOL2

其他回答

函数或类方法在源文件中使用内联说明符定义。

例如:-

主.cpp

#include "gum.h"
#include "foo.h"

int main()
{
    gum();
    foo f;
    f.bar();
    return 0;
}

foo.h(1)

#pragma once

struct foo {
    void bar() const;
};

口香糖.h(1)

#pragma once

extern void gum();

foo.cpp(1)

#include "foo.h"
#include <iostream>

inline /* <- wrong! */ void foo::bar() const {
    std::cout << __PRETTY_FUNCTION__ << std::endl;
}

gum.cpp(1)

#include "gum.h"
#include <iostream>

inline /* <- wrong! */ void gum()
{
    std::cout << __PRETTY_FUNCTION__ << std::endl;
}

如果指定gum(类似地,foo::bar)在其定义中是内联的,那么编译器将通过以下方式内联gum(如果它选择):-

没有任何独特的口香糖定义,因此不发出任何符号,链接器可以通过该符号引用口香糖的定义,而是将所有对gum的调用替换为编译后的gum主体的内联副本。

因此,如果在源文件gum.cpp中内联定义gum,则编译为对象文件gum.o,其中所有对gum的调用都是内联的并且没有定义接头可以指代口香糖的符号。当你将gum.o与另一个对象文件(例如main.o)链接到程序中引用外部符号gum时,链接器无法解析这些参考文献。因此连杆失效:

编译:

g++ -c  main.cpp foo.cpp gum.cpp

链接:

$ g++ -o prog main.o foo.o gum.o
main.o: In function `main':
main.cpp:(.text+0x18): undefined reference to `gum()'
main.cpp:(.text+0x24): undefined reference to `foo::bar() const'
collect2: error: ld returned 1 exit status

如果编译器可以在调用gum的每个源文件中看到它的定义,则只能将gum定义为内联。这意味着它的内联定义需要存在于包含在每个源文件中的头文件中您可以在其中调用gum。做两件事之一:

要么不内联定义

从源文件定义中删除内联说明符:

foo.cpp(2)

#include "foo.h"
#include <iostream>

void foo::bar() const {
    std::cout << __PRETTY_FUNCTION__ << std::endl;
}

gum.cpp(2)

#include "gum.h"
#include <iostream>

void gum()
{
    std::cout << __PRETTY_FUNCTION__ << std::endl;
}

重新生成:

$ g++ -c  main.cpp foo.cpp gum.cpp
imk@imk-Inspiron-7559:~/develop/so/scrap1$ g++ -o prog main.o foo.o gum.o
imk@imk-Inspiron-7559:~/develop/so/scrap1$ ./prog
void gum()
void foo::bar() const

成功

或正确内联

头文件中的内联定义:

foo.h(2)

#pragma once
#include <iostream>

struct foo {
    void bar() const  { // In-class definition is implicitly inline
        std::cout << __PRETTY_FUNCTION__ << std::endl;
    }
};
// Alternatively...
#if 0
struct foo {
    void bar() const;
};
inline void foo::bar() const  {
    std::cout << __PRETTY_FUNCTION__ << std::endl;
}
#endif

口香糖.h(2)

#pragma once
#include <iostream>

inline void gum() {
    std::cout << __PRETTY_FUNCTION__ << std::endl;
}

现在我们不需要foo.cpp或gum.cpp:

$ g++ -c main.cpp
$ g++ -o prog main.o
$ ./prog
void gum()
void foo::bar() const

我正在构建一个共享/动态库。它在Linux和*BSD上运行,但在Mac OS X上,完全相同的编译和链接命令会产生未解决的引用错误。有什么好处?

Mac OS X在内部与Linux和*BSD非常不同。对象/可执行文件格式为

在Linux和*BSD上,当构建共享库时,默认情况下允许未解析的引用。期望它们在加载时能够满足主可执行文件和/或其他共享库的要求。如果在加载时无法解析这些符号,则共享库将无法加载。

在Mac OS X上,构建动态库时,默认情况下不允许未解析的引用。如果希望在加载时解析引用,则需要显式启用未解析的引用。这是使用未定义的dynamic_lookup链接器标志完成的。

在构建可加载插件时,允许未解析的引用非常有用。

链接在引用库的对象文件之前使用库

您正在尝试编译程序并将其与GCC工具链链接。链接指定了所有必要的库和库搜索路径如果libfoo依赖于libbar,那么链接会正确地将libfoo放在libbar之前。链接失败,对某些错误的引用未定义。但是所有未定义的东西都在你的头文件中声明#并且实际上是在您链接的库中定义的。

示例在C中。它们也可以是C++

一个涉及您自己构建的静态库的最小示例

my_lib.c文件

#include "my_lib.h"
#include <stdio.h>

void hw(void)
{
    puts("Hello World");
}

my_lib.h

#ifndef MY_LIB_H
#define MT_LIB_H

extern void hw(void);

#endif

例如1.c

#include <my_lib.h>

int main()
{
    hw();
    return 0;
}

构建静态库:

$ gcc -c -o my_lib.o my_lib.c
$ ar rcs libmy_lib.a my_lib.o

编译程序:

$ gcc -I. -c -o eg1.o eg1.c

尝试将其与libmy_lib.a链接,但失败:

$ gcc -o eg1 -L. -lmy_lib eg1.o 
eg1.o: In function `main':
eg1.c:(.text+0x5): undefined reference to `hw'
collect2: error: ld returned 1 exit status

如果您在一个步骤中编译和链接,则会得到相同的结果,例如:

$ gcc -o eg1 -I. -L. -lmy_lib eg1.c
/tmp/ccQk1tvs.o: In function `main':
eg1.c:(.text+0x5): undefined reference to `hw'
collect2: error: ld returned 1 exit status

一个涉及共享系统库的最小示例,即压缩库libz

例如2.c

#include <zlib.h>
#include <stdio.h>

int main()
{
    printf("%s\n",zlibVersion());
    return 0;
}

编译程序:

$ gcc -c -o eg2.o eg2.c

尝试将程序与libz链接并失败:

$ gcc -o eg2 -lz eg2.o 
eg2.o: In function `main':
eg2.c:(.text+0x5): undefined reference to `zlibVersion'
collect2: error: ld returned 1 exit status

如果您一次编译并链接:

$ gcc -o eg2 -I. -lz eg2.c
/tmp/ccxCiGn7.o: In function `main':
eg2.c:(.text+0x5): undefined reference to `zlibVersion'
collect2: error: ld returned 1 exit status

示例2的一个变体涉及pkg配置:

$ gcc -o eg2 $(pkg-config --libs zlib) eg2.o 
eg2.o: In function `main':
eg2.c:(.text+0x5): undefined reference to `zlibVersion'

你做错了什么?

在要链接的对象文件和库序列中程序,您将库放在引用的对象文件之前他们您需要将库放在引用对他们来说。

正确链接示例1:

$ gcc -o eg1 eg1.o -L. -lmy_lib

成功:

$ ./eg1 
Hello World

正确链接示例2:

$ gcc -o eg2 eg2.o -lz

成功:

$ ./eg2 
1.2.8

正确链接示例2 pkg配置变体:

$ gcc -o eg2 eg2.o $(pkg-config --libs zlib) 
$ ./eg2
1.2.8

解释

从这里开始,阅读是可选的。

默认情况下,GCC在您的发行版上生成的链接命令,在中从左到右使用链接中的文件命令行序列。当它发现一个文件引用了某个并且不包含其定义,to将搜索定义在更右边的文件中。如果它最终找到了定义解析引用。如果任何引用在结束时仍未解决,链接失败:链接器没有向后搜索。

首先,示例1,使用静态库my_lib.a

静态库是对象文件的索引存档。当链接器在链接序列中找到-lmy-lib,并指出这是指到静态库/libmy_lib.a,它想知道您的程序是否需要libmy_lib.a中的任何对象文件。

libmy_lib.a中只有一个对象文件,即my_lib.o,并且只定义了一个对象在my_lib.o中,即函数hw。

链接器将决定您的程序需要my_lib.o,如果并且仅当它已经知道您的程序在一个或多个对象文件中引用hw添加到程序中,并且没有添加任何对象文件包含hw的定义。

如果这是真的,那么链接器将从库中提取my_lib.o的副本,并将其添加到您的程序中。然后,您的程序包含hw的定义,因此其对hw的引用被解析。

当您尝试链接程序时,如:

$ gcc -o eg1 -L. -lmy_lib eg1.o

链接器在看到时没有将eg1.o添加到程序中-lmy_库。因为在这一点上,它还没有看到eg1.o。您的程序尚未引用hw:it根本没有做任何引用,因为它做的所有引用在eg1.o中。

因此,链接器不会将my_lib.o添加到程序中,并且没有其他内容用于libmy_lib.a。

接下来,它找到eg1.o,并将其添加到程序中。中的对象文件链接序列总是添加到程序中。现在,该程序使对hw的引用,并且不包含hw的定义;但是链接序列中没有任何内容可以提供缺失释义对hw的引用最终无法解析,链接失败。

第二,示例2,使用共享库libz

共享库不是对象文件或类似文件的存档更像是一个没有主功能的程序而是公开它定义的多个其他符号程序可以在运行时使用它们。

今天,许多Linux发行版配置其GCC工具链,以便其语言驱动程序(GCC、g++、gfortran等)指示系统链接器(ld)根据需要链接共享库。你有一个这样的发行版。

这意味着当链接器在链接序列中找到-lz,并发现这是指到共享库(例如)/usr/lib/x86_64-linux-gnu/libz,它想知道它添加到程序中的任何尚未定义的引用是否具有libz导出的定义

如果这是真的,那么链接器将不会从libz和将它们添加到您的程序中;相反,它只会修改程序的代码从而:-

在运行时,系统程序加载器会将libz的副本加载到无论何时加载程序的副本,都可以执行与程序相同的过程。在运行时,每当程序引用libz,该引用使用中libz副本导出的定义相同的过程。

您的程序只想引用一个由libz导出的定义,即函数zlibVersion,在eg2.c中仅引用一次。如果链接器将该引用添加到程序中,然后找到定义由libz导出,引用被解析

但当您尝试链接程序时,如:

gcc -o eg2 -lz eg2.o

事件的顺序是错误的,其方式与示例1相同。在链接器找到-lz时,没有对任何内容的引用在节目中:他们都在eg2.o中,这还没有被看到。所以链接器决定它不适用于libz。当它达到eg2.o时,将其添加到程序中,然后对zlibVersion有未定义的引用,链接序列完成;该引用未解析,链接失败。

最后,示例2的pkg配置变体现在有了一个显而易见的解释。壳体膨胀后:

gcc -o eg2 $(pkg-config --libs zlib) eg2.o

变为:

gcc -o eg2 -lz eg2.o

这再次只是示例2。

我可以重复示例1中的问题,但不能重复示例2中的问题

联动装置:

gcc -o eg2 -lz eg2.o

对你来说很好!

(或者:在Fedora 23上,这种链接很好,但在Ubuntu 16.04上失败了)

这是因为链接工作的发行版是不配置其GCC工具链以根据需要链接共享库。

过去,类unix系统链接静态和共享是很正常的不同的规则。链接序列中的静态库已链接但是共享库是无条件链接的。

这种行为在链接时是经济的,因为链接器不必考虑程序是否需要共享库:如果是共享库,大多数链接中的大多数库都是共享库。但也有缺点:-

这在运行时是不经济的,因为它会导致共享库与程序一起加载,即使不需要它们。静态库和共享库的不同链接规则可能会令人困惑对于不熟练的程序员,他们可能不知道-lfo是否在他们的联系中将解析为/some/where/libfoo.a或/some/wwhere/libfoo.so,并且可能不理解共享库和静态库之间的区别无论如何

这种权衡导致了今天的分裂局面。一些发行版有更改了共享库的GCC链接规则,以便根据需要这一原则适用于所有图书馆。一些发行版沿用了旧版本方法

为什么即使同时编译和链接,我仍然会遇到这个问题?

如果我这样做:

$ gcc -o eg1 -I. -L. -lmy_lib eg1.c

当然,gcc必须首先编译eg1.c,然后链接生成的带有libmy_lib.a的对象文件,那么它怎么可能不知道该对象文件在进行链接时是否需要?

因为使用单个命令编译和链接不会更改连杆顺序。

当您运行上面的命令时,gcc发现您需要编译+链接。因此,在幕后,它生成一个编译命令,并运行然后生成一个链接命令并运行它,就像您运行了两个命令:

$ gcc -I. -c -o eg1.o eg1.c
$ gcc -o eg1 -L. -lmy_lib eg1.o

因此,如果运行这两个命令,链接就会失败。这个您在失败中注意到的唯一区别是gcc生成了编译+链接情况下的临时对象文件,因为您没有告诉它使用eg1.o.我们看到:

/tmp/ccQk1tvs.o: In function `main'

而不是:

eg1.o: In function `main':

另请参见

指定相互依赖的链接库的顺序错误

将相互依赖的库按错误的顺序排列只是一种方法在其中,您可以获取需要对即将到来的事物进行定义的文件在链接中晚于提供定义的文件。将库放在引用它们的对象文件是犯同样错误的另一种方式。

对的未定义引用WinMain@16或类似的“不寻常”main()入口点引用(特别是对于visual studio)。

您可能错过了使用实际IDE选择正确的项目类型。IDE可能希望将例如Windows应用程序项目绑定到这样的入口点函数(如上面缺失的引用中所指定的),而不是通常使用的int main(int argc,char**argv);签名

如果IDE支持普通控制台项目,您可能希望选择此项目类型,而不是windows应用程序项目。


下面是从真实世界问题中更详细地处理的案例1和案例2。

当包含路径不同时

当头文件及其关联的共享库(.lib文件)不同步时,可能会发生链接器错误。让我解释一下。

链接器是如何工作的?链接器通过比较函数声明(在头中声明)和函数定义(在共享库中)的签名来匹配它们。如果链接器找不到完全匹配的函数定义,则可能会出现链接器错误。

即使声明和定义似乎匹配,仍然可能出现链接器错误吗?对它们在源代码中看起来可能相同,但这实际上取决于编译器所看到的内容。基本上,你可能会遇到这样的情况:

// header1.h
typedef int Number;
void foo(Number);

// header2.h
typedef float Number;
void foo(Number); // this only looks the same lexically

注意,即使两个函数声明在源代码中看起来相同,但根据编译器的不同,它们确实不同。

你可能会问,一个人在这样的情况下是如何结束的?当然包括路径!如果在编译共享库时,include路径指向header1.h,而您最终在自己的程序中使用了header2.h,那么您将无法理解发生了什么(双关语)。

下面将解释这在现实世界中如何发生的一个示例。

通过示例进一步阐述

我有两个项目:graphics.lib和main.exe。两个项目都依赖common_math.h。假设库导出以下函数:

// graphics.lib    
#include "common_math.h" 
   
void draw(vec3 p) { ... } // vec3 comes from common_math.h

然后,您继续将库包含在您自己的项目中。

// main.exe
#include "other/common_math.h"
#include "graphics.h"

int main() {
    draw(...);
}

繁荣你得到了一个链接器错误,你不知道它为什么会失败。原因是公共库使用相同includecommon_math.h的不同版本(我在本例中通过包含不同的路径来说明这一点,但可能并不总是那么明显。可能编译器设置中的包含路径不同)。

注意,在这个例子中,链接器会告诉你它找不到draw(),而实际上你知道它显然是由库导出的。你可以花几个小时挠头,想知道出了什么问题。问题是,链接器看到的签名不同,因为参数类型略有不同。在本例中,就编译器而言,vec3在两个项目中都是不同的类型。这可能是因为它们来自两个稍微不同的包含文件(可能包含文件来自库的两个不同版本)。

调试链接器

如果您正在使用Visual Studio,DUMPBIN是您的朋友。我相信其他编译器也有类似的工具。

过程如下:

注意链接器错误中给出的奇怪的损坏名称。(例如。draw@graphics@XYZ)。将库中导出的符号转储到文本文件中。搜索导出的感兴趣符号,并注意损坏的名称不同。请注意,为什么被弄乱的名字最终会不同。您将能够看到参数类型不同,即使它们在源代码中看起来相同。它们不同的原因。在上面给出的示例中,它们是不同的,因为包含文件不同。

[1] 我所说的项目是指一组链接在一起以生成库或可执行文件的源文件。

编辑1:改写第一节,使其更容易理解。请在下面评论,让我知道是否需要修复其他问题。谢谢