一个.c文件是怎么跑起来的?——程序的编译

前言

我们在日常生活中总是说一个源程序要先经过编译然后才能运行,源文件是给人看的,要把它翻译成二进制文件机器才能看懂。emmm,感觉有点抽象,今天我们就来聊一聊编译这回事儿。

一. 程序的翻译环境和执行环境

一个源程序想跑起来必须经过两个环境处理:翻译环境和执行环境

翻译环境

翻译环境中有两个非常重要的工具,一个是编译器,一个是链接器,这两个工具分别完成两个过程,即编译和链接。

像VS的编译器是cl.exe,链接器是link.exe,这两个文件都可以在安装的路径底下找到。
注意我这里的说法哟,我说的是“VS的编译器”,难道VS不是一个编译器吗?还真不是。

VS是一个集成开发环境,比如DEV C++,CodeBlocks这些我们都把他们叫集成开发环境。一个集成开发环境包括编辑,编译,链接,调试这样一些功能。不同得集成开发环境得各种组件可能不一样,比如VS得编辑器是cl.exe,而CodeBlocks用的是gcc。

执行环境

执行环境就是用来运行可执行程序得,通常就是我们的操作系统

整个过程可用这样一张图表示
在这里插入图片描述

二.详解翻译环境

翻译包含两个步骤,编译+链接,我们平常很少提到链接这个过程,其实经常说的编译就是指的翻译阶段,这里为了讲清楚细节从而将它细分出来。

一个程序中的所有.c文件会经过编译器单独编译生成对应的目标文件。这种目标文件在Windows环境下后缀名是.obj,在Linux环境是.o。下文我用到的文件后缀名都是Linux环境下的命名方式

得到目标文件后,链接器会把这些目标文件和链接库链接在一起生成可执行程序。

整个过程如下:
在这里插入图片描述
下面来详细讲解编译和链接这两个过程

2.1编译

编译又可以细分为三步:预编译(也叫预处理),编译和汇编

2.1.1预编译

这步主要干三件事:

  1. 头文件的包含
  2. define定义标识符的替换
  3. 注释删除

以这样一个源文件为例:

#include <stdio.h>
#define SZ 10
//这是一个测试样例
int main()
{
	for (int i = 0; i < SZ; i++)
	{
		printf("%d ", i);
	}
	return 0;
}

那么它经过预处理阶段后就会变成这样

xxxxxxxxxxxxxxx
xxxxxxxxxxxxxxx
xxxxxxxxxxxxxxx
xxxxxxxxxxxxxxx
int main()
{
	for (int i = 0; i < 10; i++)
	{
		printf("%d ", i);
	}
	return 0;
}

其中“xxxxxxxx”是头文件里的内容,非常复杂,有几百行,同时我们还发现注释消失了,而且SZ被替换成了10,这就是预处理阶段做的3件事。

总的来说,预处理就是做一些文本替换的工作

这个过程在VS这种集成开发环境下不太好验证,有兴趣的老铁可以在B站上搜相关的教程,在VScode上搭载gcc编译器,通过命令行的方式操作验证。

2.1.2编译

这个阶段是把c语言代码转换成汇编代码,放到一个xxx.s的文件中

没错,编译器不是直接将C代码转换成二进制指令的,中间还要先转换成汇编代码然后再向二进制语言过渡。
这个过程要干这些事:

  1. 语法分析
  2. 词法分析
  3. 语义分析
  4. 符号汇总

这些过程都很复杂,有兴趣可以看看《程序员的自我修养》,里面有详细的讲解。

这里简单提一下符号汇总,因为后面还会用到它。
符号汇总顾名思义,就是把符号收集起来,不过要加个限定,是把收集全局的符号。

以同一个工程中的test.c和add.c为例(下文会一直使用这个例子)
在这里插入图片描述
前面说过,编译是对各个.c文件进行单独地处理,所以会对test.c和add.c分开汇总符号。
test.s收集到的符号就是Add和main
add.s收集到的符号只有Add

2.1.3汇编

这一步会把汇编指令转换成二进制指令

经过转换后就得到了若干个目标文件,以上面的例子为例,就是test.o和add.o。

这个过程中还发生了重要的一步,形成符号表
在上一步编译阶段汇总了符号,这里就会把符号形成一个表,每个符号既有名字也有地址
例如:
test.o的符号表为

名称地址
Add0x000
main0x104

(因为Add在test.c中只有声明,所以找不到该函数的有效地址,故用空指针代替)

add.o的符号表为

名称地址
Add0x100

到了这步可能你还是不明白收集符号有什么意义,别着急,继续往下看。

2.2链接

这一步主要干两件事
1.合并段表
2.符号表合并和重定位

合并段表
目标文件已经是二进制文件了,这种目标文件是有格式的,以Linux环境下的目标文件为例,目标文件的格式elf这种格式,目标文件被分成一段一段的,可执行程序的格式也是elf,合并段表可简单理解为将对应的段合并形成新的段
在这里插入图片描述

符号表合并和重定位
例如,test.o有Add和main两个符号,add.o有Add这一个符号,那么合并后的符号表就包含Add和main两个符号。main好说,它的地址就是0x104,那么Add呢?这个时候就要符号重定位了,Add的地址应该是那个有效地址0x100

