代码调试跟踪与优化(一) |
您所在的位置:网站首页 › gdb调试go程序 › 代码调试跟踪与优化(一) |
文章目录
前言一、GDB 调试原理1.1 GDB 调试模型1.2 GDB 与被调试程序关系
二、GDB 常用调试命令2.1 断点设置命令2.2 变量堆栈查看命令2.3 执行控制命令
三、VS Code 可视化调试四、GDB 远程调试更多文章:
前言
我们在开发软件时,免不了引入一些Bug,如何快速定位并解决这些bug 呢?工程师调试跟踪解决这些bug 的过程就像是医生给病人体检治病,医生需要借助各种医疗设备检测病人的各项指标,才能诊断病情分析病因并给出治疗方案。工程师解决这些bug,也需要借助各种调试跟踪技术,通过查看当前的执行指令、内存数据、运行日志等信息,分析出产生bug 的可能原因,再给出解决方案。 这些调试跟踪技术,可以帮我们更清晰的了解代码的执行状态,快速找到执行结果与期望结果不相符的起点或原因,比如控制逻辑或业务逻辑出现漏洞、中间结果被意外更改或损坏、低效的性能瓶颈等。 一、GDB 调试原理我们初学程序设计时,C/C++/Java 这些静态类型语言都需要先经过编译、链接、执行过程,linux 系统常用的编译链接工具是GCC。程序设计免不了出现一些非预期的Bug,要解决这些bug 首先需要我们获得当前代码足够的执行信息,GNU 也提供了GDB 调试工具,供我们查看代码的执行信息,快速定位产生bug 的代码并解决它。 一般我们在程序开发过程中,代码编辑、编译链接、代码调试这几个工具配合使用,所以很多 IDE(Intergreated Development Environment) 常将这几部分封装在一起,比如windows 平台下的Visual Studio、嵌入式开发常用的Keil MDK 等。这些IDE 工具将常用操作以菜单按钮的形式提供,让我们专注于程序开发过程,对我们隐藏了编译链接和代码调试工具的原理,本文以GDB 为例,简单介绍下代码调试原理。 1.1 GDB 调试模型目前软件开发主要在x86 平台上进行,但我们开发的目标程序有可能在其它平台比如ARM 上运行,为了方便我们在PC 上调试ARM 主机中运行的程序,GDB 根据调试程序和被调试程序是否运行在同一台电脑中,提供了如下两种调试模型: 本地调试:调试程序和被调试程序运行在同一台电脑中。 远程调试:调试程序运行在一台电脑中,被调试程序运行在另一台电脑中。 直接跟用户交互的可视化调试程序常有两种形式:一种是在终端窗口内手动输入调试命令,以字符形式显示调试信息;另一种是在IDE 内点击菜单按钮来代替手动输入调试命令,以图形加字符的形式显示调试信息。前一种形式更方便编写脚本实现自动化调试;后一种形式不需要记忆那么多调试命令,呈现的调试信息更直观、视觉辨识度更高,能提高点调试效率。 远程调试相比本地调试,多了一个GdbServer程序,该程序和目标程序(被调试程序)都运行在目标机(比如一个ARM 主板)中。上图中的红线表示GDB与GdbServer之间通过串口线或者网络进行通讯,用于传输GDB 调试消息的通讯协议可以称为GDB Remote Serial Protocol(GDB RSP)。 GDP RSP 既然是一个通讯协议,自然有标准的报文格式和内容要求,基本的报文格式如下图所示: 不管是本地调试还是远程调试,GDB 调试程序都需要有两个条件: 目标程序代码包含必要的调试信息,比如文件名、函数名、变量名、行号等符号表信息,函数堆栈、寄存器等信息。这些调试信息可以在编译阶段设置编译选项添加,比如gcc 编译工具添加"-g" 选项就可以在可执行文件中添加调试信息。由于带调试信息的可执行文件较大,嵌入式开发中资源受限,软件项目常有Debug 和Release 两个版本,前者供调试跟踪,后者更精简;GDB 可以控制被调试程序的执行,可以访问被调试程序的任何指令和内存数据。比如GDB 可以启动或者接管被调试程序的运行,控制被调试程序在指定条件下停止运行,查看并修改被调试程序的变量值、参数值、执行结果、执行顺序等运行数据。我们先使用man gdb 命令查看GDB 的简单介绍与用法: GDB 主要有三种启动方式: gdb program:使用GDB 开始执行被调试程序program,可通过GDB 命令控制program 的行为;gdb program core:使用GDB 同时执行被调试程序program 和core 文件(程序异常中止或退出时,保存的内存映像加调试信息文件,包含程序当前的内存、寄存器、堆栈等信息),便于定位分析程序异常中止或退出的原因;gdb attach PID (gdb -p PID):使用GDB 接管(attach)一个正在运行的被调试程序,PID 为被调试程序的process-ID(可通过pidof program 查看),可通过GDB 命令控制program 的行为。从GDB 与被调试程序间的关系看,GDB 的三种启动方式可以分为两类:一类是由GDB 程序调用执行一个尚未运行的被调试程序program;另一类是由GDB 程序attach 接管一个正在运行的被调试程序program。 前面也谈到,GDB 进程可以控制被调试程序的执行,可以访问被调试程序的任何指令和内存数据。GDB 进程相当于是被调试程序的父进程,GDB 进程对被调试进程program 有绝对的控制权。GDB 是如何调用或者接管一个正在运行的被调试程序呢? Linux 内核提供了一个用于进程跟踪的系统调用函数ptrace,该函数提供了一个进程(the “tracer”)监察和控制另一个进程(the “tracee”)的方法,它不仅可以监控系统调用,而且还能够检查和改变“tracee” 进程的内存和寄存器里的数据,甚至还可以拦截系统调用。GDB 进程通过系统调用函数ptrace,就可以读写被调试进程program(GDB 进程作为tracer,被调试进程作为tracee)的指令空间、数据空间、堆栈和寄存器的值,接管被调试进程program 的所有信号。这样一来,被调试进程program 的执行就被GDB 进程完全控制了,从而达到调试的目的。 我们使用man ptrace 命令查看ptrace 的简单介绍如下: 所以,不论是调试一个新程序,还是调试一个已经处于执行中状态的服务程序,通过ptrace系统调用,最终的结果都是:gdb程序是父进程,被调试程序是子进程,子进程的所有信号都被父进程gdb来接管,并且父进程gdb可查看、修改子进程的内部信息,包括:指令空间、数据空间、堆栈、寄存器等。 二、GDB 常用调试命令如何使用GDB 调试程序呢?GDB 提供了一系列命令,我们可以在启动GDB 进程后通过help 命令查看,GDB 支持的调试命令类别如下: 前面已经谈到,要想使用gdb 调试程序,被调试程序需要包含调试信息,如果使用GCC 工具链编译链接程序,则需要添加-g 参数(也可以是-Og 参数)。使用前面介绍的GDB 启动命令,可以根据启动调试后的提示信息判断被调试程序是否包含调试信息: $ gdb test ...... # 没有调试信息 Reading symbols from test...(no debugging symbols found)...done. # 包含调试信息 Reading symbols from test...done. 2.1 断点设置命令启动GDB 调试程序后,一般先设置普通断点、观察断点、捕捉断点等,以便在后续调试过程中,让程序及时暂停在我们关注的地方,查看断点处的数据和状态信息。GDB 常用的断点设置命令如下(可通过help breakpoints 查看支持的断点命令列表,可进一步通过help break 查看某个具体命令的用法): 常用断点设置命令命令描述break location在源代码指定设置location 处设置断点,程序执行到location 处暂停执行。location 可以是行号linenum、函数名function,如果不止一个源文件,还可以在前面加上文件名,比如filename:linenum 或 filename:function。break location if cond在源代码指定位置location 处设置条件断点,程序执行到location 处判断条件condition 的真假,若条件condition 为true 则暂停执行,否则继续执行。condition 是一个布尔型表达式,location 含义跟前一条指令中的相同。watch expression在程序执行过程中,监控某个变量或表达式(也即expression)的值,当观察到该变量或表达式的值发生变化时,则程序暂停执行。catch event在程序执行过程中,监控某个事件的触发,比如程序抛出指定类型异常、某动态库被加载或卸载等,当捕捉到该事件发生时,则程序暂停执行。info breakpointsinfo break可以查看当前调试环境中存在的所有断点,包括普通断点、观察断点以及捕捉断点。每个断点都有一个唯一的编号,可根据该编号使能或清除相应的断点。 disable [num]禁用当前调试环境中的某个断点,如果没有指定编号num 默认禁用所有断点。enable [num]激活当前调试环境中的某个断点,如果没有指定编号num 默认激活所有断点。clear locationdelete [num]可以删除指定位置location 处或指定编号num 的所有断点,如果没有指定位置location 默认清除编号最小的断点,如果没有指定编号num 默认删除所有断点。下面给出这几个命令的简单示例: ...... Reading symbols from test...done. (gdb) list #--> 显示带行号的源代码 1 #include 2 3 int add2(int a, int b) 4 { 5 return (a + b); 6 } 7 8 int main (int argc, char *argv[]) 9 { 10 int n = 1; (gdb) #--> 默认显示10行代码,可按Enter 健查看后续代码 11 int sum = 0; 12 13 while (n 在第13行设置普通断点 Breakpoint 1 at 0x40157f: file test.c, line 11. (gdb) break add2 #--> 在函数add2处设置普通断点 Breakpoint 2 at 0x40155a: file test.c, line 5. (gdb) tbreak 16 #--> 在第16行设置一个一次性普通断点,常用于循环中 Temporary breakpoint 3 at 0x401598: file test.c, line 16. (gdb) watch n #--> 设置变量n 为观察断点 No symbol "n" in current context. #--> 当前调试程序未运行,故当前调试环境没有符号n,需要先运行到符号n 定义后再设置其为观察断点 (gdb) run #--> 运行被调试程序到第一个断点处暂停 Starting program: D:\VSCoder\GDB\test.exe [New Thread 10816.0x42a8] [New Thread 10816.0x2b68] Thread 1 hit Breakpoint 1, main (argc=1, argv=0x1914a0) at test.c:11 11 int sum = 0; (gdb) watch n #--> 设置变量n 为观察断点 Hardware watchpoint 4: n (gdb) info break #--> 查看当前调试环境中的所有断点,Num 为唯一编号、Type 为断点类型、Disp 表示断点触发后保留还是删除,Enb 表示断点处于激活还是禁用状态,Address 显示断点在内存中的地址信息,What 显示断点所在文件与行号信息 Num Type Disp Enb Address What 1 breakpoint keep y 0x000000000040157f in main at test.c:11 breakpoint already hit 1 time 2 breakpoint keep y 0x000000000040155a in add2 at test.c:5 3 breakpoint del y 0x0000000000401598 in main at test.c:16 4 hw watchpoint keep y n (gdb) disable 1 #--> 禁用编号为1 的断点 (gdb) info break Num Type Disp Enb Address What 1 breakpoint keep n 0x000000000040157f in main at test.c:11 breakpoint already hit 1 time ...... (gdb) enable 1 #--> 激活编号为1 的断点 (gdb) info break Num Type Disp Enb Address What 1 breakpoint keep y 0x000000000040157f in main at test.c:11 breakpoint already hit 1 time ...... (gdb) clear #--> 清除编号最小的断点 Deleted breakpoint 1 (gdb) info break Num Type Disp Enb Address What 2 breakpoint keep y 0x000000000040155a in add2 at test.c:5 3 breakpoint del y 0x0000000000401598 in main at test.c:16 4 hw watchpoint keep y n (gdb) delete #--> 删除所有的断点 Delete all breakpoints? (y or n) y (gdb) info break No breakpoints or watchpoints. (gdb) 2.2 变量堆栈查看命令在被调试程序代码中设置断点,实际上就是让被调试程序暂停在我们关注的断点位置,一般断点处也是我们怀疑程序存在bug或性能瓶颈的地方。 当被调试程序停在断点处后,我们需要查看当前调试环境的状态与数据信息,比如变量当前值、调用堆栈信息、当前寄存器值等。GDB 常用的变量或堆栈信息查询命令如下: 常用变量堆栈查询命令命令描述print expression在 GDB 调试程序的过程中,输出或者修改指定变量或者表达式的值。display exprdisplay/fmt expr在调试阶段查看某个变量或表达式的值,与print 命令的区别是,每次被调试程序暂停执行时,display 都会自动显示变量或表达式的值,print 则不会。info display可以查看当前调试环境中存在的所有display 变量或表达式,每个display 变量或表达式都有唯一的编号。undisplay num删除指定编号num 的display 变量或表达式,如果没有指定编号num 默认删除所有display 变量或表达式。 frame spec可以查看当前调试环境中特定的栈帧信息,spec 可以是栈帧编号、栈帧地址、函数名等,如果不指定参数spec 则显示当前栈帧信息。up ndown n在当前栈帧的基础上(假设当前栈帧编号为m),查看编号为m + n 或m - n 的栈帧信息。如果不指定参数n 则默认n = 1。backtrace查看当前调试环境中所有的栈帧信息,也即从当前栈帧到main 函数的整个函数调用链信息。info argsinfo locals查看当前函数参数的值查看当前函数内局部变量的信息info frame可以查看当前栈帧中存储的详细信息,包括当前栈帧的编号及地址、当前函数及其调用者的地址、当前函数编程语言类型、当前函数参数和局部变量的地址及其值、当前栈帧寄存器值等。下面给出这几个命令的简单示例: ...... (gdb) break 13 Breakpoint 1 at 0x401586: file test.c, line 13. (gdb) break add2 Breakpoint 2 at 0x40155a: file test.c, line 5. (gdb) print n # --> 打印变量n 的当前值 No symbol "n" in current context. # --> 当前调试程序未运行,故当前调试环境没有符号n,需要先运行到符号n 定义后再查看其当前值 (gdb) run #--> 运行被调试程序到第一个断点处暂停 Starting program: D:\VSCoder\GDB\test.exe [New Thread 10392.0x3b60] [New Thread 10392.0x1c54] Thread 1 hit Breakpoint 1, main (argc=1, argv=0x7814a0) at test.c:13 warning: Source file is more recent than executable. 13 while (n 打印变量n 的当前值 $1 = 1 (gdb) display sum # --> 保持显示变量sum 的当前值 1: sum = 0 (gdb) info display # --> 查看当前调试环境中的所有display 变量或表达式信息 Auto-display expressions now in effect: Num Enb Expression 1: y sum (gdb) undisplay # --> 删除当前调试环境中所有的display 变量或表达式 Delete all auto-display expressions? (y or n) y (gdb) print n=5 # --> 修改变量n 的当前值为5 $2 = 5 (gdb) info locals # --> 打印当前函数内的所有局部变量的当前值 n = 5 sum = 0 (gdb) frame # --> 打印当前栈帧信息 #0 main (argc=1, argv=0x9814a0) at test.c:13 13 while (n 打印当前栈帧编号减一的栈帧信息 Bottom (innermost) frame selected; you cannot go down. (gdb) continue # --> 继续执行被调试程序到下一个断点处暂停 Continuing. Thread 1 hit Breakpoint 2, add2 (a=0, b=5) at test.c:5 5 return (a + b); (gdb) backtrace # --> 打印当前调试环境中的所有栈帧信息,也即当前函数调用链信息 #0 add2 (a=0, b=5) at test.c:5 #1 0x0000000000401595 in main (argc=1, argv=0x9814a0) at test.c:15 (gdb) info args # --> 打印当前函数的所有参数值 a = 0 b = 5 (gdb) up # --> 打印当前栈帧编号加一的栈帧信息 #1 0x0000000000401595 in main (argc=1, argv=0x9814a0) at test.c:15 15 sum = add2(sum, n); (gdb) down # --> 打印当前栈帧编号减一的栈帧信息 #0 add2 (a=0, b=5) at test.c:5 5 return (a + b); (gdb) info frame # --> 打印当前栈帧的详细信息 Stack level 0, frame at 0x61fdf0: #--> 当前栈帧编号为0,地址为0x61fdf0 rip = 0x40155a in add2 (test.c:5); saved rip = 0x401595 #--> 当前函数的存储地址为0x40155a,它的调用者的存储地址为0x401595 called by frame at 0x61fe30 #-->当前函数调用者的栈帧地址为0x61fe30 source language c. #--> 当前函数使用C语言编写的 Arglist at 0x61fde0, args: a=0, b=5 #--> 当前函数的参数地址和参数值 Locals at 0x61fde0, Previous frame's sp is 0x61fdf0 #--> 当前函数内的局部变量存储地址 Saved registers: rbp at 0x61fde0, rip at 0x61fde8, #--> 当前栈帧内部存储的寄存器信息 2.3 执行控制命令为被调试程序设置了需要特别关注的断点,也有了可以查看甚至修改当前调试环境变量值、堆栈信息、寄存器值等数据的命令,还需要能自由控制被调试程序执行顺序的命令,方便进行单步调试或断点调试。GDB 常用的执行控制命令如下: 常用执行控制命令命令描述startrun都可以用来在 GDB 调试器中启动被调试程序,start 命令执行到main 函数起始位置暂停,run 命令执行到第一个断点处暂停。next countstep count都可以控制GDB 单步执行程序,count 为执行步数缺省值为1 行。执行到函数调用代码时,next 命令不考虑函数内部代码行数,将函数调用记作一行,step 命令计算调用函数内部代码的行数。continue count控制被调试程序执行到往下数第count 个断点处暂停,也即忽略count - 1 个断点,参数count 缺省值为1,也即执行下一个断点处暂停。until location 控制被调试程序执行到特定位置location 处暂停,如果省略参数location,则执行到循环体外暂停,若无循环体则同next 命令。下面给出这几个命令的简单示例: ...... (gdb) break 13 Breakpoint 1 at 0x115b: file test.c, line 13. (gdb) start # --> 控制GDB 开始执行被调试程序,并在main 函数起始位置暂停 Temporary breakpoint 2 at 0x1141: file test.c, line 9. Starting program: /home/paul/Desktop/GDB/test Temporary breakpoint 2, main () at test.c:9 9 { (gdb) continue # --> 继续执行被调试程序到下一个断点处暂停 Continuing. Breakpoint 1, main () at test.c:13 13 while (n 打印当前函数内的所有局部变量的当前值 n = 1 sum = 0 (gdb) step 7 # --> 控制被调试程序继续执行7 行,计算被调函数add2 内的行数 15 sum = add2(sum, n); (gdb) info locals n = 2 sum = 1 (gdb) next 7 # --> 控制被调试程序继续执行7 行,被调函数add2 按照一行计算 16 n++; (gdb) info locals n = 4 sum = 10 (gdb) next 13 while (n 控制被调试程序执行到当前循环体外暂停 19 return 0; (gdb) info locals n = 101 sum = 5050 (gdb) quit # --> 退出GDB 调试进程 A debugging session is active. Inferior 1 [process 5867] will be killed. Quit anyway? (y or n) y上述GDB 调试命令都可以使用缩写形式,比如使用b 代替break、使用c 代替continue、使用bt 代替backtrace 等,不用每次输入命令全称。 三、VS Code 可视化调试使用GDB 命令来调试程序并没有那么直观,我们对可视化的图像信息更敏感些,因此很多IDE 都提供了可视化调试界面。这里使用比较轻巧的代码编辑器VS Code 搭配GCC 和GDB 工具链,实现可视化调试功能。 一般一个软件工程包含不止一个源文件,多个源文件的编译链接通常靠Makefile 控制。使用VS Code 编辑好工程代码和Makefile 文件后,可以在VS Code 启动配置文件launch.json 中配置GDB 调试器和预启动任务preLaunchTask,在任务配置文件tasks.json 中配置执行Makefile 文件中定义的make default 命令,实现对目标工程的可视化调试。 这里使用博文:VSCode+GCC+Makefile项目管理 中的示例代码,考虑到GNU工具链对Linux 系统支持更好,本文使用Ubuntu 20.04系统构建VS Code 可视化调试工程。 原博文是基于windows 系统编写的Makefile 文件和launch.json、tasks.json 配置文件,这里使用Linux 系统需要修改几个地方: 将Makefile 文件中的mingw32-make 改为make;将tasks.json 文件中的mingw32-make 改为make;将launch.json 文件中的"miDebuggerPath" 值改为 “/usr/bin/gdb”;将launch.json 文件中的"externalConsole"值改为 false(可选,设为false后使用VS Code 终端界面进行交互);配置好VS Code 后,进入“Run and Debug” 窗口界面,先设置断点和观察表达式,变量和调用栈帧信息会自动显示在想要区域,设置好断点和观察点后,点击“Start Debugging" 按钮开始执行调试任务,界面如下: GDB 调试模型有本地调试和远程调试两种,前面主要基于本地调试介绍的,我们在使用服务器编译或嵌入式调试等场景,就需要在PC 端调试远程服务器或嵌入式板子上的程序了,如何进行GDB 远程调试呢? 在博文:LwIP开发调试环境搭建 中已经展示了在windows 系统上调试虚拟机qemu-vexpress-a9 内运行的程序,调试界面如下: qemu-dbg 相比qemu 脚本主要多出来两个参数-S 和-s,这两个参数是什么意思呢?我们通过命令帮助查询如下: C:\Users\paul>qemu-system-arm -h ...... -S freeze CPU at startup (use 'c' to start execution) ...... -s shorthand for -gdb tcp::1234 ......从命令帮助可知,qemu-system-arm -s 参数可以让qemu-vexpress-a9 虚拟机在tcp::1234 端口启动gdbserver 服务,相当于在虚拟机内执行了“arm-none-eabi-gdbserver localhost:1234 rtthread.elf” 命令(虚拟机与宿主机共用IP 即localhost,ARM Cortex-A9 使用的编译器为arm-none-eabi-gcc,选用gdbserver 版本应与编译器架构一致)。 启动qemu-vexpress-a9 虚拟机时,通过-s 参数启动了gdbserver 服务,接下来在宿主机内执行如下命令: $ arm-none-eabi-gdb #--> 执行ARM 架构gdb 命令 ...... (gdb) target remote localhost:1234 #--> 连接远程目标gdbserver 服务器,IP:Port 为localhost:1234 Remote debugging using localhost:1234 0x600100d0 in ?? () (gdb) file rtthread.elf #--> 从本地读取调试信息 A program is being debugged already. Are you sure you want to change the file? (y or n) y Reading symbols from rtthread.elf...done. (gdb) break main #--> 在main 函数起始位置设置断点 Breakpoint 1 at 0x60010048: file applications\main.c, line 7. (gdb) continue #--> 继续执行到下一个断点处暂停,也即main 函数起始位置 Continuing. Breakpoint 1, main () at applications\main.c:7 7 printf("hello rt-thread\n"); (gdb) backtrace #--> 查看当前调试环境所有堆栈信息 #0 main () at applications\main.c:7 (gdb) info frame #--> 查看当前堆栈详细信息 Stack level 0, frame at 0x600b9780: pc = 0x60010048 in main (applications\main.c:7); saved pc = 0x60012884 source language c. Arglist at 0x600b977c, args: Locals at 0x600b977c, Previous frame's sp is 0x600b9780 Saved registers: r11 at 0x600b9778, lr at 0x600b977c (gdb) quit #--> 退出远程调试进程 A debugging session is active. Inferior 1 [Remote target] will be detached. Quit anyway? (y or n) y Detaching from program: E:\RT_Thread\rtthread_source\qemu-vexpress-a9\rtthread.elf, Remote target Ending remote debugging. 更多文章: 《代码调试跟踪与优化(二)— 如何调试嵌入式代码?》《VSCode+GCC+Makefile+GitHub项目管理》《LwIP开发调试环境搭建》《原来gdb的底层调试原理这么简单》 |
今日新闻 |
推荐新闻 |
CopyRight 2018-2019 办公设备维修网 版权所有 豫ICP备15022753号-3 |