All Stories

Lua调试器研究续

  本来以为自己对如何实现一个Lua调试器已经有了足够的知识储备,于是今天就提枪上马,结果无奈地发现,说是一回事,自己做是另一回事。我最终的目标是要达到Decoda那样的效果,但一开始只能慢慢一步一步来,先实现类似lldebug的debuggee部分。但是就算是这样小步开始,也仍然困难重重,很多问题其实我都没有吃透。  首先是发现在sethook时,只有按line回调是正常的,而call和return都没有回调,这让我感到很迷惑,无论是用C代码的hook还是Lua代码的hook,都是存在这个问题,而且用lldebug是正常的。所以我最早怀疑跟Lua代码有关的猜测也不成立了,只好试图从lldebug的源代码中寻找答案,当然寻找到的lldebug源代码跟我的代码没有本质的区别。最后过了很久才隐约记起来,我用的是LuaJIT,好像它对debug库的支持是有些跟官方Lua不同的地方。于是立马换回官方Lua,发现果然无论是C代码还是Lua代码都照预期的执行了。  之后是发现,hook的回调是能正常执行了,但程序还是自顾自地跑下去了,那怎么处理单步和断点呢。然后是回头看lldebug代码,发现是在hook回调函数中,处理完对应的事件后,应该要等待命令。一开始我还想不通,要怎么等待命令啊,后来突然明白过来了,比如我这个是控制台操作的,那么在这里接收一个控制台的输入就行了,不就变成运行一下,停一下了嘛!  我写了两个测试脚本,用于观察Lua的debug库三种hook回调的行为。发现Lua在执行一个脚本文件时,首先是将整个脚本文件看作一个整体,用Lua的话说,应该是一个chunk,而它执行这个chunk是个call操作,所以执行一个脚本文件会先有一个call回调,在整个文件执行完后,会有一个return回调。如果一个脚本文件中又执行了另一个脚本文件,那么就可以到嵌入call/return回调。当执行的代码是一个函数的定义时,会先在函数的end行有一个line回调,然后在函数的function行有一个line回调。当执行的代码是一次函数调用,那么会先在函数体(参数列表后面)的第一行有一次call回调,然后再在该行有一次line回调,在结束函数时,先在函数end行有一次line回调,再在该行有一次return回调。  再之后是开始考虑要支持哪些调试命令,以及怎么实现。简单地看了一下别的调试器的实现,我就暂定要支持step into,step over,run to cursor,breakpointp这几个操作了,step into大概是最容易实现的,就是debug库中按line回调即可。step over可能稍微麻烦一点,应该先假设是按line回调来处理,如果从该操作开始第一次回调的是call,那么应该记录下call的次数,并计数return回调的次数,直到return回调的次数跟call回调次数相同,再下一次line回调就达到step over的效果了。run to cursor也比较简单,按line回调直到当前光标所在行即可。breakpoint跟run to cursor的处理方法一样。  除此之外要注意的是coroutine的创建,会返回一个lua_State,所以需要拦截这个创建函数,对这个新创建出来的lua_State也sethook。因此在调试器中还得为不同的lua_State保存各自的调试上下文。  以上都是不考虑执行效率的做法,在云风的blog上有一篇文章,讲到他设计的一个提高调试器执行脚本效率的方案,似乎是勉强能提高一点性能,但要求用户修改Lua脚本来定期轮循调试器是否有新命令下发。其实在这个基础上,我们可以更进一步,修改Lua脚本的工作由调试器完成,对用户透明。而且定期轮循可以修改成只要下了断点的行的最前面自动插入一个调试器定义的函数调用,也就是将执行控制权再次转移到调试器中。这就比较像C/C++调试器中插入int3的做法了,如果断点是固定的话,那么调试器执行脚本的效率将几乎是无损耗的。除了效率上的提升外,各个调试命令的实现也将变得更加容易。但这个方案有一个问题是,这插入断点指令的操作必然是在脚本文件在被执行前完成的,如果在调试运行过程中有新增断点,那么仍然得退出原来的那种靠debug库line回调的方式了,除非重新启动调试。

