在第5章《调试器概貌》一节中,我们已经对调试器的功能性、非功能性需求和大致的技术方案进行了阐述,那当前小节内容是否有点重复了呢?第6章紧跟第5章讲述的指令级调试器的设计实现,为了平滑地过渡到这一章,第5章中的功能性需求、非功能性需求我们是有所保留的、没有罗列出来的,技术方案当然也就没有详细阐述。

本小节中作为符号级调试器开发的前置小节,正好给了我们一个契机重新分析下调试器的功能性需求、非功能性需求,以及大致的技术方案。

功能性需求

go符号级调试器的功能性需求,大家联想下常见调试器的使用经历,这个是比较直观的:

需要支持多种调试对象类型

命令 描述
godbg debug 调试当前go main module
godbg test 调试当前go package中的测试函数
godbg attach 调试一个正在运行中的process
godbg exec 启动并调试指定的go executable
godbg core 启动并调试指定的coredump

ps: 对调试当前go module、go package中测试函数的,允许自定义编译选项

需要支持多种调试模式

命令 模式
godbg <...> 正常调试模式
godbg debug --headless 类似gdbserver的headless模式
godbg dap 支持调试器适配协议DAP,以方便与VSCode等进行集成
godbg tracepoint 支持tracepoint能方便观察程序执行命中的函数
godbg <...> --disable-aslr 禁用ASLR地址随机化

需要支持显示版本号信息

方便根据版本号排查特定版本引入的问题。

需要支持多种调试会话中的调试命令

1 Running the program

命令 别名 描述
call - Resumes process, injecting a function call (EXPERIMENTAL!!!)
continue c Run until breakpoint or program termination.
next n Step over to next source line.
rebuild - Rebuild the target executable and restarts it. It does not work if the executable was not built by delve.
restart r Restart process.
step s Single step through program.
step-instruction si Single step a single cpu instruction.
stepout so Step out of the current function.
rr相关 rr相关的命令,如rnext, rstep...

2 Manipulating breakpoints

命令 别名 描述
break b Sets a breakpoint.
breakpoints bp Print out info for active breakpoints.
clear Deletes breakpoint.
clearall Deletes multiple breakpoints.
condition cond Set breakpoint condition.
on Executes a command when a breakpoint is hit.
toggle Toggles on or off a breakpoint.
trace t Set tracepoint.

3 Viewing program variables and memory

命令 别名 描述
args Print function arguments.
display Disassembler.
examinemem x Examine raw memory at the given address.
locals Print local variables.
print p Evaluate an expression.
regs Print contents of CPU registers.
set Changes the value of a variable.
vars Print package variables.
whatis Prints type of an expression.

4 Listing and switching between threads and goroutines

命令 别名 描述
goroutine gr Shows or changes current goroutine
goroutines grs List program goroutines.
thread tr Switch to the specified thread.
threads Print out info for every traced thread.

5 Viewing the call stack and selecting frames

命令 别名 描述
deferred Executes command in the context of a deferred call.
down Move the current frame down.
frame Set the current frame, or execute command on a different frame.
stack bt Print stack trace.
up Move the current frame up.

6 Other commands

命令 别名 描述
config Changes configuration parameters.
disassemble disass Disassembler.
dump Creates a core dump from the current process state
edit ed Open where you are in $DELVE_EDITOR or $EDITOR
exit quit / q Exit the debugger.
funcs Print list of functions.
help h Prints the help message.
libraries List loaded dynamic libraries
list ls / l Show source code.
source Executes a file containing a list of delve commands
sources Print list of source files.
types Print list of types.
ptype Print type info of specific datatype.

大家都有使用过调试器,上面列出的调试命令至少有一部分是比较熟悉的。上述调试能力大致是一个现代go符号级调试器所要支持的功能全集,可以达到工程上的应用要求了。如果读者有使用过go-delve/delve,你会发现上面的功能基本上全是go-delve/delve的调试命令?没错,我这里就是罗列了go-delve/delve的调试命令,额外增加了一个受gdb启发的ptype打印类型详情的命令。

写这本书的初衷是为了解释如何开发一款符号级调试器,而非为了写一个新的调试器,考虑到调试功能完整度、相关知识的覆盖度、工程的复杂度、个人时间有限等诸多因素,我最终采用了一种非常“开源”的方式,借鉴并裁剪了go-delve/delve中的代码,保留核心功能,删减与linux/amd64无关架构扩展代码,将rr (record and play)、dap(debugger adapter protocol)迁移至额外的阅读章节(可能放在附录页、扩展阅读)中进行介绍。

这样作者可以保证在2022年让这本书完成初稿,以尽快与读者以电子书形式见面(纸质的也会考虑)。

非功能性需求

做一个产品需要注重用户体验,做一个调试器也一样,需要站在开发者角度考虑如何让开发者用的方便、调试的顺利。

对于一个调试器而言,因为我们会在各种任务间穿插切换,要灵活运行调试命令是必要的。但是一个基于命令行实现的调试器,要想实现命令的输入并不是一件轻松的事情。

