【Linux学习工具篇】之make与gdb

时间:2024-10-29 13:43:37

在这里插入图片描述

????博客主页: 小镇敲码人
????代码仓库,欢迎访问
???? 欢迎关注:????点赞 ????????留言 ????收藏
???? 任尔江湖满血骨,我自踏雪寻梅香。 万千浮云遮碧月,独傲天下百坚强。 男儿应有龙腾志,盖世一意转洪荒。 莫使此生无痕度,终归人间一捧黄。????????????
❤️ 什么?你问我答案,少年你看,下一个十年又来了 ???? ???? ????

Linux学习工具篇之make与gdb

  • Makefile/make
    • Makefile/make是什么
    • 为什么要有Makefile/make
    • 如何编写Makefile文件
      • Makefile文件的基本内容
      • Makefile文件的基本格式
      • 编写简单的`makefile`文件
        • 变量和伪目标
      • make是如何工作的
      • 认识一下时间
  • gdb调试工具
    • 安装gdb
    • 使用gdb
      • 总结gdb常用的指令
      • Release版本和Debug版本
      • 指令的使用

前言:上篇博客,我们学习了Linux中的编辑器vim和编译器gcc/g++,今天这篇博客,我们来介绍一下项目自动化构建工具make/Makefile

Makefile/make

Makefile/make是什么

  1. make

    make是一个项目构建工具主要用于方便地编译、链接多个源代码文件。它能够自动决定哪些源文件需要重新编译,从而高效地构建项目make普遍用于处理C/C++项目,但也可以用于其他编程语言的项目。make工具通过读取和执行Makefile中的指令来完成项目的构建过程。

  2. Makefile

    是一个文件的名称,它可以被make工具识别,执行这个文件中的命令。

    • 它定义了一系列的规则来指定哪些文件需要先编译,哪些文件需要后编译,以及哪些文件需要重新编译等。这些规则使得整个项目的编译过程变得自动化。
    • Makefile的命名规则:Makefile的命名不区分大小写,但通常使用“Makefile”作为文件名,以便与GNU make的默认查找顺序一致。
      • make命令会在当前目录下自动查找名为“GNUmakefile”、“makefile”或“Makefile”的文件。

为什么要有Makefile/make

下面是Makefile/make的常见优势:

  • 自动化编译:一旦写好Makefile,只需要一个make命令,整个工程就可以完全自动编译,极大地提高了软件开发的效率。
  • 结构化脚本:Makefile把很多行命令分成了若干个target,使得脚本更加结构化。
  • 选择性更新:make能够判断哪些文件需要重新编译,从而避免无意义的重复编译。
  • 跨平台兼容性:虽然make主要用于Unix-like系统,但也可以通过配置在不同平台上使用。

如何编写Makefile文件

Makefile文件的基本内容

通常Makefile文件由若干条规则组成,每条规则包括了目标、依赖和更新方法。

  • 目标:通常是文件名,也可以是伪目标(如clean),代表要生成或执行的文件或任务。
  • 依赖:生成目标所需要的文件或其他目标。
  • 更新方法:生成目标的命令序列,每条命令必须以制表符(Tab键)开始。

Makefile文件的基本格式

我们已经知道Makefile文件是由一条条的规则组成,每条规则由三部分组成:目标、依赖和更新方法。

目标文件(target): 依赖文件(prerequisites)...  
	执行命令(command)
  • 这上面的规则的第一行是依赖关系,第二行是依赖方法。依赖关系告诉make为什么要帮你生成目标文件,因为存在依赖关系。而依赖方法告诉make应该如何生成目标文件,要执行相应的指令。

