~~~~
前言
本文介绍Linux下gcc/g++编译器相关的概念,以及对源文件经过预处理、编译、汇编、链接,最后形成可执行的进一步理解。介绍编译时的静态链接和动态链接,库函数命名等。
程序的翻译过程
参考C语言blog
在命令行向编译器传参
gcc test.c -DNAME//-D表示是一个宏
vim hello.c
预处理
预处理阶段编译器进行:头文件展开、去注释、宏替换、条件编译等。
-E
生成.c
,执行完预处理就停下来
gcc -E hello.c -o hello.i
头文件展开时,头文件的内容被拷贝到test.i文件中,但头文件中只包含了对应库函数的声明,想要完成编译就需要对应的库函数实现(包括动态库和静态库)。
头文件的好处:
- 写代码,分离编译时,头文件包含所有的需要包含的头文件,每个源代码文件中只需要包含对应的头文件,也可以防止多个源代码联合编译时头文件被重复包含(都在头文件中了)。
- 支持代码自动补全。
vim hello.i
编译
编译过程为 扫描程序–>语法分析–>语义分析–>源代码优化–>代码生成器–>目标代码优化;
- 扫描程序进行词法分析,从左向右,从上往下扫描源程序字符,识别出各个单词,确定单词类型;
- 语法分析是根据语法规则,将输入的语句构建出分析树parse tree或者语法树syntax tree;
- 语义分析是根据上下文分析函数返回值类型是否对应这种语义检测;
- 语法分析相当于检测一个句子主宾谓是否符合规则,而语义用于检测句子的意思是否是正确的;
- 目标代码生成指的是,把中间代码变换成为特定机器上的低级语言代码。
-
死代码删除是编译最优化技术,指的是移除根本执行不到的代码,或对程序运行结果没有影响的代码,而并不是删除被注释的代码;
-
内联函数,也叫编译时期展开函数, 指的是建议编译器将内联函数体插入并取代每一处调用函数的地方,从而节省函数调用带来的成本,使用方式类似于宏,但是与宏不同的是内联函数拥有参数类型的校验,以及调试信息,而宏只是文本替换;
-
for循环的循环控制变量,通常被cpu访问频繁,因此如果调度到寄存器中进行访问则不用每次从内存中取出数据,可以提高访问效率;
-
强度削弱是指执行时间较短的指令等价替代执行时间较长的指令,如 num % 128 与 num & 127 相较,则明显&127更加轻量;
-S
生成.s
,执行完编译就停下来,生成汇编文件。
gcc -S hello.i -o hello.s
vim hello.s
汇编
-c
生成.o
执行完汇编就停下来,生成二进制可重定位目标文件(不可执行)(机器可以识别),即使加上了执行权限也不能执行。
gcc -c hello.s -o hello.o
vim hello.o
全是二进制信息
od -x hello.o > test.txt
以十六进制格式显示hello.o的内容,并重定向到test.txt文件中:
巧记预处理、编译、汇编指令
ESc
当做键盘左上角的退出键;iso
当做镜像文件的后缀。
链接
把一个或多个目标文件进行链接,形成可执行二进制程序(包含你的代码和库代码),其中在链接的过程中,目标文件所需的库函数被链接器在系统目录下找到,然后也会与目标文件共同链接。
gcc hello.o -o hello
链接的本质就是:调用库函数时和标准库产生某种关联的过程
基本认知
我们所写的代码hello.c
和库是不同的概念;
C标准库是别人已经为使用者准备好的,我们可以直接使用标准库的各种方法;
我们在使用C标准库中的方法时,只写了对应函数的调用,没有具体的函数实现(如printf、scanf);
函数库
在我们所写的C程序中,并没有定义库函数printf的实现,包含的头文件stdio.h中也只有该函数的声明,而没有定义函数的实现。
实际上,系统把这些函数实现都打成了一个到名为 libc.so.6(c动态库)/libc.a(c静态库) 的库文件中去了。在没有特别指定时,gcc 会到系统默认的搜索路径/lib64/
下进行查找,找到 libc.so.6 库然后把我们程序中用到的库函数的实现一起与我们的目标文件进行链接(默认是动态链接,指定-static静态链接),这样我们的程序就与库函数的实现建立了联系了。
动态链接
是什么
动态库与之相反,在编译链接时并没有把库文件的代码加入到可执行文件中,而是在程序执行时由运行时链接文件加载库,这样可以节省系统的开销。动态库一般后缀名为.so
,如前面所述的 libc.so.6 就是动态库。
gcc 在编译时默认使用动态库,完成了链接之后,gcc 就可以生成可执行二进制文件。并且gcc默认生成的二进制程序是动态链接的:
gcc/g++系统目录/lib64/
一般默认包含动态库libc.so
,没有静态库libc.a
。因为系统中各种命令工具等都是以动态链接的方式形成可执行程序的,没有动态库libc.so
系统中各种工具不能正常运行和使用,所以当静态库被误删除时,系统就不能正常使用了,需要重装系统。
file filename
ls /usr/bin/ls
file /usr/bin/ls
优点
动态链接形成的可执行程序小(相对静态链接),可以节省各种资源(磁盘、内存、网络)。如果程序都使用静态链接,那么形成的可执行程序都会很大,磁盘可能是很大的(如500GB,1个TB),但是内存是宝贵的,可执行程序太大,内存也加载不了几个程序,程序那么大,从远端下载软件时也不是一件轻松的事。
多个需要调用相同库函数的程序同时加载到内存,在需要调用库函数的那一刻就会去系统目录寻找,不存在库函数冗余代码的问题。
缺点
执行到外部库函数时,需要耗费时间寻找,程序内部只保存了库函数的地址,执行到需要库函数的地方时,操作系统去找;
受到库升级或被删除的影响(程序运行依赖动态库文件),动态库文件一旦出现一些问题,程序的运行就会受到很大影响;
静态链接
是什么
gcc/g++默认进行动态链接,静态链接需要加上-static选项;
在编译链接时,把库文件的代码全部加入到可执行文件中,因此生成的文件体积比较大,但在运行时就不再需要库文件了,后缀名一般为.a
优点
不受库升级或被删除的影响;
程序内部已经拷贝了库中用到的方法(函数),直接就可以执行,不需要花时间找;
缺点
形成的可执行程序体积太大,耗费各种资源(磁盘、内存、网络);
隐性缺点:包含相同库函数的程序加载到内存,程序在自己内部调用自己内部的库函数方法,对内存来说产生了冗余代码(即内存中同时存在多个相同的库函数代码)。
时代在发展,技术在进步,硬件、软件在发展,但是时代再怎么发展,也不能容忍任何效率低下的存在。
静态库的安装
在使用-static选项时如果没有静态库,就会报错:
我们需要手动安装静态库:
查询是否有C静态库:
sudo find / -name 'libc.a'
Centos安装C静态库:
sudo yum install -y glibc-static
查询是否有C++静态库:
sudo find / -name 'libstdc++.a'
Centos安装C++静态库:
sudo yum install -y libstdc++-static
库命名规则
Linux下
动态库格式:libxxx.so
;
静态库格式:libxxx.a
;
去掉lib前缀,去掉.so/.a后缀,就是真正的库名称。
windows下
动态库格式:xxx.dll
;
静态库格式:xxx.lib
;
gcc/g++命令选项
格式 gcc [选项] 要编译的文件 [选项] [目标文件]
-E
预处理完成后停止,不进行编译。
默认不生成文件,所有信息都会直接输出到显示器上,需要手动把结果重定向到指定的文件中。
gcc -E hello.c -o hello.i
-S
编译完成后停止,不进行汇编。
默认生成.s
汇编文件
gcc -S hello.i -o hello.s
-c
汇编完成后停止,不进行链接。
默认生成.o
二进制可重定位目标文件。
gcc -c hello.s -o hello.o
-o
用于指定目标文件名称。
-g
gcc/g++默认生成release程序,会进行各种优化,没有调试信息,无法进行调试。
-g生成debug程序,向程序中添加调试符号信息,共调试器调试。
gcc -o hello hello.c -g
gcc -o hello hello.c
-static
采用静态链接的方式生成的文件。
gcc -o hello hello.c -static
-shared
尽量使用动态库(动态链接),所以生成文件比较小,但是需要系统有动态库。
-O0/-O1/-O2/-O3
编译器的优化选项的4个级别,-O0表示没有优化,-O1为缺省值,-O3优化级别最高。
-w
关闭所有的警告信息。
-Wall
开启所有警告信息,编译器在编译时输出更多的警告信息。
结语
本文主要介绍了动静态链接的概念和gcc/g++的常用选项。
T h e E n d TheEnd TheEnd