Lua调试器研究

  准备自己实现一个Lua的调试器,以前没做过这方面的工作,所以只好拿现成的开源代码来研究了。  比较容易找到的调试器代码有好几个,比如lldebug,RemDebug以及wxLua中的那个调试器,各自的实现方式有所不同,但万变不离其宗的是都使用了Lua自己的debug库sethook功能。还有个共同点就是远程调试的架构,而且都是用socket实现的。  RemDebug完全用Lua写成,一个只有两个文件,加在一起代码才500多行。其中一个文件中实现的是调试引擎,负现各种调试功能的实现,另一个文件是用于实现人机交互的接口。  lldebug是个日本人写的,简单看了一下,也是跟RemDebug类似分成两部分实现。它的人机交互部分就比较高级,用wxWidgets实现了一个GUI,可以直接在上面打断点,单步,查看回调栈,查看变量等等,可算是debugger部分,另外实现一个单独的程序用于执行Lua脚本,这里可称为debuggee,其实也就是在执行Lua脚本前设置一下调试选项,以及拦截一些Lua的C API。  wxLua带了一个调试功能,它跟lldebug比较像的一点,有一个独立的程序执行Lua脚本作为debuggee。另一部分则是用C/C++实现的Lua接口,可以由用户自己用Lua脚本来实现debugger,这是它相较于前两者比较独特的一点。  通过sethook实现的调试器,要特别关注coroutine的处理。  我一直比较羡慕的是Decoda的那种功能,简单说来就是它可以直接调试其他嵌入了Lua解释器的应用程序执行的Lua脚本。当时我一直想不清楚这是怎么实现的,只是看到它向被调试的应用程序进程注入了一个dll,于是猜测它拦截了应用程序对Lua的C API的调用,而拦截了C API后要做些什么我就不知道了。现在想起来,似乎也就是只要在应用程序执行Lua脚本前,对lua_State设置hook,于是就跟前面提到的那些传统调试器一样了。但另外要注意的问题是,抢在什么时候将这个dll注入,应该拦截哪些C API。如果能在应用程序创建Lua解释器前注入,那么拦截lua_open就可以了,通过修改PE文件的导入表,基本可以达到这个目的。如果不能保证注入的时机,那么比较合适的被拦截C API应该是lua_load,但要注意的是拦截后设置hook时应该注意一个lua_State只要设置一次hook就行了。

插件打包

  前几天,把所有的插件以插件为单位打包成zip了。不过不知道wxWidgets中的zip虚拟文件系统如何支持密码,至少是没找到相关的选项。现在的情况是验证了方案可行性,可以确认功能上的不缺失以及性能上的损耗在可接受的范围内,但真正实用是一定要有密码的,也就是为了保护插件中的源代码。以前还以为带了密码就不方便第三方开发插件了,现在想通了,其实只要我提供一个插件开发环境,其中自带插件打包功能,这样就可以用同一个密码了。既然wxWidgets没有简单方便的接口来支持密码zip,那就只好自己写一个这样的功能了,好在zip实在是一个很大众化的格式,源代码很容易找到。

《西梅北》

  昨天脑袋发热,去买了个新凯越,心里很难过,死要面子活受罪。  今天早上醒来,心情更加的压抑和沉重,深深的孤独感和失落感以及挫败感。晚上做了个梦,梦见那个小姑娘,叫我帮她的同学(?)远程协助考试,考试的范围是一篇叫《西梅北》的文章。我无比清晰地记着这个题目,想起那个小姑娘,那个让我很后悔的小姑娘。  看到《间客》中的一句话:“如果将来这个联邦要收拾你……我很想在联邦之外给你留条后路。”不禁热泪盈眶。

