Part1—Linux基础

目录

 

一、初识Linux操作系统

1、常见名词

(1)POSIX

(2) Linux 内核

 (3)命令解析器

(4)硬链接计数

 (5)软连接

 二、gcc

1、gcc工作流程

 2、GCC常用命令

 3、多文件编程

4、gcc与g++

三、静态库和动态库

静态库

动态库

动态库介绍

 生成动态链接库

 动态库的使用:

 解决动态库无法加载问题:

 库的工作原理:

 解决方案

 动态库/静态库优缺点

四、Makefile

 前言

 规则

工作原理

规则的执行

 文件的时间戳

 make的自动推导功能

makefile中的变量

模式匹配

Makefile中的函数

wildcard

 patsubst

 makefile的编写

版本1

版本2

 

版本3

版本4

版本5

最终版

 练习题

五、GDB调试

调试选项

启动GDB        

 命令行传参(通过gdb)

gdb中启动程序

退出gdb

查看代码(list)

 当前文件

 切换文件

设置显示的行数

gdb断点操作(break)

 设置断点

查看断点 (info break)

  删除断点(del)

 设置断点状态(disable/enable)

调试命令

继续运行 gdb(continue)

 手动打印信息(print、ptype)

自动打印信息(display与print)

取消自动显示

 单步调试(step,finish,....)

 设置变量值(set var=值)


一、初识Linux操作系统

1、常见名词

(1)POSIX

POSIX是可移植操作系统接口(Portable Operating System Interface of UNIX)的缩写,POSIX标准定义了操作系统应该为应用程序提供的接口标准,是在各种UNIX操作系统上运行的软件的一系列API标准的总称

POSIX 标准是对 UNIX 操作系统的经验和实践的总结,对 操作系统调用的服务接口进行了标准化,保证所编制的应用程序在源代码一级可以在多种操作系统上进行移植

(2) Linux 内核

Linux 系统从应用角度来看,分为内核空间和用户空间两个部分。内核空间是 Linux 操作系统的主要部分,但是仅有内核的操作系统是不能完成用户任务的。丰富并且功能强大的应用程序包是一个操作系统成功的必要件。

Linux 的内核主要由 5 个子系统组成:进程调度、内存管理、虚拟文件系统、网络接口、进程间通信。

  1. 进程调度 SCHED
    • 进程调度指的是系统对进程的多种状态之间转换的策略。Linux 下的进程调度有 3 种策略:SCHED_OTHER、SCHED_FIFOSCHED_RR
      • SCHED_OTHER:分时调度策略(默认),是用于针对普通进程时间片轮转调度策略

      • SCHED_FIFO:实时调度策略,是针对运行的实时性要求比较高运行时间短的进程调度策略

      • SCHED_RR:实时调度策略,是针对实时性要求比较高、运行时间比较长的进程调度策略。

  2. 内存管理 MMU
    1. 内存管理是多个进程间的内存共享策略。在 Linux 中,内存管理主要说的是虚拟内存。每个进程的虚拟内存有不同的地址空间,多个进程的虚拟内存不会冲突。
  3. 虚拟文件系统 VFS
    1. 目前 Linux 下最常用的文件格式是 ext2 和 ext3。
  4. 网络接口
  5. 进程间通信
    1. Linux 下的进程间的通信方式主要有管道、信号、消息队列、共享内存和套接字等方法。

 (3)命令解析器

命令解析器在 Linux 操作系统中就是一个进程 (运行的应用程序), 它的名字叫做 bash 通常我们更习惯将其称之为 shell (即: sh)。

当用户打开一个终端窗口,并输入相关指令, 按回车键, 这时候命令解析器就开始工作了, 具体步骤如下:

  • 在 Linux 中有一个叫做 PATH 的环境变量,里边存储了一些系统目录 (windows也有, 叫 Path)
  • 命令解析器需要依次搜索 PATH 中的各个目录,检查这些目录中是否有用户输入的指令
    • 如果找到了,执行该目录下的可执行程序,用户输入的命令就被执行完毕了
    • 如果没有找到,继续搜索其他目录,最后还是没有找到,会提示命令找不到,因此无法被执行

(4)硬链接计数

语法: ln 源文件 硬链接文件的名字(可以带路径)

硬链接计数是一个整数,如果这个数为 N (N>=1),就说明在一个或者多个目录下共有 N 个文件,但是这 N 个文件并不占用多块磁盘空间,他们使用的是同一块磁盘空间。如果通过其中一个文件修改了磁盘数据,那么其他文件中的内容也就变了。

每当我们给给磁盘文件创建一个硬链接(使用 ln),磁盘上就会出现一个新的文件名,硬链接计数加 1,但是这新文件并不占用任何的磁盘空间,文件名还是映射到原来的磁盘地址上。

创建硬链接只是多了一个新的文件名, 拷贝文件不仅多了新的文件名在磁盘上数据也进行了拷贝。

目录是不允许创建硬链接的。

硬链接和软链接不同,它是通过文件名直接找对应的硬盘地址,而不是基于路径,因此 源文件使用相对路径即可,无需为其制定绝对路径。

 (5)软连接

软连接相当于 windows 中的快捷方式。

语法: ln -s 源文件路径 软链接文件的名字(可以带路径)

在创建软链接的时候, 命令中的 源文件路径建议使用绝对路径,这样才能保证创建出的软链接文件在任意目录中移动都可以访问到链接的那个源文件。

 二、gcc

1、gcc工作流程

GCC 编译器对程序的编译分为 4 个阶段:预处理(预编译)、编译和优化、汇编和链接。

GCC 的编译器可以将这 4 个步骤合并成一个。

  1.  预处理:gcc -E。在这个阶段主要做了三件事: 展开头文件 、宏替换 、去掉注释行;
    1. 这个阶段需要 GCC 调用预处理器来完成,最终得到的还是源文件,文本格式
  2. 编译:gcc -S。检查语法规范,这个阶段消耗时间、系统资源最多。在这个阶段需要 GCC 调用编译器对文件进行编译,最终得到一个汇编文件
  3. 汇编:gcc -C。将汇编指令翻译成机器指令。这个阶段需要 GCC 调用汇编器对文件进行汇编,最终得到一个二进制文件(目标文件)
  4. 链接数据段合并,地址回填。这个阶段需要 GCC 调用链接器对程序需要调用的库进行链接,最终得到一个可执行的二进制文件

 2、GCC常用命令

-I directory (大写的 i)      :指定 include 包含文件的搜索目录( gcc *.c -o calc -I ./include)

-g                                    :在编译的时候,生成调试信息,该程序可以被调试器调试

-D                                   :在程序编译的时候,指定一个宏(gcc test.c -o app -D DEBUG)

-w                                   :不生成任何警告信息,不建议使用,有些时候警告就是错误

-Wall                              :生成所有警告信息

-On                                :n 的取值范围:0~3。

                                        编译器的优化选项的 4 个级别,-O0 表示没有优化,

                                        -O1 为缺省值,-O3 优化级别最高。

-l                                    :在程序编译的时候,指定使用的库(动态库名)

-L                                   :指定编译的时候,搜索的库的路径(动态库路径)

-shared                          :生成共享目标文件。通常用在建立共享库时

