我无法理解编译器和链接器的工作方式以及它们创建的文件。更具体地说,.cpp,.h,.lib,.dll,.o,.exe如何一起工作?我最感兴趣的是C ++,但也对Java和C#感到疑惑。任何书籍/链接将不胜感激!
答案 0 :(得分:6)
关于这个主题的书很少令人惊讶这里有一些想法:
除非您实际使用表驱动方法编写编译器,否则不要打扰Dragon Book。这是一个非常难读的abd并没有涵盖最简单的解析方法 - 递归下降 - 任何细节。警告:我还没看过最新版本。
如果您真的想编写编译器,请查看"Brinch Hansen on Pascal Compilers“,这是一个简单的阅读,并提供了一个小型pascal编译器的完整资源。不要让pascal的东西放你离开了 - 它所教授的课程适用于所有编译语言。
说到链接,资源非常少。我读过的关于这个主题的最好的书是Linkers & Loaders。
答案 1 :(得分:3)
我认为你真的不需要任何书籍。正如我理解你的问题,你只想知道每种类型的文件 是什么,以及它们与编译过程的关系。 如果您想要详细了解所有内容,或者您正在编写自己的C ++编译器,那么您显然需要阅读书籍。
但这是高级版本:
首先,让我们忽略连接器。并非每种语言都使用专用链接器,事实上,即使是C和C ++语言标准也没有提到链接。链接器是一个实现细节,通常用于使所有部分组合在一起,但从技术上讲,它根本不需要存在。
此外,这是特定于C / C ++的。编译过程对于每种语言都是不同的,特别是,C / C ++使用大多数现代语言所避免的凌乱,过时和低效的机制。
首先,你写一些代码。此代码保存在许多文件中(通常使用扩展名.c,.cc或.cpp)和多个标头(.h,.hh或.hpp)。但是,这些扩展不是必需的。它们只是一种常见的约定,但从技术上讲,您可以为文件命名。
为了举例,我们假设我们有以下文件:
foo.h中:
void foo();
Foo.cpp中:
#include "foo.h"
#include "bar.h"
void foo() {
bar();
}
bar.h:
void bar();
bar.cpp:
#include "bar.h"
void bar() {
}
编译器获取一个 .cpp文件,并对其进行处理。假设我们首先编译foo.cpp。 它首先做的是预处理:扩展所有宏,通过将包含文件的内容复制/粘贴到#include来自的位置来处理#include指令。完成后,您将拥有一个翻译单元或编译单元,它将如下所示:
void foo(); //#include "foo.h"
void bar(); //#include "bar.h"
void foo() {
bar();
}
基本上,在我们的简单示例中发生的所有事情都是标题被复制/粘贴。
现在,编译器尽可能地将其编译为机器代码。当然,鉴于它只能看到这一个代码文件,它将运行一个函数调用函数,它无法看到定义。
在我们的案例中,如何实现对bar()
的调用?它不能,因为它无法看到bar
做什么。所有它可以看到(因为它包含bar.h
是函数bar
存在,并且它不带参数并返回void。所以编译器基本上生成一点“填充在后来的“标签,基本上说”跳转到这个功能的地址,一旦我们找到了什么地址“。
现在我们编译了foo.cpp
。
此进程的输出是一个目标文件,通常扩展名为.o或.obj。
编译器现在也在bar.cpp
上调用,并且发生了很多相同的事情。包含标题,然后将代码编译为机器代码,尽管这次,我们不应该遇到任何缺少定义的问题。
所以我们现在留下foo.o
和bar.o
,其中包含两个编译单元中每个编译单元的编译代码。
现在我们处在一个有趣的无人区,C ++语言标准告诉我们该程序应该做什么,但没有更多关于如何到达那里的说法,但该程序实际上并没有这样做。我们还没有拥有程序。所以为了解决这个问题,我们调用链接器。
我们提供所有目标文件,并通过它们读取并基本上填充空白。在阅读foo.o
时,会注意到bar()
的来电,其中bar()
的地址未知。但是链接器可以访问bar.o``as well, so it is able to look up the definition of
bar(), and determine its address, which it can paste into the call site inside the
foo()`函数。它基本上将这些独立的目标文件链接在一起。当它解决了所有这些问题后,它会将所有代码一起抛出到一个二进制文件中(在Windows上具有.exe扩展名),这是您的程序。实际代码由编译器生成,然后链接器跳入并将一个文件中的定义与其他文件中的引用链接在一起。
答案 2 :(得分:2)
对于操作系统不可知和语言无关的解释,但仍有点POSIXy尝试:
Tanenbaum - 现代操作系统第3版。
它涵盖了所有这些。
答案 3 :(得分:2)
我在下面所说的仅仅是近似的,但我会相信你需要知道的一些重要事项。
在C ++中,编译的阶段是(1)预处理,(2)实际编译,以及(3)链接。
预处理阶段将 cpp 文件作为输入,并按照“#include”和“#define”等指令进行文本替换。特别是, h 文件的内容将逐字复制到“#include”指令的位置。
实际编译会生成位于 o 文件中的机器代码。 o 文件中出现的大多数指令都是处理器知道的指令,但 call function_name 除外。处理器不知道名称,只知道地址。
在(静态)链接阶段,将多个 o 文件放在一起。现在我们知道函数定义的最终位置。也就是说,我们知道它的地址。 调用function_name 指令被转换为调用function_address 指令,处理器知道如何执行。 lib 文件是 o 文件的预编译捆绑包,它们被(静态)链接器视为输入。它们包含 printf , memset 等功能的机器代码。
在静态链接期间,某些名称不会转换为地址。这些是引用其定义位于 dll 文件中的函数的名称。 (与 lib 文件一样, dll 文件也是 o 文件的捆绑包。)这些剩余的名称在程序运行时被转换为适当的地址(是,在运行时)在一个称为动态链接的过程中。此过程包括查找正确的 dll 文件并使用给定名称查找函数。
在Java中,故事有点不同。首先,没有预处理。其次,编译的结果不是机器代码而是字节码,并且存在于类文件中(不是 o 文件)。字节码类似于机器代码,但处于更高的抽象级别。特别是,在字节码中,您可以说调用function_name 。这意味着没有静态链接阶段,并且按名称查找函数始终在运行时完成。字节码不在真实机器上运行,而是在虚拟机上运行。 C#与Java类似,主要区别在于字节码(在C#的情况下称为通用中间语言)略有不同。