链接器如何知道外部函数的定义在哪里?

12

我阅读了一些帖子,并得出结论:extern告诉编译器“此函数存在,但其代码在其他地方。不要惊慌。”但链接器如何知道函数定义在哪里。

我的情况:- 我正在使用Keil uvision 4进行工作。有一个头文件grlib.h,主函数在grlib_demo.c中(它包括grlib.h)。现在,有一个函数GrCircleDraw()在Circle.c中定义,在grlib_demo.c中调用,还有一个语句

extern void GrCircleDraw(all arguments);

在grlib.h中。我的问题是链接器如何知道GrCircleDraw()的定义在哪里,因为Circle.c没有包含在grlib.h和grlib_demo.c中。

注意:文件grlib.h和Circle.c在同一个文件夹中。代码运行成功。


欢迎来到链接器的魔法世界。 - daniel gratzer
4
在某个层面上,编译器并不知道函数的具体位置;寻找函数的任务是由链接器完成的。你可以通过在命令行中指定目标文件或库文件的方式告诉链接器函数所在的位置,从而让链接器知道函数的位置。 - Jonathan Leffler
@jozefg 你是指黑魔法,对吗? - Ankit Gupta
4个回答

11
当您在ELF格式中编译.o文件时,您的.o文件上有许多内容,例如: - 包含代码的.text部分; - 包含全局变量的.data、.rodata、.rss部分; - 包含符号列表(函数、全局变量和其他)及其在文件中的位置以及.o文件使用的符号的.symtab; - 诸如.rela.text之类的部分,这些部分是重定位列表--这些是链接编辑器(和/或动态链接器)必须进行的修改,以便将程序的不同部分链接在一起。

在调用方

让我们编译一个简单的C文件:
extern void GrCircleDraw(int x);

int foo()
{
  GrCircleDraw(42);
  return 3;
}

int bla()
{
  return 2;
}

使用:

gcc -o test.o test.c -c

我正在使用系统的本地编译器,但当交叉编译到ARM时,它也可以正常工作。

您可以使用以下命令查看.o文件的内容:

readelf -a test.o

在符号表中,您将找到以下内容:
符号表'.symtab'包含10个条目:
   Num:    Value          Size Type    Bind   Vis      Ndx Name
     0: 0000000000000000     0 NOTYPE  LOCAL  DEFAULT  UND 
[...]
     8: 0000000000000000    21 FUNC    GLOBAL DEFAULT    1 foo
     9: 0000000000000000     0 NOTYPE  GLOBAL DEFAULT  UND GrCircleDraw
    10: 0000000000000015    11 FUNC    GLOBAL DEFAULT    1 bla

我们的foo函数和bla函数各有一个符号。值字段给出它们在.text部分中的位置。

对于使用的符号GrCircleDraw,有一个符号:它未定义,因为该函数未在此.o文件中定义,但仍需在其他地方找到。

.text部分的重定位表(.rela.text)中,您会发现:

重定位段'.rela.text'的偏移量为0x260,包含1个条目:
  偏移量         信息           类型           符号值       符号名 + 加数
00000000000a  000900000002 R_X86_64_PC32     0000000000000000 GrCircleDraw - 4

这个地址在foo内部:链接编辑器将会用GrCircleDraw函数的地址来修补这个地址处的指令。

被调用方实现

现在让我们编译一个GrCircleDraw的实现:

void GrCircleDraw(int x)
{

}

让我们来看一下它的符号表:

符号表'.symtab'包含9个条目:
   Num:    Value          Size Type    Bind   Vis      Ndx Name
[...]
     8: 0000000000000000     9 FUNC    GLOBAL DEFAULT    1 GrCircleDraw

它有一个条目GrCircleDraw,定义了它在.text部分中的位置。

将它们链接在一起

因此,当链接编辑器将两个文件组合在一起时,它知道:

  • 哪些函数是在哪个.o文件中定义的以及它们的位置;
  • 调用者代码中必须更新为被调用者地址的位置。

2
直到现在我才发现没有人点赞,真不敢相信。这是一个非常精心制作的答案,我很高兴看到ELF被这样分解。 - RastaJedi

9
简单来说,“编译器不需要知道,但链接器必须能找到它”。通过多个 .o 文件或者库,链接器必须能够找到 GrCircleDraw 函数的唯一定义。

@JonathanLeffler 没有第二个定义。我的问题是链接器如何找到那一个定义。 - Ankit Gupta
@GregHewgill 但是,Circle.c甚至没有被编译,我们怎么可能有circle.o呢? - Ankit Gupta
1
@AnkitGupta:所以你告诉我们:(1)有一个circle.c但它没有被编译;(2)GrCircleDraw唯一定义在circle.c中;和(3)你的程序编译,链接和运行成功。我不相信这些都是真的。特别是,我怀疑(2),并且可能确实存在另一个GrCircleDraw的定义,也许在你链接的库文件中。 - Greg Hewgill
1
你的链接器应该带有一些实用工具来帮助解决这类问题。我认为 Visual C++ 的工具叫做 dumpbin.exe,但你没有说你使用哪个编译器工具链。另一种方法是要求链接器创建一个映射文件输出,其中显示了关于链接器如何解析程序的大量详细信息。 - Greg Hewgill
@AnkitGupta 嗯,既然 circle.c 不是你的程序的一部分(假设你没有链接 circle.o),编译器和链接器就不会关心它!在程序中只能有一个定义的 GrCircleDraw - user253751
显示剩余6条评论

5
编译器只会将extern函数的名称放入.obj文件中,编译器不需要了解更多相关信息。当你开始链接时,作为开发人员,你需要将所有必要的目标文件和库文件提供给链接器。链接器将把这些函数排列成一个二进制文件。如果你没有指定正确的库或.obj文件,链接将会以“未解析的blah-blah”失败。默认库通常是隐含包括的,这使事情变得复杂并产生了幻觉。你可以始终指定不想要任何隐含库,并显式地包含所有内容。不幸的是,每个系统都有自己的方式来处理这个问题。

这段代码是一个示例代码。我想知道链接器如何知道GrCircleDraw()的定义在哪里,因为Circle.c没有被包含在任何文件中。 - Ankit Gupta
1
由于您的函数原型在 grlib.h 中,很可能该函数的主体位于 grlib.libgrlib.a 中。请查看构建日志或 .map 文件,它们可能会给您一些线索。 - Kirill Kobelev

0

链接通常是这样进行的:迭代命令行并使用给定的每个参数:

  1. 如果它是一个对象文件,则直接使用;
  2. 在需要的范围内使用(=以满足到目前为止未解决的所有引用)。

最后,必须满足每个引用才能成功链接。链接器命令行中给出的行的顺序很重要。


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