基本搞定语法树视图

  今天在宿主程序中做了修改,并添加了插件,可以在适当的时候触发刷新语法树视图。  刷新语法树的时机其实并不好选择,总的说来,应该在这两个时刻进行刷新:一,文档刚刚打开;二,做出了影响语法树内容的修改。第一点,比较容易做到,但第二点就比较困难了,它包括当前活动文档的切换,以及当前活动文档被修改。切换也是比较容易响应的,但怎样判断一个修改是否影响了语法树内容,就不简单了。我现在的简单做法是,每当有新的行时,就进行刷新,笨了点,但勉强能用。  刷新语法树的方式,也是需要仔细实现的,我现在的做法完全是为了简单起见,先停止刷新窗口,然后清空整个树,再把整个树的内容都重新添加一遍,再刷新窗口。这样的缺点是在刷新时,能明显地看到整个树视图的闪烁。如果要规避这个问题,应该比较当前树视图中的内容和待刷新的数据,在树视图中逐个判断有新增的,或需要删除的节点,这样就比较流畅了。但这个算法就相对来说复杂,而且可能更耗资源。  目前我只是让Lua中的函数定义作为语法树视图中的唯一的元素,目前看来效果还可以。其实单就Lua来讲,还可以添加变量的定义以及表的构造。但是眼前可以预见的问题是,变量可能有很多,全列出来可能干扰视线。而表的构造表达方式可以很复杂,而且表是可以在程序中动态增删元素的,这要如何处理。

初步搞定语法树

  看了两天小说,呃,又堕落了。由于已经看完了,今天就比较认真地折腾起flex和bison。其实之前已经把lex和yacc脚本写完大部分了,至少可以从控制台打印结果出来了。今天就修改一下yacc脚本,把原来打印到控制台的内容保存在内存中,到时候转储成xml格式。因为只是要显示在界面的树视图中,我想了想,也就只有函数定义值得这么显示一下,所以也就只处理了这部分。  总的说来,感觉yacc有点土,它只能接受一种输入接口,而我用flex时发现可以生成C++代码,所以要给bison用的话,仍然需要把这个C++类的接口再适配成bison可用的C接口。  这种任务果然是实践性非常强的工作,本来看过一些资料,当然,能找到的资料也基本内容一样,翻来覆去那么几句话几个例子,等到自己要做时,不时地有些迷惑,只得慢慢尝试,倒也捣鼓出来了。  在yacc中可以为每个token或type指定一个union中的某个成员,其实这个成员的指定只在规则描述段中的action中有用,就我看来各种资料、教程中说的那一堆实在是扰乱视线。对于一个C/C++程序员来说,这种用法只是万千技巧中的一种,实在没必要说得那么严肃仔细,好像不那么用就不行了似的。  再说个我觉得yacc土的地方,由于这种格式上的限制,在action中只能访问一些全局的变量、对象等,至少在思维逻辑上很不连贯,其实lex也有这个问题。要我说,比较让现代化的做法是它应该生成一个类,每组action触发时,应该调用该类中的某个回调函数或虚函数,这里形式有好几种,都可以考虑,不知道boost.spirit是不是这种形式的,也许ANTLR等其他类似的工具就是这么做的。  最后抱怨一下,Lua Reference Manual中附录的complete syntax不能直接用的,至少不能直接用于yacc,有好些地方似乎没写全。

