自己动手实现Lua调试器
這段時(shí)間在qnode項(xiàng)目中新增了一個(gè)叫l(wèi)db的子項(xiàng)目,它的作用是使用C語(yǔ)言實(shí)現(xiàn)了一個(gè)lua調(diào)試器,后面將會(huì)在qnode中嵌入對(duì)調(diào)試lua腳本的支持。
先來(lái)簡(jiǎn)單提一下ldb的用法,在ldb目錄的子目錄test中,有一個(gè)main.c文件,其中使用ldb庫(kù)提供的API實(shí)現(xiàn)對(duì)lua腳本的調(diào)試演示:
#include <stdio h=""> #include "ldb.h"ldb_t *ldb;static int c_break(lua_State *state) {ldb_step_in(state, 1);return 0; }int main() {int i;const char *file = "my.lua";lua_State *L = lua_open();luaL_openlibs(L);lua_register(L, "c_break", c_break);ldb = ldb_new(L);luaL_dofile(L, file);ldb_destroy(ldb);return 0; } </stdio>在這里,ldb提供了創(chuàng)建和銷(xiāo)毀ldb庫(kù)的API,分別是ldb_new和ldb_destroy。另外提供了一個(gè)API ldb_step_in,main文件中通過(guò)向lua層提供一個(gè)C api c_debug,在該函數(shù)中調(diào)用這個(gè)API,實(shí)現(xiàn)對(duì)lua的調(diào)試。
main函數(shù)將讀取my.lua文件來(lái)執(zhí)行,它的內(nèi)容如下:
function test()local ab = 2034print("test") endlocal t = {out=10} local a = 1014 b = 2024 print("before debug")c_break() test() print("after debug")來(lái)看看ldb庫(kù)當(dāng)前實(shí)現(xiàn)的調(diào)試功能有哪些:
(ldb) h Lua debugger written by Lichuang(2013) cmd:help(h) : print help infoprint(p) <varname> : print var valuebacktrace(bt) : print backtrace infolist(l) : list file sourcestep(s) : one instruction exactlynext(n) :next linebreak(b) [function|filename:line] : break at function or line in a filedisable(dis) breakpoint : disable a breakpointenable(en) breakpoint : enable a breakpointdelete(del) breakpoint : delete a breakpointinfo(i) :show all break infocontinue(c) : continue execute when hit a break point </varname>括號(hào)中的都是該命令的縮寫(xiě)。當(dāng)前支持添加/禁止斷點(diǎn),打印變量,查看文件內(nèi)容,查看當(dāng)前調(diào)用棧,step in模式支持逐行執(zhí)行的調(diào)試,以及next模式會(huì)跳過(guò)函數(shù)執(zhí)行等最基本的調(diào)試命令。
介紹完簡(jiǎn)單的使用和功能,下面來(lái)介紹一下lua中為支持調(diào)試功能提供了哪些API,以及通過(guò)這些API如何實(shí)現(xiàn)一個(gè)lua調(diào)試器。
1)lua提供的hook功能
lua為了支持調(diào)試,提供了hook功能,使用者可以根據(jù)需要添加不同的hook處理函數(shù)供條件觸發(fā)時(shí)回調(diào),包括以下幾種:
具體包括以下幾種hook類(lèi)型:
這里用的最多的是LUA_HOOKCALL,LUA_HOOKLINE,如果注冊(cè)了這兩個(gè)類(lèi)型的HOOK函數(shù),則會(huì)分別在調(diào)用某函數(shù)和執(zhí)行每一行代碼之后調(diào)用注冊(cè)的HOOK函數(shù)。
使用lua中提供的API,可以如下方式注冊(cè)HOOK函數(shù):
有了HOOK函數(shù),就可以在lua代碼每次執(zhí)行的時(shí)候做一些動(dòng)作了。
除了HOOK函數(shù)之外,lua自身還提供了lua_Debug結(jié)構(gòu)體,這個(gè)結(jié)構(gòu)體包括以下成員:
source 函數(shù)的定義位置。如果函數(shù)在字符串內(nèi)被定義(通過(guò)loadstring 函數(shù)),source就是該字符串,如果函數(shù)在文件中被定義,source就是帶“@”前綴的文件名。short_src source的簡(jiǎn)短版本(60個(gè)字符以?xún)?nèi)),對(duì)錯(cuò)誤信息很有用。linedefined source中函數(shù)被定義處的行號(hào)。what 函數(shù)類(lèi)型。如果foo是普通的Lua函數(shù),結(jié)果為“Lua”;如果是C函數(shù),結(jié)果為“C”;如果是Lua的主代碼段,結(jié)果為“main”。name 函數(shù)的名稱(chēng)。namewhat name域的含義。可能的取值為:“global”、“l(fā)ocal”、“method”、“field”,或者空字符串。空字符串意味著Lua無(wú)法找到這個(gè)函數(shù)名。nups 函數(shù)中的Upvalues的個(gè)數(shù)。func 函數(shù)本身。稍后介紹。 可以通過(guò)lua_getinfo得到一些很重要的信息,它的調(diào)用方式是:
lua_getinfo(state, params, ar),第二個(gè)參數(shù)是一個(gè)字符串,支持傳入多個(gè)字母,每個(gè)字母有不同的含義:
2) 如何打印變量?
變量分為局部和全局變量,因此搜索某個(gè)變量的時(shí)候是從內(nèi)到外的方式搜索,搜索局部變量時(shí),使用lua提供的API lua_getlocal函數(shù),逐個(gè)搜索,具體可以看ldb.c中的search_local_var函數(shù)。
如果在局部變量中搜索不到,則還得使用lua_getglobal函數(shù)進(jìn)行全局變量的搜索,具體見(jiàn)ldb.c中的search_global_var函數(shù)。
但是以上的過(guò)程僅僅還只能在相應(yīng)的地方查找到同名的變量,在真正需要打印變量值的時(shí)候,還需要根據(jù)變量的類(lèi)型具體來(lái)打印數(shù)據(jù),代碼太多,不在這里列出,見(jiàn)ldb.c中的print_var函數(shù)。
3)如何查看文件的內(nèi)容
查看文件的內(nèi)容相對(duì)簡(jiǎn)單,因?yàn)楫?dāng)lua被HOOK住的時(shí)候,可以通過(guò)lua_getinfo函數(shù)得到當(dāng)前l(fā)ua的一些信息,比如lua文件名,行號(hào),在C實(shí)現(xiàn)的Lua調(diào)試器中,會(huì)維護(hù)一個(gè)已經(jīng)讀取過(guò)的文件列表,如果當(dāng)前所在的文件還沒(méi)有被讀取到內(nèi)存中,那么會(huì)讀取到內(nèi)存中,再根據(jù)所在的行號(hào)就可以得到文件內(nèi)容的信息了。
4)斷點(diǎn)的添加
調(diào)試器的斷點(diǎn)分為兩種,一種是基于文件:行號(hào)形式的,一種則是基于函數(shù)調(diào)用形式的。
C實(shí)現(xiàn)的調(diào)試器中,首先需要定義一個(gè)數(shù)據(jù)結(jié)構(gòu)類(lèi)型,用于表示斷點(diǎn):
這些信息包括斷點(diǎn)所在的文件,行號(hào),函數(shù)名,當(dāng)前是否被激活,被觸發(fā)的計(jì)數(shù),等等。
先來(lái)看第一種形式斷點(diǎn)的實(shí)現(xiàn)。這種形式的斷點(diǎn)相對(duì)簡(jiǎn)單。做法是創(chuàng)建一個(gè)新的斷點(diǎn)數(shù)據(jù)結(jié)構(gòu),保存下文件和行號(hào),在每次HOOK函數(shù)中都去根據(jù)當(dāng)前的文件和行號(hào)信息去查找是否匹配了某個(gè)斷點(diǎn)的信息,當(dāng)然,當(dāng)前的實(shí)現(xiàn)中這個(gè)查找是線(xiàn)性的,可能對(duì)性能有一定影響。
來(lái)看第二種形式斷點(diǎn)的實(shí)現(xiàn)。由于函數(shù)在lua中也是一種類(lèi)型的變量,既然是變量那么就涉及到作用域。比如你在A(yíng)模塊中斷點(diǎn)時(shí),想給B模塊的fun函數(shù)下斷點(diǎn),那么就不能簡(jiǎn)單的寫(xiě)”b func”,而應(yīng)該是”b B.func”。所以在實(shí)現(xiàn)對(duì)函數(shù)進(jìn)行斷點(diǎn)的時(shí)候要注意這一點(diǎn)。另外,當(dāng)給某一個(gè)函數(shù)下斷點(diǎn)時(shí),還需要添加LUA_HOOKCALL類(lèi)型的HOOK,也就是在函數(shù)調(diào)用時(shí)被觸發(fā)。之所以這么做,是因?yàn)樵诓檎覕帱c(diǎn)時(shí),當(dāng)首先使用文件名和行號(hào)都查找不到時(shí),會(huì)判斷一下當(dāng)前這次的HOOK調(diào)用,是不是一個(gè)函數(shù)調(diào)用觸發(fā)的HOOK,如果是的話(huà)再繼續(xù)根據(jù)斷點(diǎn)的函數(shù)名進(jìn)行查找匹配。
5)如何查看當(dāng)前堆棧信息
也就是模擬gdb中的bt指令的功能。lua對(duì)這個(gè)已經(jīng)有支持了,可以使用lua_getstack函數(shù),具體見(jiàn)ldb.c中的dump_stack函數(shù)。
6)step和next指令的實(shí)現(xiàn)
首先來(lái)看一個(gè)子問(wèn)題,如何得到當(dāng)前的函數(shù)堆棧數(shù)量,或者說(shuō)當(dāng)前的調(diào)用層次,通過(guò)反復(fù)調(diào)用前面提到的lua_getstack函數(shù)可以獲取到:
這兩個(gè)指令的實(shí)現(xiàn)稍微有點(diǎn)難度,所以放在最后一個(gè)講解。step就是逐行執(zhí)行代碼,即使調(diào)用函數(shù)的時(shí)候也會(huì)跟進(jìn)該函數(shù)中,而next指令會(huì)在調(diào)用函數(shù)的時(shí)候不跟進(jìn)函數(shù)的調(diào)用。所以這兩者的區(qū)別在于調(diào)用時(shí)的函數(shù)堆棧,因此這兩個(gè)指令的區(qū)別僅在于遇到函數(shù)的時(shí)候是不是繼續(xù)跟進(jìn)去。所以我的做法是新增一個(gè)變量用于保存當(dāng)前的函數(shù)棧索引,當(dāng)step模式時(shí)將這個(gè)值置為-1,next模式時(shí)只會(huì)保存為當(dāng)前的函數(shù)棧索引,如果某個(gè)指令是調(diào)用一個(gè)函數(shù)時(shí),這時(shí)通過(guò)get_calldepth函數(shù)獲得的函數(shù)棧就會(huì)比之前的大,這樣就可以知道當(dāng)前的這個(gè)指令是不是調(diào)用一個(gè)函數(shù)了,next指令可以在這個(gè)時(shí)候返回不做任何處理,而step指令可以繼續(xù)執(zhí)行下去。
以上就是簡(jiǎn)單的原理性介紹,如果想真正了解lua調(diào)試器的實(shí)現(xiàn),還是具體看看代碼,自己跑一下測(cè)試demo吧。
總結(jié)
以上是生活随笔為你收集整理的自己动手实现Lua调试器的全部?jī)?nèi)容,希望文章能夠幫你解決所遇到的問(wèn)題。
- 上一篇: 利用Win32 Debug API打造自
- 下一篇: C++ 十字链表图转java版