如果您是C/C ++开发人员,则可能遇到此烦人的链接器错误之前:
未定义的引用“符号”
在这篇文章中,我确切地解释了此错误的含义以及为什么发生。我们进入对象文件和链接过程的详细信息。
注意:这篇文章假定Linux环境。
tl; dr
此错误意味着以下一个:
- 您声明了一个函数并在不提供其定义的情况下调用它。
- 您包含了库的标头文件,并在此标头文件中调用一个函数,但您没有链接到库本身。
C/C ++汇编步骤
在以下步骤中进行了C/C ++程序的汇编:
- 预处理:获取原始的C/C ++源文件并生成中间的C/C ++源文件。
- 汇编:获取中间C/C ++源文件并生成汇编代码文件。
- 汇编:获取汇编代码文件并生成一个对象文件。
- 链接:获取对象文件并链接它们以生成最终的可执行文件。
我们将检查一个简单的C ++程序,并将在步骤2“编译”中停止其汇编以检查输出组件文件,然后在步骤3“ assembly”中检查输出对象文件。
。C/C ++组装
我将在C中解释C。
看以下C程序:
void defined_function() {}
void undefined_function();
void main()
{
defined_function();
undefined_function();
}
我们定义一个带有空白的函数,我们声明一个名为undefined_function
的函数,而无需定义它,我们将两个函数称为主函数。
假设该程序位于名为main.c
的文件中。我们使用-s选项将其与GCC一起编译,以在编译步骤中停止并检查输出组件文件:
gcc -S main.c
输出组件文件main.s
将是这样的:
.file "main.c"
.text
.globl defined_function
.type defined_function, @function
defined_function:
.LFB0:
.cfi_startproc
pushq %rbp
.cfi_def_cfa_offset 16
.cfi_offset 6, -16
movq %rsp, %rbp
.cfi_def_cfa_register 6
nop
popq %rbp
.cfi_def_cfa 7, 8
ret
.cfi_endproc
.LFE0:
.size defined_function, .-defined_function
.globl main
.type main, @function
main:
.LFB1:
.cfi_startproc
pushq %rbp
.cfi_def_cfa_offset 16
.cfi_offset 6, -16
movq %rsp, %rbp
.cfi_def_cfa_register 6
movl $0, %eax
call defined_function
movl $0, %eax
call undefined_function
nop
popq %rbp
.cfi_def_cfa 7, 8
ret
.cfi_endproc
.LFE1:
.size main, .-main
.ident "GCC: (GNU) 12.2.0"
.section .note.GNU-stack,"",@progbits
现在,让我们快速组装刷新,然后再继续。汇编程序包含CPU一一执行的指令列表。这些说明可以是运动说明,例如mov
,计算说明,例如add
和sub
,控制传输仪器,例如jmp
和call
,等等。
一个组装程序还可以在label:
表格的线路开头包含标签。在这样的线条开头编写标签可以定义它们。标签只是在内存中这个位置的标签,可以在其他说明中引用jmp
和call
。因此,例如jmp label
意味着跳到label
标记的内存位置。
现在让我们检查上面的汇编程序。该程序包含许多我们不需要的细节,我们只专注于重要部分。
注意标签defined_function:
和main:
:这些对应于我们C程序中的定义功能。另外,请注意指令call defined_function
和call undefined_function
:这些对应于我们的C程序中的函数调用。还请注意,未定义的函数undefined_function
。
汇编到对象文件
现在,我们使用-c选项将CCC编译为CCC,以在汇编步骤中停止:
gcc -c main.c
另外,我们可以使用GNU汇编器组装汇编文件:
as main.s -o main.o
两种方法都会产生相同的输出对象文件main.o
。
对象文件格式
您可能知道汇编步骤将汇编代码转换为二进制,但是这是对象文件中唯一存在的二进制文件吗?答案是否。
对象文件包含有关该程序的其他元数据,例如节信息,符号表和重定位信息。在Linux中,对象文件的格式称为 elf(可执行和可链接的格式)。有许多格式,例如Windows中使用的PE(便携式可执行文件)格式和称为A.Out的旧格式。在这篇文章中,我们将重点介绍精灵格式。
我们如何查看精灵文件的内容? Linux中有一个名为readelf
的实用程序,我们可以使用。我们现在仅对符号表感兴趣,因此我们在对象文件上使用Readelf并将其传递给仅查看符号表:
readelf -s main.o
输出如下:
Symbol table '.symtab' contains 6 entries:
Num: Value Size Type Bind Vis Ndx Name
0: 0000000000000000 0 NOTYPE LOCAL DEFAULT UND
1: 0000000000000000 0 FILE LOCAL DEFAULT ABS main.c
2: 0000000000000000 0 SECTION LOCAL DEFAULT 1 .text
3: 0000000000000000 7 FUNC GLOBAL DEFAULT 1 defined_function
4: 0000000000000007 27 FUNC GLOBAL DEFAULT 1 main
5: 0000000000000000 0 NOTYPE GLOBAL DEFAULT UND undefined_function
让我们分析此输出。
请注意,defined_function
和main
有符号,其NDX是一个数字,这意味着它们已定义。汇编器在汇编代码中定义的每个标签的符号表中创建一个定义的符号。因为有一些行以defined_function:
和main:
开头的汇编代码,所以它们是符号表中定义的符号。
还请注意,undefined_function
有一个符号,其NDX是UND,这意味着未定义。汇编器在指令中引用但未定义的每个标签的符号表中创建一个未定义的符号。因为undefined_function
在指令中引用(call undefined_function
),并且在汇编代码中没有从undefined_function:
开始的行,所以它是符号表中的一个未定义符号。
另外,请注意,我们的三个符号defined_function
,undefined_function
和main
具有全局绑定,这意味着它们是全局符号。这很重要,因为当链接器链接文件时,它仅看到全局符号。
可执行文件和库文件
在编译的最后阶段,对象和库文件被链接在一起以产生可执行文件或库文件。可执行文件是具有入口点的文件。库文件是对象文件的集合,每个对象文件都有一个函数集合,并且没有输入点。库有两种类型:静态和动态库。在这篇文章中,我们只对静态库感兴趣。在Linux中,静态库具有.a扩展名,有时称为静态档案,它们也以精灵格式。
链接
链接的输入是对象文件和库文件。链接时,链接器会在所有输入对象和库文件中读取所有全局符号。对于每个未定义的符号,链接器检查是否有一个定义的符号,并从另一个文件中取出相同名称。如果有一个定义的符号,每个未定义的符号的符号具有相同的名称,则链接可以成功进行。另一方面,如果有未定义的符号没有定义具有相同名称的符号,则链接器会发出错误对它们的'符号'的不确定引用。
在我们的示例中,如果链接器的输入仅为main.o
,则链接器将发出以下错误:
未定义的引用'undefined_function'
这是因为undefined_function
是一个未定义的符号,没有具有相同名称的定义符号。
此错误的解决方案要么是在main.c
中定义undefined_function
,要使用具有undefined_function
定义的另一个C文件进行编译,或者与已定义的undefined_function
的库链接。
结论
总而言之,输入C文件中的每个定义函数都将在输出对象文件中具有定义的符号,并且在输入C文件中声明并调用但未定义的函数将在输出对象文件中具有未定义的符号。在链接过程中,当一个未定义的符号没有另一个具有相同名称的定义符号时,链接器会发出不确定的参考错误。
就是这样。希望您现在真正了解为什么会发生此错误,希望您也深入了解对象文件的内容和链接过程ð