编写简单的makefile文件

  1. 生成简单的单个可执行文件。

    伪目标是不对应实际文件的目标,它们通常用于执行特定的命令,如清理构建文件。为了避免与同名文件冲突,你可以使用.PHONY声明伪目标:

    main: main.o  
    	gcc -o main main.o  
      
    main.o: main.c  
    	gcc -c main.c  
    
    .PHONY:clean
    clean:  
    	rm *.o main
    
    • 在上面的例子中main是最终要生成的目标文件,它依赖于main.o,main.o又依赖于main.cclean是一个伪目标,用于删除构建过程中生成的文件。

      外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

    • 使用make指令后,会先执行下面的方法生成main.o,最后执行上面的方法生成可执行文件main

    • 如果不清理旧文件,我们的文件也没有更新内容,执行make指令不会做任何事情。只要可执行文件的生成时间比所有的源文件的最近修改时间都要新,就证明它的最新的可执行文件。

      外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

      • make clean:清理构建项目中生成的文件。

        外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

  2. 生成多个可执行文件。

    如果你的项目中有多个源文件,并且你想生成多个可执行文件,你可以这样编写Makefile:

    prog1: prog1.o utils.o                                                                                                                                                     
       gcc -o prog1 prog1.o utils.o                                                
                                                                                   
    prog1.o: prog1.c                                                                
       gcc -c prog1.c                                                              
                                                                                   
    utils.o: utils.c                                                                
       gcc -c utils.c                                                              
                                                                                   
    prog2: prog2.o utils.o                                                          
       gcc -o prog2 prog2.o utils.o                                                
                                                                                   
    prog2.o: prog2.c                                                                
       gcc -c prog2.c                                                              
                                                                                   
    .PHONY:clean                                                                    
    clean:                                                                          
       rm *.o prog1 prog2 
    
    • make之后,我们发现make工具只构建了一个目标。这是因为make工具从上到下扫描makefile文件,默认形成的是第一个目标文件,默认只形成一个,但它为了形成这个目标文件会把它依赖的文件都形成。

      外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

    • 我们可以定义一个伪目标all,它依赖prog1prog2,make会帮助我们构建第一个出现的目标,这样我们就能构建prog1prog2了,在先前的makefile文件的首部增加下面的规则。

      .PHONY:all                                                          
      all:prog1 prog2 
      
    • make clean后再make这次同时生成了两个可执行文件:

      外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

  3. 使用自动变量$@$^来写Makefile文件。

    • $@:表示当前规则中的目标文件。

    • $^:表示当前规则中所有的依赖文件列表,这些文件是构建目标文件所必需的。

      修改刚刚的Makefile文件如下:

      .PHONY:all
      all:prog1 prog2
      
      prog1: prog1.o utils.o  
      	gcc -o $@ $^
      
      prog1.o: prog1.c  
      	gcc -c $^  
      
      utils.o: utils.c  
      	gcc -c $^
      
      prog2: prog2.o utils.o  
      	gcc -o $@ $^
      
      prog2.o: prog2.c  
      	gcc -c $^  
      
      .PHONY:clean
      clean:  
      	rm *.o prog1 prog2
      
    • make clean后再make,自动化构建结果一致:

      外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

    • 从 上面的Makefile我们还能发现一个问题,我们给每一个.o文件都单独设立了规则,其实这是没必要的,我们其实不需要为每个 .o 文件都定义一个规则,因为 make 可以自动推断出如何构建它们(只要它知道如何编译 .c 文件到 .o 文件)。因此,我们可以简化 Makefile 如下:

      .PHONY:all
      all:prog1 prog2
      
      prog1: prog1.o utils.o  
      	gcc -o $@ $^
      
      
      
      prog2: prog2.o utils.o  
      	gcc -o $@ $^
      
      # 使用模式规则来编译所有的 .c 文件到 .o 文件  
      %.o: %.c  
      	gcc -c $^ -o $@
      #   gcc -c $<
      #   gcc -c $^
      
      .PHONY:clean
      clean:  
      	rm *.o prog1 prog2
      
      • $< 是一个自动变量,它代表规则中的第一个依赖文件名(在这个例子中是 .c 文件)。

        外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

    • 生成之后,此时我们执行make不会帮我们执行,但是如果我们修改了某个源文件,就不同了,但make会根据时间选择性更新,避免重复编译:

      外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

变量和伪目标

Makefile文件是可以设置变量的,上面我们介绍了自动变量。Makefile文件中的变量的值用来存储字符串值,这些值可以在其它地方被引用。相当于给字符串起别名:

  • 定义变量:使用变量名=值的形式来定义变量,=两边不能有空格。

  • 引用变量:使用$变量名就可以引用变量,快速使用一下:

    G=gcc
    main: main.o  
    	$G -o $@ $^
      
    main.o: main.c  
    	$G -c $^  
    
    .PHONY:clean
    clean:  
    	rm *.o main
    

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

伪目标也称虚拟目标或者假目标,它们虽然是目标,但是不会生成任何文件,只会用于执行一系列命令。伪目标通常通过.PHONY特殊目标来声明,这告诉make这些目标不是真正的文件名,因此即使它们与文件名冲突,make也会执行它们后面的命令。

  • 伪目标常用于以下场景:
    • 清理构建目标过程出现的文件,如.o文件,可执行文件。
    • 生成多个可执行文件。

make是如何工作的

  1. 查找Makefile文件,使用make工具后,它首先会查找当前目录下有没有Makefile文件,如果没有就会报错。

    外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

  2. make工具会查找Makefile文件中的第一个目标(通常是最终的可执行文件或库文件),并将其作为最终的目标文件。然后,它会检查这个目标文件是否存在,以及它的依赖文件是否是最新的。

  3. 编译依赖文件:如果这个目标文件不存在,或者它的依赖文件中有任何一个比目标文件新(即依赖文件被修改过),那么make工具会按照Makefile文件中定义的规则来编译这些依赖文件。这通常涉及到将源文件(如.c或.cpp文件)编译成目标文件(.o文件)(只会编译那些没有或者更需要更新的)。

  4. 一旦所有的依赖文件都被编译成目标文件,make工具就会将这些目标文件链接在一起,生成最终的可执行文件或库文件。

  5. 除了第一个目标之外,Makefile文件中可能还定义了其他目标(如清理生成的文件、测试等)。这些目标可以通过在make命令后面加上相应的目标名来执行,例如“make clean”用于清理生成的文件。