-std                                :指定 C 方言,如:-std=c99,gcc 默认的方言是 GNU C

-o                                   : 指定生成的文件名(gcc test.c -o app)

 3、多文件编程

GCC 可以自动编译链接多个文件,不管是目标文件还是源文件,都可以使用同一个命令编译到一个可执行文件中。

# 假设string.h,string.c,main.c都在同一个目录。

# 方式1:直接生成可执行程序 test
$ gcc -o test string.c main.c

# 方式2:先将源文件编成目标文件,然后进行链接得到可执行程序
$ gcc –c string.c main.c
$ gcc –o test string.o main.o

# 运行可执行程序
$ ./test

4、gcc与g++

  1. 在代码编译阶段(第二个阶段):
    1. 后缀为 .c 的,gcc 把它当作是 C 程序,而 g++ 当作是 C++ 程序

    2. 后缀为.cpp 的,两者都会认为是 C++ 程序,C++ 的语法规则更加严谨一些

    3. g++ 会调用 gcc,对于 C++ 代码,两者是等价的,也就是说 gcc 和 g++ 都可以编译 C/C++ 代码

  2. 链接阶段(最后一个阶段):
    1. gcc 和 g++ 都可以自动链接到标准 C 库
    2. g++ 可以自动链接到标准 C++ 库,gcc 如果要链接到标准 C++ 库需要加参数 -lstdc++
  3. 关于 __cplusplus 宏的定义
    1. g++ 会自动定义__cplusplus 宏,但是这个不影响它去编译 C 程序

    2. gcc 需要根据文件后缀判断是否需要定义 __cplusplus 宏 (规则参考第一条)

# 编译 c 程序
$ gcc test.c -o test	# 使用gcc
$ g++ test.c -o test	# 使用g++

# 编译 c++ 程序
$ g++ test.cpp -o test              # 使用g++
$ gcc test.cpp -lstdc++ -o test     # 使用gcc

三、静态库和动态库

        程序中调用的库有两种 静态库和动态库,不管是哪种库文件本质是还是源文件,只不过是二进制格式只有计算机能够识别,作为一个普通人就无能为力了。

        在项目中使用库一般有两个目的,一个是为了使程序更加简洁不需要在项目中维护太多的源文件,另一方面是为了源代码保密,毕竟不是所有人都想把自己编写的程序开源出来。

静态库

在 Linux 中,静态库由程序 ar 生成,现在静态库已经不像之前那么普遍了,这主要是由于程序都在使用动态库。关于静态库的命名规则如下:

  • Linux 中静态库 lib 作为前缀.a 作为后缀,中间是库的名字自己指定即可,即: libxxx.a
  • Windows 中静态库一般以 lib 作为前缀,以 lib 作为后缀,中间是库的名字需要自己指定,即: libxxx.lib

 生成静态链接库:

  1. 对源文件进行汇编操作 (使用参数 -c) ,得到二进制格式的目标文件 (.o 格式)。
  2. 通过 ar 工具将目标文件打包就可以得到静态库文件了 (libxxx.a)。

 使用 ar 工具创建静态库的时候需要三个参数:

  1. 参数c创建一个库,不管库是否存在,都将创建。
  2. 参数s创建目标文件索引,这在创建较大的库时能加快时间。
  3. 参数r:在库中插入模块 (替换)。默认新的成员添加在库的结尾处,如果模块名已经在库中存在,则替换同名的模块。
# 第一步
# 执行如下操作, 默认生成二进制的 .o 文件
# -c 参数位置没有要求
$ gcc 源文件(*.c) -c	

# 第二步
$ ar rcs 静态库的名字(libxxx.a) 原材料(*.o)

# 第三步
# 发布静态库
	1. 提供头文件 **.h
	2. 提供制作出来的静态库 libxxx.a

静态库的使用:

当我们得到了一个可用的静态库之后,需要将其放到一个目录中,然后根据得到的头文件编写测试代码,对静态库中的函数进行调用。

# 1. 首先拿到了发布的静态库
	`head.h` 和 `libcalc.a`
	
# 2. 将静态库, 头文件, 测试程序放到一个目录中准备进行测试
.
├── head.h          # 函数声明
├── libcalc.a       # 函数定义(二进制格式)
└── main.c          # 函数测试

# 3. 编译测试程序 main.c
$ gcc main.c -o app

# 4. 编译的时候指定库信息
	-L: 指定库所在的目录(相对或者绝对路径)
	-l: 指定库的名字, 掐头(lib)去尾(.a) ==> calc
# -L -l, 参数和参数值之间可以有空格, 也可以没有  -L./ -lcalc
$ gcc main.c -o app -L ./ -l calc

# 查看目录信息, 发现可执行程序已经生成了
$ tree
.
├── app   		# 生成的可执行程序
├── head.h
├── libcalc.a
└── main.c

动态库

动态库介绍

动态链接库是程序运行时加载的库,当动态链接库正确部署之后运行的多个程序可以使用同一个加载到内存中的动态库,因此在 Linux 中动态链接库也可称之为共享库

动态链接库是目标文件的集合,目标文件在动态链接库中的组织方式是按照特殊方式形成的。库中函数和变量的地址使用的是相对地址(静态库中使用的是绝对地址),其真实地址是在应用程序加载动态库时形成的。生成与位置无关代码。

写在源代码里的函数,相对 main 函数偏移是一定的,链接时(数据段合并,地址回填),回填 main 函数地址之后,其他源代码里的函数也就得到了地址。动态库里的函数会用一个@plt 来标识,当动态库加载到内存时,再用加载进去的地址将@plt 替换掉。动态库的函数地址是延迟绑定。 

动态库里的函数地址都是偏移,当动态库加载到内存之后,有一个基址指向动态库头部,基址+偏移就能找到相应的 函数。

 关于动态库的命名规则如下:

  1. 在 Linux 中动态库以 lib 作为前缀,以.so 作为后缀,中间是库的名字自己指定即可,即: libxxx.so
  2. 在 Windows 中动态库一般以 lib 作为前缀,以 dll 作为后缀,中间是库的名字需要自己指定,即: libxxx.dll

 生成动态链接库

  1. 生成动态链接库是直接使用 gcc 命令并且需要添加 -fPIC(-fpic) 以及 -shared 参数
  2. -fPIC 或 -fpic 参数的作用是使得 gcc 生成的代码是与位置无关的,也就是使用相对位置,函数挂上@plt 标识,等待动态绑定。
  3. -shared参数的作用是告诉编译器生成一个动态链接库。

 生成动态链接库的具体步骤如下:

  • (1)将源文件进行汇编操作,需要使用参数 -c, 还需要添加额外参数 -fpic /-fPIC,得到二进制目标文件(*.o)
# 得到若干个 .o文件
$ gcc 源文件(*.c) -c -fpic
  • (2)将得到的.o 文件打包成动态库,还是使用 gcc, 使用参数 -shared 指定生成动态库 (位置没有要求)
$ gcc -shared 与位置无关的目标文件(*.o) -o 动态库(libxxx.so)
  • (3)发布动态库和头文件
    # 发布
     	1. 提供头文件: xxx.h
     	2. 提供动态库: libxxx.so

例子:  

