在此阶段做的事情:
- 头文件展开:把我们编写的代码中的包含头文件的代码替换成头文件本身
- 删除所有的注释
- #define定义的符号和宏全部替换
- 执行条件编译
在Linux下,我们可以通过指令让gcc只执行预处理操作
gcc -E test.c -o test.i
# -E 表示从现在开始,进行程序的翻译,当预处理结束时停下来
# -o 表示指明产生的文件的名称
可以看到经过预编译之后,#include包含头文件的代码没有了,但是文件中多了几百行,这些多的就是头文件的内容被拷贝进来了,注释部分被删除,宏定义被替换了,条件编译也转变成了执行过后的结果。
此阶段做的事情:
- 语法分析
- 词法分析
- 语义分析
- 符号汇总
最终的结果就是把C语言代码变成汇编语言代码
在Linux下需要执行的指令是:
gcc -S test.i -o test.s
# -S 表示从现在开始,执行程序的翻译,做完编译工作之后,变成汇编代码就停下来
# 变成的汇编代码的后缀名是.s
打开test.s之后我们可以发现,里面的代码意见已经变成汇编指令了。
此阶段做的事情:把汇编代码变成二进制(这里的二进制不是可执行的,叫做二进制目标文件)
在Linux下需要执行的指令是:
gcc -c test.s -o test.o
# -c 表示从现在开始,进行程序的翻译,做完汇编工作,变成可重定向的目标二进制,就停下来
# 重定向的目标二进制文件的后缀名是.o
可以看到,此时文件内已经变成了我们看不懂的二进制代码,当他以二进制的形式打开时,是这样的
此阶段做的事情:把本地编写的代码和c标准库中的代码合并,形成可执行的二进制文件
- 合并段表:编译器会把在汇编阶段生成的多个目标文件中相同格式的数据合并在一起,最终形成一个 .exe 文件。
- 符号表的合并和重定位:符号表的合并是指编译器会把在汇编阶段生成的多个符号表合并为一个符号表;重定位则是指当同一个符号出现在两个符号表中时,编译器会选取其中和有效地址相关的那一个,舍弃另外一个
在Linux下需要执行的指令是:
gcc test.o -o mytest
# 链接阶段是程序翻译的最后一个阶段,不需要加任何选项
# 默认生成的可执行文件的文件名是a.out,我们可以通过-o选项指定
可以看到,产生的文件mytest就是可执行的程序。
注:
- 对于上述的几个Linux下gcc指令的选项和产生文件的后缀名,这里有一个方便记忆的小技巧,预编译、编译、链接的选项分别是ESc,对应着键盘左上角的按键Esc,产生的文件后缀名是iso,对应着光盘映像文件的后缀名
- 上述的分段执行只是为了方便我们能够更加细致的看到程序翻译的过程,在实际使用gcc的时候,只需要使用指令
gcc 原文件名 -o 产生的可执行文件名
或者gcc 原文件名
即可。
我们在写代码的过程中,会经常用到库函数,类似printf,scanf,strlen等函数,这些函数在我们的代码中只是调用了它们,并没有实现,那么是谁实现的呢?答案是库函数,是别人预先写好的
同时,程序在预处理、编译和汇编阶段处理的都是我们自己编写的代码,只有在链接的时候,库函数的实现才会和我们的代码关联起来 (符号表的重定位);所以,链接的本质是我们在调用库函数时如何与标准库相关联的问题
程序的链接方式一共有两种:动态链接与静态链接
动态链接是指执行代码时,如果遇到库函数调用就跳转到动态库中对应函数的定义处,然后执行该函数,执行完毕后再跳转回原程序并继续往下执行;它的优点是形成的可执行程序小,缺点是受到动态库变动 (删除、升级等) 的影响
静态链接则是直接将本程序内部要使用的库函数从对应的静态库中拷贝一份过来;它的优点是不与静态库产生关联,即不受静态库变动 (删除、升级等) 的影响;缺点是形成的可执行程序非常大。
函数库是一些事先写好的,用于给别人复用的函数的集合,函数库一般分为静态库和动态库两种
静态库是指在编译链接时,把包含的库文件全部拷贝到可执行文件中,然后在运行时就不再需要库文件了,但是由于拷贝了全部内容,所以生成的文件会很大。静态库在Linux下的后缀名是.a,在Windows下后缀名是.lib
动态库:也叫共享库,与静态库相反,在编译链接时并没有把库文件的代码加入到可执行文件中,而是在程序执行时由运行时链接文件加载库,这样可以节省系统的开销。动态库一般后缀名为.so,在Windows下后缀名是.dll
那么,我们验证一下,我们在Linux下编译是怎么链接的
验证方法:使用file指令可以看到调用的库是动态库还是静态库
我们可以看到,gcc默认的链接方式是动态链接,那么怎么让它使用静态链接呢?只需要在编译指令后面加上-static
这个时候,我们查看一下两个文件的详细信息
可以看到,使用静态链接产生的可执行文件,大小比动态链接产生的文件大得多。
这里补充一点非常重要的事情:一定不要删除系统中的C动态库,因为Linux系统中的基本上所有指令都是使用C语言写的,如果没有C动态库,会导致很多指令都无法使用,最终的解决方案只能是重装系统。
gcc 选项
- -E 只激活预处理,这个不生成文件,你需要把它重定向到一个输出文件里面
- -S 编译到汇编语言不进行汇编和链接
- -c 编译到目标代码
- -o 文件输出到 文件
- -static 此选项对生成的文件采用静态链接
- -g 生成调试信息。GNU 调试器可利用该信息。
- -shared 此选项将尽量使用动态库,所以生成文件比较小,但是需要系统由动态库.
- -O3 编译器的优化选项的4个级别,-O0表示没有优化,-O1为缺省值,-O3优化级别最高
- -w 不生成任何警告信息。
- -Wall 生成所有警告信息
我们在使用静态链接的方式编译的时候,可能会发现报错,因为有部分Linux机器没有安装C静态库,所以需要我们手动安装
# 手动安装C静态库
sudo yum install -y glibc-static
同时,部分Linux机器也是没有安装g++的,也需要我们手动安装
# 安装g++
sudo yum install -y gcc-c++
# 安装c++静态库
sudo yum install -y libstdc++-static
debug和release是程序编译的类型版本,debug是调试版本,其中包含了程序的调试信息,release是程序的发布版本,其中没有调试信息,并且进行了部分优化(例如对死循环的优化)。
可以看到,debug版本比release版本要大一点,其中多的内容就是调试信息,所以gdb调试必须要在debug模式下调试,如果是release版本下不可执行调试
会显示no debugging symbols found(没有找到调试标志)
Linux下gcc/g++编译出来的程序默认是release版本
到这里我们总结一下之前所学到的关于Linux下的一些默认行为
gcc/g++的默认行为
默认连接方式是动态连接(静态链接需要加-static)
默认编译版本是release(编译debug版本需要加-g)
vim的默认行为
- 打开后的默认模式是命令模式
sudo yum install -y gdb
- list/l 行号:显示binFile源代码,接着上次的位置往下列,每次列10行。
- list/l 函数名:列出某个函数的源代码。
- r或run:运行程序。
- n 或 next:单条执行。
- s或step:进入函数调用
- break(b) 行号:在某一行设置断点
- break 函数名:在某个函数开头设置断点
- info break :查看断点信息。
- finish:执行到当前函数返回,然后挺下来等待命令
- print§:打印表达式的值,通过表达式可以修改变量的值或者调用函数
- p 变量:打印变量值。
- set var:修改变量的值
- continue(或c):从当前位置开始连续而非单步执行程序
- run(或r):从开始连续而非单步执行程序
- delete breakpoints:删除所有断点
- delete breakpoints n:删除序号为n的断点
- disable breakpoints:禁用断点
- enable breakpoints:启用断点
- info(或i) breakpoints:参看当前设置了哪些断点
- display 变量名:跟踪查看一个变量,每次停下来都显示它的值
- undisplay:取消对先前设置的那些变量的跟踪
- until X行号:跳至X行
- breaktrace(或bt):查看各级函数调用及参数
- info(i) locals:查看当前栈帧局部变量的值
- quit:退出gdb
下面会使用实例来演示部分指令
list/l 行号:显示binFile源代码,接着上次的位置往下列,每次列10行。
list/l 函数名:列出某个函数的源代码
break(b) 行号:在某一行设置断点
info break :查看断点信息
break 函数名:在某个函数开头设置断点
d + 断点编号:删除断点
delete breakpoints:删除所有断点
r或run:运行程序
s或step:进入函数调用
进入了AddToVal函数内部,遇到断点停下
n 或 next:单条执行
每个n执行一行
print§:打印表达式的值,通过表达式可以修改变量的值或者调用函数
display 变量名:跟踪查看一个变量,每次停下来都显示它的值
undisplay:取消对先前设置的那些变量的跟踪
finish/fin:执行到当前函数返回,然后停下来等待命令
到此,我们学习了Linux下的基本指令与操作,权限相关的概念,学习了yum工具,能够在Linux下进行软件的安装,学习了vim的使用,能够在Linux下写代码,学习了gcc/g++的使用,能够在Linux下编译代码,学习了gdb的使用,能够在Linux下调试代码,学习了make/makefile,能够在Linux下使用多文件编程,为我们在Linux下编程提供了便利,编写了我们的第一条Linux程序----进度条,学会了使用git命令行,能够把Linux下的代码上传到Gitee/Github上。Linux的工具篇到此结束,下面,我们将会遇到Linux的第一座大山----进程。
本章完