【Linux】— Linux编译器-gcc/g++、调试器-gdb、项目自动化构建工具-make/Makefile 使用

一、Linux编译器-gcc/g++

1.1 gcc/g++ 使用方法

格式: gcc [选项] 要编译的文件 [选项] [目标文件]gcc / g++安装: sudo yum install -y gcc-c++。安装后的编译器默认的版本是较低的,我们可以使用选项-std=c99(即使用c99标准),-std=c++11(即使用c++11的标准)来进行版本提升。使用-o选项,可以将编译生成的可执行重命名。最后使用./可执行,来运行程序。如下:

在这里插入图片描述

Linux中一个文件要真正的执行:1. 有x权限;2. 就是一个可执行文件。 很明显文件test.exe满足上述条件!

1.2 程序的翻译过程

程序的翻译过程,可以说是生成二进制方案的过程,具体可以细分为以下几步:

  • 预处理:

预处理功能主要包括宏定义,文件包含,条件编译,去注释等。 预处理指令是以#号开头的代码行。

实例: gcc -E test.c -o test.i。选项"-E",该选项的作用是让gcc从现在开始进行程序的翻译,预处理完成就停下! 选项"-o"是指目标文件,".i"文件为已经过预处理的C原始程序。

  • 编译(生成汇编):

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

实例: gcc -S test.i -o test.s。用户可以使用"-S"选项来进行查看,该选项的作用是让gcc从现在开始进行程序的翻译,编译完成就停下! 可以从test.c开始编译,也可以从test.i开始。

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