调试器的易用性

1 调试命令众多,需要降低记忆、使用成本

  • 首先调试器有很多调试命令,如何记忆这些命令是有一定的学习成本的,而基于命令行的调试器会比基于GUI的调试器学习曲线更陡;
  • 基于命令行的调试器需考虑调试命令输入效率的问题,比如输入命令以及对应的参数。GUI调试器在源码某行处添加一个断点通常是很简单的事情,鼠标点一下即可,但基于命令行的调试器则需要用户显示提供一个源码位置,如"break main.go:15",或者"break main.main";
  • 调试器诸多调试命令,需要考虑自动补全命令、自动补全参数,如果支持别名,将会是一个不错的选项。调试器还需要记忆上次刚使用过的调试命令,以方便重复使用,例如频繁地逐语句执行命令序列 ,可以通过命令序列 代替,回车键默认使用上次的命令,这样对用户来说更方便;
  • 每一个命令、命令参数都应该有明确的help信息,用户可以通过help cmd来方便地查看命令cmd是做什么的,包含哪些选项、各个选项是做什么的。

2 命令行调试器,需要能同时显示多个观测值

  • 基于命令行的调试器,其UI基于终端的文本模式进行显示,而非图形模式,这意味着它不能像GUI界面一样非常灵活方便地展示多种信息,如同时显示源码、断点、变量、寄存器、调用栈信息等;
  • 但是调试器也需要提供类似的能力,这样用户执行一条调试命令(如next、step)后能观测到多个变量、寄存器的状态。且在这个过程中,用户应该是不需要手动操作的。且多个观测变量、寄存器值的刷新动作耗时要短,要和执行next、step的耗时趋近。

调试器的扩展性

1 命令、选项的扩展要有良好简洁的支持

  • 调试器有多种启动方式,对应多个启动命令,如godbg exec <prog>godbg debug <module>godbg attach <pid>godbg core <coredump>,各自有不同的参数。此外调试器也有多种交互式的调试命令,如break <locspec>break <locspec> cond <expression>等,各自也有不同的参数。如何可扩展地管理这些命令及其选项是需要仔细考虑的;
  • 命令的选项,尽量遵循GNU/POSIX选项风格,这更符合大家的使用习惯,且选项在可以消除歧义的情况下尽量同时支持长选项、短选项,给开发输入时提供更多的便利;

2 调试器应满足个性化定义以满足不同调试习惯

  • 好的产品塑造用户习惯,但是更好的习惯应该只有用户自己知道,一个可配置化的调试器是比较合适的,如允许用户自定义命令的别名信息,等等;

3 跨平台、支持不同调试后端、支持与IDE集成

  • 调试器本身,可能需要考虑未来的应用情况,其是否具备足够的适应性以在各种应用场景中使用,如能否在GoLand、VSCode等IDE中使用,或者可能的远程调试场景等。这些也对调试器本身的软件架构设计提出了要求;
  • 应该考虑将来扩展到darwin/windows以及powerpc、i386等不同的系统、平台上,在软件设计时应提供必要的抽象设计,将抽象、实现分离;
  • 调试器实现不是万能的,存在这样的场景我们需要借助其他调试器实现,来完成某种功能,原因可能是我们的实现不支持被调试程序所在的系统、平台,或者其他调试器实现方法更优,举个例子,Mozillar rr(record and replay),记录重放的实现比较复杂,gdb、lldb、dlv的逆向调试基本上都是在rr基础上构建的。这就要求调试器要实现前后端分离式架构,而且后端部分接口与实现要分离,满足可替换,如能轻松地从dlv切换成Mozillar rr;

调试器的健壮性

  • 调试器本身是依赖于一些操作系统能力的支持的,如Linux ptrace系统调用的支持,该系统调用的使用是有些约束条件的,比如ptrace_attach之后的tracee后续接收到的ptrace requests必须来自同一个tracer。还有syscall.Wait系统调用时Linux平台的一些特殊情况…这类情况有不少,调试器应该考虑到这些情况做兼容处理;
  • go调试器也依赖go编译工具链生成的一些调试信息,不同的go版本编译出的产物数据类型表示上、信号处理方面会有差异,调试器实现时应该考虑到这些情况做必要的处理,尽可能做到健壮。比如可以限制当前支持的go工具链版本,如果编译产物对应的go版本不匹配就放弃调试;

非功能性需求很多,我们从易用性,到命令管理的可维护性,到选型的规范性,到如何扩展到不同的操作系统、硬件平台、调试器后端实现,自身的健壮性等方面进行了描述。除了调试功能本身,这也是影响一个调试器能否被大家接受的很重要的因素。

本节小结

本节对go符号级调试器的功能型需求、非功能性需求进行了详细的一个分析,这个就是我们的一个目标了,后面我们要带着这些目标去一步步设计实现我们的符号级调试器。

results matching ""

    No results matching ""