Categories

Tags

起因很简单,在弄emacs的调试器,然后想到的问题。本文主要思考了CC、haskell、python与ruby的调试器。在IDE中,在源代码窗口中打断点其实就相当于在对应上下文或调试器启动时,增加给编译器或解释器使用的中断信息,如C语言中的调试用宏,lldb/gdb/ghci中break相关命令的参数,ruby中的byebug等。

对大部分程序这一过程运行良好,但是对于从外部启动的程序(特别是web应用,如rails、django),他们的外部启动器(如django serverrails server)会附带额外的启动配置,并且调用我们所编写的项目内容。我们无法先打开调试器(或解释器)来直接运行我们的程序进行调试。同时,这启动过程中,可能是再次通过外部程序来调用的,如rails server中使用exec来调用后续代码。相当于一个新的进程,在这个进程中,哪怕与原程序是同样的语言,但是运行环境不同(父子进程不共享内存,何况子进程不一定是同样语言环境,虽然这里是相同的)。调试器难以获取其内部的信息。

我所写的启动器是指包含了整体程序的入口程序并调用了开发者所编写代码的模块。我要强调的是入口。当入口不为我们所直接调用时,这会影响到调试器来进行设置,如下所言。

在编辑器中,就难以仅仅通过一个公用的接口来完成不同情况的调试,比如解释行语言ruby,我们无法使用inf来简单的直接去调试rails代码(在最开始的时候,我们一般得从web接口开始,去查看问题出现于哪个点,并且dump出上下文环境。有了上下文环境才可方便的从解释环境中仅加载相应的模块来进行测试)。我们需要从rails server中来主动浮出解释环境,方便我们查看上下文。

Haskell与Yesod

Haskell是一个编译型语言,但他有一个解释环境。当然这个解释环境不同于一般的解释型语言,他是比较特殊的,与实际的执行环境存在差异(因为整个解释环境都在IO单子内)。而Yesod的使用,是通过Template来实现的,即我们使用了宏来编写了入口。入口是定义在我们自己编写的文件中的。换句话说,我们自己的程序才是主体,通过调用了外部的框架来快速实现了一些功能。

当然,在haskell中就可以使用ghci来load我们的入口,从而直接执行整个程序。由于这些代码是不是通过调用外部程序调用的,调试器当然可以读到所有的上下文。

CC与GDB/LLDB

C家族的编译器,可以在编译时选择调试选项,把源代码内容编译进二进制包中。而调试器则通过查看这类信息,来快速定位。

而对于C家族的语言来说,必需需要编译,那么启动器必然是编译入你所生成的二进制文件中的。即,整体代码的启动入口一定是与我们编写的代码,在同一程序中。并不会出现上文所述的子进程。所以不论如何,调试器都可以获取到运行的上下文。

类似于MFC、CLS、Qt、macOS的开发等,他们其实都是隐性的使用了固定的入口,然后在编译前生成这些文件与我们所编写文件的关联,最后得到执行文件。虽然入口隐藏了,但入口还存在于我们最后所得的执行文件中。其本质上与haskell是相同的(从时间上来讲,应该是haskell与他们是相同的),只不过是由开发环境在编译前自动处理了。

Python/Ruby与Django/Rails

这类是解释型语言,非常不同于上述两种情况,我们的程序是客体,是由外部程序来调用管理的。即启动过程不为我们所直接/间接控制。那么我们就不可以像ghci或gdb那样,来加载一个包含入口的程序来进行调试。需要注意,这里是入口,如果我们不使用入口,直接从我们所编写的子模块开始调试(手动准备所有的参数)当然是没有问题,但对于最开始的时候,我们需要dump出错误产生时的上下文来观察奇点是什么。否则我们直接从子模块开始调试的话,要如何准备那些参数?

现存的调试工具如pdb、byebug或pry,都是在代码中添加调用,到执行时,来浮出交互式的解释器。因为我们无法控制入口,当然没办法从解释器中调用整个程序(当然其实可以,只不过非常麻烦,就是我们要用代码来调用一个个boot,而这些boot一般是通过环境的PATH来查找的,手动加载就等于得把这些全部手动写入。另外如上文所提,如果启动器本身是使用了子进程,那么最后仍是无法获取到内层的上下文)。

所以浮出是一个好的方案,即执行到我们需要调试的点了,浮出一个交互程序,这个程序与启动器是不相干的(只包含我们现在的执行上下文,同时不会受父进程的影响)。

调试器

现在理清关系后,问题来了,我们要如何针对第三种情况来操作调试器?因为emacs的交互用的是单独的mode,从eshell中转入会失去交互mode的特性,当然,可以人工切换,但这很不值得。比较好的想法是,把交互mode的运行指令改成对应启动器的启动,然后用人工的方法来添加浮出交互的代码。当然,这样就推动了从源代码窗口打断点的方便性了。但这个打断点的原理很简单,其实就是相当于加了额外的执行语句,封个函数就行,但emacs中并没有这样的操作就是了。因为现有的断点都是以调试器指令的方式来添加的,而不是通过添加实际代码的方法,而添加代码又会出现缓冲区与保存的问题,执行是需要先保存到硬盘的。

结论?结论就是我们修改交互的启动命令就能在emacs里做一样的事了。