目录
一、gcc/g++
简单认识
- gcc:用于编译C语言的编译器
- g++:用于编译C++的编译器,同时兼容编译C代码
程序的翻译过程认识gcc
预处理(宏替换)
作用:完成宏定义,头文件的展开,去注释以及条件编译等。
gcc指令:gcc -E xxx.c -o xxx.i
解释一下:
-E:让gcc在预处理后停止编译过程
-o:☞生成目标文件,
“.i”后缀的文件是经过预处理的原始C程序
演示:
看看里面的内容:
vim test.i看看
可以看到代码代码量达到惊人的八百多行,实际上头文件的展开占大部分。。
头文件的展开
实际上就是将系统中对应头文件的内容拷贝到源代码中,Linux系统的头文件一般都在/usr/include目录下,这些头文件在安装gcc或者其他编译器时都会安装到系统中,因此不需要我们自己去配置。
可以看到该目录下存在一大批头文件。。。。头文件的展开就是将这里内容拷贝到你的源代码中。所以说头文件在预处理后就没有用处了。
条件编译
场景:
在下载一些编译器时,都会发现存在一个社区版和一个专业版,比如window系统的vscode集成开发环境以及java的IDE等。两者区别就是:社区版是免费的但功能没有专业版齐全,专业版收费但功能齐全。但是呢大家有没有想过这样的一个问题,这样的集成开发环境的代码需要维护几份?实际上是一份,如果是两份的话,万一社区版出现问题,修正BUG的同时也要去修正专业版的BUG,十分的麻烦。所以实际中都是采用条件编译的方式去维护一份代码,因为条件编译的用处就是可以完成对代码的动态裁剪工作!
演示:
运行结果:
我们再来看看预处理阶段代码的变化:
可以看到预处理后只剩下有指定v1宏定义的那段代码了,完成了裁剪工作,所以这样采用条件编译的代码维护起来就十分的方便,仅仅只需要维护这一份代码。
对于这样的条件编译,每次都需要打开代码,在定义宏,这样很麻烦,gcc存在命令行定义宏的方式。就是带上-D选项
指令:gcc/g++ -D宏 源代码文件
举例:
注意:g++也是一样的方法!!!!
编译
作用:gcc/g++会检查代码的规范性、是否有语法错误等,无误后,把预处理阶段生成的“.i”代码翻译成汇编语言。
指令:gcc -S xxx.i -o xxx.s
举例:
-S:只进行编译,编译完成后就停止,生成汇编代码。
注意:指令后面加上-std=c99是因为小编的gcc是4.x的老版本,没有更新,有的语法不支持。加上后就支持了,如果是g++,就加上-std=c++11,当然最新的编译器就不需要加。
看看test.s
汇编
作用:把编译阶段生成的“.s”汇编代码翻译成二进制目标文件,“.o”后缀,即机器可识别的代码
指令:gcc -c xxx.s -o xxx.o
演示:
打开看看
看不懂吧!我也看不懂,机器懂就行!
链接
宏观认识
该阶段的主要作用:将汇编生成的二进制代码翻译生成可执行文件".exe"。
指令:gcc xxx.o -o xxx.exe
演示:
如何理解(核心)
什么是链接?
实际上就是程序和库的结合并生成可执行程序,每一种编程语言都会有自己的标准库,这些库里面包含大量的头文件,以便开发者使用,能极大的提高开发的效率。
来看看上面的可执行程序的链接:
指令:ldd 可执行程序
值得一提的是:函数库/标准库
实际上函数库分为动态库(.so为后缀)和静态库(.a为后缀)两种,在库里面都存在着对应函数的定义/方法。
实例:
我们都知道声明函数的同时也应该去实现该函数,但在c程序中我们所使用的“printf”函数,我们包含的头文件“stdio.h”仅仅只是函数的声明而并没有实现printf定义,程序也能运行,那是在哪里实现的定义?
实际上系统把这些函数实现都被做到名为 libc.so.6 的库文件中去了,在没有特别指定时,gcc 会到系统默认的搜索路径“/usr/lib”下进行查找,也就是链接到 libc.so.6 库函数中去,这样就能实现函 数“printf”了,而这也就是链接的作用。
链接的分类
分为动态链接和静态链接两种。
- 动态链接
就是能够链接到动态库的链接。编译期间就知道动态库的地址,程序执行时,会根据链接文件链接到相应的动态库中,并找到相应的方法去执行,在返回结果。
优点:节省空间!
缺点:动态库不能丢失,一旦动态库缺失,所有的动态链接这个库的程序,都无法执行!
- 静态链接
程序在编译的时候,把静态库(.a)中的方法拷贝到自己的可执行程序中。
优点:不关心任何的库文件,即使你库文件丢失了,我也没有影响!
缺点:浪费资源,开销大。
演示看看:
可以看到静态链接的方式生成的可执行文件大小是很大的,十分的占空间,就是因为将静态库中的方法拷贝到可执行程序下了。一般情况下,Linux都不会安装有静态库,我们也没有必要去安装,小编仅仅是为了演示演示。
所以,实际上gcc在编译时默认都是使用动态库的,默认生成的都是动态链接!!
注意:上述操作都是在演示gcc,是因为gcc/g++的操作都大差不差。
二、gdb
基本的认识
编译器实际上都是有两种模式的:debug和release模式,
- debug模式
debug模式是可以被调试的,因为编译器形成可执行程序的时候会给可执行程序添加调试信息。因此该模式下的文件大小会比release模式的大。。
- release模式
release模式是不可以调试的,同时该模式下的代码优化程度高,也是给用户使用的版本。
值得注意的是:在Linux系统下,gcc/g++默认编译出的可执行程序都是release模式的。所以要想让其编译出debug模式,需要带上-g选项。
测试:
可以看到debug模式下的可执行程序的大小比release模式的大。。。。
基本操作及指令
安装gdb
sudo yum -y install gdb
启动gdb
- 指令:gdb debug模式下的可执行程序
显示源代码(list)
- 指令1:list 或者l/ 行号,默认显示十行,同时gdb会记住最近的一次命令,所以可以按回车键继续显示
会发现并没有全部的显示,因为是从第七行开始的,所以我们可以输入list/l +0 从头显示
- 指令2:list/l 函数名,列出某个函数的源代码!
- 指令3:显示指定文件的源代码:list/l 文件名:行号/函数
运行程序(r)
- 指令:r/run
使用这个指令,只是单单的运行程序而已,没有调试,类似于VS按下F5,需要配合断点使用!
断点 (b)
设置断点(b)
- 指令1:b 行号,在某一行设置断点
- 指令2:b 函数,在某个函数的入口设置断点
查看断点信息 (i)
- 指令:info(i) b
重要参数解释:
Num:代表断点的编号
Type:断点类型
Enb:断点使能,y代表可用,n代表禁用。禁用不是删除!!
删除断点 (d)
- 指令:delete(d) 断点编号
注意:这里用的是断点的编号,而不是行号!!!行号是无法执行的!如果没有指明断点编号,也就是只是输入d,那就会自动将所有的断点都删除。
使能(打开/关闭)断点
- 关闭指令:disable 断点编号
- 打开指令:enable 断点编号
注意:和删除断点一样,如果没有指明断点编号,默认就是全部的断点关闭或者打开!!
逐过程(n)
- 指令:next/n,类似VS的F10
逐语句(s)
- 指令:step/s ,类似VSF11。可以进入函数内部
打印变量&&地址(p)
- 指令:print/p 变量/地址
这样比较麻烦,需要手动打开。。。。
变量/地址的常显示
- 打开指令:display 变量/地址
注意:display也是有编号的!!为的就是关闭常显示!!
- 关闭指令:undisplay number(编号)
修改变量
- 指令:set var 变量名=值
注意:一旦使用这条指令,那结果肯定不一样了,因为改的是内存级别的值!!
转至下一断点(c)
- 指令:continue/c
直接跳过中间的过程,运行至下一个断点处!!!
结束函数
- 指令:finish
当我们进入函数内部,但不想一步步看问题时,可以采用这条指令,一次性运行完该函数,并给出最终结果!
转至指定行
- 指令:until 行号
注意:使用该指令时,中间的程序都是运行了的!
演示一波:
查看栈帧
- 指令:bt
这条指令可以看到函数调用入栈的过程!!!
说两句
可以看到在Linux下使用gdb调试对于小量代码来说还说得过去,量大时就十分的难受了。。所以实际上我们基本都是图形化界面,因为我们更喜欢图像化界面的调试,方便。。简明。。但是学习底层gdb还是十分的有用的,因为一些集成开发环境都是gdb套了层壳!!!!!
好了,本次内容就分享到这,如果对你有帮助,欢迎三连,你的鼓励就是我前进的动力!!