编译器、链接器和解释器
编译器
编译器的作用就是将高级编程语言翻译为机器代码。
编译器工作过程一般分为:
- 词法分析:将高级语言解析成 Token 集合;
- 语法分析:将 Token 集合构建成语法树,在这个过程可以判断出语法是否有误,比如
while
后面是否{
等等; - 语义分析:判断语法树是否有明显的语义错处,比如:string 不能与 number 相加;
- 中间代码生成:在一些编译器中,源代码可能会被转换成中间代码,这是一种介于高级语言和底层机器代码之间的表示形式。中间代码易于优化和跨平台生成。
- 优化:编译器会进行一系列的优化操作,以提高生成的机器代码的性能。这包括代码消除、循环展开、内联函数等优化技术。
- 目标代码生成:目标代码生成阶段将中间代码或其他中间表示翻译为特定体系结构的机器代码。这些机器代码可以由计算机直接执行。
链接器
编译器生成了一堆二进制文件,怎么运行这些二进制文件呢?链接器的作用就是将多个目标文件(object files)链接为一个可执行文件或库。
1. 符号解析(Symbol Resolution):
符号指的是全局变量
和 函数
。
每个文件都要确认两个事,自己有哪些符号可以供别的文件使用
和 引用别的文件的符号真实存在
。
链接器会从目标文件和库文件中提取这些符号,并建立符号表,记录每个符号的名称和地址。如果有多个目标文件或库中存在相同名称的符号,链接器会根据不同的规则解决冲突。
目标文件通常是由编译器生成的二进制文件,包含函数和变量的定义以及对其他符号的引用;而库文件则包含预编译的目标文件(静态链接,如
.a
或.lib
文件)。
2. 重定位(Relocation):
目标文件和库文件通常会包含相对于文件起始位置的相对地址,这些地址需要在最终可执行文件中被映射到正确的内存地址上。链接器会遍历目标文件中的重定位信息(.relo.text
、.relo.data
),将这些相对地址替换为实际的绝对地址。这样,可执行文件就可以正确地在内存中加载和执行。
3. 库依赖解析(Library Dependency Resolution):
3.1 静态链接(Static Linking):
在静态链接中,链接器会将程序所依赖的库(如 .a
或 .lib
文件)的代码和数据直接嵌入到最终的可执行文件中。当您运行可执行文件时,不需要额外加载外部的库文件,因为所有需要的代码和数据已经在可执行文件内部。
3.2 动态链接(Dynamic Linking):
在动态链接中,可执行文件只包含对库函数和变量的引用,而不包含实际的库代码和数据。这些库代码和数据存储在系统的共享库中(也称为动态链接库或共享对象,如 .so
或 .dll
文件)。多个程序可以共享同一个库的实例,减少了存储空间和系统资源的浪费。
动态链接可能发生在两个时机:
- 加载时的动态链接:操作系统会在执行可执行文件之前,将所需的共享库加载到内存中。这时,链接器会解析可执行文件中的引用,将这些引用关联到所加载的共享库中的实际函数和变量。
- 运行时的动态链接:共享库已经在加载时加载到了内存中,但链接的最终步骤是在程序运行时进行的。这时,操作系统会确保程序可以正确地访问所需的共享库中的函数和变量。程序在运行期间,可以根据需要调用共享库中的函数,操作系统会负责将这些调用关联到实际的库代码。
4. 生成可执行文件(Executable File Generation):
在完成所有的符号解析、重定位和库依赖解析后,链接器会根据上述步骤的结果生成最终的可执行文件。这个文件包含了所有目标文件和库文件的代码和数据,以及链接器添加的一些元信息。
可执行文件其实和目标文件是很相似的,都有代码区和数据区,只不过在可执行文件中还有一个特殊的符号 _start
,CPU 正是从这个地址开始执行机器指令的,经过一系列的准备工作后正式从程序的 main 函数开始运行。
解释器
解释器是一种能够直接执行源代码的程序或系统组件。
解释器会逐行读取源代码,并将其翻译为机器指令或直接在虚拟机中执行。因此,您可以在没有编译步骤的情况下运行源代码。
一些解释性语言具有良好的跨平台性,因为解释器可以在不同的操作系统上运行。这使得编写一次代码,多平台运行成为可能。
一些典型的解释性编程语言包括 Python、Ruby、JavaScript、Perl 等。这些语言通常用于脚本编程、Web 开发、数据分析等领域。
JVM(Java虚拟机)可以被看作是一种解释器。JVM 是用于执行 Java 程序的虚拟机,它将 Java 源代码编译成字节码(Java 中间代码),然后在运行时通过解释器将字节码转换为机器指令执行。