调试器开发:需求分析
调试器需要支持哪些常见操作才能满足调试需求?本节先从功能性需求、非功能性需求分析角度切入,分析下接下来的调试器开发过程中要做什么、注意些什么。
本章还有个任务,要先搭建一个基础的调试器框架,方便扩展调试器命令、命令选项、命令参数、查看帮助信息、命令自动补全等。在此基础上,后续章节实现不同调试命令时,我们只需要添加子命令及对应的处理逻辑即可。
大家在理解了这个调试器雏形之后,后续阅读到相关调试动作的具体实现章节时,会自然联想到如何定位工程中对应的代码,也有益于我们后续章节内容组织、方便读者阅读。
功能性需求
调试器需要支持哪些功能?大家联想下常见调试器的使用经历,这个是比较直观的:
- 允许调试可执行程序、调试运行中进程、调试coredump文件;
- 允许对golang代码自动编译构建、调试完成后清理临时构建产物;
- 允许查看源码信息 list;
- 允许对二进制文件进行反汇编 disass;
- 允许在源码中添加断点 breakpoint
file:lineno
; - 允许在源码中添加条件断点 breakpoint
file:lineno
ifexpr
; - 允许逐语句执行 next;
- 允许逐指令执行(也能允许进入函数)step;
- 允许从function退出 finish;
- 允许显示变量信息、寄存器信息 print、display;
- 允许更新变量、寄存器信息 set;
- 允许打印变量类型 ptype;
- 允许对函数进行临时调用 call;
- 允许查看调用堆栈信息 bt;
- 允许选择调用栈中的特定栈帧 frame;
- 允许查看goroutines列表、切换goroutine执行;
- 允许查看threads列表、切换thread执行;
- ...
- 其他;
调试器的功能性需求,相对来说是比较直观的,需求会变化,功能也会进行调整。
比如,调试过程中经常不小心错过一个非常关键的事件,想退回几步语句继续调试。通常,我们只能restart调试会话,然后在事件发生位置加断点,然后continue,在代码规模比较大的时候,或者不是很容易复现事件的时候,这种方式也不一定能胜任。
为了进一步提升调试的便利性,就可以为调试器添加 record and replay 的功能,该功能能够对调试过程进行跟踪记录,并能在需要的时候进行回放,就方便多了。
ps:在实现了指令级调试、符号级调试的主体内容后,我们将介绍下 record and replay 在调试领域的具体实现项目,mozilla/rr: You record a failure once, then debug the recording, deterministically, as many times as you want ...
非功能性需求
做一个产品需要注重用户体验,做一个调试器也一样,需要站在开发者角度考虑如何让开发者用的方便、调试的顺利。
对于一个调试器而言,因为我们会在各种任务间穿插切换,要灵活运行调试命令是必要的。但是一个基于命令行实现的调试器,要想实现命令的输入并不是一件轻松的事情。
- 首先调试器有很多调试命令,如何记忆这些命令是有一定的学习成本的,而基于命令行的调试器会比基于GUI的调试器学习曲线更陡;
- 基于命令行的调试器,其UI基于终端的文本模式进行显示,而非图形模式,这意味着它不能像GUI界面一样非常灵活方便地展示多种信息,如同时显示源码、断点、变量、寄存器、调用栈信息等;
- 基于命令行的调试器需考虑调试命令输入效率的问题,比如输入命令以及对应的参数。GUI调试器在源码某行处添加一个断点通常是很简单的事情,鼠标点一下即可,但基于命令行的调试器则需要用户显示提供一个源码位置,如"break main.go:15",或者"break main.main";
- 调试器诸多调试命令,需要考虑自动补全命令、自动补全参数,如果支持别名,将会是一个不错的选项。调试器还需要记忆上次刚使用过的调试命令,以方便重复使用,例如频繁地逐语句执行命令序列
,可以通过命令序列 代替,回车键默认使用上次的命令,这样对用户来说更方便; - 调试器有多种启动方式,对应多个启动命令,如
godbg exec <prog>
、godbg debug <module>
、godbg attach <pid>
、godbg core <coredump>
,各自有不同的参数。此外调试器也有多种交互式的调试命令,如break <locspec>
、break <locspec> if <expression>
等,各自也有不同的参数。如何高效、合理地管理这些命令是一个需要考虑的事情; - 好的产品塑造用户习惯,但是更好的习惯应该只有用户自己知道,一个可配置化的调试器是比较合适的,如允许用户自定义命令的别名信息,等等;
- 调试器本身,可能需要考虑未来的应用情况,其是否具备足够的适应性以在各种应用场景中使用,如能否在GoLand、VSCode等IDE中使用,或者可能的远程调试场景等。这些也对调试器本身的软件架构设计提出了要求;
- 可扩展性,除了使用的便利性,也要考虑其未来的扩展性,如何支持一门新的编程语言,如何支持采用不同调试信息标准的程序调试,如何便利地与其他开发工具集成;
- 健壮性、正确性,如何保证调试器本身的健壮性、正确性,可以借助测试覆盖来改进;
- ...
- 其他。
本节小结
本节我们从调试器的功能性需求和非功能性需求两个维度,梳理了现代调试器应当具备的核心能力。功能性需求方面,调试器需要支持多种调试对象(可执行程序、运行中进程、coredump)、丰富的调试操作(断点、单步、查看变量、调用栈等),以及更高级的特性(如record and replay)。非功能性需求方面,则强调了用户体验、命令管理、自动补全、可配置性、可扩展性、健壮性等对调试器产品化的重要影响。
这些需求的梳理为后续调试器架构设计和实现方案的选择奠定了基础。下一节我们将进一步探讨调试器的架构设计,以及如何通过合理的技术方案满足上述需求,打造一个易用、可扩展的调试器。