【Linux】深入理解GCC/G++编译流程及库文件管理

目录

1.背景知识

2.gcc/g++如何完成编译

[(1) 预处理(进行宏替换)](#(1) 预处理(进行宏替换))

[(2) 编译(生成汇编)](#(2) 编译(生成汇编))

[(3) 汇编(生成机器可识别代码)](#(3) 汇编(生成机器可识别代码))

[(4) 链接(生成可执行文件或库文件)](#(4) 链接(生成可执行文件或库文件))

[(5) 总结](#(5) 总结)

[(6) 函数库](#(6) 函数库)

[① 静态函数库](#① 静态函数库)

[② 动态函数库](#② 动态函数库)

[③ 动静态库比较](#③ 动静态库比较)

[④ 验证动/静态链接](#④ 验证动/静态链接)

[⑤ ldd - 程序的动态函数库解析](#⑤ ldd - 程序的动态函数库解析)

[(7) gcc选项](#(7) gcc选项)


1.背景知识

(1) 预处理(宏替换,条件编译)

(2) 编译(生成汇编)

(3) 汇编(生成机器可识别代码)

(4) 链接(生成可执行文件或库文件)

2.gcc/g++如何完成编译

格式 gcc [选项] 要编译的文件 [选项] [目标文件]

(1) 预处理(进行宏替换)

● 预处理功能个主要包括宏定义,文件包含,条件编译,去注释等。

● 预处理指令是以#号开头的代码行

● 【-E】选项的作用是,从现在开始进入程序编译,在预处理的时候就停下来

● 【-o】选项是指定目标文件,【.i】文件为已经预处理过的C原始程序

● 实例:gcc -E test.c -o test.i将原始C语言代码预处理后的内容写到指定的 test.i 文件里

bash 复制代码
[zyt@iZ2vcf9wvlgcetfeub9f11Z ~]$ gcc -E test.c -o test.i
[zyt@iZ2vcf9wvlgcetfeub9f11Z ~]$ ll
-rw-rw-rw- 1 zyt zyt   270 Nov 18 09:16 test.c
-rw-rw-r-- 1 zyt zyt 16951 Nov 18 09:16 test.i

● 得到的【.i】文件还是C语言文件

● 预处理后的 【.i 】文件相比原来大很多

那是因为预处理器会将#include指令包含的头文件内容直接插入到源文件中(这些头文件都是被提前装在系统的/usr/include 目录下);预处理器宏定义展开;预处理器还会处理条件编译指令,如#ifdef#ifndef#endif等,这些指令可能会根据条件包含或排除某些代码段。

● 如何理解条件编译?

  • 对软件进行专业度,收费情况进行区分(业务),使用条件编译,可以进行代码动态裁剪。

  • 内核源代码也是用条件编译进行代码裁剪。

  • 开发工具,应用软件也是用条件编译对功能代码动态裁剪。

(2) 编译(生成汇编)

● 在这个阶段中,gcc首先要检查代码的规范性、是否有语法错误等,以确定代码实际要做的工作,在检查无误后,gcc把代码翻译成汇编语言。

● 使用【-S】选项,gcc 会将C语言代码编译成汇编语言,并将结果输出到一个文件中,该文件的扩展名通常是【.s】

● 实例:gcc -S test.i -o test.s

bash 复制代码
[zyt@iZ2vcf9wvlgcetfeub9f11Z ~]$ gcc -S test.i -o test.s
[zyt@iZ2vcf9wvlgcetfeub9f11Z ~]$ ll
total 28
-rw-rw-rw- 1 zyt zyt   284 Nov 18 09:36 test.c
-rw-rw-r-- 1 zyt zyt 16945 Nov 18 09:37 test.i
-rw-rw-r-- 1 zyt zyt   589 Nov 18 09:39 test.s

我们打开.s文件看看,得到的是汇编文件

(3) 汇编(生成机器可识别代码)

● 汇编阶段是把编译阶段生成的【.s】文件转成目标文件

● 这个目标文件其实就是可重定位目标文件,此时已经是二进制文件了,但是无法直接执行,即使加上可执行权限。那是因为这个.o文件只是把我写的源文件编译成二进制了,而我们写的源文件中会包含很多的库方法,这些库方法还没有跟我们写的内容关联起来,所以是不可能运行的。(形象来说,就是我写的代码里用到了ptintf方法,但.o文件里没有print方法的实现,它的实践是在库里面实现的),win上形成的是XXX.obj。

● 使用【-c】选项就可以看到汇编代码转化为.o后缀的二进制目标代码了

● 实例:gcc -c test.s -o test.o

bash 复制代码
[zyt@iZ2vcf9wvlgcetfeub9f11Z ~]$ gcc -c test.s -o test.o
[zyt@iZ2vcf9wvlgcetfeub9f11Z ~]$ ll
total 32
-rw-rw-rw- 1 zyt zyt   284 Nov 18 09:36 test.c
-rw-rw-r-- 1 zyt zyt 16945 Nov 18 09:37 test.i
-rw-rw-r-- 1 zyt zyt  1672 Nov 18 09:57 test.o
-rw-rw-r-- 1 zyt zyt   589 Nov 18 09:39 test.s

vim打开【.o】文件后:

(4) 链接(生成可执行文件或库文件)

● gcc 本身是编译C语言的,会在系统里找出可执行程序依赖的库

● 链接器它将一个或多个.o文件与所需的库文件链接起来,解决所有的外部引用,并生成一个单一的可执行文件。

● 实例:gcc test.o -o test

bash 复制代码
[zyt@iZ2vcf9wvlgcetfeub9f11Z ~]$ gcc test.o -o test
[zyt@iZ2vcf9wvlgcetfeub9f11Z ~]$ ll
total 44
-rwxrwxr-x 1 zyt zyt  8496 Nov 18 10:13 test
-rw-rw-rw- 1 zyt zyt   284 Nov 18 09:36 test.c
-rw-rw-r-- 1 zyt zyt 16945 Nov 18 09:37 test.i
-rw-rw-r-- 1 zyt zyt  1672 Nov 18 09:57 test.o
-rw-rw-r-- 1 zyt zyt   589 Nov 18 09:39 test.s
# 运行一下
[zyt@iZ2vcf9wvlgcetfeub9f11Z ~]$ ./test
Hello!,100
Hello!
hello N!

(5) 总结

我们上面将gcc完成编译的整个过程通过【-E】【-S】【-c】选项显性的展示成后缀为【.i】【.s】【.o】的临时文件,但是正常编译时,这些后缀文件不会以文件的形式写到磁盘上,而是在gcc编译器启动之后将这些编译形成的临时文件全都写到编译器内部,在内存中就处理好了,最终直接给我们呈现一个可执行文件。

选项记忆技巧:将编译带选项时联想到键盘上的ESC键,即-E-S-c,依次生成的文件可以联想到.iso镜像文件,即.i.s.o

(6) 函数库

● 我们的C程序中,并没有定义"printf"的函数实现,且在预编译中包含的"stdio.h"中也只有该函数的声明,而没有定义函数的实现,那么是在哪里实现的?

● 其实是:系统把这些函数实现都放到名为【libc.so.6】的库文件中了,在没有特别指定时,gcc会到系统默认的搜索路径【/usr/lib】下进行查找,也就是链接到【libc.so.6】库函数中去就能实现函数"printf"了,而这也就是链接的作用。

● 函数库一般分为两大类,分别是静态(static)与动态(dynamic)函数库。

● 什么叫做动静态链接?如何理解?

① 静态函数库

● 扩展名:libxxx.a
● 编译操作

编译链接时,把库文件的代码全部加入到可执行文件中,因此生成的文件比较大。但在运行时也就不再需要库文件了,也就是编译成功的可执行文件可以独立运行
● 升级难易程度

虽然执行文件可以独立执行,但因为函数库是直接整合到执行文件中的,所以若函数库升级时,整个执行文件必须要重新编译才能将新版的函数库整合到程序中。也就是说,在升级方面只要函数库升级了,所有使用此函数库的程序都要重新编译。

② 动态函数库

● 扩展名:libxxx.so
● 编译操作

与静态函数被整个整合到程序中不同的是,动态函数库在编译时,在程序里面只有一个【指针】的位置而已(地址上产生关联,让我的程序能找到库里面方法的地址)。也就是说,动态函数库的内容并没有被整合到执行文件当中,而是当执行文件要使用到函数库的功能时,程序才会去读取函数库来使用(跳转到库里面执行,完了再返回)。由于执行文件当中仅具有指向动态函数库所在的指针而已,并不包含函数库的内容,所以它的文件会比较小。
● 独立执行状态

这类函数库所编译出的程序不能被独立执行,因为当我们使用到函数库的功能时,程序才会去读取函数,所以函数库文件【必须要存在】才行,而且,函数库的【所在目录也不能改变】,因为我们的可执行文件里面仅有【指针】,亦即当要使用该动态函数库时,程序会主动去某个路径下读取,所以动态函数库可不能随意移动或删除,会影响很多依赖的程序软件。
● 升级难易程度

当函数库升级后,执行文件根本不需要进行重新编译的操作,因为执行文件会直接指向新的函数库文件(前提是函数库新旧版本的文件名相同)。
**●**gcc默认生成的二进制程序,是动态链接的,这点可以通过file命令验证。
● 动态库的本质:使语言层面的公共代码在内存中只出现一份。

在执行gcc动态链接形成可执行文件时,这个动态库会跟该文件一样被加载到内存里,后续再用gcc编译其他文件时要是也用到这个库,就不用在加载了,直接跳转到内存中的库即可。

③ 动静态库比较

1、动态库形成的可执行程序体积一定很小

2、可执行程序对静态库的依赖度很小,但动态库不能缺失

3、程序运行需要加载到内存,静态链接时,会在内存中出现大量的重复代码,动态链接时,比较节省内存和磁盘资源。

④ 验证动/静态链接

1、验证gcc默认是动态链接

test.c文件里面简单写入:

cpp 复制代码
  1 #include<stdio.h>
  2 int main()
  3 {
  4     printf("Hello!\n");
  5     return 0;
  6 }

用gcc完成编译后用 ldd,file 查看该文件详细信息: 得到的 test 是64位、可执行、动态链接的文件 。

链接的动态库就是libc-2.17.so(这是系统里本来就预装的)

bash 复制代码
[zyt@iZ2vcf9wvlgcetfeub9f11Z ~]$ ll /usr/lib64/libc.so*
-rw-r--r-- 1 root root 253 Jul  3  2019 /usr/lib64/libc.so
lrwxrwxrwx 1 root root  12 Jul 11  2019 /usr/lib64/libc.so.6 -> libc-2.17.so

2、如果我们想要用静态库连接

前提:系统里就必须要存在C静态库,但我们指明**【-satic】**执行后发现系统里没有C静态库。

bash 复制代码
[zyt@iZ2vcf9wvlgcetfeub9f11Z ~]$ gcc test.c -o test -static
/usr/bin/ld: cannot find -lc
collect2: error: ld returned 1 exit status

安装glic静态库用【sudo yum install -y glibc-static 】 系统会默认把它装到【/usr/lib64/】下,文件名就叫做libc.a。(g++使用也与之类似**【sudo yum install libstdc++-static】**)

bash 复制代码
[zyt@iZ2vcf9wvlgcetfeub9f11Z ~]$ ll /usr/lib64/libc.a
-rw-r--r-- 1 root root 5105516 Jun  4 23:05 /usr/lib64/libc.a

然后再进行gcc静态编译,发现这个可执行文件会特别的大。比之前动态链接生成的可执行文件大了100倍。用ldd、file观察也显示的是静态链接。

bash 复制代码
[zyt@iZ2vcf9wvlgcetfeub9f11Z ~]$ gcc test.c -o test -static
[zyt@iZ2vcf9wvlgcetfeub9f11Z ~]$ ll
total 848
-rwxrwxr-x 1 zyt zyt 861336 Nov 18 15:43 test
-rw-rw-rw- 1 zyt zyt     71 Nov 18 15:21 test.c

⑤ ldd - 程序的动态函数库解析

我们如何判断某个可执行的二进制文件含有什么动态函数库?

ldd [-vdr] [filename]

-v:列出所有内容信息

-d:重新将数据有遗失的链接点显示出来

-r:将ELF有关的的错误内容显示出来(某些特定信息,比如ELF头信息、节信息等。这些信息对于调试和分析ELF文件非常有用,尤其是在遇到与ELF文件格式相关的问题时)

用ldd查看一下我们刚刚实现的可执行文件test,我们观察到【libc.so.6】就是我们使用的动态链接库,【libc.so.6】是C标准库实现的,是大多数 Linux 程序运行时所依赖的核心库之一。

bash 复制代码
[zyt@iZ2vcf9wvlgcetfeub9f11Z ~]$ ldd -v test
	linux-vdso.so.1 =>  (0x00007ffea694a000)
	libc.so.6 => /lib64/libc.so.6 (0x00007fefa2f56000)
	/lib64/ld-linux-x86-64.so.2 (0x00007fefa3323000)

	Version information:
	./test:
		libc.so.6 (GLIBC_2.2.5) => /lib64/libc.so.6
	/lib64/libc.so.6:
		ld-linux-x86-64.so.2 (GLIBC_2.3) => /lib64/ld-linux-x86-64.so.2
		ld-linux-x86-64.so.2 (GLIBC_PRIVATE) => /lib64/ld-linux-x86-64.so.2

(7) gcc选项

● -D:进行命令行级别的宏定义

这个选项后面跟着你想要定义的宏名称,如果你还想要为宏指定一个值,如果宏没有值,GCC 会定义它为 1。

test.c文件里面的内容:

cpp 复制代码
  1 #include<stdio.h>
  2 #define M 100
  3 
  4 int main()
  5 {
  6     printf("Hello!%d\n",M);
  7     //printf("Hello!");
  8     //printf("Hello!");    
  9     printf("Hello!\n");
 10 
 11     #ifdef N
 12         printf("hello N!\n");
 13     #else
 14         printf("hello no N!\n");
 15     #endif
 16         return 0;
 17 }

我们命令行新定义的一个宏N(不能与源代码中定义的宏相同)

bash 复制代码
[zyt@iZ2vcf9wvlgcetfeub9f11Z ~]$ gcc test.c -o test -DN=10
[zyt@iZ2vcf9wvlgcetfeub9f11Z ~]$ ./test
Hello!100
Hello!
hello N!

-E 只激活预处理,这个不生成文件,你需要把它重定向到一个输出文件里面
-S 编译到汇编语言不进行汇编和链接
-c 编译到目标代码
-o 文件输出到 文件
-static 此选项对生成的文件采用静态链接
-g 生成调试信息。GNU 调试器可利用该信息。
-shared 此选项将尽量使用动态库,所以生成文件比较小,但是需要系统由动态库.
-O0
-O1
-O2
-O3 编译器的优化选项的4个级别,-O0表示没有优化,-O1为缺省值,-O3优化级别最高
-w 不生成任何警告信息。
-Wall 生成所有警告信息。

相关推荐
mengao12341 分钟前
centos 服务器 docker 使用代理
服务器·docker·centos
布鲁格若门3 分钟前
CentOS 7 桌面版安装 cuda 12.4
linux·运维·centos·cuda
Eternal-Student8 分钟前
【docker 保存】将Docker镜像保存为一个离线的tar归档文件
运维·docker·容器
C-cat.10 分钟前
Linux|进程程序替换
linux·服务器·microsoft
怀澈12212 分钟前
高性能服务器模型之Reactor(单线程版本)
linux·服务器·网络·c++
DC_BLOG15 分钟前
Linux-Apache静态资源
linux·运维·apache
学Linux的语莫16 分钟前
Ansible Playbook剧本用法
linux·服务器·云计算·ansible
码农小丘16 分钟前
一篇保姆式centos/ubuntu安装docker
运维·docker·容器
耗同学一米八42 分钟前
2024 年河北省职业院校技能大赛网络建设与运维赛项样题二
运维·网络·mariadb
skywalk81631 小时前
树莓派2 安装raspberry os 并修改成固定ip
linux·服务器·网络·debian·树莓派·raspberry