编译和链接详解

发布时间:2024年01月21日


前言

提示:这里可以添加本文要记录的大概内容:

在软件开发的世界中,编译和链接是构建程序的两个关键步骤。编写代码只是整个过程的一部分,而将源代码转换成可执行文件的过程涉及到编译器和链接器的协同工作。理解编译和链接的机制不仅有助于提高代码的执行效率,还有助于解决各种与构建过程相关的问题。本文将深入探讨编译和链接的原理,帮助读者更好地理解这两个必不可少的开发环节。


提示:以下是本篇文章正文内容,下面案例可供参考

翻译环境和运行环境

在ANSI C的任何一种实现中,存在两个不同的环境

  1. 第一种是翻译环境,在这个环境中源代码被转换成可执行的机器指令。
  2. 第二种是执行环境,它用于实际执行代码。

关于ANSI C帮大家补充一下,就当是拓展内容,看看就好!

ANSI C(American National Standards Institute C)是一种标准化的C语言规范,由美国国家标准协会(ANSI)制定。该标准于1989年首次发布,是对早期C语言的规范化和标准化。ANSI C的目标是提供一个一致的编程接口,使得C语言在不同的计算机系统上能够保持一致性,增强了可移植性。

ANSI C引入了一些新的特性和改进,以便更好地支持结构化编程和提高代码的可读性。此外,它还定义了标准C库,包含了一组通用的函数,例如输入输出、字符串处理等,使得这些函数在不同的系统上有着相同的接口和行为。

ANSI C标准后来成为国际标准(ISO C),最新版本是ISO C17,它继续保持了与ANSI C相近的特性,为C语言的发展奠定了基础。

翻译环境和运行环境图解

在这里插入图片描述

翻译环境

翻译环境是如何将源代码转换成可执行的机器指令的?接下来,我们就详细讲解一下翻译环境所做的那些事情。
翻译环境是由编译链接两个过程组成的,而编译又可以分解成:预处理(也叫预编译)、编译、汇编三个过程
在这里插入图片描述

一个C项目中,通常会有对个.c文件,那么多个.c文件是如何生成可执行程序的呢?

  • 多个.c文件单独经过编译处理,产生对应的目标文件
  • 在windows环境下的目标文件后缀是(.obj),linux下目标文件后缀是.o
  • 多个目标文件和链接库一起经过链接器处理生成最终的可执行程序。
  • 链接器是指运行时库(它支持程序运行的基本函数集合)或者第三方库。

如果要把编译的过程展开来说,就是如下过程

在这里插入图片描述

编译

预处理(预编译)阶段

预处理阶段主要处理那些源文件中#开始的预编译指令。比如#include,#define,处理的规则如下:

  • 将所有的 #define 删除,并展开所有的宏定义。
  • 处理所有的条件编译指令,如: #if、#ifdef、#elif、#else、#endif 。
  • 处理#include 预编译指令,将包含的头文件的内容插入到该预编译指令的位置。这个过程是递归进行的,也就是说被包含的头文件也可能包含其他文件。
  • 删除所有的注释
  • 添加行号和文件名标识,方便后续编译器生成调试信息等。
  • 或保留所有的#pragma的编译器指令,编译器后续会使用。
    经过预处理后的.i文件中不再包含宏定义,因为宏已经被展开。并且包含的头文件都被插入到.i文件中。所以当我们无法知道宏定义或者头文件是否包含正确的时候,可以查看预处理后的.i文件来确认。

编译

编译过程是将高级程序语言代码转换为目标机器代码的过程,通常包括词法分析、语法分析、语义分析和优化等阶段。下面是这三个部分的简要说明和示例:

  1. 词法分析(Lexical Analysis):

    • 任务: 将源代码转换为令牌序列,识别出程序中的基本单元,如关键字、标识符、运算符等。
    • 示例: 对于C语言代码片段int sum = a + b;,词法分析可能生成令牌序列为<int, keyword> <sum, identifier> <=, operator> <a, identifier> <+ , operator> <b, identifier> ;, delimiter>
  2. 语法分析(Syntax Analysis):

    • 任务: 将令牌序列转换为语法树,确保代码的结构符合语法规则。
    • 示例: 对于上述的令牌序列,语法分析可能生成以下语法树:
      (assignment_statement
         (variable_declaration
            (type int)
            (identifier sum))
         (expression
            (identifier a)
            (operator +)
            (identifier b)))
      
  3. 语义分析(Semantic Analysis):

    • 任务: 确保程序语义的正确性,检查变量的声明和使用、函数调用等语义规则。
    • 示例: 在语义分析阶段,编译器会检查是否定义了变量ab,以及它们的类型是否兼容。还会检查表达式中的运算符是否支持操作数的类型。
  4. 优化(Optimization):

    • 任务: 对生成的中间代码进行优化,提高程序执行效率。
    • 示例: 优化可能包括常量折叠、循环展开、代码内联等技术。例如,将循环中的常量计算移出循环体,以减少重复计算,从而提高程序的执行速度。

