编译的整个过程:预编译、编译、汇编、链接

编译说明

步骤未编译预编译编译汇编链接
文件hello.chello.ihello.shelle.ohello

详解

1. 预编译

预编译过程主要做四件事

  1. 展开头文件,在写有#include 或#include "filename"的文件中,将文件filename展开,通俗来说就是将filename文件中的代码写入到当前文件中:
  2. 宏替换
  3. 去掉注释
  4. 条件编译,即对#ifndef #define #endif进行判断检查,也正是在这一步,#ifndef #define #endif的作用体现出来,即防止头文件被多次重复引用

2. 编译

将代码转换成汇编代码,并且在这个步骤中做了两件重要的的工作:

  • 编译器在每个文件中保存一个函数地址符表,该表中存储着当前文件内包含的各个函数的地址;
  • 因为这步要生成汇编代码,即一条一条的指令,而调用函数的代码会被编译成一条call指令,call指令后面跟的是jmp指令的汇编代码地址,而jmp指令后面跟的才是(被调用的函数编译成汇编代码后的第一条指令“的地址,但是给call指令后面补上地址的工作是在链接的时候做的事情。

3. 汇编

将汇编代码转成机器码

4.链接

编译器将生产的多个.o文件链接到一起生成也给可执行.exe文件;

但是这个过程中,编译器做的一个重要的事情是将每个文件中call指令后面的地址补充上;

方式是从当前文件的函数地址符表中开始找,如果没有,继续向别的文件的函数地址符表中找,找到后补在call指令后面,如果找不到,则链接失败

综述

从一个源文件(.c)到可执行程序到底经历了哪几步,我想大多数的人都知道,到时到底每一步都做了什么,我估计也没多少人能够说得清清楚楚,明明白白。

其实总的流程是这样的。

.c文件—>预编译—>编译—>链接(包括库文件)—>可执行程序


实验操作

编辑hello.c

#include <stdio.h>

int main(int argc, char **argv)
{
	printf("Hello World!\n");
	return 0;
}

【第一步】预处理

预处理过程实质上是处理“#”,将#include包含的头文件直接拷贝到hell.c当中;将#define定义的宏进行替换,同时将代码中没用的注释部分删除等

具体做的事儿如下:

  1. 将所有的#define删除,并且展开所有的宏定义。说白了就是字符替换
  2. 处理所有的条件编译指令,#ifdef #ifndef #endif等,就是带#的那些
  3. 处理#include,将#include指向的文件插入到该行处
  4. 删除所有注释
  5. 添加行号和文件标示,这样的在调试和编译出错的时候才知道是是哪个文件的哪一行
  6. 保留#pragma编译器指令,因为编译器需要使用它们。

gcc -E hello.c -o hello.i 先将.c文件预编译为.i,可以生成预处理后的文件。通过查看文件内容和文件大小可以得知a.c讲stdio.h和stdlib.h包含了进来。

zhou@ubuntu:~/eclipse-workspace/hello$ gcc -E hello.c -o hello.i
zhou@ubuntu:~/eclipse-workspace/hello$ ls -l
total 28
-rwxr--r-- 1 zhou zhou    95 Feb 14 10:50 hello.c
-rw-rw-r-- 1 zhou zhou  17177 Feb 14 10:59 hello.i
-rwxr--r-- 1 zhou zhou 754 Feb 14 10:58 Makefile

【第二步】编译

gcc -S hello.i -o hello.s

编译的过程实质上是把高级语言翻译成机器语言的过程,即对hello.i做了这些事儿

  1. 词法分析,
  2. 语法分析
  3. 语义分析
  4. 优化后生成相应的汇编代码

从 高级语言->汇编语言->机器语言(二进制)

zhou@ubuntu:~/eclipse-workspace/hello$ gcc -S hello.i -o hello.s
zhou@ubuntu:~/eclipse-workspace/hello$ ls -l
total 32
-rwxr--r-- 1 zhou zhou    95 Feb 14 10:50 hello.c
-rw-rw-r-- 1 zhou zhou 17177 Feb 14 10:59 hello.i
-rw-rw-r-- 1 zhou zhou   502 Feb 14 11:01 hello.s
-rwxr--r-- 1 zhou zhou   754 Feb 14 10:58 Makefile

汇编代码如下:

	.file	"hello.c"
	.section	.rodata
.LC0:
	.string	"Hello World!"
	.text
	.globl	main
	.type	main, @function
main:
.LFB0:
	.cfi_startproc
	pushq	%rbp
	.cfi_def_cfa_offset 16
	.cfi_offset 6, -16
	movq	%rsp, %rbp
	.cfi_def_cfa_register 6
	subq	$16, %rsp
	movl	%edi, -4(%rbp)
	movq	%rsi, -16(%rbp)
	movl	$.LC0, %edi
	call	puts
	movl	$0, %eax
	leave
	.cfi_def_cfa 7, 8
	ret
	.cfi_endproc
.LFE0:
	.size	main, .-main
	.ident	"GCC: (Ubuntu 4.8.4-2ubuntu1~14.04.4) 4.8.4"
	.section	.note.GNU-stack,"",@progbits

【第三步】汇编

  • gcc -c hello.s -o hello.o,将源文件翻译成二进制文件。类Uinx系统编译的结果生生成.o文件,Windows系统是生成.obj文件。
  • 编译的过程就是把hello.c翻译成二进制文件
zhou@ubuntu:~/eclipse-workspace/hello$ gcc -c hello.s -o hello.o
zhou@ubuntu:~/eclipse-workspace/hello$ ls -l
total 36
-rwxr--r-- 1 zhou zhou    95 Feb 14 10:50 hello.c
-rw-rw-r-- 1 zhou zhou 17177 Feb 14 10:59 hello.i
-rw-rw-r-- 1 zhou zhou  1512 Feb 14 11:04 hello.o
-rw-rw-r-- 1 zhou zhou   502 Feb 14 11:01 hello.s
-rwxr--r-- 1 zhou zhou   754 Feb 14 10:58 Makefile

【第四步】链接

就像刚才的hello.c它使用到了C标准库的东西“printf”,但是编译过程只是把源文件翻译成二进制而已,这个二进制还不能直接执行,这个时候就需要做一个动作,

将翻译成的二进制与需要用到库绑定在一块。打个比方编译的过程就向你对你老婆说,我要吃雪糕。你只是给你老婆发出了你要吃雪糕的诉求而已,但是雪糕还没有到。

绑定就是说你要吃的雪糕你的老婆已经给你买了,你可以happy。

gcc hello.c -o hello可以生成可执行程序。即gcc不带任何参数。ldd就可以看到你的可执行程序依赖的库。

zhou@ubuntu:~/lvhui/eclipse-workspace/hello$ ls -lh
total 48K
-rwxrwxr-x 1 zhou zhou 8.4K Feb 14 12:26 hello
-rwxr--r-- 1 zhou zhou   95 Feb 14 10:50 hello.c
-rw-rw-r-- 1 zhou zhou  17K Feb 14 10:59 hello.i
-rw-rw-r-- 1 zhou zhou 1.5K Feb 14 11:04 hello.o
-rw-rw-r-- 1 zhou zhou  502 Feb 14 11:01 hello.s
-rwxr--r-- 1 zhou zhou  754 Feb 14 10:58 Makefile
zhou@ubuntu:~/lvhui/eclipse-workspace/hello$ ./hello
Hello World!
zhou@ubuntu:~/lvhui/eclipse-workspace/hello$ ldd hello
	linux-vdso.so.1 =>  (0x00007ffcc26b4000)
	libc.so.6 => /lib/x86_64-linux-gnu/libc.so.6 (0x00007f9a27f37000)
	/lib64/ld-linux-x86-64.so.2 (0x0000562d05411000)

可以看到hello.o的大小是1.5k,毕竟他只是把源文件翻译成二进制文件。hello却有8.4k,应该是他多了很多“绳子”吧。在运行的时候这些“绳子”就将对应的库函数“牵过来”。很形象的比喻是不是?哈哈。libc.so.6 中就对咱们用的printf进行了定义。

这就是编写的整个流程,(⊙o⊙)。谢谢各位看官。不足的地方请不吝赐教。

文章地址1

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
预编译头文件今天在改一个很大的程序,慢慢看,慢慢改。突然发现一个.c文件,里面什么也没有,就几个头文件,我一看,我靠,这不是把简单的问题搞复杂了吗,随手删掉那个c文件。结果不能编译了,我靠:fatal error C1083: Cannot open precompiled header file: \'Debug/v13_3.pch\':No such file or directory怎么rebuild all都不行。上网查了一下,才搞懂了:----------------总结------如果工程很大,头文件很多,而有几个头文件又是经常要用的,那么1。把这些头文件全部写到一个头文件里面去,比如写到preh.h2。写一个preh.c,里面只一句话:#include "preh.h"3。对于preh.c,在project setting里面设置creat precompiled headers,对于其他.c文件,设置use precompiled header file//哈哈我试了一下,效果很明显,不用precompiled header,编译一次我可以去上个厕所,用precompiled header,编译的时候,我可以站起来伸个懒腰,活动活动就差不多啦---------转载的文章----------预编译头的概念:所谓的预编译头就是把一个工程中的那一部分代码,预先编译好放在一个文件里(通常是以.pch为扩展名的),这个文件就称为预编译头文件这些预先编译好的代码可以是任何的C/C++代码--------甚至是inline的函数,但是必须是稳定的,在工程开发的过程中不会被经常改变。如果这些代码被修改,则需要重新编译生成预编译头文件。注意生成预编译头文件是很耗时间的。同时你得注意预编译头文件通常很大,通常有6-7M大。注意及时清理那些没有用的预编译头文件。也许你会问:现在的编译器都有Time stamp的功能,编译器在编译整个工程的时候,它只会编译那些经过修改的文件,而不会去编译那些从上次编译过,到现在没有被修改过的文件。那么为什么还要预编译头文件呢?答案在这里,我们知道编译器是以文件为单位编译的,一个文件经过修改后,会重新编译整个文件,当然在这个文件里包含的所有头文件中的东西(.eg Macro, Preprocesser )都要重新处理一遍。VC的预编译头文件保存的正是这部分信息。以避免每次都要重新处理这些头文件。预编译头的作用:根据上文介绍,预编译头文件的作用当然就是提高便宜速度了,有了它你没有必要每次都编译那些不需要经常改变的代码。编译性能当然就提高了。预编译头的使用:要使用预编译头,我们必须指定一个头文件,这个头文件包含我们不会经常改变的代码和其他的头文件,然后我们用这个头文件来生成一个预编译头文件(.pch文件)想必大家都知道 StdAfx.h这个文件。很多人都认为这是VC提供的一个“系统级别”的,编译器带的一个头文件。其实不是的,这个文件可以是任何名字的。我们来考察一个典型的由AppWizard生成的MFC Dialog Based 程序的预编译头文件。(因为AppWizard会为我们指定好如何使用预编译头文件,默认的是StdAfx.h,这是VC起的名字)。我们会发现这个头文件里包含了以下的头文件:#include // MFC core and standard components#include // MFC extensions#include // MFC Automation classes#include // MFC support for Internet Explorer 4Common Controls#include <br
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值