一、前言上篇文章我們聊了gdb的底層調(diào)試機(jī)制,明白了gdb是利用操作系統(tǒng)提供的系統(tǒng)信號(hào)來調(diào)試目標(biāo)程序的。很多朋友私下留言了,看到能幫助到大家,我心里還是很開心的,其實(shí)這也是我繼續(xù)輸出文章的最大動(dòng)力!后面我會(huì)繼續(xù)把自己在項(xiàng)目開發(fā)中的實(shí)戰(zhàn)經(jīng)驗(yàn)進(jìn)行總結(jié)。
由于gdb的代碼相對(duì)復(fù)雜,沒有辦法從代碼層面仔細(xì)的分析調(diào)試細(xì)節(jié),所以這次我們選擇一個(gè)小巧、開源的Lua腳本語言,深入到最底層的代碼中去探究一下代碼調(diào)試真正是怎么一回事。
不過請放心,雖然深入到代碼最底層,但是理解難度并不大,只要C語言掌握的沒問題,其他就都不是問題。另外,這篇文章重點(diǎn)不是介紹代碼,而是介紹實(shí)現(xiàn)一個(gè)調(diào)試器應(yīng)該如何思考,解決問題的思路是什么。
通過閱讀這篇文章,能有什么收獲?
- 如果你使用過Lua語言,那么你能夠從源代碼級(jí)別了解到調(diào)試庫的代碼邏輯。
- 如果你對(duì)Lua不了解,可以從設(shè)計(jì)思想、實(shí)現(xiàn)架構(gòu)上學(xué)習(xí)到一門編程語言是如何進(jìn)行調(diào)試程序的。
二、Lua 語言簡介
1. Lua是什么鬼?
喜歡玩游戲的小伙伴可能會(huì)知道,Lua語言在游戲開發(fā)中使用的比較多。它是一個(gè)輕量、小巧的腳本語言,用標(biāo)準(zhǔn)C語言編寫,源碼開放。正因?yàn)檫@幾個(gè)原因,所以我才選擇它作為剖析對(duì)象。
如果對(duì)于Lua語言還是沒有感覺,Python語言總應(yīng)該知道吧?廣告滿天飛,你就把Lua想象為類似Python一樣的腳本語言,只不過體積比Python要輕量的得多。
這里有1張圖可以了解下,2020年12月份的編程語言市場占有率。
在上圖中看不到Lua的身影,因?yàn)槭袌稣加新侍土?,大概是位?0幾名。但是再看看下面這張圖,從工資的角度再體會(huì)一下Lua的高貴:
遠(yuǎn)遠(yuǎn)的把C/C++、JAVA甩在了身后,是不是有點(diǎn)沖動(dòng)想學(xué)一下Lua語言了?先別激動(dòng),學(xué)習(xí)任何東西,先要想明白可以用在什么地方。如果僅僅是從找工作的角度來,Lua可以不用考慮了,畢竟市場需求量比較小。
2. 為什么選擇Lua語言作為研究對(duì)象?
雖然Lua語言在招聘網(wǎng)站中處于小眾需求,但是這并不妨礙我們利用Lua來深入的學(xué)習(xí)、研究一門編程語言,Lua語言雖小,但是五臟俱全。就像我們?nèi)绻雽W(xué)習(xí)Linux內(nèi)核的設(shè)計(jì)思想,你是愿意從最開始的版本(幾千行代碼)開始呢?還是愿意從當(dāng)前最新的內(nèi)核代碼(2780萬行代碼,66492個(gè)文件)開始呢?
看一下當(dāng)前最新版的Lua代碼體積:
同樣的思路,如果我們想深入研究一門編程語言,選擇哪一種語言,對(duì)于我們的積極性和學(xué)習(xí)效率是非常重要的。每個(gè)人的職業(yè)生涯都很長,花一些時(shí)間沉下心來研究透一門語言,對(duì)于一個(gè)開發(fā)者來說,還是蠻有成就的,對(duì)于職業(yè)的發(fā)展是非常有好處的,你會(huì)有一覽眾山小的感覺!
再看一下Lua代碼量與Python代碼量的對(duì)比:
從功能上來說,Lua與Python之間是沒有可比性的,但是我們的目的不是學(xué)習(xí)一個(gè)編程工具,而是研究一門編程語言本身,因此選擇Lua腳本語言進(jìn)行學(xué)習(xí)、研究,沒有錯(cuò)!
言歸正傳。
三、Lua源代碼5.3.5
1. Lua程序是如何執(zhí)行的?
Lua 是一門擴(kuò)展式程序設(shè)計(jì)語言,被設(shè)計(jì)成支持通用過程式編程,并有相關(guān)數(shù)據(jù)描述設(shè)施。同時(shí)對(duì)面向?qū)ο缶幊?、函?shù)式編程和數(shù)據(jù)驅(qū)動(dòng)式編程也提供了良好的支持。它作為一個(gè)強(qiáng)大、輕量的嵌入式腳本語言,可供任何需要的程序使用。
作為一門擴(kuò)展式語言,Lua沒有"main"程序的概念:它只能嵌入一個(gè)宿主程序中工作,該宿主程序被稱為被嵌入程序或者簡稱宿主。宿主程序可以調(diào)用函數(shù)執(zhí)行一小段Lua代碼,可以讀寫Lua變量,可以注冊C函數(shù)讓Lua代碼調(diào)用。依靠C函數(shù),Lua可以共享相同的語法框架來定制編程語言,從而適用不同的領(lǐng)域。
也就是說,我們寫了一個(gè)test.lua程序,是沒有辦法直接運(yùn)行它的。而實(shí)需要一個(gè)“宿主”程序,來加載test.lua文件。
宿主程序可以是一個(gè)最簡單的C程序,Lua官方提供了一個(gè)宿主程序。
我們也可以自己寫一個(gè),如下:
// 引入Lua頭文件
int main(int argc, char *argv[])
{
// 創(chuàng)建一個(gè)Lua虛擬機(jī)
lua_State *L = luaL_newstate();
// 打開LUA中的標(biāo)準(zhǔn)庫
luaL_openlibs(L);
// 加載 test.lua 程序
if (luaL_loadfile(L, "test.lua") || lua_pcall(L, 0, 0, 0))
{
printf("Error: %s \\n", lua_tostring(g_lua_handle.L, -1));
lua_close(g_lua_handle.L);
}
// 其他代碼
}
2. Lua語法
在語法層面,Lua涵蓋的內(nèi)容還是比較全面的,它是一門動(dòng)態(tài)類型語言,基本概念包括:八種基本數(shù)據(jù)類型,表是唯一的數(shù)據(jù)結(jié)構(gòu),環(huán)境與全局變量,元表及元方法,協(xié)程,閉包,錯(cuò)誤處理,垃圾收集。具體的信息可以看一下Lua5.3參考手冊。
這篇文章主要從調(diào)試器這個(gè)角度進(jìn)行分析,因此我不會(huì)在這里詳細(xì)的貼出很多代碼細(xì)節(jié),而只是把與調(diào)試有關(guān)的代碼貼出來進(jìn)行解釋。
我之前在學(xué)習(xí)Lua源碼時(shí)(5.3.5版本),在代碼文件中記錄了很多注釋,可以很好的幫助理解,主要是因?yàn)槲业耐员容^好。
其實(shí)我更建議大家自己去下載源碼學(xué)習(xí),經(jīng)過自己的理解、加工,印象會(huì)更深刻。在之前的工作中,由于項(xiàng)目需要,我對(duì)源碼進(jìn)行了一些優(yōu)化,這部分代碼就不放出來了,添加注釋的源碼是完完全全的Lua5.3.5版本,大概是這個(gè)樣子:
如果有小伙伴需要加了注釋的源碼,請?jiān)诠娞?hào)(IOT物聯(lián)網(wǎng)小鎮(zhèn))里留言給我。
四、Lua調(diào)試庫相關(guān)
我們可以停下來稍微想一下,對(duì)一個(gè)程序進(jìn)行調(diào)試,需要考慮的問題有3點(diǎn):
- 如何讓程序暫停執(zhí)行?
- 如何獲取程序的內(nèi)部信息?
- 如果修改程序的內(nèi)部信息?
帶著這些問題,我們來逐個(gè)擊破。
1. 鉤子函數(shù)(Hook):讓程序暫停執(zhí)行
Lua虛擬機(jī)(也可稱之為解釋器)內(nèi)部提供了一個(gè)接口:用戶可以在應(yīng)用程序中設(shè)置一個(gè)鉤子函數(shù)(Hook),虛擬機(jī)在執(zhí)行指令碼的時(shí)候會(huì)檢查用戶是否設(shè)置了鉤子函數(shù),如果設(shè)置了,就調(diào)用這個(gè)鉤子函數(shù)。本質(zhì)上就是設(shè)置一個(gè)回調(diào)函數(shù),因?yàn)槎际怯肅語言來實(shí)現(xiàn)的,虛擬機(jī)中只要把這個(gè)鉤子函數(shù)的地址記住,然后在某些場合回調(diào)這個(gè)函數(shù)就可以了。
那么,虛擬機(jī)在哪些場合回調(diào)用戶設(shè)置的鉤子函數(shù)呢?
我們在設(shè)置Hook函數(shù)的時(shí)候,可以通過mask參數(shù)來設(shè)置回調(diào)策略,也就是告訴虛擬機(jī):在什么時(shí)候來回調(diào)鉤子函數(shù)。mask參數(shù)可以是下列選項(xiàng)的組合操作:
- LUA_MASKCALL:調(diào)用一個(gè)函數(shù)時(shí),就調(diào)用一次鉤子函數(shù)。
- LUA_MASKRET:從一個(gè)函數(shù)中返回時(shí),就調(diào)用一次鉤子函數(shù)。
- LUA_MASKLINE:執(zhí)行一行指令時(shí),就回調(diào)一次鉤子函數(shù)。
- LUA_MASKCOUNT:執(zhí)行指定數(shù)量的指令時(shí),就回調(diào)一次鉤子函數(shù)。
設(shè)置鉤子函數(shù)的基礎(chǔ)API原型如下:
void lua_sethook (lua_State *L, lua_Hook f, int mask, int count);
第二個(gè)參數(shù)f需要指向我們自己定義的鉤子函數(shù),這個(gè)鉤子函數(shù)原型為:
typedef void (*lua_Hook) (lua_State *L, lua_Debug *ar);
我們也可以通過下面即將介紹的調(diào)試庫中的函數(shù)來設(shè)置鉤子函數(shù),效果是一樣的,因?yàn)檎{(diào)試庫函數(shù)的內(nèi)部也是調(diào)用基礎(chǔ)函數(shù)。
debug.sethook ([thread,] hook, mask [, count])
再來看一下虛擬機(jī)中的相關(guān)代碼。當(dāng)執(zhí)行完上一條指令,獲取下一條指令之后,調(diào)用函數(shù) *luaG_traceexec(lua_State L) :
void luaG_traceexec (lua_State *L) {
// 獲取mask掩碼
lu_byte mask = L->hookmask;
int counthook = (--L->hookcount == 0 && (mask & LUA_MASKCOUNT));
if (counthook)
resethookcount(L);
else if (!(mask & LUA_MASKLINE))
return;
if (counthook)
luaD_hook(L, LUA_HOOKCOUNT, -1); // 按指令次數(shù)調(diào)用鉤子函數(shù)
if (mask & LUA_MASKLINE) {
Proto *p = ci_func(ci)->p;
int npc = pcRel(ci->u.l.savedpc, p);
int newline = getfuncline(p, npc);
if (npc == 0 ||
ci->u.l.savedpc <= L->oldpc ||
newline != getfuncline(p, pcRel(L->oldpc, p)))
luaD_hook(L, LUA_HOOKLINE, newline); // 按行調(diào)用鉤子函數(shù)
}
}
可以看到,當(dāng)mask掩碼中包含了LUA_MASKLINE時(shí),就調(diào)用函數(shù)luaD_hook(),如下代碼:
voidluaD_hook (lua_State *L, intevent, int line) {
lua_Hook hook = L->hook;
if (hook && L->allowhook) {
// 為鉤子函數(shù)準(zhǔn)備參數(shù),其中包括了各種調(diào)試信息
lua_Debug ar;
ar.event = event;
ar.currentline = line;
ar.i_ci = ci;
// 調(diào)用鉤子函數(shù)
(*hook)(L, &ar);
}
}
只要進(jìn)入了用戶設(shè)置的鉤子函數(shù),那么我們就可以在這個(gè)函數(shù)中為所欲為了。
比如:獲取程序內(nèi)部信息,讀取、修改變量的值,查看函數(shù)調(diào)用棧信息等等,這就是下面要講解的內(nèi)容。
2. Lua調(diào)試庫是什么?
首先說一下Lua中的標(biāo)準(zhǔn)庫。所謂的標(biāo)準(zhǔn)庫就是Lua為開發(fā)者提供的一些有用的函數(shù),可以提高開發(fā)效率,當(dāng)然我們可以選擇不使用標(biāo)準(zhǔn)庫,或者只使用部分標(biāo)準(zhǔn)庫,這是可以裁剪的。
這里我們只介紹一下基礎(chǔ)庫、操作系統(tǒng)庫和調(diào)試庫這3個(gè)家伙。
基礎(chǔ)庫
基礎(chǔ)庫提供了Lua核心函數(shù),如果你不將這個(gè)庫包含在你的程序中,就需要小心檢查程序是否需要自己提供其中一些特性的實(shí)現(xiàn),這個(gè)庫一般都是需要使用的。
操作系統(tǒng)庫
這個(gè)庫提供與操作系統(tǒng)進(jìn)行交互的功能,例如提供了函數(shù):
os.date
os.time
os.execute
os.exit
os.getenv
調(diào)試庫
先看一下庫中提供的幾個(gè)重要的函數(shù):
debug.gethook
debug.sethook
debug.getinfo
debug.getlocal
debug.setlocal
debug.setupvalue
debug.traceback
debug.getregistry
上面已經(jīng)說到,Lua給用戶提供了設(shè)置鉤子的API函數(shù)lua_sethook,用戶可以直接調(diào)用這個(gè)函數(shù),此時(shí)傳入的鉤子函數(shù)的定義格式需要滿足要求。
為了簡化用戶編程,Lua還提供了調(diào)試庫來幫助用戶降低編程難度。調(diào)試庫其實(shí)也就是把基礎(chǔ)API函數(shù)進(jìn)行封裝了一下,我們以設(shè)置鉤子函數(shù)debug.sethook為例:文件ldblib.c中,定義了調(diào)試庫支持的所有函數(shù):
staticintdb_sethook(lua_State *L) {
lua_sethook(L1, func, mask, count);
}
static const luaL_Reg dblib[] = {
// 其他接口函數(shù)都刪掉了,只保留這一個(gè)來講解
{"sethook", db_sethook},
{NULL, NULL}
};
// 這個(gè)函數(shù)用來把調(diào)試庫中的函數(shù)注冊到全局變量表中
LUAMOD_API intluaopen_debug(lua_State *L) {
luaL_newlib(L, dblib);
return 1;
}
可以看到,調(diào)試庫的debgu.sethook()函數(shù)最終也是調(diào)用基礎(chǔ)API函數(shù):lua_sethook()。
在后面的調(diào)試器開發(fā)講解中,我就是用debug庫來實(shí)現(xiàn)一個(gè)遠(yuǎn)程調(diào)試器。
3. 獲取程序內(nèi)部信息
在鉤子函數(shù)中,可以通過如下API函數(shù)還獲取程序內(nèi)部的信息了:
int lua_getinfo (lua_State *L, const char *what, lua_Debug *ar);
在這個(gè)API函數(shù)中:
第二個(gè)參數(shù)用來告訴虛擬機(jī)我們想獲取程序的哪些信息
第三個(gè)參數(shù)用來存儲(chǔ)獲取到的信息
結(jié)構(gòu)體lua_Debug比較重要,成員變量如下:
typedef structlua_Debug {
int event;
const char *name; /* (n) */
const char *namewhat; /* (n) */
const char *what; /* (S) */
const char *source; /* (S) */
int currentline; /* (l) */
int linedefined; /* (S) */
int lastlinedefined; /* (S) */
unsigned char nups; /* (u) 上值的數(shù)量 */
unsigned char nparams; /* (u) 參數(shù)的數(shù)量 */
char isvararg; /* (u) */
char istailcall; /* (t) */
char short_src[LUA_IDSIZE]; /* (S) */
/* 私有部分 */
其它域
} lua_Debug;
- source:創(chuàng)建這個(gè)函數(shù)的代碼塊的名字。如果 source 以 '@' 打頭, 指這個(gè)函數(shù)定義在一個(gè)文件中,而 '@' 之后的部分就是文件名。
- linedefined: 函數(shù)定義開始處的行號(hào)。
- lastlinedefined: 函數(shù)定義結(jié)束處的行號(hào)。
- currentline: 給定函數(shù)正在執(zhí)行的那一行。
其他字段可以在參考手冊中查詢。例如:如果想知道函數(shù) f 是在哪一行定義的, 你可以使用下列代碼:
lua_Debug ar;
lua_getglobal(L, "f"); /* 取得全局變量 'f' */
lua_getinfo(L, ">S", &ar);
printf("%d\\n", ar.linedefined);
同樣的,也可以調(diào)用調(diào)試庫debug.getinfo()來達(dá)到同樣的目的。
4. 修改程序內(nèi)部信息
經(jīng)過上面的講解,已經(jīng)看到我們獲取程序信息都是通過Lua提供的API函數(shù),或者是利用調(diào)試庫提供的接口函數(shù)來完成的。那么修改程序內(nèi)部信息也同樣如此。Lua提供了下面這2個(gè)API函數(shù)來修改函數(shù)中的變量:
- 修改當(dāng)前活動(dòng)記錄總的局部變量的值:
const char *lua_setlocal (lua_State *L, const lua_Debug *ar, int n);
- 設(shè)置閉包上值的值(上值upvalue就是閉包使用了外層的那些變量)
const char *lua_setupvalue (lua_State *L, int funcindex, int n);
同樣的,也可以利用調(diào)試庫中的debug.setlocal和debug.setupvalue來完成同樣的功能。
5. 小結(jié)
到這里,我們就把Lua語言中與調(diào)試有關(guān)的機(jī)制和代碼都理解清楚了,剩下的問題就是如何利用它提供的這些接口,來編寫一個(gè)類似gdb一樣的調(diào)試器。
就好比:Lua已經(jīng)把材料(米、面、菜、肉、佐料)擺在我們的面前了,剩下的就需要我們把這些材料做成一桌美味佳肴。## 五、Lua調(diào)試器開發(fā)
1. 與gdb調(diào)試模型做類比
上一篇文章說過,gdb調(diào)試模型有兩種:本地調(diào)試和遠(yuǎn)程調(diào)試。
本地調(diào)試
遠(yuǎn)程調(diào)試
那么,我們也可以按照這個(gè)思路來實(shí)現(xiàn)兩種調(diào)試模型,只要把其中的gdb替換成ldb,gdbserver替換成ldbserver即可。
本地調(diào)試
遠(yuǎn)程調(diào)試
這兩種調(diào)試模型本質(zhì)是一樣的,只是調(diào)試程序和被調(diào)試程序是否運(yùn)行在同一臺(tái)電腦上而已。
如果是遠(yuǎn)程調(diào)試,ldbserver調(diào)用接口函數(shù)對(duì)被調(diào)試程序進(jìn)行控制,然后把結(jié)果通過TCP網(wǎng)絡(luò)傳遞給ldb,ldbserver就相當(dāng)于一個(gè)傳話筒。
至于選擇實(shí)現(xiàn)哪一種調(diào)試模型?這個(gè)要根據(jù)實(shí)際場景的需求來決定。我在這里實(shí)現(xiàn)的是遠(yuǎn)程調(diào)試,因?yàn)楸徽{(diào)試程序是需要運(yùn)行在ARM板子(下位機(jī))中的,但是調(diào)試器是需要運(yùn)行在PC電腦上(上位機(jī))的,通過遠(yuǎn)程調(diào)試,只需要把ldbserver和被調(diào)試程序放到下位機(jī)中運(yùn)行,ldb嵌入到上位機(jī)的集成開發(fā)環(huán)境(IDE)中運(yùn)行就可以了。
另外,遠(yuǎn)程調(diào)試模型同樣也可以全部運(yùn)行在同一臺(tái)PC電腦中,這個(gè)時(shí)候ldb與ldbserver之間就是在本機(jī)中進(jìn)行TCP網(wǎng)絡(luò)連接。
這里有2個(gè)內(nèi)容需要補(bǔ)充一下:
- TCP鏈接可以直接利用第三方庫luasocket。
- ldb與ldbserver之間的通訊協(xié)議可以參照gdb與gdbserver之間的協(xié)議,也可以自定義。我借鑒了HTTP協(xié)議,簡化了很多。
2. ldbserver如何實(shí)現(xiàn)
思考一個(gè)問題:被調(diào)試程序在執(zhí)行時(shí)調(diào)用鉤子函數(shù),在鉤子函數(shù)中我們可以做各種調(diào)試操作,但是在執(zhí)行到鉤子函數(shù)的最后,是需要返回到被調(diào)試程序中的下一行指令碼繼續(xù)執(zhí)行的,我們不能打斷被調(diào)試程序的執(zhí)行序列。
但是,調(diào)試操作又需要通過TCP連接與上位機(jī)進(jìn)行通信協(xié)議的交互,比如:設(shè)置斷點(diǎn)、查看變量的值、查看函數(shù)信息等等。所以,被調(diào)試程序的執(zhí)行與調(diào)試器ldbserver的執(zhí)行是2個(gè)并發(fā)的執(zhí)行序列,可以理解為2個(gè)線程在并發(fā)執(zhí)行。我們需要在這2個(gè)執(zhí)行序列之間進(jìn)行協(xié)調(diào),比如:
- ldbserver在等待用戶輸入指令時(shí)(running),被調(diào)試程序應(yīng)該處于暫停狀態(tài)(pending)。
- ldbserver接收到用戶指令后(eg: run),自己應(yīng)該暫停執(zhí)行(pending),讓被調(diào)試程序繼續(xù)執(zhí)行(running)。
上圖中,兩條紅色箭頭表示兩個(gè)執(zhí)行序列。這兩個(gè)執(zhí)行序列并不是同時(shí)在執(zhí)行的,而是交替執(zhí)行,如下圖所示:
那么怎么樣才能讓這2個(gè)執(zhí)行序列交替執(zhí)行呢?
如果是在C語言中,我們可以通過信號(hào)量、互斥鎖等各種方法實(shí)現(xiàn),但這是在Lua語言中,應(yīng)該利用什么機(jī)制來實(shí)現(xiàn)這個(gè)功能?
柳暗花明又一村!
Lua中提供了協(xié)程機(jī)制!下面這段話是從參考手冊中摘抄過來:
- Lua 支持協(xié)程,也叫協(xié)同式多線程。一個(gè)協(xié)程在 Lua 中代表了一段獨(dú)立的執(zhí)行線程。然而,與多線程系統(tǒng)中的線程的區(qū)別在于, 協(xié)程僅在顯式調(diào)用一個(gè)讓出(yield)函數(shù)時(shí)才掛起當(dāng)前的執(zhí)行。
- 調(diào)用函數(shù)coroutine.create可創(chuàng)建一個(gè)協(xié)程。
- 調(diào)用coroutine.resume函數(shù)執(zhí)行一個(gè)協(xié)程。
- 通過調(diào)用coroutine.yield使協(xié)程暫停執(zhí)行,讓出執(zhí)行權(quán)。
我們可以讓ldbserver運(yùn)行在一個(gè)協(xié)程中,被調(diào)試程序運(yùn)行在主程序中。當(dāng)虛擬機(jī)執(zhí)行一條被調(diào)試程序的指令碼之后,調(diào)用鉤子函數(shù),在鉤子函數(shù)中通過coroutine.resume讓協(xié)程運(yùn)行,主程序停止。前面說到,ldbserver運(yùn)行在運(yùn)行在一個(gè)協(xié)程中,此時(shí)就可以在ldbserver中利用阻塞函數(shù)(例如:TCP 中的receive),接收用戶的調(diào)試指令。
假設(shè)用戶發(fā)送來全速執(zhí)行指令(run),ldbserver就調(diào)用coroutine.yield讓自己掛起,此時(shí)被調(diào)試程序所在的主程序就可以繼續(xù)執(zhí)行了。
進(jìn)行到這里,基本上大功告成!剩下的就是一些代碼細(xì)節(jié)問題了。
3. ldb如何實(shí)現(xiàn)
這部分就比較簡單了,從功能上來說包括3部分內(nèi)容:
- 與ldbserver之間建立TCP連接。
- 讀取調(diào)試人員輸入的指令,發(fā)送給ldbserver。
- 接收ldbserver發(fā)來的信息,顯示給調(diào)試人員。
可以在調(diào)試終端中手動(dòng)輸入、顯示調(diào)試信息,也可以把ldb嵌入到一個(gè)可視化的編輯工具中,例如:
local functionprint_commands()
print("setb -- sets a breakpoin" )
print("step -- run one line, stepping into function")
print("next -- run one line, stepping over function")
print("goto -- goto line in a function" )
// 其他指令
end
六、調(diào)試指令舉例
1. break指令的實(shí)現(xiàn)
(1)設(shè)置鉤子函數(shù)
ldbserver通過調(diào)試庫的debug.sethook函數(shù),設(shè)置了一個(gè)鉤子函數(shù),調(diào)用參數(shù)是:
debug.sethook(my_hook, "lcr")
第二個(gè)參數(shù)"lcr"的含義是:
'c': 每當(dāng) Lua 調(diào)用一個(gè)函數(shù)時(shí),調(diào)用鉤子。
'r': 每當(dāng) Lua 從一個(gè)函數(shù)內(nèi)返回時(shí),調(diào)用鉤子。
'l': 每當(dāng) Lua 進(jìn)入新的一行時(shí),調(diào)用鉤子。
也即是說:虛擬機(jī)進(jìn)入一個(gè)函數(shù)、從一個(gè)函數(shù)返回、每執(zhí)行一行代碼,都調(diào)用一次鉤子函數(shù)。注意:這里的一行指定是被調(diào)試程序中的一行Lua代碼,而不是二進(jìn)制文件中的一行指令碼,一行Lua代碼可能被會(huì)編譯生成多行指令碼。
這里還有一點(diǎn)需要注意:鉤子函數(shù)雖然是定義在用戶代碼中,但是它是被虛擬機(jī)調(diào)用的,也就是說鉤子函數(shù)是處于主程序的執(zhí)行序列中。
(2)設(shè)置斷點(diǎn)
ldb向ldbserver發(fā)送設(shè)置斷點(diǎn)的指令:setb test.lua 10,即:在test.lua文件的第10行設(shè)置一個(gè)斷點(diǎn),ldbserver接收到指令后,在內(nèi)存中記錄這個(gè)信息(文件名-行號(hào))。
(3)捕獲斷點(diǎn)
虛擬機(jī)在調(diào)用鉤子函數(shù)時(shí),傳入兩個(gè)參數(shù)(注意:鉤子函數(shù)是被虛擬機(jī)調(diào)用的,所以它是處于主程序的執(zhí)行序列中),
local function my_hook(event, line)
在鉤子函數(shù)中,查找這個(gè)line是否被用戶設(shè)置為斷點(diǎn),如果是那么就通過coroutine.resume讓主程序暫停,讓協(xié)程中的ldbserver執(zhí)行。此時(shí),ldbserver就可以在TCP網(wǎng)絡(luò)上繼續(xù)等待ldb發(fā)來的下一個(gè)調(diào)試指令。
2. next指令的實(shí)現(xiàn)
next指令與step指令類似,區(qū)別在于當(dāng)下一條指令是一個(gè)函數(shù)調(diào)用時(shí):
step指令: 進(jìn)入到函數(shù)內(nèi)部。
next指令: 不進(jìn)入函數(shù)內(nèi)部,而是直接把這個(gè)函數(shù)執(zhí)行完。
next指令的實(shí)現(xiàn)主要依賴于鉤子函數(shù)的第一個(gè)參數(shù)event,上面在設(shè)置鉤子函數(shù)的時(shí)候,告訴虛擬機(jī)在3種條件下調(diào)用鉤子函數(shù),重新貼一下:
'c': 每當(dāng) Lua 調(diào)用一個(gè)函數(shù)時(shí),調(diào)用鉤子
'r': 每當(dāng) Lua 從一個(gè)函數(shù)內(nèi)返回時(shí),調(diào)用鉤子
'l': 每當(dāng) Lua 進(jìn)入新的一行時(shí),調(diào)用鉤子
在進(jìn)入鉤子函數(shù)之后,event參數(shù)會(huì)告訴我們:為什么會(huì)調(diào)用鉤子函數(shù)。代碼如下:
function my_hook(event, line)
if event == "call" then
// 進(jìn)入了一個(gè)函數(shù)
func_level = func_level + 1
elseif event == "return" then
// 從一個(gè)函數(shù)返回
func_level = func_level - 1
else
// 執(zhí)行完一行代碼
end
所以就可以利用event參數(shù)來記錄進(jìn)入、退出函數(shù)層數(shù),然后在鉤子函數(shù)中判斷:是否需要暫停主程序,把執(zhí)行的機(jī)會(huì)讓給協(xié)程。
3. goto指令的實(shí)現(xiàn)
在調(diào)試過程中,如果我們想跳過當(dāng)前執(zhí)行函數(shù)中的某幾行,可以發(fā)送goto指令,被調(diào)試程序就從當(dāng)前停止的位置直接跳轉(zhuǎn)到goto指令中設(shè)置的那行代碼。
目前goto指令有一個(gè)限制:
因?yàn)長ua虛擬機(jī)中的所有代碼都是以函數(shù)為單位的,通過函數(shù)調(diào)用棧把所有的代碼串接在一起,因此只能goto到當(dāng)前函數(shù)內(nèi)的指定行。
這部分功能Lua源碼中并沒有提供,需要擴(kuò)展調(diào)試庫的功能。核心步驟就是:強(qiáng)制把虛擬機(jī)中的PC指針設(shè)置為指定的那行Lua代碼所對(duì)應(yīng)的第一個(gè)指令碼。
ar->i_ci->u.l.savedpc = cl->p->code + 需要跨過的指令碼
ar變量就是調(diào)試庫為我們準(zhǔn)備的:
const lua_Debug *ar
(如果你能跟著思路看到這里,我心里時(shí)非常非常的感激,能容忍我這么嘮叨這么久。到這里我想表達(dá)的內(nèi)容也差不多結(jié)束了,后面兩個(gè)模塊如果有興趣的話可以稍微了解一下,不是重點(diǎn)。)
七、其他重要的模塊
這部分先空著,如果有小伙伴想要詳細(xì)了解的話,請?jiān)诠娞?hào)(IOT物聯(lián)網(wǎng)小鎮(zhèn))中留言給我,單獨(dú)整理成文檔。比較重要的內(nèi)容包括:
- 標(biāo)準(zhǔn)庫的加載過程
- 函數(shù)調(diào)用棧
- 同時(shí)調(diào)試多個(gè)程序
- 如何處理中斷信號(hào)
- 如何處理中斷信號(hào)嵌套問題
- 如何添加自己的庫
- 如何同時(shí)調(diào)試多個(gè)程序
- 其他指令的實(shí)現(xiàn)機(jī)制:查看、修改變量,查看函數(shù)調(diào)用棧,多個(gè)被調(diào)試程序的切換等等。
八、調(diào)試操作步驟
關(guān)于實(shí)際操作步驟,用文檔表達(dá)起來比較費(fèi)勁,全部是黑乎乎的終端窗口。計(jì)劃錄一個(gè)60分鐘左右的視頻,把上面提到的內(nèi)容都操作演示一遍,這樣效果會(huì)更好一下。有興趣的話可以在B站搜一下我的ID(道哥分享)。內(nèi)容主要包括:
- 在Linux平臺(tái)下:編譯和調(diào)試步驟。
- Windows平臺(tái)下:編譯和調(diào)試步驟。
- 簡單的圖形調(diào)試界面,就是把ldb嵌入到IDE中。
-
代碼
+關(guān)注
關(guān)注
30文章
4788瀏覽量
68616 -
gdb
+關(guān)注
關(guān)注
0文章
60瀏覽量
13303 -
lua腳本
+關(guān)注
關(guān)注
0文章
21瀏覽量
7590
發(fā)布評(píng)論請先 登錄
相關(guān)推薦
評(píng)論