总体而言,这些阶段协同工作,确保源代码在转换为目标机器代码的过程中能够正确、高效地执行。

汇编

编译中的汇编过程通常包含了将汇编语言源代码翻译成机器代码的步骤。下面是一个简化的例子,以说明这个过程:

考虑以下C语言的简单源代码:(当前示例是在linux下演示的

// 文件: example.c
#include <stdio.h>

int main() {
    printf("Hello, World!\n");
    return 0;
}

接下来,我们将通过编译器将这个C源代码编译成可执行文件的过程,其中包括了汇编过程:

  1. 预处理(Preprocessing):

    gcc -E example.c -o example.i
    

    这个步骤将执行预处理,处理#include指令、宏定义等,生成一个预处理后的文件 example.i

  2. 编译(Compilation):

    gcc -S example.i -o example.s
    

    编译器将 example.i 转换成汇编代码,保存在 example.s 文件中。

  3. 汇编(Assembly):

    gcc -c example.s -o example.o
    

    汇编器将 example.s 中的汇编代码转换成目标文件 example.o,其中包含了机器代码的二进制表示。

  4. 链接(Linking):

    gcc example.o -o example
    

    链接器将目标文件 example.o 与系统库和其他必要的文件链接,生成最终的可执行文件 example

现在,你可以执行生成的可执行文件 ./example,它将输出 “Hello, World!”。

在这个例子中,编译中的汇编过程主要涉及了编译器将高级源代码转换为汇编代码,然后通过汇编器将汇编代码转换为目标文件的过程。这个目标文件最终被链接器用于生成可执行文件。整个过程是复杂的,但这个例子可以帮助理解编译中的汇编过程的基本流程。

链接

链接过程是编译过程的最后一步,它负责将多个目标文件(Object Files)合并成一个可执行文件或者库文件。链接过程主要包括符号解析、地址绑定和重定位等步骤。下面是一个简化的例子,以说明链接过程:

假设我们有两个源文件,分别是 main.cfunctions.c

  1. main.c:

    // 文件: main.c
    #include <stdio.h>
    
    extern int add(int a, int b);
    
    int main() {
        int result = add(3, 4);
        printf("Result: %d\n", result);
        return 0;
    }
    
  2. functions.c:

    // 文件: functions.c
    int add(int a, int b) {
        return a + b;
    }
    

接下来,我们将通过编译器将这两个源文件编译成目标文件,然后通过链接器将它们合并为一个可执行文件:

  1. 编译(Compilation):

    gcc -c main.c -o main.o
    gcc -c functions.c -o functions.o
    

    这里,-c 选项表示编译成目标文件而不进行链接。分别编译 main.cfunctions.c 得到两个目标文件:main.ofunctions.o

  2. 链接(Linking):

    gcc main.o functions.o -o my_program
    

    链接器将 main.ofunctions.o 合并,并解析它们之间的符号引用关系。在这个例子中,main.o 中引用了 add 函数,而 functions.o 中定义了 add 函数。链接器将这两者关联起来,生成一个可执行文件 my_program

  3. 执行:

    ./my_program
    

    运行可执行文件,输出 “Result: 7”。

链接过程实际上包括了三个主要步骤:

  • 符号解析(Symbol Resolution): 解决目标文件中的符号引用,确保它们能够正确地与定义相匹配。

  • 地址绑定(Address Binding): 将符号绑定到实际的内存地址。这可能涉及到代码段、数据段等不同部分的地址绑定。

  • 重定位(Relocation): 调整目标文件中的地址,使其能够正确地在合并后的可执行文件中运行。这包括相对地址的调整等操作。

整个链接过程的目标是生成一个可执行文件,其中包含了所有必要的代码和数据,使得程序能够在内存中正确运行。这个过程也用于生成共享库等形式的输出。

运行环境

  1. 程序必须载入内存中。在有操作系统的环境中:?般这个由操作系统完成。在独立的环境中,程序的载入必须由手工安排,也可能是通过可执行代码置入只读内存来完成。
  2. 程序的执行便开始。接着便调用main函数。
  3. 开始执行程序代码。这个时候程序将使用?个运行时堆栈(stack),存储函数的局部变量和返回地址。程序同时也可以使用静态(static)内存,存储于静态内存中的变量在程序的整个执行过程?直保留他们的值。
  4. 终止程序。正常终止main函数;也有可能是意外终止。

总结

编译和链接作为软件开发过程中不可或缺的步骤,承担着将高级源代码转化为可执行文件的责任。编译器负责将源代码转换为中间代码,而链接器则将各个模块组合成最终的可执行文件。通过深入了解编译和链接的原理,开发者能够更好地优化代码、解决依赖关系、提高执行效率。在不同的编程语言和开发环境中,编译和链接的实现方式可能存在差异,但它们共同构成了程序构建的核心。

文章来源:https://blog.csdn.net/m0_68002296/article/details/135732410
本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。