Lua語言:實現一個偵錯程式玩具
前言
這一章我們來做一個偵錯程式的玩具,說它是玩具,是因為要做一個真正可用的偵錯程式並不容易,這裡只是想透過這個程式來理解debug庫的使用。
debug庫概述
下面介紹幾個重要的debug函式:
debug。debug 進入命令列互動模式,在這裡相當於進入了虛擬機器內部,可以檢視全域性變數,但是這是一個獨立的上下文環境,不能檢視呼叫它的函式的區域性變數。後面將利用這個API和一些技巧,實現除錯的互動操作,那時就可能檢視斷點處的區域性變數,upvalue等。
debug。getinfo ([thread,] f [, what]) 取函式除錯資訊,f可以是一個函式,也可以是呼叫堆疊的層數,1表示呼叫getinfo的函式,2表示再上一層,以此類推。what指定想查哪些資訊,不指定表示全部資訊。
debug。getlocal ([thread,] f, local) 取函式的本地變數,注意f是棧的層次,和上面一樣1表示呼叫debug。getlocal的函式。local從1開始取函式引數和本地變數,從-1開始是可變引數,返回名字和值。如果取不到返回nil
debug。getupvalue (f, up) 取函式引用的upvalue,f是一個函式物件,up從1開始,返回名字和值。如果取不到返回nil。
debug。sethook ([thread,] hook, mask [, count]),設定一個hook,當指定事件發生時,這個hook會被回撥,hook是回撥函式,mask表明想觸發什麼事件,如果mask為空,count須指定一個值表示執行多少條指令後觸發。mask是下面值的組合:
‘c’ 每次呼叫一個函式觸發。
‘l’ 每執行一行觸發。
‘r’ 每次從一個函式返回觸發
debug。traceback ([thread,] [message [, level]]) 返用呼叫堆疊的資訊,message表示返回的頭部加一段資訊,level表示棧的層次,1表示呼叫debug。traceback那個函式,往上類推。
debug。setupvalue (f, up, value) 給函式f設定upvalue,up同樣是一個序號
debug。setlocal ([thread,] level, local, value) 給函式設定本地變數, level是棧層次,local是本地變數的序號,value是值。
斷點偵錯程式
準備好API,現在想想如何實現這個偵錯程式,為了簡單起見,我們想象這個使用場景:
假設有一個debugger。lua實現了偵錯程式功能,要除錯的檔案是test。lua
現在執行lua debugger。lua test。lua就可以啟動偵錯程式,並載入test。lua開始除錯。
一開始進入test。lua,馬上進入互動狀態,輸入命令,然後再輸入cont就繼續執行。
這些命令大概有:
dbg。h() —— 幫助
dbg。bp(line) —— 在第幾行斷點
dbg。si() —— 單步執行
dbg。so() —— 單步執行(跳過函式)
dbg。all() —— 列印所有的資訊
dbg。name() —— 列印函式資訊
dbg。uv() —— 列印upvalue
http://
dbg。lv()
—— 列印區域性變數(包括引數)
dbg。av() —— 列印可變引數
dbg。setuv(n, v) —— 設定upvalue,n是變數的序號
dbg。setlv(n, v) —— 設定區域性變數,n是變數的序號
現在來想想邏輯怎麼組織:
有一個全域性變數dbg提供上面那些指令,之所以要全域性變數是因為互動狀態下,只能訪問全域性變數。
有一個本地變數dbgd,它提供cmdlist命令列表,dbg將命令先壓入這裡,等退出互動狀態時才執行。
dbgd在進入互動狀態時,透過debug庫抓取上下文資訊儲存起來;這樣在互動狀態下,dbg就可以透過dbg。all()列印現來。
偵錯程式程式碼是這樣的:
—— debugger。lua
—— 偵錯程式後臺,處理具體的命令
local
dbgd
=
{
cmdlist
=
{},
—— 將執行的命令列表
info
=
{},
—— 當前的除錯資訊
opt
=
{},
—— 選項
}
—— 對外介面,是一個全域性變數,呼叫它的函式完成操作
dbg
=
{
}
—— 壓入命令
function
dbgd
。
pushcmd
(
cmd
,
。。。)
dbgd。cmdlist
[
#
dbgd。cmdlist
+
1
]
=
{
cmd
,
table。unpack
{。。。}}
end
—— 執行命令
function
dbgd
。
execcmd
()
local
cmdlist
=
dbgd。cmdlist
dbgd。cmdlist
=
{}
for
_
,
cmdinfo
in
ipairs
(
cmdlist
)
do
local
cmd
=
cmdinfo
[
1
]
if
dbgd
[
cmd
]
then
dbgd
[
cmd
](
table。unpack
(
cmdinfo
,
2
))
else
(
string。format
(
“error - unknown cmd: %s”
,
cmd
))
end
end
end
—— 取函式名
function
getname
(
n
)
if
n。what
==
“C”
then
return
n。name
end
local
lc
=
string。format
(
“%s:%d”
,
n。short_src
,
n。currentline
)
if
n。what
~=
“main”
and
n。namewhat
~=
“”
then
return
string。format
(
“%s (%s)”
,
lc
,
n。name
)
else
return
lc
end
end
—— 儲存函式的各種資訊
function
dbgd
。
capinfo
()
local
level
=
4
local
finfo
=
debug。getinfo
(
level
,
“nSlf”
)
local
info
=
{}
—— function info
info。name
=
getname
(
finfo
)
info。func
=
finfo。func
—— upvalues
info。uv
=
{}
local
i
=
1
while
true
do
local
name
,
value
=
debug。getupvalue
(
finfo。func
,
i
)
if
name
==
nil
then
break
end
if
string。sub
(
name
,
1
,
1
)
~=
“(”
then
table。insert
(
info。uv
,
{
name
,
value
,
i
})
end
i
=
i
+
1
end
—— local values
info。lv
=
{}
i
=
1
while
true
do
local
name
,
value
=
debug。getlocal
(
level
,
i
)
if
not
name
then
break
end
if
string。sub
(
name
,
1
,
1
)
~=
“(”
then
table。insert
(
info。lv
,
{
name
,
value
,
i
})
end
i
=
i
+
1
end
—— vararg arguments
info。av
=
{}
i
=
-
1
while
true
do
local
name
,
value
=
debug。getlocal
(
level
,
i
)
if
not
name
then
break
end
if
string。sub
(
name
,
1
,
1
)
~=
“(”
then
table。insert
(
info。av
,
{
name
,
value
,
i
})
end
i
=
i
-
1
end
dbgd。info
=
info
end
—— 進入互動介面
local
function
interactive
()
dbgd。resume
()
(
debug。traceback
(
nil
,
3
))
dbgd。capinfo
()
debug。debug
()
dbgd。execcmd
()
end
——- 偵錯程式Hook回撥
function
dbgd
。
hook
(
evt
,
arg
)
if
evt
==
‘call’
then
interactive
()
elseif
evt
==
“line”
then
if
dbgd。opt
。
type
==
“line”
then
if
dbgd。opt
。
line
==
arg
then
interactive
()
end
elseif
dbgd。opt
。
type
==
“stepin”
then
interactive
()
elseif
dbgd。opt
。
type
==
“stepover”
then
if
debug。getinfo
(
2
,
“f”
)。
func
==
dbgd。info
。
func
then
interactive
()
end
end
end
end
——- 在某行加一個斷點
function
dbgd
。
breakpoint
(
line
)
dbgd。opt
。
type
=
“line”
dbgd。opt
。
line
=
line
debug。sethook
(
dbgd。hook
,
“l”
)
end
—— 單步進入
function
dbgd
。
stepin
()
dbgd。opt
。
type
=
“stepin”
debug。sethook
(
dbgd。hook
,
“l”
)
end
—— 單步跳過
function
dbgd
。
stepover
()
dbgd。opt
。
type
=
“stepover”
debug。sethook
(
dbgd。hook
,
“l”
)
end
—— 設定upvalue
function
dbgd
。
setupvalue
(
n
,
v
)
debug。setupvalue
(
dbgd。info
。
func
,
n
,
v
)
end
—— 設定本地變數
function
dbgd
。
setlocalvalue
(
n
,
v
)
debug。setlocal
(
5
,
n
,
v
)
end
——- 刪除Hook,繼續執行
function
dbgd
。
resume
()
debug。sethook
()
end
————————————————————————————-
—— 對外的命令介面
—— 幫助
function
dbg
。
h
()
(
“dbg。h()
\t\t\t
print help”
)
—— 幫助
(
“dbg。bp(line)
\t\t
add a breakpoint to a line”
)
—— 在第幾行斷點
(
“dbg。si()
\t\t
step into next function call”
)
—— 單步執行
(
“dbg。so()
\t\t
step over next function call”
)
—— 單步執行(跳過函式)
(
“dbg。all()
\t\t
print all debug info”
)
—— 列印所有的資訊
(
“dbg。name()
\t\t
print function name”
)
—— 列印函式資訊
(
“dbg。uv()
\t\t
print up values”
)
—— 列印upvalue
(
“dbg。lv()
\t\t
print local values”
)
—— 列印區域性變數(包括引數)
(
“dbg。av()
\t\t
print vararg arguments”
)
—— 列印可變引數
(
“dbg。setuv(n, v)
\t\t
change a upvalue”
)
—— 設定upvalue,n是變數的序號
(
“dbg。setlv(n, v)
\t\t
change a local value”
)
—— 設定區域性變數,n是變數的序號
end
local
function
print_vars
(
msg
,
vars
)
(
msg
)
if
vars
then
for
_
,
v
in
ipairs
(
vars
)
do
(
“”
,
v
[
3
],
v
[
1
],
v
[
2
])
end
end
end
function
dbg
。
name
()
(
“name: ”
)
(
string。format
(
“ %s”
,
dbgd。info
。
name
))
end
function
dbg
。
uv
()
print_vars
(
“up values: ”
,
dbgd。info
。
uv
)
end
function
dbg
。
lv
()
print_vars
(
“local values: ”
,
dbgd。info
。
lv
)
end
function
dbg
。
av
()
print_vars
(
“vararg argument: ”
,
dbgd。info
。
av
)
end
function
dbg
。
all
()
dbg。name
()
dbg。uv
()
dbg。lv
()
dbg。av
()
end
function
dbg
。
bp
(
ln
)
dbgd。pushcmd
(
“breakpoint”
,
ln
)
end
function
dbg
。
si
()
dbgd。pushcmd
(
“stepin”
)
end
function
dbg
。
so
()
dbgd。pushcmd
(
“stepover”
)
end
function
dbg
。
setuv
(
n
,
v
)
dbgd。pushcmd
(
“setupvalue”
,
n
,
v
)
end
function
dbg
。
setlv
(
n
,
v
)
dbgd。pushcmd
(
“setlocalvalue”
,
n
,
v
)
end
local
function
run
(
luacode
)
local
chunk
=
loadfile
(
luacode
)
debug。sethook
(
dbgd。hook
,
“c”
)
chunk
()
debug。sethook
()
end
run
(
select
(
1
,
。。。))
再看看test。lua
01
local
function
map
(
a
,
f
)
02
for
i
=
1
,
#
a
do
03
a
[
i
]
=
f
(
a
[
i
])
04
end
05
return
a
06
end
07
local
prefix
=
“[DEBUG]”
80
local
function
printlist
(
a
)
90
local
c
=
table。concat
(
a
,
“, ”
)
10
local
s
=
string。format
(
“%s {%s}”
,
prefix
,
c
)
11
(
s
)
12
end
13
local
function
test
()
14
local
a
=
map
({
1
,
2
,
3
},
function
(
e
)
15
return
e
*
2
16
end
)
17
printlist
(
a
)
18
end
19
test
()
現在在命令列啟動偵錯程式:
lua debugger。lua test。lua
啟動之後會馬上進入互動模式:
stack
traceback
:
。\
test
。
lua
:
6
:
in
local
‘chunk’
。\
debugger
。
lua
:
226
:
in
local
‘run’
。\
debugger
。
lua
:
230
:
in
main
chunk
[C]
:
in
?
lua_debug
>
每次進入互動模式,前面都會列印呼叫棧出來,假設想在map裡斷點,在互動命令列中輸入:
lua_debug> dbg。bp(2)
lua_debug> cont
dbg。bg(2)表示在第2行下一個斷點,cont繼續執行(這一點確實比較麻煩)。當代碼執行到第二行時,又再一次進入互動模式:
stack
traceback
:
。\
test
。
lua
:
2
:
in
upvalue
‘map’
。\
test
。
lua
:
14
:
in
local
‘test’
。\
test
。
lua
:
19
:
in
local
‘chunk’
。\
debugger
。
lua
:
226
:
in
local
‘run’
。\
debugger
。
lua
:
230
:
in
main
chunk
[C]
:
in
?
lua_debug
>
可以看到堆疊顯示是在第2行,使用bp。all()把所有資訊打印出來看看:
lua_debug
>
dbg
。
all
()
name
:
。\
test
。
lua
:
2
(
map
)
up
values
:
local
values
:
1
a
table
:
0000000000629320
2
f
function
:
000000000062b470
vararg
argument
:
接下來試著將斷點設定在第10行:
lua_debug
>
dbg
。
bp
(
10
)
lua_debug
>
cont
stack
traceback
:
。\
test
。
lua
:
10
:
in
upvalue
‘printlist’
。\
test
。
lua
:
17
:
in
local
‘test’
。\
test
。
lua
:
19
:
in
local
‘chunk’
。\
debugger
。
lua
:
226
:
in
local
‘run’
。\
debugger
。
lua
:
230
:
in
main
chunk
[C]
:
in
?
lua_debug
>
現在斷點在printlist裡面,列印一下全量資訊:
lua_debug
>
dbg
。
all
()
name
:
。\
test
。
lua
:
10
(
printlist
)
up
values
:
1
_ENV
table
:
00000000010225f0
2
prefix
[DEBUG]
local
values
:
1
a
table
:
0000000000629320
2
c
2
,
4
,
6
vararg
argument
:
試試把prefix和c修改一下,看看最終輸出:
lua_debug
>
dbg
。
setuv
(
2
,
“[ERROR]”
)
lua_debug
>
dbg
。
setlv
(
2
,
“——————”
)
lua_debug
>
cont
[ERROR]
{——————}
我們呼叫dbg。setuv和dbg。setlv把prefix和c給修改了,本來應該輸出:
[DEBUG]{2, 4, 6}
,結果變成
[ERROR] {——————}
,這說明修改成功了。
偵錯程式到此完成,功能很簡單,使用起來也不方便。不過它仍然能滿足一個偵錯程式最基礎的功能。