汇编阶段是把编译阶段生成的".s"文件转成目标文件(即将汇编语言编译成为,二进制目标文件

实例: gcc -c test.s -o test.o。读者在此可使用选项"-c",该选项的作用是让gcc从现在开始进行程序的翻译,汇编完成就停下!。就可看到汇编代码已转化为".o"的二进制目标代码了。

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

实例: gcc hello.o –o hello。在成功编译之后,就进入了链接阶段,最后形成可执行程序


那么为什么会有这些过程呢?

这就要说到编程语言的历史了。最初的计算机只能处理二进制,二进制是处理器的“母语”。这叫“机器语言”或者“机器码”。在计算机早期阶段,必须用机器码写程序(即二进制编程)(前段时间很火的《三体1》中,早期叶文洁当时就是使用的二进制编程,还用到了打孔带,上面记录的就是二进制信息)。

在1940-1950年代,程序员开发一种新语言(汇编语言),它为每个操作码分配一个简单名字,叫“助记符”,助记符后面紧跟数据,形成完整指令。当然,计算机并不认识助记符,它只认得二进制码,然后就有了汇编的编译器,它可以读懂文字指令,自动转换成二进制指令,这种程序叫“汇编器”

汇编语言直接对应机器码,虽然已经很方便了。但是,汇编器仍然强迫程序员思考,用什么寄存器和内存地址,如果我们突然要用额外一个数,可能要改很多代码。这时候就出现了一些高级语言和与之对应的编译器(如:C,C++,Java…)。

因为计算机只认识二进制。以c语言为例,所以我们要先将c语言代码转化为汇编代码,然后再转为二进制目标文件。 那为什么不直接将c语言转为二进制目标文件呢?因为前人已经写好了汇编的编译器,我们只需站在巨人的肩膀上,再将高级语言转为较简单的汇编代码即可。

那么如何形成第一个汇编写的汇编编译器呢?

在这里插入图片描述
先用二进制写一个汇编编译器(此时汇编代码便可通过此编译器形成软件)。汇编编译器本质上就是软件,那么便可先用汇编语言写一个新的编译器,再将这个新的编译器放在二进制写的汇编编译器上执行,最后形成新的汇编编译器(软件)。日后只需维护汇编代码即可! 这个过程也可叫做编译器的自举(bootstrap),c语言的第一个编译器也是如此形成的!

1.3 链接 – 动静态链接特点及区别

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

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

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

  • 静态库是指编译链接时,把库文件的代码全部加入到可执行文件中,因此生成的文件比较大,但在运行时也就不再需要库文件了。其后缀名一般为".a"
  • 动态库与之相反,在编译链接时并没有把库文件的代码加入到可执行文件中,而是在程序执行时由运行时链接文件加载库,这样可以节省系统的开销。动态库一般后缀名为".so",如前面所述的 libc.so.6就是动态库。gcc在编译时默认使用动态库。完成了链接之后,gcc就可以生成可执行文件,如下所示。 gcc test.o –o test
  • gcc默认生成的二进制程序,是动态链接的,这点可以通过 file命令验证。

验证:

  1. -static指令,作用是链接时使用静态链接:

在这里插入图片描述
2. ldd 可执行指令,作用是查看链接的库:

在这里插入图片描述

  1. file filename指令,查看链接属性:

在这里插入图片描述


动态库 && 动态链接的优缺点:1. 不能丢失。共享动态库,但是一旦动态库缺失,所有的动态链接这个库的程序,都无法执行! 2. 节省资源。

静态库 && 静态链接的优缺点:1. 一旦形成,与库无关。在编译的时候,把库中的方法,拷贝到我自己的可执行程序中,此时程序将不再关心任何的库; 2. 浪费资源,占用空间大。

二、Linux调试器-gdb

2.1 背景

程序的发布方式有两种,debug模式和release模式。Linux gcc/g++出来的二进制程序,默认是release模式。 要使用gdb调试,必须在源代码生成二进制程序的时候,加上 -g选项,此时编译器形成可执行程序的时候,会给可执行程序添加调试信息。 我们也可以发现可调式的可执行程序比普通的程序占用内存大。

在这里插入图片描述

调试信息大致如下:

在这里插入图片描述

2.2 使用方法

gdb binFile退出: ctrl + dquit调试命令:

  • list/l 行号: 显示binFile源代码,接着上次的位置往下列,每次列10行。
  • list/l 函数名: 列出某个函数的源代码。
  • r或run: 运行程序(对比VS F5)。
  • n 或 next: 单条执行,逐过程(对比VS F10)。
  • s或step: 进入函数调用,逐语句(对比VS F11)。
  • break(b) 行号: 在某一行设置断点。
  • break 函数名: 在某个函数开头设置断点。
  • info break : 查看断点信息。
  • finish: 执行到当前函数返回,然后停下来等待命令。
  • print(): 打印表达式的值,通过表达式可以修改变量的值或者调用函数。
  • p 变量: 打印变量值。
  • set var: 修改变量的值。
  • continue(或c): 从当前位置开始连续而非单步执行程序。
  • delete breakpoints(b): 删除所有断点。
  • delete breakpoints((b) n: 删除序号为n的断点。
  • disable breakpoints(b): 禁用断点。
  • enable breakpoints(b): 启用断点。
  • info(或i) breakpoints(b): 参看当前设置了哪些断点。
  • display 变量名: 跟踪查看一个变量,每次停下来都显示它的值。
  • undisplay: 取消对先前设置的那些变量的跟踪。
  • until X行号: 跳至X行。
  • breaktrace(或bt): 查看各级函数调用及参数。
  • info(i) locals: 查看当前栈帧局部变量的值。
  • quit: 退出gdb。

三、Linux项目自动化构建工具-make/Makefile

3.1 背景

  • 会不会写makefile,从一个侧面说明了一个人是否具备完成大型工程的能力;
  • 一个工程中的源文件不计数,其按类型、功能、模块分别放在若干个目录中,makefile定义了一系列的规则来指定,哪些文件需要先编译,哪些文件需要后编译,哪些文件需要重新编译,甚至于进行更复杂的功能操作;
  • makefile带来的好处就是——“自动化编译”,一旦写好,只需要一个make命令,整个工程完全自动编译,极大的提高了软件开发的效率;
  • make是一个命令工具,是一个解释makefile中指令的命令工具,一般来说,大多数的IDE都有这个命令,比如:DelphimakeVisual C++nmakeLinuxGNUmake。可见,makefile都成为了一种在工程方面的编译方法;
  • make是一条命令,makefile是一个文件,两个搭配使用,完成项目自动化构建。

3.2 原理

make是如何工作的,在默认的方式下,也就是我们只输入make命令。那么,

  1. make在当前目录下找名字叫"Makefile""makefile"的文件。
  2. 如果找到,它会找文件中的第一个目标文件(target),在上面的例子中,他会找到"code.exe"这个文件,并把这个文件作为最终的目标文件。
  3. 如果code.exe文件不存在,或是code.exe所依赖的后面的code.o文件的文件修改时间要比code.exe这个文件新(可以用 touch测试 - 前面讲过touch可以修改文件最近一次被修改的时间),那么,他就会执行后面所定义的命令来生成code.exe这个文件。
  4. 如果code.exe所依赖的code.o文件不存在,那么make会在当前文件中找目标为code.o文件的依赖性,如果找到则再根据那一个规则生成code.o文件。(这有点像一个堆栈的过程
  5. 当然,你的C文件和H文件是存在的啦,于是make会生成 code.o文件,然后再用 code.o文件声明make的终极任务,也就是执行文件code.exe了。
  6. 这就是整个make的依赖性,make会一层又一层地去找文件的依赖关系,直到最终编译出第一个目标文件。
  7. 在找寻的过程中,如果出现错误,比如最后被依赖的文件找不到,那么make就会直接退出,并报错,而对于所定义的命令的错误,或是编译不成功,make根本不理。
  8. make只管文件的依赖性,即,如果在我找了依赖关系之后,冒号后面的文件还是不在,那么对不起,我就不工作啦。

makefile / make会自动根据文件中的依赖关系,进行自动推导,帮助我们执行所有的相关的依赖方法。

在这里插入图片描述


  • makefile文件中,保存了编译器和链接器的参数选项,并且描述了所有源文件之间的关系。make程序会读取makefile文件中的数据,然后根据规则调用编译器,汇编器,链接器产生最后的输出。
  • Makefile里主要包含了五个东西:显式规则、隐晦规则、变量定义、文件指示和注释。1. 显式规则说明了,如何生成一个或多个目标文件。2. make有自动推导的功能,所以 隐晦规则可以让我们比较粗糙地简略地书写makefile ,比如源文件与目标文件之间的时间关系判断之类。3. 在makefile中可以定义变量makefile被执行时,其中的变量都会被扩展到相应的引用位置上。4. 通常使用 $(var) 表示引用变量文件指示。包含在一个makefile中引用另一个makefile,类似C语言中的include。5. 注释makefile中可以 使用 # 在行首表示行注释
  • 默认的情况下,make命令会在当前目录下按顺序找寻文件名为“GNUmakefile”、“makefile”、“Makefile”的文件

3.3 项目清理

  • 工程是需要被清理的
  • clean这种,没有被第一个目标文件直接或间接关联,那么它后面所定义的命令将不会被自动执行,不过,我们可以显示要make执行。 即命令——“make clean”,以此来清除所有的目标文件,以便重编译。
  • 但是一般我们这种clean的目标文件,我们将它设置为伪目标,用.PHONY 修饰,伪目标的特性是,总是被执行的。 makefile中的伪对象表示对象名称并不代表真正的文件名,与实际存在的同名文件没有相互关系,因此伪对象不管同名目标文件是否存在都会执行对应的生成指令。伪对象的作用有两个,1. 使目标对象无论如何都要重新生成。2. 并不生成目标文件,而是为了执行一些指令.

在这里插入图片描述
于是我们便可使用vim编写如上内容。里面$相当于取值操作,@最终会被替换目标文件,而^会被替换为依赖方法。


我的博客即将同步至腾讯云开发者社区,邀请大家一同入驻:https://cloud.tencent.com/developer/support-plan?invite_code=12k49c75rav0l

本图文内容来源于网友网络收集整理提供,作为学习参考使用,版权属于原作者。
THE END
分享
二维码
< <上一篇
下一篇>>