解决程序不支持老机器的问题

  昨天偶尔发现,在笔记本上编译的工程在台式机上启动即崩溃。一开始的时候我认为是因为lua装入的一些第三方库dll没有设置好依赖的dll路径和正确的manifest配置,后来发现问题不是那么简单。  首先是我用了LuaJIT的dll,而之后发现LuaJIT要求CPU支持SSE2指令集的,我那台式机是2002年的配置,当然没有。换成官方Lua后,问题仍然存在。  接着发现,安装程序没有把配置文件复制到正确的文件夹下。看了一下安装程序的脚本,果然没写对。但是当把配置文件的路径修改正确后,问题仍然存在。  因为台式机上装了Win2000ProSP4,WinXPProSP2和Win2003,所以当XP上仍然不能运行时,我切换到Win2000和Win2003上进行测试,发现问题同样存在。我考虑了一下两台机器软硬件的区别,猜想是不是仍然是软件配置的问题。于是我在笔记本上用VirtualBox建了个干净的XP系统,第一次运行时,居然报了个错,说ltxml加载不成功。幸好我前天已经自己写了个Xerces C++的绑定,而且用于的地方现在还不多,立马全都把使用ltxml的地方改成用xerces的。之后虚拟机上可以正常运行了。  到了这一点,说明可以排除安装部署的问题。再想想两台机器的区别,台式机上的3个系统都是原生的中文版系统,而笔记本上和虚拟机上的都是英文版的,其中笔记本上的打了个可以处理中文的多国语言包。我有点怀疑会不会是因为这个缘故,但又觉得可能性不大,如果真是这个原因的话就不好处理了。我试着把安装后的那些mo文件都删掉,结果仍然没用。  最后我没有办法了,只好修改源代码,从程序开始处依次加入日志,看到底在哪一步引起崩溃的。经过逐步排查,最后定位到使用Crypto++的初始化RSA公钥的对象构造时崩溃了,连异常都没有抛出。这时我想到了LuaJIT对CPU的要求,猜想会不会Crypto++编译时也受CPU的影响。用CPU-Z看了一下虚拟机的CPU,是跟宿主的CPU几乎相同的。不再继续猜想,直接在台式机上编译了一遍Crypto++,拿到笔记本上可以链接上,再把exe复制到台式机上,程序也可以正常运行了。  至此,问题解决!这时我不禁有些担心,我这样一直用笔记本写的程序会不会都有这种问题,一遇到老机器,就会罢工呢!

给Lua绑定个Xerces-C++

  本来是有ltxml这个库的,这个库使用TinyXML和TinyXPath,本来用着,也勉强,可以读取xml文件,但也不时地出点小问题,不过都让我规避掉了。这回是想写xml文件了,结果发现ltxml的接口我很不习惯,嗯,不喜欢,于是想自己重新写一个新的绑定。  我首先看了一下libxml2,发现它的文档和例子都不是很清晰,不想继续投入精力去研究了。剩下两个选择,MSXML和Xerces C++。这两个库我都用C++的类做了一层简单的封装,可以适配应用STL中的算法。后来想想,MSXML的那个封装因为当初只考虑配合MFC/WTL使用,所有的字符串都用CString了,而且另外一点是MSXML是只在Windows上可用,Xerces C++是跨平台的,特别是可以用gcc编译,于是最后就选择了Xerces C++。  本来我的那个封装是只针对DOM接口的,同时有4个类,分别对应DOM文档、DOM节点、DOM节点列表,以及DOM节点列表的迭代器。绑定的过程参考了ltxml的实现,但只给DOM文档和DOM节点定义了userdata,因为Lua有table,所以可以用来表示DOM节点列表。  绑定完成后,简单试用了一下,读取,写入都基本满足当前的需求,除了不知道怎么让它写入的时候进行format pretty print,老是多加个换行符,中间有空行可一点都不pretty。

插件支持国际化

  宿主程序提供了界面国际化,那么插件不能提供国际化就说不过去了,至少得有这样的机制以供支撑该种需求嘛。  得益于wxWidgets对国际化的良好支持,要让嵌入的Lua解释执行的Lua脚本也能根据宿主程序的本地化信息进行正确的处理非常简单。wxWidgets中对要进行国际化的字符串用_()进行包裹,其实这是一个宏,用于调用真正的翻译功能,比如wxGetTranslation。所以在嵌入的Lua中将wxGetTranslation函数注册到Lua中即可,然后在Lua脚本的最开始处将该函数换个更简单的名字,比如_,这是最好的名字了,哈哈。这样就可以在Lua脚本中对需要进行国际化的字符串也用_()进行包裹,它会调用wxGetTranslation函数。  GNU的国际化方案套餐中,提供一个叫xgettext的工具,可以从众多编程语言的源代码文件中提取出字符串,生成po文件以供翻译生成mo文件。比较不幸的是,xgettext不能支持Lua语言,同时由于我这个项目中使用的插件描述信息中有一部分界面信息是在xml文件中的,这也是一种自定义的格式,所在很不幸地xgettext更是不能处理啊!所以我在想,我是否要先写个可以支持Lua和我这种xml格式的类似xgettext的工具呢?Poedit太扯蛋了,居然只能认它自己生成的那种po格式的文件,稍微改一点就报错了!