⚙️ 第八章 从源文件到可执行文件
📄 一句话概括

编译出来的是目标文件,还缺一步链接——把【我需要谁】和【我提供了谁】配起来;未定义符号就是这么报出来的。

📝 本节和前面几节的关系

8.3 讲完了翻译,这一节说明【翻译完还没结束】。

一、目标文件还是半成品

编译一个源文件,得到的是目标文件,不是可执行文件。 原因很实在:一个源文件常常调用了别的文件里的函数。 编译这个文件的时候,编译器还没看到那些函数,自然不知道它们的地址。 于是它只能先留个空位、记下名字——等后面有人来补。

二、两张表:定义了谁、还要谁

目标文件里因此带着两张表: 它定义了谁——自己提供了哪些函数。 它还需要谁——用到了哪些别人提供的函数。 举例:math.o 定义了 add、不需要别人;main.o 定义了 main、但需要 add 和 printf。 注意一个细节:需要 add、需要 printf,这两件事性质不同——前者在别的目标文件里,后者在标准库里。

三、链接器在配对

链接器的工作就是逐条去比对:把每个【还需要谁】找到出处,接上。 找得到就接上;找不到就报错——这就是【未定义符号】的来历。 所以那类报错的意思很直白:你用了某个名字,可谁都没提供它。 常见原因有三种:名字拼错了、忘了把那个目标文件加进来、忘了指定要链接哪个库。

四、完整链条

到这里整条链就齐了: 源码 → 编译 → 目标文件 → 链接 → 可执行文件。 两头都是文件,中间那一步最容易被忽略,也最容易出问题。 记住一句话:编译管【一个文件翻译得对不对】,链接管【这些文件能不能拼到一起】。

五、动手 Demo:仅靠编译无法得到可执行文件

编译出来的是目标文件,还是半成品。 它带着两张表:自己定义了谁、还要谁。链接器负责把这两张表配上。

objs = {"math.o": {"def": {"add"}, "use": set()},
        "main.o": {"def": {"main"}, "use": {"add", "printf"}}}
defined = set()
for o in objs.values():
    defined |= o["def"]
libs = {"printf"}
for nm in ("add", "printf"):
    print(nm, "在目标文件里" if nm in defined else "在库里")

实跑输出:

--------------------------------------------------------------------
仅靠编译无法得到可执行文件
--------------------------------------------------------------------

编译出来的**不是可执行文件**,而是**目标文件**——**它还是半成品。**
因为一个源文件常常**调用了别的文件里的函数**,那些地址编译时还不知道。

  所以目标文件里要带两张表:**自己定义了谁、还要谁。**

  目标文件     它定义了     它需要别人提供
  -----------------------------------------
  math.o       add          (无)
  main.o       main         add、printf

  链接器就是来配对这些表:**逐条去找【还需要谁】里的名字,找到就接上。**

  需要的名字     在目标文件里找到     在库里找到     结果
  ----------------------------------------------------------
  add            是                   否             接上了
  printf         否                   是             接上了

  **这正是【未定义符号】这类链接错误的来历:**
  你用了某个名字,可**谁都没提供它**——编译能过,链接会失败。
  所以完整链条是:**源码 → 编译 → 目标文件 → 链接 → 可执行文件。** 少一步都不成。

add 在别的目标文件里、printf 在标准库里,两个都接上了。 如果谁都没提供这个名字,链接器就报【未定义符号】。

30 秒实验

  1. 写两个小程序,一个调用另一个里的函数,先只编译不链接,看报什么错。
  2. 想想:为什么把函数声明写成另一个名字,编译能过而链接才报错。

常见误区

  1. 以为编译能过的程序就一定能跑。链接失败同样出不了可执行文件。
  2. 以为链接只是【把文件拼起来】。它还要修正每个符号的实际地址。
自测三问(点开对答案) 1. 编译一个源文件得到的是什么? —— 目标文件——还带着未解决的外部符号,不是可执行文件。 2. 目标文件里的两张表是什么? —— 自己定义了哪些符号;还需要别人提供哪些符号。 3. 【未定义符号】错误是怎么来的? —— 链接器在【还需要谁】里发现某个名字谁都没有提供。

相关笔记