认识一下时间

在Linux系统中,每个文件都有三种时间戳,分别是:

  1. Access Time (atime): 文件最后一次被访问的时间。这通常意味着读取文件内容的时间。
  2. Modify Time (mtime): 文件内容最后一次被修改的时间。这包括向文件中写入数据或者修改文件内容。
  3. Change Time (ctime): 文件元数据(metadata)最后一次被修改的时间。这包括文件的权限、所有权、链接数等属性的变化。注意,修改文件内容也会导致ctime的更新,因为修改内容本质上也会修改文件的元数据(例如文件大小)。
  1. 我们可以通过指令stat filename查看某个文件的三种时间,stat指令在Linux中用于显示文件和文件系统的详细状态信息,包括文件的属性

    外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

    • 这个创建时间有些系统不存储,我们不用管。
  2. 我们可以通过指令touch修改文件的时间。

    • touch -t [[CC]YY]MMDDhhmm[.ss] filename:指定特定的时间更新文件的访问时间和修改时间

      其中,[[CC]YY]MMDDhhmm[.ss] 是时间的指定格式,例如 202310011234.56 表示 2023 年 10 月 1 日 12 时 34 分 56 秒。注意,年份可以是两位数或四位数,但如果是两位数,则 69-99 表示 1969-1999 年,00-68 表示 2000-2068 年。
      外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

    • 由于我们改变了文件的修改时间,所以文件的属性发生变化,文件的ctime就会更新,我们修改文件的访问时间,也是一种对文件元数据的修改,ctime也会更新。

    • touch -a -t [[CC]YY]MMDDhhmm[.ss] filename:仅更新文件的访问时间。

      外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

    • touch -m -t [[CC]YY]MMDDhhmm[.ss] filename:仅更新文件的修改时间。

      外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

    • touch filename:更新文件的访问时间和修改时间为当前时间。文件的ctime,不是touch直接控制的,而是因为atimectime改变,由文件系统在底层自动处理的。

      外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

  3. 验证我们修改最近一次某个目标文件的源文件的最近修改时间,使其比目标文件的要新,make就会重新编译这个源文件:

    • 修改其ctime,执行make不会重新编译:

      外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

    • 修改mtime,执行make会重新编译:

      外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

      • touch -m filename是把文件的mtime时间变为当前时间。

gdb调试工具

上面介绍项目中常用到的自动化构建工具,下面来介绍一下Linux中常用的调试工具gdb

安装gdb

ubuntu系统:apt install -y gdb,我们已经安装了:

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

使用gdb

总结gdb常用的指令

下面表格是对gdb常用指令的总结:

GDB常用指令表格(按功能分类)

功能分类 指令/缩写 描述
查看源代码 list (l) 显示源代码的一部分或整个文件
设置断点 break (b) 在指定的行或函数上设置断点
管理断点 info b 显示当前所有断点的信息
delete (d) 删除指定的断点或观察点
disable 禁用指定的断点或观察点
enable 启用之前禁用的断点或观察点
单步执行 step (s) 执行下一行代码,并进入函数内部
next (n) 执行下一行代码,但不进入函数内部
运行控制 run 运行程序,直到遇到断点或程序结束
stop 停止当前运行的程序
continue © 继续运行程序,直到遇到下一个断点或程序结束
查看变量和表达式 print § 打印变量或表达式的值
调用栈管理 backtrace (bt) 显示当前调用栈的回溯信息
自动显示 display 设置在每次程序停止时自动显示的变量或表达式
undisplay 取消之前设置的自动显示
程序控制 finish 运行直到当前函数返回
until 运行程序直到指定的行号或地址
变量修改 set var name=value 设置变量的值

Release版本和Debug版本

我们都知道Windows中的集成开发环境中,可执行程序有两个版本,一个是DEBUG,另外一个是release版本,只有处于Debug版本下才能进行调试,Linux的gcc工具,默认不带选项就是release模式。

  • 要使用gdb调试,必须在源代码生成.o文件的时候加上-g选项。在链接步骤中,通常不需要再次指定-g选项,因为链接器会保留已编译对象文件中的调试信息。然而,如果在链接时再次指定-g选项,也不会导致错误。

以下是一个关于Release版本的可执行程序和Debug版本的可执行程序区别的表格总结:

项目 Debug版本 Release版本
主要用途 开发和调试阶段使用 部署和分发,供最终用户使用
调试信息 包含详细的调试信息,如符号表、行号等 通常不包含调试信息,以减小文件大小
优化级别 针对可读性进行优化,便于调试 经过编译器优化,以提高代码的执行速度和效率
性能 运行速度较慢,占用存储空间较多 运行速度较快,占用存储空间较少
变量初始化 未初始化的变量可能会被自动初始化 未使用的变量通常不会被自动初始化
断言检查 断言被激活,用于检测逻辑错误 断言检查通常被禁用,以提高性能
功能完整性 保留所有功能,包括测试用的功能 可能省略某些调试用的功能,以提升安全性和性能
安全性 较低,因为包含调试信息和未优化的代码 较高,因为去除了调试信息并进行了优化
稳定性 可能存在未经过充分测试的代码 经过详细测试,确保在各种条件下稳定运行
  • 因为Deubg版本下的可执行文件包含调试信息,它的大小比Release版本下的可执行文件要大一些:

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

  • 编译器在生成目标文件的同时也会生成调试信息,这些信息包括了源代码与机器码之间的映射关系,如变量名、函数名、行号等,它们以特定的格式(如DWARF)编码,并存储在可执行文件或专门的调试信息文件中。

    外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

    指令的使用

    gdb中有很多命令用来帮助我们调试,我们下面来一一介绍一下:

    1. gdb 可执行程序,进入调试界面。

      外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

    2. list/l 行号显示源代码指定行之后的代码。

      外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

    3. 程序运行起来才会开始调试,在程序运行前,我们需要先打断点。打断点,可以用指令break/b 行号/函数名/文件名外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

    4. 可以用指令info b/break,查看断点的情况:

      外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

      • Disp列:Disp列表示断点的部署(disposition)状态,它有三种可能的状态:

        • keep:表示这是一个普通的断点,不会自动删除。
        • del:表示这是一个临时断点(通常通过设置tbreak命令创建),当程序在断点处停住一次后,断点会自动删除。
        • dis:表示断点当前被禁用(disabled),即断点存在但不会触发程序停住。
      • Enb列:Enb列表示断点的启用(enable)状态。它有两种状态:yn

        • y:表示断点当前是启用的,即当程序运行到该断点时,会触发程序停住。

        • n:表示断点当前是禁用的,即断点存在但不会触发程序停住(与disp列的dis状态类似,但Enb列更专注于启用/禁用状态,而disp列更侧重于断点的生命周期管理)。

      • 前面是断点编号,d 断点编号删除一个断点。

    5. 使用指令disable/enable,禁用/使能断点:

      外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

    6. 使用指令run,程序运行起来才能开始调试,这和VS2019的机制是一样的:

      外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

      • 由于我们把第10行的断点,禁用了,所以程序直接跳到了第11行。
    7. 使用指令n逐过程调试,s逐语句调试。逐过程调试会把每一句语句看成一个整体,类似于VS2019中的F10,而s会进入函数内部,类似于VS2019中的F11

      外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

    8. 在调试的运行过程中,我们也可以使用指令p 变量名/变量地址,查看它的值:

      外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

      • 但是它只会显示一次,不会一直显示,我们可以使用指令display 变量名/变量取地址,查看变量的值或者它的地址,它会一直更新:

        外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

        • 前面是它的编号,我们使用指令undisplay 编号可以取消这个对这个变量或者地址的查看:

          外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

  1. 在调试过程我们还可以查看程序此时调用的堆栈信息,使用指令bp

    外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

  2. 除了n逐过程调试,s逐语句调试,还有一些指令与过程调试有关:

    • c:从一个断点,直接运行到下一个断点:

      外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

    • finish:执行完一个函数后会停下来。

      外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

      • 只有在函数体内执行这个指令才有效,当函数中有其它有效断点时,执行这个指令,会先跳到断点处,如果没有断点,执行执行完函数才会停下。
    • until:在一定范围内,直接跳转到指定行。

      外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

      • 中间行也不能有有效的断点,如果有until会停在第一个断点处。
  3. 指令set var name=value修改变量的值。
    外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

    • 如果程序由于某些输入数据而崩溃或行为异常,你可以通过修改这些输入数据(即相关变量的值)来绕过错误数据,继续调试其他部分的代码。
    • 当你怀疑某个变量导致问题时,可以通过修改它的值来验证你的假设。如果修改后的值解决了问题,那么你就更接近问题的根源了。
    • 在某些情况下,程序的状态可能非常复杂,难以通过正常输入达到。通过修改变量值,你可以模拟这些复杂状态,以测试代码在这些情况下的行为。
  • 本人知识、能力有限,若有错漏,烦请指正,非常非常感谢!!!
  • 转发或者引用需标明来源。