自己动手实现Lua调试器
這段時間在qnode項目中新增了一個叫ldb的子項目,它的作用是使用C語言實現了一個lua調試器,后面將會在qnode中嵌入對調試lua腳本的支持。
先來簡單提一下ldb的用法,在ldb目錄的子目錄test中,有一個main.c文件,其中使用ldb庫提供的API實現對lua腳本的調試演示:
#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提供了創建和銷毀ldb庫的API,分別是ldb_new和ldb_destroy。另外提供了一個API ldb_step_in,main文件中通過向lua層提供一個C api c_debug,在該函數中調用這個API,實現對lua的調試。
main函數將讀取my.lua文件來執行,它的內容如下:
function test()local ab = 2034print("test") endlocal t = {out=10} local a = 1014 b = 2024 print("before debug")c_break() test() print("after debug")來看看ldb庫當前實現的調試功能有哪些:
(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>括號中的都是該命令的縮寫。當前支持添加/禁止斷點,打印變量,查看文件內容,查看當前調用棧,step in模式支持逐行執行的調試,以及next模式會跳過函數執行等最基本的調試命令。
介紹完簡單的使用和功能,下面來介紹一下lua中為支持調試功能提供了哪些API,以及通過這些API如何實現一個lua調試器。
1)lua提供的hook功能
lua為了支持調試,提供了hook功能,使用者可以根據需要添加不同的hook處理函數供條件觸發時回調,包括以下幾種:
具體包括以下幾種hook類型:
這里用的最多的是LUA_HOOKCALL,LUA_HOOKLINE,如果注冊了這兩個類型的HOOK函數,則會分別在調用某函數和執行每一行代碼之后調用注冊的HOOK函數。
使用lua中提供的API,可以如下方式注冊HOOK函數:
有了HOOK函數,就可以在lua代碼每次執行的時候做一些動作了。
除了HOOK函數之外,lua自身還提供了lua_Debug結構體,這個結構體包括以下成員:
source 函數的定義位置。如果函數在字符串內被定義(通過loadstring 函數),source就是該字符串,如果函數在文件中被定義,source就是帶“@”前綴的文件名。short_src source的簡短版本(60個字符以內),對錯誤信息很有用。linedefined source中函數被定義處的行號。what 函數類型。如果foo是普通的Lua函數,結果為“Lua”;如果是C函數,結果為“C”;如果是Lua的主代碼段,結果為“main”。name 函數的名稱。namewhat name域的含義。可能的取值為:“global”、“local”、“method”、“field”,或者空字符串。空字符串意味著Lua無法找到這個函數名。nups 函數中的Upvalues的個數。func 函數本身。稍后介紹。 可以通過lua_getinfo得到一些很重要的信息,它的調用方式是:
lua_getinfo(state, params, ar),第二個參數是一個字符串,支持傳入多個字母,每個字母有不同的含義:
2) 如何打印變量?
變量分為局部和全局變量,因此搜索某個變量的時候是從內到外的方式搜索,搜索局部變量時,使用lua提供的API lua_getlocal函數,逐個搜索,具體可以看ldb.c中的search_local_var函數。
如果在局部變量中搜索不到,則還得使用lua_getglobal函數進行全局變量的搜索,具體見ldb.c中的search_global_var函數。
但是以上的過程僅僅還只能在相應的地方查找到同名的變量,在真正需要打印變量值的時候,還需要根據變量的類型具體來打印數據,代碼太多,不在這里列出,見ldb.c中的print_var函數。
3)如何查看文件的內容
查看文件的內容相對簡單,因為當lua被HOOK住的時候,可以通過lua_getinfo函數得到當前lua的一些信息,比如lua文件名,行號,在C實現的Lua調試器中,會維護一個已經讀取過的文件列表,如果當前所在的文件還沒有被讀取到內存中,那么會讀取到內存中,再根據所在的行號就可以得到文件內容的信息了。
4)斷點的添加
調試器的斷點分為兩種,一種是基于文件:行號形式的,一種則是基于函數調用形式的。
C實現的調試器中,首先需要定義一個數據結構類型,用于表示斷點:
這些信息包括斷點所在的文件,行號,函數名,當前是否被激活,被觸發的計數,等等。
先來看第一種形式斷點的實現。這種形式的斷點相對簡單。做法是創建一個新的斷點數據結構,保存下文件和行號,在每次HOOK函數中都去根據當前的文件和行號信息去查找是否匹配了某個斷點的信息,當然,當前的實現中這個查找是線性的,可能對性能有一定影響。
來看第二種形式斷點的實現。由于函數在lua中也是一種類型的變量,既然是變量那么就涉及到作用域。比如你在A模塊中斷點時,想給B模塊的fun函數下斷點,那么就不能簡單的寫”b func”,而應該是”b B.func”。所以在實現對函數進行斷點的時候要注意這一點。另外,當給某一個函數下斷點時,還需要添加LUA_HOOKCALL類型的HOOK,也就是在函數調用時被觸發。之所以這么做,是因為在查找斷點時,當首先使用文件名和行號都查找不到時,會判斷一下當前這次的HOOK調用,是不是一個函數調用觸發的HOOK,如果是的話再繼續根據斷點的函數名進行查找匹配。
5)如何查看當前堆棧信息
也就是模擬gdb中的bt指令的功能。lua對這個已經有支持了,可以使用lua_getstack函數,具體見ldb.c中的dump_stack函數。
6)step和next指令的實現
首先來看一個子問題,如何得到當前的函數堆棧數量,或者說當前的調用層次,通過反復調用前面提到的lua_getstack函數可以獲取到:
這兩個指令的實現稍微有點難度,所以放在最后一個講解。step就是逐行執行代碼,即使調用函數的時候也會跟進該函數中,而next指令會在調用函數的時候不跟進函數的調用。所以這兩者的區別在于調用時的函數堆棧,因此這兩個指令的區別僅在于遇到函數的時候是不是繼續跟進去。所以我的做法是新增一個變量用于保存當前的函數棧索引,當step模式時將這個值置為-1,next模式時只會保存為當前的函數棧索引,如果某個指令是調用一個函數時,這時通過get_calldepth函數獲得的函數棧就會比之前的大,這樣就可以知道當前的這個指令是不是調用一個函數了,next指令可以在這個時候返回不做任何處理,而step指令可以繼續執行下去。
以上就是簡單的原理性介紹,如果想真正了解lua調試器的實現,還是具體看看代碼,自己跑一下測試demo吧。
總結
以上是生活随笔為你收集整理的自己动手实现Lua调试器的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 利用Win32 Debug API打造自
- 下一篇: C++ 十字链表图转java版