当前位置:首页 » 《我的小黑屋》 » 正文

C语言编译与链接(29)

25 人参与  2024年10月10日 12:02  分类 : 《我的小黑屋》  评论

点击全文阅读


文章目录

前言一、翻译环境和执行环境二、翻译环境预处理(预编译)编译词法分析语法分析语义分析 汇编链接 三、运行环境总结


前言

  本篇同样是一篇修炼内功的文章
  很重要!它将会让你对程序的构建运行有一个更深的认识


一、翻译环境和执行环境

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

翻译环境,在这个环境中源代码被转换为可执行的机器指令(二进制指令执行环境,它用于实际执行代码

在这里插入图片描述

在本篇中,我们会来详细介绍一下翻译环境

二、翻译环境

翻译环境是由编译和链接两个大过程组成的,将源代码转换为可执行的机器指令,编译又可以分解成:预处理(预编译)、编译、汇编三个过程
在这里插入图片描述

再简要一点,就像如下图
在这里插入图片描述

其中,几个.c文件经过编译器的处理,生成了几个.obj文件就叫做编译器,它们再与链接库一起经过链接器处理生成可执行程序.exe,这就叫做链接

具体来说,多个 .c 文件如何生成可执行程序呢?
在我细说之前,你先得有以下认识:

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

然后就跟我一起从头开始,构建一个完整的程序的过程吧~

预处理(预编译)

在预处理阶段,源文件和头文件会被处理成为.i为后缀的文件

此阶段主要处理那些源文件中#开始的预编译指令,例如#include,#define

具体规则如下:

将所有的 #define 删除,并展开所有的宏定义处理所有的条件编译指令,如: #if、#ifdef、#elif、#else、#endif处理#include预编译指令,将包含的头文件的内容插⼊到该预编译指令的位置这个过程是递归进行的,也就是说被包含的头文件也可能包含其他文件删除所有的注释添加行号和文件名标识,方便后续编译器生成调试信息等,或保留所有的#pragma的编译器指令,编译器后续会使用

如果想在gcc环境下观察,可以输入以下命令行:
gcc -E test.c -o test.i

请注意!经过预处理后的.i文件中不再包含宏定义,因为宏已经被展开。并且包含的头文件都被插⼊到.i⽂件中。所以当我们无法知道宏定义或者头⽂件是否包含正确的时候,可以查看预处理后的.i⽂件来确认

如该图,我们发现#include <stdio.h>被展开,竟然有好几百行!且添加了文件名标识
至于上述其他规则特性,就交由大家自行验证了
在这里插入图片描述

编译

编译过程:将预处理后的文件进行一系列词法分析、语法分析、语义分析以及优化,生成相应的汇编代码文件

指令为:
gcc -S test.i -o test.s

假设这个时候来了个语句:

nums[index] = (index + 4) * (2 + 6);

词法分析

将源代码程序被输入扫描器,扫描器的任务就是简单的进行词法分析,把代码的字符分割成一系列的记号(关键字、标识符、字面量、特殊字符等。)
在这里插入图片描述

语法分析

将对扫描产生的记号进行语法分析,从而产生语法树(以表达式为节点的树)
在这里插入图片描述

语义分析

由语义分析器完成语义分析,即对表达式的语法层面分析。编译器所能做的分析是语义的静态分析。静态分析通常包括声明和类型的匹配,类型的转换等。这个阶段会报告错误的语法信息
在这里插入图片描述

到这里,程序的后缀已经变成.s了
在这里插入图片描述

汇编

由汇编器将汇编代码转为机器指令(二进制指令),每一句汇编语句几乎都对应一条机器指令。按照汇编指令和机器指令的对照表进行一一的进行翻译,不做指令优化

指令为:
gcc -c test.s -o test.o

然后你会发现看不了,看不了就对了,就算能看也是乱码
在这里插入图片描述

链接

将一堆文件链接在一起生成可执行程序,为解决一个项目中多文件、多模块之间互相调用的问题,主要过程:地址和空间分配,符号决议和重定位等步骤,事实上,这很复杂,我也只是粗略点拨一下

现在我们来个具体例子,现在在一个C的项目里面,有两个.c文件(main.c、add.c)

//test.c#include <stdio.h>//声明外部函数extern int Add(int x, int y);//声明外部的全局变量extern int g_val;int main(){int a = 10;int b = 20;int sum = Add(a, b);printf("%d\n", sum);return 0;}
//add.cint g_val = 2024;int Add(int x, int y){return x + y;}

现在我们开始运行程序,首先通过前文我们知道,两个.c文件经过编译后生成两个.o目标文件,也就是说

test.c 经过编译器处理生成 test.o
add.c 经过编译器处理生成 add.o

  而我们在 test.c 的文件中使用了 add.c 文件中的 Add 函数和 g_val 变量,在 test.c 文件中每⼀次使用 Add 函数和 g_val 的时候必须确切的知道 Add 和 g_val 的地址(是的,函数也有地址,这在前面讲过),但是由于每个文件是单独编译的,在编译器编译 test.c 的时候并不知道 Add 函数和 g_val变量的地址,所以暂时把调用 Add 的指令的目标地址和 g_val 的地址搁置。

  等待最后链接的时候由链接器根据引用的符号 Add 在其他模块中查找 Add 函数的地址,然后将 test.c 中所有引用到Add 的指令重新修正,让他们的目标地址为真正的 Add 函数的地址,对于全局变量 g_val 也是类似的方法来修正地址

这个地址修正的过程也被叫做:重定位

事实上,我第一次知道链接是这样子运行的时候,我还是蛮激动的,这太巧妙了,不得不感叹计算机殿堂里先辈们的智慧

  我对这个过程有个还算贴切的比方,假如小举和小帆是好兄弟,小举年纪到了要买房子,首付三十万,此时手里只能拿出二十五万,剩下五万,要找小帆借,小帆答应了,小举就去签字了
  这时候,买房子就相当于是要调用Add函数和g_val变量,可是在自己的.c文件没有,对应差五万,找小帆借,小帆答应了,对应着声明,可是声明还需要有定义,此时还只是口头承诺,若另一个文件真的有声明,就能修正为正确的地址,对应小举真的拿到钱,另一种情况则是由于某些原因小帆无法借钱,就像是另一个文件里没有定义,这就导致之前错误的地址无法修正,承诺未能兑现,程序出错,小帆也就交不了首付了,Bad Ending~

三、运行环境

  可能你会觉得编译的过程好难啊,或者我讲得好浅啊!

那你感觉对了,我在这里只是讲了点皮毛,大学计科对于编译有一门专门的课程叫做《编译原理》
这可能是计算机类专业最无聊、相对来说最难的一门课了~

那我们接着来讲运行环境吧,在这里我就讲些有关注意点吧:

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

总结

在这里插入图片描述

  来个总览,本篇应该能让你有些启发,下篇我们会对预处理那一部分再详细介绍一下


点击全文阅读


本文链接:http://m.zhangshiyu.com/post/169948.html

<< 上一篇 下一篇 >>

  • 评论(0)
  • 赞助本站

◎欢迎参与讨论,请在这里发表您的看法、交流您的观点。

关于我们 | 我要投稿 | 免责申明

Copyright © 2020-2022 ZhangShiYu.com Rights Reserved.豫ICP备2022013469号-1