首页 文章

编译/链接过程如何工作?

提问于
浏览
348

编译和链接过程如何工作?

(注意:这是Stack Overflow的C FAQ的一个条目 . 如果你想批评在这个表单中提供常见问题解答的想法,那么发布所有这些的meta上的帖子就是这样做的地方 . 这个问题在C聊天室中受到监控,其中FAQ的想法首先出现在那里,所以你的答案很可能被那些提出这个想法的人阅读 . )

6 回答

  • 22

    GCC通过4个步骤将C / C程序编译成可执行文件 .

    例如,“ gcc -o hello.exe hello.c ”执行如下:

    1. Pre-processing

    通过GNU C预处理器(cpp.exe)进行预处理,其中包括头文件(#include)并展开宏(#define) .

    cpp hello.c> hello.i

    生成的中间文件“hello.i”包含扩展的源代码 .

    2. Compilation

    编译器将预处理的源代码编译为特定处理器的汇编代码 .

    gcc -S hello.i

    -S选项指定生成汇编代码,而不是目标代码 . 生成的程序集文件是“hello.s” .

    3. Assembly

    汇编程序(as.exe)将汇编代码转换为目标文件“hello.o”中的机器代码 .

    as -o hello.o hello.s

    4. Linker

    最后,链接器(ld.exe)将目标代码与库代码链接以生成可执行文件“hello.exe” .

    ld -o hello.exe hello.o ... libraries ...

  • 28

    C程序的编译包括三个步骤:

    • 预处理:预处理器采用C源代码文件并处理 #include#define 和其他预处理程序指令 . 此步骤的输出是没有预处理器指令的"pure" C文件 .

    • 编译:编译器获取预处理器的输出并从中生成目标文件 .

    • 链接:链接器获取编译器生成的目标文件,并生成库或可执行文件 .

    预处理

    预处理器处理预处理程序指令,如 #include#define . 它与C的语法无关,这就是必须谨慎使用的原因 .

    它一次在一个C源文件上工作,将 #include 指令替换为相应文件的内容(通常只是声明),替换宏( #define ),并根据 #if#ifdef#ifndef 选择不同的文本部分指令 .

    预处理器适用于预处理令牌流 . 宏替换被定义为用其他标记替换标记(操作符 ## 允许在有意义时合并两个标记) .

    在所有这些之后,预处理器产生单个输出,该输出是由上述变换产生的令牌流 . 它还添加了一些特殊的标记,告诉编译器每行的来源,以便它可以使用它们来产生合理的错误消息 .

    通过巧妙地使用 #if#error 指令,可以在此阶段产生一些错误 .

    编译

    编译步骤在预处理器的每个输出上执行 . 编译器解析纯C源代码(现在没有任何预处理器指令)并将其转换为汇编代码 . 然后调用底层后端(工具链中的汇编程序)将该代码组装成机器代码,生成某种格式的实际二进制文件(ELF,COFF,a.out,...) . 此目标文件包含输入中定义的符号的编译代码(以二进制形式) . 目标文件中的符号由名称引用 .

    对象文件可以引用未定义的符号 . 使用声明时就是这种情况,并且没有为它提供定义 . 编译器不介意这一点,只要源代码格式正确,编译器就会愉快地生成目标文件 .

    编译器通常会让您在此时停止编译 . 这非常有用,因为有了它,您可以单独编译每个源代码文件 . 这提供的优点是,如果只更改单个文件,则无需重新编译所有内容 .

    生成的目标文件可以放在称为静态库的特殊存档中,以便以后重用 .

    正是在这个阶段,报告了“常规”编译器错误,如语法错误或失败的重载解析错误 .

    链接

    链接器是从编译器生成的目标文件生成最终编译输出的内容 . 此输出可以是共享(或动态)库(虽然名称类似,但它们与前面提到的静态库没有多少共同之处)或可执行文件 .

    它通过用正确的地址替换对未定义符号的引用来链接所有目标文件 . 这些符号中的每一个都可以在其他目标文件或库中定义 . 如果它们是在标准库以外的库中定义的,则需要告知链接器它们 .

    在此阶段,最常见的错误是缺少定义或重复定义 . 前者意味着定义不存在(即它们没有被写入),或者它们所在的目标文件或库没有被赋予链接器 . 后者是显而易见的:在两个不同的目标文件或库中定义了相同的符号 .

  • 12

    CProgramming.com讨论了该主题:
    https://www.cprogramming.com/compilingandlinking.html

    这是作者写的:

    编译并不完全相同创建可执行文件!相反,创建可执行文件是一个多阶段过程,分为两个部分:编译和链接 . 实际上,即使程序“编译好”,它也可能因链接阶段的错误而无法正常工作 . 从源代码文件到可执行文件的整个过程可能更好地称为构建 . 编译编译是指处理源代码文件(.c,.cc或.cpp)以及创建“对象”文件 . 此步骤不会创建用户实际可以运行的任何内容 . 相反,编译器仅生成与编译的源代码文件相对应的机器语言指令 . 例如,如果编译(但不链接)三个单独的文件,则将创建三个作为输出的目标文件,每个文件的名称为.o或.obj(扩展名取决于您的编译器) . 这些文件中的每一个都包含源代码文件到机器语言文件的转换 - 但是您无法运行它们!您需要将它们转换为操作系统可以使用的可执行文件 . 这就是链接器的用武之地 . 链接链接是指从多个目标文件创建单个可执行文件 . 在此步骤中,链接器通常会抱怨未定义的函数(通常是main本身) . 在编译期间,如果编译器找不到特定函数的定义,它只会假定该函数是在另一个文件中定义的 . 如果不是这种情况,编译器就无法知道 - 它不会一次查看多个文件的内容 . 另一方面,链接器可能会查看多个文件,并尝试查找未提及的函数的引用 . 您可能会问为什么有单独的编译和链接步骤 . 首先,以这种方式实现它可能更容易 . 编译器做了它的事情,链接器做了它的事情 - 通过保持功能分离,减少了程序的复杂性 . 另一个(更明显的)优点是,这允许创建大型程序,而无需在每次更改文件时重做编译步骤 . 相反,使用所谓的“条件编译”,只需要编译那些已经改变的源文件;对于其余部分,目标文件是链接器的足够输入 . 最后,这使得实现预编译代码库变得简单:只需创建目标文件并像任何其他目标文件一样链接它们 . (事实上,每个文件与其他文件中包含的信息分开编译的事实称为“单独的编译模型” . )为了获得条件编译的全部好处,可能更容易让程序帮助您而不是尝试并记住自上次编译以来您更改了哪些文件 . (当然,您可以重新编译时间戳大于相应目标文件的时间戳的每个文件 . )如果您正在使用集成开发环境(IDE),它可能已经为您解决了这个问题 . 如果您正在使用命令行工具,那么大多数* nix发行版都会附带一个名为make的漂亮实用程序 . 除了条件编译之外,它还有其他一些很好的编程功能,例如允许对程序进行不同的编译 - 例如,如果你有一个版本产生用于调试的详细输出 . 了解编译阶段和链接阶段之间的差异可以更容易地捕获错误 . 编译器错误本质上通常是语法错误 - 缺少分号,附加括号 . 链接错误通常与缺失或多个定义有关 . 如果从连接器中多次定义函数或变量的错误,则表明错误是两个源代码文件具有相同的函数或变量 .

  • 8

    在标准方面:

    • 翻译单元是源文件,包含的头文件和源文件的组合,减去条件包含预处理器指令跳过的任何源行 .

    • 标准定义了翻译中的9个阶段 . 前四个对应于预处理,接下来的三个是编译,下一个是模板的实例化(生成实例化单元),最后一个是链接 .

    在实践中,第八阶段(模板的实例化)通常在编译过程中完成,但是一些编译器将它延迟到链接阶段有些人把它分散在两者中 .

  • 2

    瘦的是CPU从内存地址加载数据,将数据存储到内存地址,并按顺序从内存地址执行指令,并处理指令序列中的一些条件跳转 . 这三类指令中的每一类涉及计算要在机器指令中使用的存储器单元的地址 . 因为机器指令具有可变长度,具体取决于所涉及的特定指令,并且因为我们在构建机器代码时将它们的可变长度串在一起,所以在计算和构建任何地址时涉及两步过程 .

    首先,在我们知道每个单元格究竟是什么之前,我们尽可能地分配内存 . 我们计算出字节,单词或任何形成指令和文字以及任何数据的内容 . 我们只是开始分配内存并构建将在我们开始时创建程序的值,并记下我们需要返回并修复地址的任何地方 . 在那个地方我们放了一个假人来填充位置,这样我们就可以继续计算内存大小了 . 例如,我们的第一个机器代码可能需要一个单元 . 下一个机器代码可能需要3个单元,包括一个机器代码单元和两个地址单元 . 现在我们的地址指针是4.我们知道机器单元中的内容,即操作码,但我们必须等待计算地址单元格中的内容,直到我们知道数据的位置,即将是什么该数据的机器地址 .

    如果只有一个源文件,理论上编译器可以在没有链接器的情况下生成完全可执行的机器代码 . 在两遍过程中,它可以计算任何机器加载或存储指令所引用的所有数据单元的所有实际地址 . 它可以计算任何绝对跳转指令引用的所有绝对地址 . 这是简单的编译器,如Forth中的编译器,没有链接器 .

    链接器允许单独编译代码块 . 这可以加快构建代码的整个过程,并允许稍后如何使用块的灵活性,换句话说,它们可以重新定位在内存中,例如向每个地址添加1000以便将块扫描1000个地址单元 .

    那么编译器输出的是粗略的机器代码,这些机器代码尚未完全构建,但是已经布局,因此我们知道所有内容的大小,换句话说,我们可以开始计算所有绝对地址的位置 . 编译器还输出一个符号列表,这些符号是名称/地址对 . 这些符号将模块中机器代码中的存储器偏移与名称相关联 . 偏移量是模块中符号的存储位置的绝对距离 .

    这就是我们到达链接器的地方 . 链接器首先将所有这些机器代码块首尾相连,并记下每个机器代码开始的位置 . 然后,它通过将模块内的相对偏移量和模块在较大布局中的绝对位置相加来计算要修复的地址 .

    显然我已经过度简化了这一点,所以你可以试着去掌握它,我故意不使用对象文件,符号表等术语,这对我来说是混乱的一部分 .

  • 460

    查看URL:http://faculty.cs.niu.edu/~mcmahon/CS241/Notes/compile.html
    C的完整编译过程在此URL中清晰介绍 .

相关问题