前言

這一章我們來做一個偵錯程式的玩具,說它是玩具,是因為要做一個真正可用的偵錯程式並不容易,這裡只是想透過這個程式來理解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

print

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

()

print

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

()

print

“dbg。h()

\t\t\t

print help”

—— 幫助

print

“dbg。bp(line)

\t\t

add a breakpoint to a line”

—— 在第幾行斷點

print

“dbg。si()

\t\t

step into next function call”

—— 單步執行

print

“dbg。so()

\t\t

step over next function call”

—— 單步執行(跳過函式)

print

“dbg。all()

\t\t

print all debug info”

—— 列印所有的資訊

print

“dbg。name()

\t\t

print function name”

—— 列印函式資訊

print

“dbg。uv()

\t\t

print up values”

—— 列印upvalue

print

“dbg。lv()

\t\t

print local values”

—— 列印區域性變數(包括引數)

print

“dbg。av()

\t\t

print vararg arguments”

—— 列印可變引數

print

“dbg。setuv(n, v)

\t\t

change a upvalue”

—— 設定upvalue,n是變數的序號

print

“dbg。setlv(n, v)

\t\t

change a local value”

—— 設定區域性變數,n是變數的序號

end

local

function

print_vars

msg

vars

print

msg

if

vars

then

for

_

v

in

ipairs

vars

do

print

“”

v

3

],

v

1

],

v

2

])

end

end

end

function

dbg

name

()

print

“name: ”

print

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

print

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] {——————}

,這說明修改成功了。

偵錯程式到此完成,功能很簡單,使用起來也不方便。不過它仍然能滿足一個偵錯程式最基礎的功能。