一個簡易版本的lua debugger實現

introduction

工欲善其事,必先利其器。lua作爲一門動態語言,雖然我已經習慣了使用print來進行代碼調試,但是還是有很多童鞋覺得一款好用的調試器能更好的進行lua代碼編寫。所以在以前接手遊戲的lua結合層之後,自然就需要提供一個debuger工具了。

我們只需要的是一個能快速進行lua代碼調試的工具,所以不需要gdb那種額外複雜的功能,只需要提供幾種簡單的功能就行了,如下:

  • c/continue 繼續執行
  • bt/backtrace 列出當前堆棧
  • f/frame n 跳轉到frame n
  • l/list b e 列出源代碼,b爲起始行,e爲結束行
  • p/print v 打印v的值
  • n/next 執行,跳過下一行,包括跳過子函數
  • s/step 執行,直到碰到不同的一行
  • return 執行,直到該函數結束

雖然調試器實現的功能很簡單,但是對於大多數應用來說,已經完全足夠使用。

lua debug library

lua提供了一個debug library,我們就通過這個庫來實現一個調試器。 首先,我們需要註冊一個lua debug hook,並且綁定LUA_MASKLINE,LUA_MASKRET,LUA_MASKCALL事件,這樣當lua代碼執行的時候如果碰到相應的事件,則會調用我們註冊的debug hook。

當debug hook調用的時候,程序就進入debug模式,這時候就可以輸入對應的命令進行執行。

db

只要註冊了debug hook,那麼每次lua代碼執行的時候碰到對應的事件就會調用註冊hook,如果每次調用都進入debug模式,那是很影響程序運行的,所以我們需要一種機制,只在需要的時候進入debug模式。

在gdb裏面,我們通過設置斷點來進入可調式模式,雖然在lua裏面也可以這麼做,但這裏我們採用了一種更簡單的方法。我們給lua註冊一個db函數,當lua執行到db函數的時候,程序纔會進入debug模式。因爲lua是動態語言,如果我們需要在另一個地方也進行調試,只需要再次加入db函數,重啓程序即可。

所以這裏我們debug hook函數內部實現是一個狀態機,當沒有進入db的時候,雖然lua也會進行調用該hook,但該hook內部不作任何處理。只有當執行db函數進入debug模式,hook內部纔會有相應處理。

我們在debug hook裏面提供了多種狀態,包括none hook,step hook,next hook和return hook。

  • none hook,沒有進入debug模式,該hook不做任何處理
  • step hook,進入debug的step模式,當lua代碼執行到新的一行代碼時候做處理
  • next hook,進入debug的next模式,當lua代碼跳過下一行時候做處理
  • return hook,進入debug的return模式,當lua執行到當前函數退出時候做處理

continue

continue會讓程序繼續執行。該命令會讓hook切入none hook狀態,直到下次lua執行db函數進入debug模式。

step

step會讓hook切入step hook狀態,該hook會監聽LUA_MASKLINE事件,當該事件發生時候,step hook進行處理,打印當前代碼,並再次進入debug模式,供下次命令輸入。

next

next會讓hook切入next hook狀態,該hook也會監聽LUA_MASKLINE事件,但是next跟step最大的區別在於next是跳過一行,也就是說如果執行的lua代碼下一行是一個函數調用,step會進入函數內部,而next則會執行該函數,並跳過該函數這一行直到下一行。

所以next需要進行判斷LUA_MASKLINE是否進入了一個新的函數,這裏我們通過函數堆棧深度來進行,當lua代碼執行到一個新的函數的時候,它的函數堆棧深度會加1,所以我們只需要記錄當前的堆棧深度a,next執行到下一次LUA_MASKLINE時候,獲取堆棧深度b,如果a小於b,那麼表明進入了一個新的函數,所以我們不需要處理,直到再次獲取的堆棧深度等於a。

return

return會讓hook切入return hook狀態,該hook會監聽LUA_MASKRET事件,當該事件發生,return hook進行處理。這裏我們仍然需要進入是否進入新函數調用的判斷,因爲我們只想監聽的是當前函數的LUA_MASKRET事件,所以我們仍需要像next那樣進行堆棧深度的判斷。

backtrace/frame/list

backtrace,frame,list這幾個命令這裏列在一起,是因爲他們都跟lua_getstack,lua_getinfo這兩個函數有關係。我們通過lua_getstack初始化指定棧幀的lua_Debug結構,然後在通過調用lua_getinfo獲取相關棧幀信息。

print value

print value命令可能算是最複雜的一個命令,因爲有多個邏輯處理。當我們通過frame定位到某一層棧幀之後,就可以通過print打印相關的對象數據,供調試使用。

當print value的時候,首先我們查找value是否在當前函數裏面local變量裏面,如果沒有則查找該函數的upvalue,如果仍然沒有,則查找global,如果都沒找到,則輸出nil。

code

代碼在luahelper的debughelper,只是一個簡單的實現,還有一些問題需要考慮。

因爲lua的debug hook註冊的時候只能提供一個hook,所以爲了簡單起見,我對DebugHelper使用了單例模式,但是最好是一個lua實例對應一個debuger。要做到這樣,自己想到了兩種可能方法:

  • 使用LUAI_EXTRASPACE,並通過luai_userstateopen,luai_userstateclose將debuger綁定到lua實例上面,並通過luai_userstatethread進行debuger在coroutine的遷移。這種方法需要重新編譯lua代碼,適合集成lua源碼的項目。
  • debuger內部使用一個map來進行對應,但是在lua裏面需要替換coroutine的創建,因爲創建的coroutine也需要對應到同一個debuger上面。

兩種方法都懶得弄了,以後有機會去嘗試一下。

end

這裏只是簡單的實現了一個lua debuger,但是功能我覺得足以可以在實際項目中應用了。只是越來覺得,對於動態語言,print和log纔是我最喜歡的代碼調試方式,因爲簡單而且強迫你去思考整個程序的運行流程。不過把debuger放在這裏,也算是對自己以前遊戲開發的一個總結吧。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章