程序的编译与链接

思考

我们在VS创建一个test.c文件,输入以下内容:

int main()
{
	printf("hello world!!!");
	return 0;
}

在VS上运行这段代码,毫无疑问会在我们的控制台窗口打印hello world!!!在这段文字

那么大家有没有想过,这期间到底发生了什么?

难道仅仅就是:计算机阅读源代码,然后输出结果吗?

实际上,我们.c后缀的源代码首先会形成一个.exe后缀的可执行文件,可执行文件运行之后才会在相关的输出设备做出反馈。

上面这段代码,其实就是printf经过翻译环境,形成了相应的二进制指令,这段二进制指令在CPU中运行,调用了系统调用api,在控制台窗口进行打印。

翻译环境
运行环境
test.c
(源代码)
test.exe
(可执行的二进制机器指令)
结果

接下来我们就来详细分析一下,在翻译环境运行环境中到底做了什么。

翻译环境

接下来我们用一个模拟加减计算器的简单程序解释编译的过程:
add.c

extern int Add(int x, int y)
{
	return x + y;
}

sub.c

extern int Sub(int x, int y)
{
	return x - y;
}

test.c

extern int Add(int x, int y);//加法函数声明
extern int Sub(int x, int y);//减法函数声明
int main()
{
	int a = 1;
	int b = 2;
	int c = 0;
    int d = 0;
	c = Add(a, b);
    d = Sub(a, b);
	printf("%d", c);
}

整个翻译环境大致上可分为编译链接两部分。

翻译环境
编译(编译器)
链接(链接器)

在编译阶段,编译器会将所有的源文件(.c格式)逐个转化成目标文件(.obj格式),

所有目标文件再加上链接库,经过链接器的链接,形成一个可执行程序

test.c
add.c
sub.c
test.obj
add.obj
sub.obj
编译器
链接器
链接库
test.exe
(可执行文件)

链接库:

test.c函数中我们使用了printf这个函数,但是我们并没有写,它的具体实现方式封装在了库里,如果想让最后的可执行程序正常运行,那必须把这部分文件也链接进去。

以下就是printf这个库函数对应的链接库,这些静态库都是以.LIB作为后缀的。

image-20220429200433172

编译

整个翻译环境可分为编译链接两个大阶段,而编译又可细分为三个阶段。

编译
预编译
编译
汇编

接下来,我们用linux的文件命名规则,为大家展示一下这三个阶段,和这三个阶段发生的操作。

预编译

预处理
test.c
test.i

注:预处理也可叫预编译

这个阶段做一些文本替换,替换之后的test.i文件也是C语言的语法

  1. 头文件的包含(#include):用头文件的所有内容替换写#include<>的那一行
  2. 条件编译(#if):判断是否删除或保留#if和#endif中间的代码
  3. 删除注释:将注释掉的代码用一个空格替换
  4. 宏替换(#define定义符号的替换):将宏的文本内容替换到代码中

具体预处理指令,如”#define 、 #if 、 #include 、 #pragma……“,大家可以看一下这篇博客:点这里

编译

test.i
编译
test.s

这个阶段是把C语言的代码转化成汇编代码,这里红框框里的就是翻译之后的汇编代码。

image-20220429210404425

在这个过程,做如下事情:

  1. 语法分析:编译错误就是在这个阶段发现的。
  2. 词法分析
  3. 语义分析
  4. 符号汇总

这里符号汇总就是把整个.i文件中出现的函数名汇总起来。

汇总
汇总
汇总
test.i
main
Add
printf
add.i
Add
sub.i
Sub

汇编

test.s
汇编
test.o

注:这里的.o文件是linux的命名方式,与Windows的.obj相同

把汇编代码转换成二进制的指令,如果说一直到.s文件的汇编语言是我们肉眼能看懂的,那到了.o文件,就成了01组成的二进制指令,就只有CPU能看懂了。

汇编过程我们只区了解一个操作:

  1. 形成那个符号表。

是否还记得编译过程中的符号汇总,这里将给汇总的所有符号加一个地址,形成符号表:

image-20220429221108911

如果只有函数声明,那将其地址进行标记,全部置0.

链接

test.obj
add.obj
sub.obj
链接器
链接库
test.exe
(可执行程序)

这个过程我们研究两件事:

  1. 合并段表
  2. 符号表的合并和重定位

合并段表

在生成.o文件的过程中,将文件分成了一个一个的,每个段有自己的作用,在链接阶段,把每个.o文件中相同作用的段进行合并。

image-20220429230114121

符号表的合并和重定位

把之前编译阶段生成的符号表进行合并,其中由于申明置的全0,现在把定义位置的地址赋过去。

在这个多文件链接的过程中,同时也会通过符号表查看来自外部的符号是否存在,平时我们遇到的链接错误就是在这里发现的。

image-20220429224444503

我们想想,如果在main函数中没有对Add、Sub进行声明,那么会出错吗?

答案是虽然有警告,但是依然能过。

就是因为即使没有声明,符号表合并之后也能找到它们的地址。

总结

链接
汇编
编译
预编译
包含
包含
包含
包含
包含
test.exe
(可执行程序)
链接
(链接器)
链接库
汇编
test.o
sub.o
add.o
编译
test.s
add.s
sub.s
预编译
test.c
add.c
sub.c
test.i
add.i
sub.i
翻译环境
编译
(编译器)

运行环境

程序执行的过程:

  1. 程序必须载入内存中。在有操作系统的环境中:一般这个由操作系统完成。在独立的环境中,程序的载入必须由手工安排,也可能是通过可执行代码置入只读内存来完成。

  2. 程序的执行便开始。接着便调用main函数。

  3. 开始执行程序代码。这个时候程序将使用一个运行时堆栈(stack),存储函数的局部变量和返回地址。程序同时也可以使用静态(static)内存,存储于静态内存中的变量在程序的整个执行过程一直保留他们的值。

  4. 终止程序。正常终止main函数;也有可能是意外终止。

  • 13
    点赞
  • 50
    收藏
    觉得还不错? 一键收藏
  • 2
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值