# 1. 将.c汇编得到.o, 需要额外的参数 -fpic/-fPIC,head.h在include目录
$ gcc add.c div.c mult.c sub.c -c -fpic -I ./include/

# 2. 将得到 .o 打包成动态库, 使用gcc , 参数 -shared
$ gcc -shared add.o div.o mult.o sub.o -o libcalc.so 

├── include
│   └── `head.h   ===> 和动态库一起发布
├── `libcalc.so   ===> 生成的动态库
├── main.c
├── mult.c
├── mult.o
├── sub.c
└── sub.o

 动态库的使用:

当我们得到了一个可用的动态库之后,需要将其放到一个目录中,然后根据得到的头文件编写测试代码,对动态库中的函数进行调用。

# 1. 拿到发布的动态库
	`head.h   libcalc.so
# 2. 基于头文件编写测试程序, 测试动态库中提供的接口是否可用
	`main.c`
# 示例目录:
.
├── head.h          ==> 函数声明
├── libcalc.so      ==> 函数定义
└── main.c          ==> 函数测试

# 在编译的时候指定动态库相关的信息: 库的路径 -L, 库的名字 -l
$ gcc main.c -o app -L./ -lcalc

# 查看是否生成了可执行程序
$ tree
.
├── app 			# 生成的可执行程序
├── head.h
├── libcalc.so
└── main.c

# 执行生成的可执行程序, 错误提示 ==> 可执行程序执行的时候找不到动态库
# gcc 通过指定的动态库信息生成了可执行程序,但是可执行程序运行却提示无法加载到动态库。
$ ./app 
./app: error while loading shared libraries: libcalc.so: cannot open shared object file: No such file or directory

 解决动态库无法加载问题:

 库的工作原理:

  1. 静态库如何被加载
    1. 在程序编译的最后一个阶段也就是链接阶段,提供的静态库会被打包到可执行程序中
    2. 当可执行程序被执行,静态库中的代码也会一并被加载到内存中,因此不会出现静态库找不到无法被加载的问题。
  2. 动态库如何被加载
    1. 在程序编译的最后一个阶段也就是链接阶段
      1. 在 gcc 命令中虽然指定了库路径 (使用参数 -L ), 但是这个路径并没有记录到可执行程序中只是检查了这个路径下的库文件是否存在
      2. 同样对应的动态库文件也没有被打包到可执行程序中,只是在可执行程序中记录了库的名字。
    2. 可执行程序被执行起来之后:
      1. 程序执行的时候会先检测需要的动态库是否可以被加载,加载不到就会提示上边的错误信息
      2. 动态库中的函数在程序中被调用, 这个时候动态库才加载到内存,如果不被调用就不加载到内存
      3. 动态库的检测和内存加载操作都是由动态链接来完成的
        1. 动态链接器:动态链接器是一个独立于应用程序的进程属于操作系统工作于程序运行阶段,工作时需要提供动态库所在目录位置。当用户的程序需要加载动态库的时候动态连接器就开始工作了,很显然动态连接器根本就不知道用户通过 gcc 编译程序的时候通过参数 -L 指定的路径。
        2. 那么动态链接器是如何搜索某一个动态库的呢,在它内部有一个默认的搜索顺序,按照优先级从高到低的顺序分别是:
          1. 可执行文件内部的 DT_RPATH
          2. 系统的环境变量 LD_LIBRARY_PATH

          3. 系统动态库的缓存文件 /etc/ld.so.cache;

          4. 存储动态库 / 静态库的系统目录 /lib/, /usr/lib 等 

        3. 按照以上四个顺序,依次搜索,找到之后结束遍历,最终还是没找到,动态连接器就会提示动态库找不到的错误信息。

 解决方案

可执行程序生成之后,根据动态链接器的搜索路径,我们可以提供三种解决方案,我们只需要将动态库的路径放到对应的环境变量或者系统配置文件中,同样也可以将动态库拷贝到系统库目录(或者是将动态库的软链接文件放到这些系统库目录中)。

  1.  方案 1: 将库路径添加到环境变量 LD_LIBRARY_PATH
    1. 找到相关的配置文件
      用户级别: ~/.bashrc —> 设置对当前用户有效
      系统级别: /etc/profile —> 设置对所有用户有效
    2. 使用 vim 打开配置文件,在文件最后添加这样一句话
      # 自己把路径写进去就行了
      export LIBRARY_PATH=$LIBRARY_PATH:动态库的绝对路径
    • 让修改的配置文件生效:

      修改了用户级别的配置文件,关闭当前终端,打开一个新的终端配置就生效了

      修改了系统级别的配置文件,注销或关闭系统,再开机配置就生效了

      不想执行上边的操作,可以执行一个命令让配置重新被加载:

      # 修改的是哪一个就执行对应的那个命令
      # source 可以简写为一个 . , 作用是让文件内容被重新加载
      $ source ~/.bashrc          (. ~/.bashrc)
      $ source /etc/profile       (. /etc/profile)

  2. 方案 2: 更新 /etc/ld.so.cache 文件
    • 找到动态库所在的绝对路径(不包括库的名字)比如:/home/robin/Library/
    • 使用 vim 修改 /etc/ld.so.conf 这个文件,将上边的路径添加到文件中 (独自占一行)
    • 更新 /etc/ld.so.conf 中的数据到 /etc/ld.so.cache 中($ sudo ldconfig )
  3. 方案 3: 拷贝动态库文件到系统库目录 /lib/ 或者 /usr/lib 中 (或者将库的软链接文件放进去)
    • # 库拷贝
      sudo cp /xxx/xxx/libxxx.so /usr/lib
      
      # 创建软连接
      sudo ln -s /xxx/xxx/libxxx.so /usr/lib/libxxx.so

验证 :

在启动可执行程序之前,或者在设置了动态库路径之后,我们可以通过一个命令检测程序能不能够通过动态链接器加载到对应的动态库,这个命令叫做 ldd

# 语法:
$ ldd 可执行程序名

# 举例:
$ ldd app
	linux-vdso.so.1 =>  (0x00007ffe8fbd6000)
    libcalc.so => /home/robin/Linux/3Day/calc/test/libcalc.so (0x00007f5d85dd4000)
    libc.so.6 => /lib/x86_64-linux-gnu/libc.so.6 (0x00007f5d85a0a000)
    /lib64/ld-linux-x86-64.so.2 (0x00007f5d85fd6000)  ==> 动态链接器, 操作系统提供

 动态库/静态库优缺点

静态库:

  1. 优点:
    1. 静态库被打包到应用程序中加载速度快
    2. 发布程序无需提供静态库,移植方便
  2. 缺点:
    1. 相同的库文件数据可能在内存中被加载多份,消耗系统资源,浪费内存(例如,静态库占用1M,2000个这样的程序运行,差不多占用2GB的空间)
    2. 库文件更新需要重新编译项目文件,生成新的可执行程序,浪费时间。

动态库:

  1. 优点:
    1. 可实现不同进程间的资源共享(动态库在内存中只存在一份拷贝,避免了静态库浪费空间的问题)
    2. 动态库升级简单,只需要替换库文件无需重新编译应用程序
    3. 程序猿可以控制何时加载动态库,不调用库函数的话,动态库不会被加载
  2. 缺点:
    1. 加载速度比静态库慢,以现在计算机的性能可以忽略
    2. 发布程序需要提供依赖的动态库

四、Makefile

 前言

使用 GCC 的命令行进行程序编译在单个文件是比较方便。但是当工程中的文件逐渐增多,变得十分庞大的时候,使用 GCC 命令编译就会变得力不从心。这种情况下我们需要借助项目构造工具 make 帮助我们完成这个艰巨的任务。

make 是一个命令工具,是一个解释 makefile 中指令的命令工具

make 工具在构造项目的时候需要加载一个叫做 makefile 的文件。

什么是makefile:

        makefile 关系到了整个工程的编译规则。一个工程中的源文件不计数,其按类型、功能、模块分别放在若干个目录中,makefile 定义了一系列的规则来指定哪些文件需要先编译,哪些文件需要后编译,哪些文件需要重新编译甚至于进行更复杂的功能操作,因为 makefile 就像一个 Shell 脚本一样,其中也可以执行操作系统的命令。

        makefile 带来的好处就是 ——“自动化编译”,一旦写好,只需要一个 make 命令,整个工程完全自动编译,极大的提高了软件开发的效率。

        makefile 文件有两种命名方式 makefile 和 Makefile。构建项目的时候在哪个目录下执行构建命令 make ,这个目录下的 makefile 文件就会别加载,因此在一个项目中可以有多个 makefile 文件,分别位于不同的项目目录中。

 规则

        Makefile 的框架是由规则构成的。make 命令执行时先在 Makefile 文件中查找各种规则,对各种规则进行解析后运行规则。规则的基本格式为:

# 每条规则的语法格式:
target1,target2...: depend1, depend2, ...
	command
	......
	......

每条规则由三个部分组成分别是,目标(target), 依赖(depend) 和命令(command)。

  1.  命令(command): 当前这条规则的动作,一般情况下这个动作就是一个 shell 命令。
    1. 如:通过某个命令编译文件、生成库文件、进入目录等。
    2. 动作可以是多个,每个命令前必须有一个Tab缩进并且独占一行
  2. 依赖(depend): 规则所必需的依赖条件,在规则的命令中可以使用这些依赖。
    1. 例如:生成可执行文件的目标文件(*.o)可以作为依赖使用
    2. 如果命令中不需要任何依赖,那么规则的依赖可以为空
    3. 当前规则中的依赖可以是其他规则中的某个目标,这样就形成了规则之间的嵌套
    4. 依赖可以根据要执行的命令的实际需求,指定很多个
  3. 目标(target): 规则中的目标,这个目标和规则中的命令是对应的
    1. 通过执行规则中的命令,可以生成一个和目标同名的文件
    2. 规则中可以有多个命令,因此可以通过这多条命令来生成多个目标
    3. 通过执行规则中的命令,可以只执行一个动作不生成任何文件,这样的目标被称为伪目标

# 举例: 有源文件 a.c b.c c.c head.h, 需要生成可执行程序 app
################# 例1 #################
app:a.c b.c c.c
	gcc a.c b.c c.c -o app

################# 例2 #################
# 有多个目标, 多个依赖, 多个命令
app,app1:a.c b.c c.c d.c
	gcc a.c b.c -o app
	gcc c.c d.c -o app1
	
################# 例3 #################	
# 规则之间的嵌套
app:a.o b.o c.o
	gcc a.o b.o c.o -o app
# a.o 是第一条规则中的依赖
a.o:a.c
	gcc -c a.c
# b.o 是第一条规则中的依赖
b.o:b.c
	gcc -c b.c
# c.o 是第一条规则中的依赖
c.o:c.c
	gcc -c c.c

工作原理

        下面剖析一下通过提供的 makefile 文件,构建工具 make 什么时候编译项目中的所有文件,什么时候只选择更新项目中的某几个文件。另外再研究一下如果 makefile 里边有多个规则它们之间是如何配合工作的。

规则的执行

        在调用 make 命令编译程序的时候,make 会首先找到 Makefile 文件中的第 1 个规则分析并执行相关的动作。但是,好多时候要执行的动作(命令)中使用的依赖是不存在的,如果使用的依赖不存在,这个动作也就不会被执行。        

        对应的解决方案是先将需要的依赖生成出来,我们就可以在 makefile 中添加新的规则,将不存在的依赖作为这个新的规则中的目标,当这条新的规则对应的命令执行完毕,对应的目标就被生成了,同时另一条规则中需要的依赖也就存在了。

        这样,makefile 中的某一条规则在需要的时候,就会被其他的规则调用(规则调用规则),直到 makefile 中的第一条规则中的所有的依赖全部被生成,第一条规则中的命令就可以基于这些依赖生成对应的目标,make 的任务也就完成了。

# makefile
# 规则之间的嵌套
# 规则1
app:a.o b.o c.o
	gcc a.o b.o c.o -o app
# 规则2
a.o:a.c
	gcc -c a.c
# 规则3
b.o:b.c
	gcc -c b.c
# 规则4
c.o:c.c
	gcc -c c.c

        当依赖不存在的时候,make 就是查找其他的规则,看哪一条规则是用来生成需要的这个依赖的,找到之后就会执行这条规则中的命令。因此规则 2, 规则 3, 规则 4 里的命令会相继被执行,当规则 1 中依赖全部被生成之后对应的命令也就被执行了,因此规则 1 的目标被生成,make 工作结束。

        如果想要执行 makefile 中非第一条规则对应的命令,那么就不能直接 make, 需要将那条规则的目标也写到 make 的后边,比如只需要执行规则 3 中的命令,就需要: make b.o。

 文件的时间戳

        make 命令执行的时候会根据文件的时间戳判定是否执行 makefile 文件中相关规则中的命令。

  1. 目标是通过依赖生成的,因此正常情况下:目标时间戳 > 所有依赖的时间戳 , 如果执行 make 命令的时候检测到规则中的目标和依赖满足这个条件,那么规则中的命令就不会被执行
  2. 依赖文件被更新了,文件时间戳也会随之被更新,这时候 目标时间戳 < 某些依赖的时间戳 , 在这种情况下目标文件会通过规则中的命令被重新生成。
  3. 如果规则中的目标对应的文件根本就不存在那么规则中的命令肯定会被执行
# makefile
# 规则之间的嵌套
# 规则1
app:a.o b.o c.o
	gcc a.o b.o c.o -o app
# 规则2
a.o:a.c
	gcc -c a.c
# 规则3
b.o:b.c
	gcc -c b.c
# 规则4
c.o:c.c
	gcc -c c.c

根据上文的描述,先执行 make 命令,基于这个 makefile 编译这几个源文件生成对应的目标文件。然后再修改例子中的 a.c, 再次通过 make 编译这几个源文件,那么这个时候先执行规则 2 更新目标文件 a.o, 然后再执行规则 1 更新目标文件 app其余的规则是不会被执行的。

 make的自动推导功能

         make 有自动推导的能力,不会完全依赖 makefile。

        比如:使用命令 make 编译扩展名为.c 的 C 语言文件的时候,源文件的编译规则不用明确给出。这是因为 make 进行编译的时候会使用一个默认的编译规则,按照默认规则完成对.c 文件的编译,生成对应的.o 文件。它使用命令 cc -c 来编译.c 源文件。在 Makefile 中只要给出需要构建的目标文件名(一个.o 文件),make 会自动为这个.o 文件寻找合适的依赖文件(对应的.c 文件),并且使用默认的命令来构建这个目标文件


假设本地项目目录中有以下几个源文件:

$ tree
.
├── add.c
├── div.c
├── head.h
├── main.c
├── makefile
├── mult.c
└── sub.c

目录中 makefile 文件内容如下:

# 这是一个完整的 makefile 文件
calc:add.o  div.o  main.o  mult.o  sub.o
        gcc  add.o  div.o  main.o  mult.o  sub.o -o calc

通过 make 构建项目:

$ make
cc    -c -o add.o add.c
cc    -c -o div.o div.c
cc    -c -o main.o main.c
cc    -c -o mult.o mult.c
cc    -c -o sub.o sub.c
gcc  add.o  div.o  main.o  mult.o  sub.o -o calc

我们可以发现上边的 makefile 文件中只有一条规则,依赖中所有的 .o 文件在本地项目目录中是不存在的,并且也没有其他的规则用来生成这些依赖文件,这时候 make 会使用内部默认的构造规则先将这些依赖文件生成出来,然后在执行规则中的命令,最后生成目标文件 calc。

makefile中的变量

 makefile 中的变量分为三种:自定义变量,预定义变量和自动变量。

  1. 自定义变量
    1. 用 Makefile 进行规则定义的时候,用户可以定义自己的变量,称为用户自定义变量。makefile 中的变量是没有类型的,直接创建变量然后给其赋值就可以了
    2. # 如果将变量的值取出?
      $(变量的名字)
      
      # 举例 add.o  div.o  main.o  mult.o  sub.o
      # 定义变量并赋值
      obj=add.o  div.o  main.o  mult.o  sub.o
      # 取变量的值
      $(obj)
      
      
      # 这是一个规则,普通写法
      calc:add.o  div.o  main.o  mult.o  sub.o
              gcc  add.o  div.o  main.o  mult.o  sub.o -o calc
              
      # 这是一个规则,里边使用了自定义变量
      obj=add.o  div.o  main.o  mult.o  sub.o
      target=calc
      $(target):$(obj)
              gcc  $(obj) -o $(target)
      
  2. 预定义变量
    1. 在 Makefile 中有一些已经定义的变量,用户可以直接使用这些变量,不用进行定义,这些预定义变量的名字一般都是大写的。
      # 这是一个规则,普通写法
      calc:add.o  div.o  main.o  mult.o  sub.o
              gcc  add.o  div.o  main.o  mult.o  sub.o -o calc
              
      # 这是一个规则,里边使用了自定义变量和预定义变量
      obj=add.o  div.o  main.o  mult.o  sub.o
      target=calc
      CFLAGS=-O3 # 代码优化。编译器的优化选项的 4 个级别,-O0 表示没有优化, -O1 为缺省值,-O3 优化级别最高。
      $(target):$(obj)
              $(CC)  $(obj) -o $(target) $(CFLAGS)
      
        
  3. 自动变量 
    1. Makefile 中的规则语句中经常会出现目标文件和依赖文件自动变量用来代表这些规则中的目标文件和依赖文件,并且它们只能在规则的命令中使用
    2.  
    # 这是一个规则,普通写法
    calc:add.o  div.o  main.o  mult.o  sub.o
            gcc  add.o  div.o  main.o  mult.o  sub.o -o calc
            
    # 这是一个规则,里边使用了自定义变量
    # 使用自动变量, 替换相关的内容
    calc:add.o  div.o  main.o  mult.o  sub.o
    	gcc $^ -o $@ 			# 自动变量只能在规则的命令中使用
    

模式匹配

 先看这样一个makefile:

calc:add.o  div.o  main.o  mult.o  sub.o
        gcc  add.o  div.o  main.o  mult.o  sub.o -o calc

# 语法格式重复的规则, 将 .c 编译成 .o, 使用的命令都是一样的 gcc *.c -c
add.o:add.c
        gcc add.c -c

div.o:div.c
        gcc div.c -c

main.o:main.c
        gcc main.c -c

sub.o:sub.c
        gcc sub.c -c

mult.o:mult.c
        gcc mult.c -c

         从第二个规则开始到第六个规则做的是相同的事情,但是由于文件名不同不得不在文件中写出多个规则,这就让 makefile 文件看起来非常的冗余,我们可以将这一系列的相同操作整理成一个模板所有类似的操作都通过模板去匹配 。makefile 会因此而精简不少,只是可读性会有所下降。

# 模式匹配 -> 通过一个公式, 代表若干个满足条件的规则
# 依赖有一个, 后缀为.c, 生成的目标是一个 .o 的文件, % 是一个通配符, 匹配的是文件名
#  $< 表示依赖中第一个依赖文件的名称

calc:add.o div.o main.o mult.o sub.o
    gcc add.o div.o main.o mult.o sub.o -o calc

%.o:%.c    # %会依次匹配add、div、main...,
	gcc $< -c

模式匹配相当于是一个模版。第一个规则中依赖的生成,都需要基于这规则来完成,得到所有的依赖,模式规则被执行了五次,模式规则中的 % 对应的文件名是实时变化,因此命令的依赖的名字,必须要使用自动变量。

Makefile中的函数

         makefile 中有很多函数并且所有的函数都是有返回值的

        其写法是这样的: $(函数名 参数1, 参数2, 参数3, ...)。这里为大家介绍两个 makefile 中使用频率比较高的函数:wildcard patsubst

wildcard

        这个函数的主要作用是获取指定目录下指定类型的文件名,其返回值是以空格分割的、指定目录下的所有符合条件的文件名列表。函数原型如下:

# 该函数的参数只有一个, 但是这个参数可以分成若干个部分, 通过空格间隔
$(wildcard PATTERN...)
	参数:	指定某个目录, 搜索这个路径下指定类型的文件,比如: *.c

 参数功能:

  1. PATTERN 指的是某个或多个目录下的对应的某种类型的文件,比如当前目录下的.c 文件可以写成 *.c
  2. 可以指定多个目录,每个路径之间使用空格间隔

返回值:

  1. 得到的若干个文件的文件列表, 文件名之间使用空格间隔
  2. 示例:$(wildcard *.c ./sub/*.c)
    1. 返回值格式: a.c b.c c.c d.c e.c f.c ./sub/aa.c ./sub/bb.c

 函数举例:

# 使用举例: 分别搜索三个不同目录下的 .c 格式的源文件
src = $(wildcard /home/robin/a/*.c /home/robin/b/*.c *.c)  # *.c == ./*.c
# 返回值: 得到一个大的字符串, 里边有若干个满足条件的文件名, 文件名之间使用空格间隔
/home/robin/a/a.c /home/robin/a/b.c /home/robin/b/c.c /home/robin/b/d.c e.c f.c

 patsubst

这个函数的功能是按照指定的模式替换指定的文件名的后缀函数原型如下:

# 有三个参数, 参数之间使用 逗号间隔
$(patsubst <pattern>,<replacement>,<text>)

参数功能:

  1. pattern: 这是一个模式字符串,需要指定出要被替换的文件名中的后缀是什么
    1. ​​​​​​​​​​​​​​文件名和路径不需要关心,因此使用 % 表示即可 [通配符是 %]
    2. 在通配符后边指定出要被替换的后缀,比如: %.c, 意味着 .c 的后缀要被替换掉
  2. replacement: 这是一个模式字符串,指定参数 pattern 中的后缀最终要被替换为什么
    1. ​​​​​​​​​​​​​​还是使用 % 来表示参数 pattern 中文件的路径和名字
    2. 在通配符 % 后边指定出新的后缀名,比如: %.o 这表示原来的后缀被替换为 .o
  3. text: 该参数中存储这要被替换的原始数据
  4. 返回值:函数返回被替换过后的字符串。
src = a.cpp b.cpp c.cpp e.cpp
# 把变量 src 中的所有文件名的后缀从 .cpp 替换为 .o
obj = $(patsubst %.cpp, %.o, $(src)) 
# obj 的值为: a.o b.o c.o e.o

 makefile的编写

# 项目目录结构
.
├── add.c
├── div.c
├── head.h
├── main.c
├── mult.c
└── sub.c
# 需要编写makefile对该项目进行自动化编译

版本1

calc:add.c  div.c  main.c  mult.c  sub.c
        gcc add.c  div.c  main.c  mult.c  sub.c -o calc

这个版本的优点:书写简单

这版本的缺点:只要依赖中的某一个源文件被修改,所有的源文件都需要被重新编译,太耗时、效率低

改进方式:提高效率,修改哪一个源文件,哪个源文件被重新编译不修改就不重新编译

版本2

# 默认所有的依赖都不存在, 需要使用其他规则生成这些依赖
# 因为 add.o 被更新, 需要使用最新的依赖, 生成最新的目标
calc:add.o  div.o  main.o  mult.o  sub.o
        gcc  add.o  div.o  main.o  mult.o  sub.o -o calc

# 如果修改了add.c, add.o 被重新生成
add.o:add.c
        gcc add.c -c

div.o:div.c
        gcc div.c -c

main.o:main.c
        gcc main.c -c

sub.o:sub.c
        gcc sub.c -c

mult.o:mult.c
        gcc mult.c -c

这个版本的缺点:规则比较冗余,需要精简

改进方式:在 makefile 中使用变量 和 模式匹配

版本3

# 添加自定义变量 -> makefile中注释前 使用 # 
obj=add.o  div.o  main.o  mult.o  sub.o
target=calc

$(target):$(obj)
        gcc $(obj)  -o $(target)

%.o:%.c
        gcc $< -c  #  $< 表示依赖项中第一个依赖的文件名

这个版本的缺点:变量 obj 的值需要手动的写出来,如果需要编译的项目文件很多,都用手写出来不现实

改进方式:在 makefile 中使用函数

版本4

# 添加自定义变量 -> makefile中注释前 使用 # 
# 使用函数搜索当前目录下的源文件 .c
src=$(wildcard *.c)
# 将源文件的后缀替换为 .o
# % 匹配的内容是不能被替换的, 需要替换的是第一个参数中的后缀, 替换为第二个参数中指定的后缀
# obj=$(patsubst %.cpp, %.o, $(src)) 将src中的关键字 .cpp 替换为 .o
obj=$(patsubst %.c, %.o, $(src))
target=calc

$(target):$(obj)
        gcc $(obj)  -o $(target)

%.o:%.c
        gcc $< -c

  • 这个版本的优点:解决了自动加载项目文件的问题,解放了双手
  • 这个版本的缺点:没有文件删除的功能,不能删除项目编译过程中生成的目标文件(*.o)和可执行程序
  • 改进方式:在 makefile 文件中添加新的规则用于删除生成的目标文件(*.o)和可执行程序

版本5

# 添加自定义变量 -> makefile中注释前 使用 # 
# 使用函数搜索当前目录下的源文件 .c
src=$(wildcard *.c)
# 将源文件的后缀替换为 .o
obj=$(patsubst %.c, %.o, $(src))
target=calc
# obj 的值 xxx.o xxx.o xxx.o xx.o
$(target):$(obj)
        gcc $(obj)  -o $(target)

%.o:%.c
        gcc $< -c

# 添加规则, 删除生成文件 *.o 可执行程序
# 这个规则比较特殊, clean根本不会生成, 这是一个伪目标
clean:
        rm $(obj) $(target)
  1. 这个版本的优点:添加了新的规则(16 行)用于文件的删除,直接 make clean 就可以执行规则中的删除命令了。
  2. 这个版本的缺点:如果在这个项目目录中添加一个叫做 clean 的文件(和规则中的目标名称相同),再进行 make clean 发现这个规则就不能正常工作了。
  3. 改进方式:在 makefile 文件中声明 clean 是一个伪目标,让 make 放弃对它的时间戳检测。
  4. 声明 clean 是一个伪目标,这样 make 就不会对文件的时间戳进行检测,规则中的命令也就每次都会被执行了。
# 在项目目录中添加一个叫 clean的文件, 然后在 make clean 这个规则中的命令就不工作了
$ ls
add.c  calc   div.c  head.h  main.o    mult.c  sub.c
add.o  div.o  main.c  makefile  mult.o  sub.o  clean  ---> 新添加的

# 使用 makefile 中的规则删除生成的目标文件和可执行程序
$ make clean
make: 'clean' is up to date. 

# 查看目录, 发现相关文件并没有被删除, make clean 失败了
$ ls
add.c  calc   div.c  head.h  main.o    mult.c  sub.c
add.o  clean  div.o  main.c  makefile  mult.o  sub.o

在 makefile 中声明一个伪目标需要使用 .PHONY 关键字,声明方式为: .PHONY:伪文件名称

最终版

# 添加自定义变量 -> makefile中注释前 使用 # 
# 使用函数搜索当前目录下的源文件 .c
src=$(wildcard *.c)
# 将源文件的后缀替换为 .o
obj=$(patsubst %.c, %.o, $(src))
target=calc

$(target):$(obj)
        gcc $(obj)  -o $(target)

%.o:%.c
        gcc $< -c

# 添加规则, 删除生成文件 *.o 可执行程序
# 声明clean为伪文件
.PHONY:clean
clean:
        # shell命令前的 - 表示强制这个指令执行, 如果执行失败也不会终止
        -rm $(obj) $(target) 
        echo "hello, 我是测试字符串"

 练习题

# 目录结构
.
├── include
│   └── head.h    ==> 头文件, 声明了加减乘除四个函数
├── main.c        ==> 测试程序, 调用了head.h中的函数
└── src
    ├── add.c    ==> 加法运算
    ├── div.c    ==> 除法运算
    ├── mult.c  ==> 乘法运算
    └── sub.c   ==> 减法运算

# 最终的目标名 app
target = app

# 搜索当前项目目录下的源文件
src=$(wildcard *.c ./src/*.c)

# 将文件的后缀替换掉 .c -> .o
obj=$(patsubst %.c, %.o, $(src))

# 头文件目录
include=./include

# 第一条规则
# 依赖中都是 xx.o yy.o zz.o
# gcc命令执行的是链接操作
$(target):$(obj)
        gcc $^ -o $@

# 模式匹配规则
# 执行汇编操作, 前两步: 预处理, 编译是自动完成
%.o:%.c
        gcc $< -c -I $(include) -o $@

# 添加一个清除文件的规则
.PHONY:clean

clean:
        -rm $(obj) $(target) -f

五、GDB调试

gdb 是由 GNU 软件系统社区提供的调试器,同 gcc 配套组成了一套完整的开发环境,可移植性很好。GDB 是一套字符界面的程序集,可以使用命令 gdb 加载要调试的程序。 

调试选项

        程序如果是为了进行调试而编译时, 必须要打开调试选项 (-g)。另外还有一些可选项,比如:在尽量不影响程序行为的情况下关掉编译器的优化选项 (-O0),-Wall 选项打开所有 warning,也可以发现许多问题,避免一些不必要的 bug。

        -g 选项的作用是在可执行文件中加入源代码的信息,比如可执行文件中第几条机器指令对应源代码的第几行,但并不是把整个源文件嵌入到可执行文件中,所以在调试时必须保证 gdb 能找到源文件。

启动GDB        

gdb 是一个用于应用程序调试的进程,需要先将其打开,一定要注意 gdb进程启动之后, 需要的被调试的应用程序是没有执行的。打开 Linux 终端,切换到要调试的可执行程序所在路径,执行如下命令就可以启动 gdb 了。

# 在终端中执行如下命令
# gdb程序启动了, 但是可执行程序并没有执行
$ gdb 可执行程序的名字

# 使用举例:
$ gdb app
(gdb) 		# gdb等待输入调试的相关命令

 命令行传参(通过gdb)

        有些程序在启动的时候需要传递命令行参数,如果要调试这类程序,这些命令行参数必须要在应用程序启动之前通过调试程序的 gdb 进程传递进去(set args)。下面是一段带命令行参数的程序:

// args.c
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <string.h>

#define NUM 10

// argc, argv 是命令行参数
// 启动应用程序的时候
int main(int argc, char* argv[])
{
    printf("参数个数: %d\n", argc);
    for(int i=0; i<argc; ++i)
    {
        printf("%d\n", NUM);
        printf("参数 %d: %s\n", i, argv[i]);
    }
    return 0;
}

第一步:编译出带条信息的可执行程序。$ gcc args.c -o app -g

第二步:启动 gdb 进程,指定需要 gdb 调试的应用程序名称。$ gdb app

第三步:在启动应用程序 app 之前设置命令行参数。gdb 中设置参数的命令叫做 set args ...,查看设置的命令行参数命令是 show args。 语法格式如下:

# 设置的时机: 启动gdb之后, 在应用程序启动之前
(gdb) set args 参数1 参数2 .... ...
# 查看设置的命令行参数
(gdb) show args
# 非gdb调试命令行传参
# argc 参数总个数,argv[0] == ./app, argv[1] == "11"  argv[2] == "22"  ...  argv[5] == "55"
$ ./app 11 22 33 44 55		# 这是数据传递给main函数
 
# 使用 gdb 调试
$ gdb app
GNU gdb (Ubuntu 7.11.1-0ubuntu1~16.5) 7.11.1
Copyright (C) 2016 Free Software Foundation, Inc.

# 通过gdb给应用程序设置命令行参数
(gdb) set args 11 22 33 44 55
# 查看设置的命令行参数
(gdb) show args
Argument list to give program being debugged when it is started is "11 22 33 44 55".

gdb中启动程序

在 gdb 中启动要调试的应用程序有两种方式,一种是使用 run 命令,另一种是使用 start 命令启动。在整个 gdb 调试过程中,启动应用程序的命令只能使用一次。

  1.  run: 可以缩写为 r, 如果程序中设置了断点会停在第一个断点的位置,如果没有设置断点,程序就执行完了
  2. start: 启动程序,最终会阻塞在 main 函数的第一行,等待输入后续其它 gdb 指令

 如果想让程序 start 之后继续运行,或者在断点处继续运行,可以使用 continue 命令,可以简写为 c。

退出gdb

退出 gdb 调试,就是终止 gdb 进程,需要使用 quit 命令,可以缩写为 q。

查看代码(list)

gdb 提供了查看代码的命令,这样就可以轻松定位要调试的代码行的位置了。

查看代码的命令叫做 list 可以缩写为 l, 通过这个命令我们可以查看项目中任意一个文件中的内容,并且还可以通过文件行号,函数名等方式查看。

 当前文件

        一个项目中一般是有很多源文件的,默认情况下通过 list 查看到代码信息位于程序入口函数 main 对应的的那个文件中。因此如果不进行文件切换 main 函数所在的文件就是当前文件,如果进行了文件切换,切换到哪个文件哪个文件就是当前文件。查看文件内容的方式如下:

# 使用 list 和使用 l 都可以
# 从第一行开始显示
(gdb) list 

# 列值这行号对应的上下文代码, 默认情况下只显示10行内容
(gdb) list 行号

# 显示这个函数的上下文内容, 默认显示10行
(gdb) list 函数名

通过 list 去查看文件代码,默认只显示 10 行,如果还想继续查看后边的内容,可以继续执行 list 命令,也可以直接回车(再次执行上一次执行的那个 gdb 命令)。

 切换文件

在查看文件内容的时候,很多情况下需要进行文件切换,我们只需要在 list 命令后边将要查看的文件名指定出来就可以了,切换命令执行完毕之后,这个文件就变成了当前文件。

# 切换到指定的文件,并列出这行号对应的上下文代码, 默认情况下只显示10行内容
(gdb) l 文件名:行号

# 切换到指定的文件,并显示这个函数的上下文内容, 默认显示10行
(gdb) l 文件名:函数名

设置显示的行数

可以通过 set listsize 设置显示更多代码。

# 以下两个命令中的 listsize 都可以写成 list
(gdb) set listsize 行数

# 查看当前list一次显示的行数
(gdb) show listsize

gdb断点操作(break)

想要通过 gdb 调试某一行或者得到某个变量在运行状态下的实际值,就需要在在这一行设置断点,程序指定到断点的位置就会阻塞,我们就可以通过 gdb 的调试命令得到我们想要的信息了。

设置断点的命令叫做 break 可以缩写为 b。

 设置断点

  • 常规断点:断点的设置有两种方式,一种是常规断点,程序只要运行到这个位置就会被阻塞。
  • 条件断点:只有指定的条件被满足了程序才会在断点处阻塞。

调试程序的断点可以设置到某个具体的行,也可以设置到某个函数上,具体的设置方式如下 :

  • 设置普通断点到当前文件:
# 在当前文件的某一行上设置断点
# break == b
(gdb) b 行号
(gdb) b 函数名		# 停止在函数的第一行
  • 设置普通断点到某个非当前文件上
    # 在非当前文件的某一行上设置断点
    (gdb) b 文件名:行号
    (gdb) b 文件名:函数名		# 停止在函数的第一行

  • 设置条件断点 
  • # 必须要满足某个条件, 程序才会停在这个断点的位置上
    # 通常情况下, 在循环中条件断点用的比较多
    (gdb) b 行数 if 变量名==某个值

查看断点 (info break)

 断点设置完毕之后,可以通过 info break 命令查看设置的断点信息,其中 info 可以缩写为 i

# info == i
# 查看设置的断点信息
(gdb) i b   #info break

# 举例
(gdb) i b
Num     Type           Disp Enb Address            What
1       breakpoint     keep y   0x0000000000400cb5 in main() at test.cpp:12
2       breakpoint     keep y   0x0000000000400cbd in main() at test.cpp:13
3       breakpoint     keep y   0x0000000000400cec in main() at test.cpp:18
4       breakpoint     keep y   0x00000000004009a5 in insertionSort(int*, int) 
                                                   at insert.cpp:8
5       breakpoint     keep y   0x0000000000400cdd in main() at test.cpp:16
6       breakpoint     keep y   0x00000000004009e5 in insertionSort(int*, int) 
                                                   at insert.cpp:16

  • Num: 断点的编号,删除断点或者设置断点状态的时候都需要使用
  • Enb: 当前断点的状态,y 表示断点可用,n 表示断点不可用
  • What: 描述断点被设置在了哪个文件的哪一行或者哪个函数上

  删除断点(del)

 如果确定设置的某个断点不再被使用了,可用将其删除,删除命令是 delete 断点编号 , 这个 delete 可以简写为 del 也可以再简写为 d。

删除断点的方式有两种:

  • 删除(一个或者多个)指定断点
  • 删除一个连续的断点区间
# delete == del == d
# 需要 info b 查看断点的信息, 第一列就是编号
(gdb) d 断点的编号1 [断点编号2 ...]
# 举例: 
(gdb) d 1          # 删除第1个断点
(gdb) d 2 4 6      # 删除第2,4,6个断点

# 删除一个范围, 断点编号 num1 - numN 是一个连续区间
(gdb) d num1-numN
# 举例, 删除第1到第5个断点
(gdb) d 1-5

 设置断点状态(disable/enable)

如果某个断点只是临时不需要了,我们可以将其设置为不可用状态,设置命令为 disable 断点编号,当需要的时候再将其设置回可用状态,设置命令为 enable 断点编号。 

# 让断点失效之后, gdb调试过程中程序是不会停在这个位置的
# disable == dis
# 设置某一个或者某几个断点无效
(gdb) dis 断点1的编号 [断点2的编号 ...]

# 设置某个区间断点无效
(gdb) dis 断点1编号-断点n编号

调试命令

继续运行 gdb(continue)

如果调试的程序被断点阻塞了又想让程序继续执行,这时候就可以使用 continue 命令。程序会继续运行,直到遇到下一个有效的断点。continue 可以缩写为 c。

 手动打印信息(print、ptype)

当程序被某个断点阻塞之后,可以通过一些命令打印变量的名字或者变量的类型,并且还可以跟踪打印某个变量的值。

 打印变量值:

        gdb 调试的时候如果需要打印变量的值, 使用的命令是 print, 可缩写为 p。如果打印的变量是整数还可以指定输出的整数的格式,格式化输出的整数对应的字符表如下:

# print == p
(gdb) p 变量名

# 如果变量是一个整形, 默认对应的值是以10进制格式输出, 其他格式请参考上表
(gdb) p/fmt 变量名

打印变量类型: 

如果在调试过程中需要查看某个变量的类型,可以使用命令 ptype, 语法格式如下:

# 语法格式
(gdb) ptype 变量名
# 打印变量类型
(gdb) ptype i
type = int
(gdb) ptype array[i]
type = int
(gdb) ptype array
type = int [12]

自动打印信息(display与print)

和 print 命令一样,display 命令也用于调试阶段查看某个变量或表达式的值,它们的区别是,使用 display 命令查看变量或表达式的值,每当程序暂停执行(例如单步执行)时,GDB 调试器都会自动帮我们打印出来,而 print 命令则不会。因此,当我们想频繁查看某个变量或表达式的值从而观察它的变化情况时,使用 display 命令可以一劳永逸。display 命令没有缩写形式,常用的语法格式如下 2 种:

# 在变量的有效取值范围内, 自动打印变量的值(设置一次, 以后就会自动显示)
(gdb) display 变量名

# 以指定的整形格式打印变量的值, 关于 fmt 的取值, 请参考 print 命令
(gdb) display/fmt 变量名

对于使用 display 命令查看的目标变量或表达式,都会被记录在一张列表(称为自动显示列表)中。通过执行 info dispaly 命令。

# info == i
(gdb) info display
Auto-display expressions now in effect:
Num Enb Expression
1:   y  i
2:   y  array[i]
3:   y  /x array[i]

Num : 变量或表达式的编号,GDB 调试器为每个变量或表达式都分配有唯一的编号

Enb : 表示当前变量(表达式)是处于激活状态还是禁用状态,如果处于激活状态(用 y 表示),则每次程序停止执行,该变量的值都会被打印出来;反之,如果处于禁用状态(用 n 表示),则该变量(表达式)的值不会被打印。

Expression :被自动打印值的变量或表达式的名字。


取消自动显示

对于不需要再打印值的变量或表达式,可以将其删除或者禁用。

# 查看显示列表
(gdb) info display
Auto-display expressions now in effect:
Num Enb Expression
1:   y  i
2:   y  array[i]
3:   y  /x array[i]

# 删除变量显示, 需要使用 info display 得到的变量/表达式编号
(gdb) undisplay 1 2

# 查看显示列表, 只剩下一个了
(gdb) i display
Auto-display expressions now in effect:
Num Enb Expression
3:   y  /x array[i]


# 如果不想删除自动显示的变量,也可以禁用自动显示列表中处于激活状态下的变量或表达式
# 命令中的 num 是通过 info display 得到的编号, 编号可以是一个或者多个
(gdb) disable display num [num1 ...]
(gdb) enable  display num [num1 ...]
# num1 - numN 表示一个范围
(gdb) disable display num1-numN
(gdb) disable display num1-numN


 单步调试(step,finish,....)

当程序阻塞到某个断点上之后,可以通过以下命令对程序进行单步调试:

  1. step 命令可以缩写为 s, 命令被执行一次代码被向下执行一行,如果这一行是一个函数调用,那么程序会进入到函数体内部。
  2. 如果通过 s 单步调试进入到函数内部,想要跳出这个函数体, 可以执行 finish 命令。如果想要跳出函数体必须要保证函数体内不能有有效断点,否则无法跳出。
  3. next 命令和 step 命令功能是相似的,只是在使用 next 调试程序的时候不会进入到函数体内部,next 可以缩写为 n
  4. 通过 until 命令可以直接跳出某个循环体,这样就能提高调试效率了。如果想直接从循环体中跳出,必须要满足以下的条件,否则命令不会生效:
    1. 要跳出的循环体内部不能有有效的断点
    2. 必须要在循环体的开始 / 结束行执行该命令
(gdb) until
(gdb) next

 设置变量值(set var=值)

在调试程序的时候,我们需要在某个变量等于某个特殊值的时候查看程序的运行状态,但是通过程序运行让变量等于这个值又非常困难,这种情况下就可以在 gdb 中直接对这个变量进行值的设置,或者是在单步调试的时候通过设置循环因子的值直接跳出某个循环,值设置的命令格式为:

set var 变量名=值

# 可以在循环中使用, 直接设置循环因子的值
# 假设某个变量的值在程序中==90的概率是5%, 这时候可以直接通过命令将这个变量值设置为90
(gdb) set var 变量名=值

  • 2
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

心之所向便是光v

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值