名称地址
Add0x100
main0x104

符号表用来干什么的呢?千呼万唤开出来。现在揭晓答案。
在链接这个阶段,符号表合并合并好之后,链接器会检查各文件内的符号是否在符号表中。比如我因为手误,在main函数里面写成了add()函数,链接器在符号表中查找,发现没有这个符号,就会给你报错。
在这里插入图片描述
至此也就能解释为什么同一个工程中的不同文件里面的全局变量/函数能互相使用了,就是因为符号表这个东西记录了各符号的地址,所以能通过符号表找到对应的函数和变量。

在我以前的文章中讲到过static的用法,若是在函数或全局变量前加上static,其它文件就无法使用它,这是因为改变了它的外部链接属性,现在理解起来就很清晰了。
static的用法

其它的文件中能不能访问这个符号,就是由该符号的链接属性决定的。正是因为它具有外部链接属性,才会进入符号表,通过符号表将它和其它文件链接起来,而改变外部链接属性后,这个符号压根就不会进入符号表,外部自然就访问不到它了。

三.总结

所有内容可用这样一张图来概括
在这里插入图片描述

本次分享结束,欢迎评论区留下宝贵意见。

### 配置VS Code以编译和运行C++程序 #### 安装必要的工具链和支持包 为了能够在VS Code中顺利编译和执行C++代码,首先需要确认本地已经安装了合适的编译器。可以通过命令行输入`g++ --version`来检验是否存在有效的C++编译环境[^3]。 对于Windows用户来说,推荐通过MinGW-w64获取GCC/G++编译套件;而对于Linux或macOS平台,则通常自带GNU Compiler Collection (GCC),只需确保其版本满足需求即可。 #### 插件安装 接着,在VS Code内部需安装特定的扩展支持: - **C/C++** 扩展由微软官方提供,用于语法高亮显示、智能感知等功能; - **Code Runner** 可简化测试流程,允许一键快速执行单个文件而无需额外设置复杂的构建任务; - **C/C++ Compile Run** 这款插件可以帮助更方便地管理不同项目的编译参数[^4]。 这些插件可通过点击左侧活动栏中的“扩展”图标,随后在搜索框内键入名称逐一查找并完成安装过程。 #### 创建工作区与基础配置 创建一个新的文件夹作为项目根目录,并利用File -> Open Folder...将其加载到编辑器当中。此时会自动弹出提示询问是否信任此空间——选择同意以便后续操作不受限。 当首次保存`.cpp`源码文档时,IDE底部状态条可能会提醒缺少某些必需项。按照指示依次添加launch.json(启动配置) 和 c_cpp_properties.json(C/C++属性定义)[^2] 文件至隐藏于顶层路径下的`.vscode`子文件夹里。 #### 设置JSON配置文件 针对上述提到的关键配置文件内容举例说明如下: ##### `c_cpp_properties.json` 该文件主要用于指定目标平台架构以及头文件搜索路径等信息。一个典型的实例可能看起来像这样: ```json { "configurations": [ { "name": "Win32", "includePath": ["${workspaceFolder}/**"], "defines": [], "compilerPath": "C:/Program Files/mingw-w64/x86_64-8.1.0-posix-seh-rt_v6-rev0/mingw64/bin/g++.exe", // Windows环境下G++可执行文件位置 "intelliSenseMode": "${default}", "browse": { "path": ["${workspaceFolder}"] } } ], "version": 4 } ``` 请注意根据实际情况调整其中涉及的具体路径表达式。 ##### `tasks.json` 这是用来描述具体编译指令的任务清单之一。下面给出了一种适用于大多数场景的方式: ```json { "version": "2.0.0", "tasks": [ { "label": "build hello world", "type": "shell", "command": "g++", "args": [ "-g", "-o", "${fileDirname}/${fileBasenameNoExtension}.out", "${file}" ], "group": { "kind": "build", "isDefault": true }, "problemMatcher": ["$gcc"], "detail": "Generated task from snippet" } ] } ``` 这里定义了一个名为"build hello world"的新建/更新二进制输出的过程,它接受当前激活的CPP脚本作为输入,并生成相同命名但带有.out后缀的目标文件放在同一级目录之下。 ##### `launch.json` 最后则是关于调试模式下如何调用之前建立好的EXE档来进行断点跟踪等方面的规定: ```json { "version": "0.2.0", "configurations": [ { "name": "(gdb) Launch", "type": "cppdbg", "request": "launch", "program": "${fileDirname}/${fileBasenameNoExtension}.out", "args": [], "stopAtEntry": false, "cwd": "${workspaceFolder}", "environment": [], "externalConsole": true, "MIMode": "gdb", "miDebuggerPath": "/usr/bin/gdb",// Linux/Mac OS X 用户应保留此项不变; Windows 则指向 MinGW 下 gdb.exe 的实际存放处 "setupCommands": [ { "description": "Enable pretty-printing for gdb", "text": "-enable-pretty-printing", "ignoreFailures": true } ], "preLaunchTask": "build hello world", "internalConsoleOptions": "openOnSessionStart" } ] } ``` 以上即完成了整个准备工作的大致框架搭建。现在可以尝试编写简单的HelloWorld案例加以验证效果啦!
评论 5
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值