天天看点

gcc/g++命令使用及编译原理二

一、概述

对于编译原理,如果要系统的学习的话,知识点多的完全可以写成一本书,推荐大家可以去阅读一下《编译原理》这本书,可以全面深入的学习编译原理,很经典。接下来的文章我仅仅对GCC/G++编译原理写一些记录。

其中gcc命令是对c语言文件的编译,g++命令是对c++文件的编译,其命令的原理其实是一样的。

二、编译流程

gcc命令编译一般分为四个步骤及调用的命令:

  • 预处理(preprocessing)  :gcc -E 
  • 编译(compilation) :gcc -S
  • 汇编(assembly) :gcc -c对应调用的是as命令
  • 连接(linking)  :gcc对应调用的是ld 命令

先上一个丑陋的概念图

gcc/g++命令使用及编译原理二

源文件,以下是hello.c代码

#include<stdio.h>

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

预处理阶段

预处理阶段主要处理#include和#define,它把#include包含进来的.h 文件插入到#include所在的位置,把源程序中使用到的用#define定义的宏用实际的字符串代替,删除注释。

执行命令,会生成hello.i文件

$ gcc -E hello.c -o hello.i

# 1 "hello.c"
# 1 "/usr/include/stdio.h" 1 3 4
# 29 "/usr/include/stdio.h" 3 4
# 1 "/usr/include/_ansi.h" 1 3 4
# 10 "/usr/include/_ansi.h" 3 4
# 1 "/usr/include/newlib.h" 1 3 4
# 14 "/usr/include/newlib.h" 3 4
# 1 "/usr/include/_newlib_version.h" 1 3 4
# 15 "/usr/include/newlib.h" 2 3 4
# 11 "/usr/include/_ansi.h" 2 3 4
# 1 "/usr/include/sys/config.h" 1 3 4



# 1 "/usr/include/machine/ieeefp.h" 1 3 4
# 5 "/usr/include/sys/config.h" 2 3 4
# 1 "/usr/include/sys/features.h" 1 3 4
# 6 "/usr/include/sys/config.h" 2 3 4
# 234 "/usr/include/sys/config.h" 3 4
# 1 "/usr/include/cygwin/config.h" 1 3 4
# 235 "/usr/include/sys/config.h" 2 3 4
# 12 "/usr/include/_ansi.h" 2 3 4
# 30 "/usr/include/stdio.h" 2 3 4

.....此处忽略一千行

# 797 "/usr/include/stdio.h" 3 4

# 2 "hello.c" 2


# 3 "hello.c"
int main(){
 printf("hello world!");
 return 0;
}
           

编译阶段

编译阶段首先检查代码的规范性,语法错误,最终把代码翻译成汇编语言

执行命令:

$ gcc -S hello.i -o hello.s

以下是生成的hello.s汇编代码,有兴趣的同学可以研究一下,对于反汇编,这一份代码是必需看懂的^-^。

.file	"hello.c"
	.text
	.def	__main;	.scl	2;	.type	32;	.endef
	.section .rdata,"dr"
.LC0:
	.ascii "hello world!\0"
	.text
	.globl	main
	.def	main;	.scl	2;	.type	32;	.endef
	.seh_proc	main
main:
	pushq	%rbp
	.seh_pushreg	%rbp
	movq	%rsp, %rbp
	.seh_setframe	%rbp, 0
	subq	$32, %rsp
	.seh_stackalloc	32
	.seh_endprologue
	call	__main
	leaq	.LC0(%rip), %rcx
	call	printf
	movl	$0, %eax
	addq	$32, %rsp
	popq	%rbp
	ret
	.seh_endproc
	.ident	"GCC: (GNU) 7.3.0"
	.def	printf;	.scl	2;	.type	32;	.endef
           

汇编阶段

汇编阶段把hello.s文件翻译成二进制机器指令,把hello.s文件转换成hello.o文件,执行命令:

$ gcc -c hello.s -o hello.o

生成的文件是二进制格式,普通人是看不懂了,这是一份让机器理解的代码

gcc/g++命令使用及编译原理二

当然,我们也可以通过ida,od等工具反汇编得到汇编代码

gcc/g++命令使用及编译原理二

链接阶段

很多人都会认为到了汇编阶段产出的.o文件就可以使用了,当然这个想法是错误的,还需要最后一个阶段,就是链接阶段(Linking),可以通俗的理解为关联系统的api,我们的程序调用stdio.h中的printf函数,而这个标准的打印函数是由系统提供的,而链接阶段就是告诉我们应用调用prinft的地方,该去哪里调用这个函数的实现地方,有可能是系统的标准库函数库,也有可能是外面提供的函数库,反正结果就是让我们的应用知道在哪里去找到这个函数。

执行命令,最终生成hello可执行的文件:

$ gcc hello.o -o hello

函数库一般分为静态库和动态库两种

  • 静态库是指编译链接时,把库文件的代码全部加入到可执行文件中,因此生成的文件比较大,但在运行时也就不再需要库文件了,其后缀名一般为”.a”,如前面的应用,会把printf函数相关的依赖函数都会打包进来。
  • 动态库与之相反,在编译链接时并没有把库文件的代码加入到可执行文件中,而是在程序执行时由运行时链接文件加载库,这样可以节省系统的开销。动态库一般后缀名为”.so”,gcc在编译时默认使用动态库,如果系统把printf函数不小心删除了,前面的应用在运行的时候就会报找不到libc.so.6库的错误提示。

文章所有的例子代码工程