我们写的代码是如何一步步变成可执行程序(.EXE)的?
文章目录
- 1. 程序的翻译环境和执行环境
- 2. 编译详解
- 2.1翻译环境介绍
- 2.2 编译详解
- 2.2.1 预处理(预编译)
- 2.2.2 编译
- 2.2.3 汇编
- 2.3 链接详解
- 2.3.1 合并段表
- 2.3.2 符号表的合并和重定位
- 3. 运行环境
这篇文章,我们来探讨一下,我们写的代码,是如何一步步变成可执行程序,最终运行得出结果的,一起来学习吧!!!
1. 程序的翻译环境和执行环境
在ANSI C(美国国家标准协会(ANSI)及国际标准化组织(ISO)推出的关于C语言的标准)的任何一种实现中,程序都存在两个不同的环境。
第1种是翻译环境,在这个环境中源代码被转换为可执行的机器指令。
第2种是执行环境,用于实际代码执行。
也就是说:
我们写好的任何一个源代码,到最终产生结果,都要经历这两个环境。
比如,我们写好了一个test.c
的源文件,它需要先经过翻译环境生成可执行程序test.exe
,然后再经过执行环境产生最终的结果。
2. 编译详解
2.1翻译环境介绍
对于翻译环境呢,又分为编译和链接
1. 有时候我们的一个程序可能不止一个源文件,组成一个程序的每个源文件通过编译过程分别转换成目标代码(object code)。
2. 每个目标文件由链接器(linker)捆绑在一起,形成一个单一而完整的可执行程序。
3. 链接器同时也会引入标准C函数库中任何被该程序所用到的函数,而且它可以搜索程序员个人的程序库,将其需要的函数也链接到程序中,最终生成可执行程序。
那下面我们就在vs2022
写一个代码,让大家粗略的感受一下编译和链接的这个过程:
看这个程序,包含了两个源文件。
那我们现在
vs上
对该程序生成解决方案:
然后我们进入到文件所在路径进行观察:
我们发现,经过编译过程,test.c和add.c
已经生成了对应的目标文件。
然后:
链接器会把这些目标文件和链接库链接在一起,最终生成可执行程序。
那因为在test.c
中使用了add.c
中的add
函数,所以这两个源文件要被链接在一起,这我们能想通。
那还有一个链接库,这是个什么鬼?
大家有没有注意到我们刚才的程序中还使用到了一个库函数——printf
,像这些我们调用到的标准C函数库中的函数,就是放在链接库中的,链接器也会引入标准C函数库中这些被程序所用到的函数。
刚刚在上面的过程中我们提到了编译器和链接器这两个东西。
而对于我们平时写代码使用的这些工具,就比如我现在使用的这个
vs2022
,它其实不单单有编译和链接的功能,我们平时用的这些工具,它们都是一个集成开发环境(IDE),像常见的有Visual Studio、Dev C++、Xcode、Visual C++ 6.0、C-Free、Code::Blocks
等。集成开发环境就是一系列开发工具的组合套装,比如编辑器,编译器,链接器,调式器等。
我们可以在上面编辑代码,编译和链接代码,以及调式代码等。
这个大家了解一下。
2.2 编译详解
对于编译本身,又可以划分为3个阶段:预编译(预处理)、编译、汇编。
下面我们一起来看一下:
就还看上面那段代码,首先,大概的过程是这样的:
紧接着我们就来分析一下其中的细节:
注:接下来的大部分演示将在Linux环境下利用
gcc
进行,因为vs上面有些东西我们不好观察,所有有些操作大家不必关心,只要明白我们在干什么就行了。
当然这里面用到的一些命令大家可以了解一下:
- 预处理 选项 gcc -E test.c -o test.i
预处理完成之后就停下来,预处理之后产生的结果都放在test.i文件中。- 编译 选项 gcc -S test.c
编译完成之后就停下来,结果保存在test.s中。- 汇编 gcc -c test.c
汇编完成之后就停下来,结果保存在test.o中。
然后我们写这样一段代码:
我们接下来对我们写的源文件
test.c
直接编译,然后生成了一个a.out
的可执行程序,运行,我们看到成功打印了1到10的数字。
但是我们刚刚直接完成了整个编译过程,并没有观察到其中的具体细节。
2.2.1 预处理(预编译)
下面我们就分别观察一下其中的细节:
首先我们利用gcc -E test.c -o test.i
让程序在预编译(预处理)之后停下来,并把内容输出到test.i
文件中:
我们看到里面有很多内容,八百多行,但里面包含了我们写的代码。
那为什么多了这么多内容呢?
大家有没有注意到我们在代码的第一行就包含了一个头文件
stdio.h
,那test.i
中八百多行的内容中,在我们写的代码之前的那一大部分的内容是不是都是都文件带进来的内容。
是的,预编译之后的test.i
中前面的那么多内容都是来自头文件stdio.h
的内容。
我们可以验证一下,我们就打开一下stdio.h
看看它里面的内容(具体操作大家不必关心):
我们能够看到它们里面的有些内容是完全一样的。
那从这里我们就能够得出一个结论:
在预编译阶段需要做的事情之一是头文件的包含这件事。
那我们继续探讨一下,预处理阶段还会做其它哪些事情呢?
我们现在对刚才的代码做一些修改:
我们现在不打印数组的元素了,那自然stdio.h
我们也不用包含了,然后我们又添加了一行注释,并用#define
定义了一个标识符,赋给了变量m
。
然后我们再把预处理后的内容写到test.i
文件中,一起来看一下:
这次我们再来看,前面就没有那一大堆#include <stdio.h>
带进来的东西了。因为这次我们把头文件的包含注释掉了。
而且我们注释掉的代码和自己写的注释也没有出现在test.i
中。
另外,我们定义的标识符#define MAX 100
,也没有,而是直接将MAX
替换成了100。
所以,我们就知道,在预编译阶段还做了:
注释的删除
#define定义的符号的替换
当然,肯定还不止这些事情,我们现在只是大致了解一下,后面我们会给大家详细介绍预处理。
2.2.2 编译
那我们接下来就来研究一下编译阶段会发生什么?
还是这段代码:
我们这次让它在编译之后停下来,然后我们来观察:
这时编译之后的内容,如果大家之前在自己的编译器上查看过汇编代码的话,会发现这和汇编代码非常像,其实这就是产生的汇编代码。
所以,在编译过程中,会把预处理之后的C语言代码转换成汇编代码。
那在转换的过程中,又会做什么呢?
1.语法分析
2.词法分析
3.语义分析
4.符号汇总
那这几步又是干什么呢?
大家如果不知道也没关系,不重要,不过这里我们需要去了解一下符号汇总。
那接下来,我们就了解一下符号汇总
我们再来写这样一段代码:
我们知道这段代码在完成整个编译过程之后,就会产生对应的可执行程序(a.out)。
而这个可执行程序是按照一定的文件格式来进行组织的,这个格式叫做elf
,a.out文件的内部呢,按照这个格式会划分成一个一个的段,分别存放不同内容的数据,其中有一个叫做符号表的东西。
那我们怎么查看a.out这个文件呢?
我们去直接打开的话是不行的:
不过我们可以使用vim编辑器打开它,但是我们也看不懂:
因为a.out其实是个二进制的文件,不过我们可以借助readelf来查看。
我们可以利用相关命令只看符号表的内容:
我们发现,从中能找到一些我们在代码中定义的符号,我们定义的全局变量g_val
,还有main
函数和Add
函数的函数名。
但是我们定义的一些局部变量a,b,c
好像并没有在里面找到。
所以:
符号汇总其实就会把我们程序中的这些全局变量,函数名这种符号给汇总起来。
那这其实就是符号汇总的一个作用,为什么要单独解释一下符号汇总呢?
因为在链接的部分我们需要用到这些知识。
2.2.3 汇编
那接下来就是汇编了,编译的最后一步。
那经过汇编之后,编译结束,是不是就产生对应的目标文件了呢 ?
是的。
那我们现在执行相关的命令让它在汇编之后停止:
我们发现汇编之后又多了一个文件test.o
,这个文件其实就是生成的目标文件。
而它,是一个二进制文件。
而机器指令就是二进制的。
所以:
汇编这一步做的其实就是把汇编指令转化为二进制的机器指令。
而生成的目标文件test.o
其实也是elf
格式的,我们打开她也能看到相关的符号:
所以,除了把汇编指令转化为二进制的机器指令,这一步还会做什么呢?
就是把上一步汇总的符号形成符号表。
2.3 链接详解
通过上面的学习,我们知道,整个编译过程完成后,会产生目标文件,然后链接器就要对这样目标文件进行链接了。
那链接过程又会发生什么呢?
1. 合并段表
2. 符号表的合并和重定位
2.3.1 合并段表
那什么是合并段表呢?
我们上面提到过生成的目标文件
test.o
其实也是elf
格式的,而按照这个格式呢,会把文件分成一个一个的段,分别用来存放表示不同用途的数据。
那就拿我们最开始在vs上写的那个代码来说:
两个.c
的源文件test.c和add,c
,那编译之后就生成两个目标文件test.o和add.o
,它们都是elf
格式的文件,按同样的方式划分。
而最终链接之后生成的可执行文件是不是也是elf
格式的啊,那这个时候,它们就会把这些相同段的内容都放在一起,最终生成一个可执行程序:
这就是合并段表。
2.3.2 符号表的合并和重定位
那符号表的合并和重定位又是什么呢?
我们已经知道了在汇编阶段会生成符号表,这些符号往往是一些全局变量和函数名。
我们还来看这段代码:
那这两个文件最后要生成一个可执行文件呀,所以就需要对它们的符号表进行合并。
那合并的时候就会有一个问题,两个文件中都有一个
add
符号,地址应该选哪一个呢?
选add.c
中的,为什么?
因为函数add.c
在test.c
中只是声明了一下,而真正的函数add
的实现是在add.c
中的,所以,最终要选择add.c
中函数add
的地址作为最终add
的地址:
这就是符号表的合并和重定位。
那这些东西有什么用处呢?
当链接过程中进行了符号表的合并和重定位之后,
test.c
中main函数
调用add
的时候是不是就能通过符号表中重定位之后的有效的函数地址找到add
函数并调用它。
当然如果add.c
中没有定义add函数,或者函数名我们写错的情况下,是不是也会因为符号表中没有有效的信息而报错。
我们可以验证一下,相信大家也遇到过这种情况:
如果调用时函数名写错呢?
这就体现了符号表的用处。
3. 运行环境
最后,我们来了解一下一个程序执行的过程:
- 程序必须载入内存中。在有操作系统的环境中:这个过程一般由操作系统完成。在独立的环境中,程序的载入必须由手工安排,也可能是通过可执行代码置入只读内存来完成。
- 程序的执行便开始。接着便调用main函数。
- 开始执行程序代码。这个时候程序将使用一个运行时堆栈(stack),存储函数的局部变量和返回地址。程序同时也可以使用静态(static)内存,存储于静态内存中的变量在程序的整个执行过程一直保留他们的值。
- 终止程序。正常终止main函数;也有可能是意外终止。
这篇文章,我们比较笼统的介绍了一个程序从编译到链接,再到最后执行的过程,下一篇文章,我们将详细的介绍一下预处理过程。
这篇文章就到这里,希望能帮助到大家,也欢迎大家指正!!!
我们写的代码是如何一步步变成可执行程序(.EXE)的?相关推荐
- 天天写业务代码的那些年,我们是如何成长过来的
比起写业务代码更不幸的是,主要工作是修 Bug,bug,buG, bUg. 在一家大的公司里,不同的人总会有不同的运气: 运气好的人遇上一个好的项目,升职加薪,从此就走上了人生的巅峰. 运气差的人摊上 ...
- 当写烂代码的人离职之后....
半个月没有写blog了,正好是春节假期. 静下来,想想自己写的代码,真是有些汗颜,更有些惶恐. 加油吧~ 偶尔看到拉勾网上的一篇文章,记录下来,鞭策自己: 作者:蛋疼的AXB 来源:程序人生 休息一直 ...
- 二、SSM整合按步骤详解(清晰的思路加代码)从零开始一步步整合【二】(完结)
接上一章节继续SSM整合按步骤详解(清晰的思路加代码)从零开始一步步整合[一] 上面讲了Spring和SpringMVC的整合,现在开始下一步,先来搭建一下MyBatis的环境 我们先来看看目录结构图 ...
- Python写的代码打包成.exe可执行文件
Python写的代码打包成.exe可执行文件 1. 安装pyinstaller 2. [在线生成icon](http://www.ico51.cn/) 3. 打包命令 pyinstaller -i x ...
- 转程序员,都去写一写前端代码吧
转自: http://www.oschina.net/news/36972/programmer-write-frond-end-code 你可以认为我是一个极端的人,就像有许多人专注于自己的领域而不 ...
- 多些时间能少写些代码(转自酷壳 – CoolShell.cn)
我在我的微博上说过这样一段话,我想在这里把我的这个观点阐述地更完整一些. @左耳朵耗子:聪明的程序员使用50%-70%的时间用来思考,尝试和权衡各种设计和实现,而用30% – 50%的时间是在忙碌着编 ...
- 不写一行代码,也能玩转Kaggle竞赛?
整理 | Jane 出品 | AI科技大本营(ID:rgznai100) [导读]AI科技大本营会给大家分享一些 Kaggle 上的资源,如 Kaggle 开放的数据集,也会分享一些好的竞赛方案或有意 ...
- 雷军22年前写的代码 你见过吗?
源 / 程序师 文 / 程序君 作为小米科技的创始人.董事长和首席执行官,雷军的名字如雷贯耳.网上出现一篇"刘强东的代码水平如何"的文章,有网友在下面回复"代 ...
- 同事说,我写Java代码像写诗
文章来源:http://33h.co/kntu3 前几天空闲时间写了一遍关于平时自己写代码的一些习惯,这里跟大家分享一下. 定义配置文件信息✦ 有时候我们为了统一管理会把一些变量放到 yml 配置文件 ...
最新文章
- python mysql 错误处理_Python-MySQL中的错误处理
- Volatile原子性一致性JVM指令重排
- 线程被中断跟被阻塞有什么区别_宠物加盟店跟自营店有什么区别
- EDGE浏览器配合阿呆喵设置广告过滤
- 【王道考研计算机网络】—速率相关的性能指标
- ORACLE11G在LINUX6下安装及报错 C [ld-linux-x86-64.so.2+0x14d70]的解决方法
- 研究生最忌讳的几点是什么?读研期间你该主动做什么?
- php 模拟登陆(不带验证码)采集数据
- 一位38岁的老码农在退休前和年轻码农们的互动
- springAOP(Aspect)权限访问页面
- 浅谈Mysql底层索引原理
- QQ去水印小程序源码全新系统
- 汽车底盘线控与动力学域控制技术
- H5+canvas+js实现连线题
- Chrome浏览器修改繁体为中文简体
- 基于似然比检验统计量的异常轨迹检测
- For循环—函数初步概念
- 北航超算运行matlab,北航学子在ASC17世界大学生超级计算机竞赛全球总决赛中荣获亚军...
- Jmeter 4.0自带代理接口录制脚本
- can t find dynamically referenced class android os systemproperties