有鑑於 Debugger.jl 的說明文件有點難懂,就自己來寫一篇教學。

Debugger.jl 是在 Julia 上,由官方開發及維護的除錯器(debugger),debugger 如同其他語言的 debugger 一樣,可以預先設定斷點(breakpoint)、在丟出例外(exception)時停下來、逐行執行及檢查變數等等功能。這篇文章會示範最基礎的 debugger 的用法。

安裝 Debugger

安裝 Julia 套件都是差不多的方式,切換到套件模式,並且 add PACKAGE

1
]add Debugger

等他跑好就把 Debugger 安裝好了。

在 REPL 執行 Debugger

基本上,Debugger 是一種直譯器,所以比較好的執行環境是在 REPL 中。

當然在 VS code 也有內建,但是我個人嘗試過,他跑得蠻慢的,不知道為什麼。= ="

所以以下的示範都是在 REPL 上。

逐行執行

為了示範,所以就先定義兩個函式。

1
2
3
4
5
6
7
8
9
10
11
julia> using Debugger

julia> function foo(x, y)
z = x + 2
y = y*(y + z)
return y
end
foo (generic function with 1 method)

julia> bar(x) = foo(x, 5)
bar (generic function with 1 method)

逐步執行

Debugger 都會有個進入點,進入點需要在前面標記 @enter,後面可以接著一個表示式。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
julia> @enter bar(3)
In bar(x) at REPL[2]:1
>1 bar(x) = foo(x, 5)

About to run: (foo)(3, 5)
1|debug> s
In foo(x, y) at REPL[1]:1
1 function foo(x, y)
>2 z = x + 2
3 y = y*(y + z)
4 return y
5 end

About to run: _J3
1|debug> s
In +(x, y) at int.jl:87
>87 (+)(x::T, y::T) where {T<:BitInteger} = add_int(x, y)

About to run: (Core.Intrinsics.add_int)(3, 2)

以上進入了 bar(3) 這個函式呼叫。剛進入的時候,他會停在函式呼叫的第一步:(foo)(3, 5)

接著,他會進入除錯(debug)模式,在這個模式下,就可以藉由指令來操作這個 debugger。

如果輸入 s,他會逐步執行程式碼,所以可以看到他進入到 foo 函式中,並且指標指到第二行的 z = x + 2

如果再輸入一次 s,讓他執行下一步,這時候加法就會被執行,所以就會進到 (Core.Intrinsics.add_int)(3, 2) 的執行。

逐行執行

如果我們希望他可以逐行執行,可以用 n

1
2
3
4
5
6
7
8
9
10
11
12
julia> @enter bar(3)
In bar(x) at REPL[2]:1
>1 bar(x) = foo(x, 5)

About to run: (foo)(3, 5)
1|debug> n
In bar(x) at REPL[2]:1
>1 bar(x) = foo(x, 5)

About to run: return %J1
1|debug> n
50

如果輸入 n 之後,debugger 會直接計算該行的計算結果,foo(x, 5) 就會被執行,並回傳 50

執行到丟出例外

比較實用的是讓他執行直到一個例外被拋出。這時我們需要修改一下函式:

1
2
3
4
5
6
function foo(x, y)
z = x + 2
throw(ErrorException("This is an error."))
y = y*(y + z)
return y
end

在第二行中插入一條拋出 ErrorException 例外的表示式。

要執行到直到丟出例外,並且停下,有幾種方法。

在除錯模式,例外前插入斷點法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
julia> @enter bar(3)
In bar(x) at REPL[2]:1
>1 bar(x) = foo(x, 5)

About to run: (foo)(3, 5)
1|debug> bp on throw
Breaking on throw
1|debug> c
Breaking for error:
ERROR: This is an error.
Stacktrace:
[1] foo(x::Int64, y::Int64)
@ Main REPL[10]:3
[2] bar(x::Int64)
@ Main REPL[2]:1

In foo(x, y) at REPL[10]:1
1 function foo(x, y)
2 z = x + 2
>3 throw(ErrorException("This is an error."))
4 y = y*(y + z)
5 return y
6 end

About to run: (throw)(ErrorException("This is an error."))

一樣是利用 @enter 來製造進入點,接著利用 bp on throw 來設定斷點,他的意思是在拋出例外時設定斷點。

相對,這個斷點是可以關閉的,用 bp off throw

接著,可以輸入 c 讓程式持續執行,最後他就會停在斷點處。

在 REPL 模式,例外前插入斷點法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
julia> break_on(:error)

julia> @run bar(3)
Breaking for error:
ERROR: This is an error.
Stacktrace:
[1] foo(x::Int64, y::Int64)
@ Main REPL[10]:3
[2] bar(x::Int64)
@ Main REPL[2]:1

In foo(x, y) at REPL[10]:1
1 function foo(x, y)
2 z = x + 2
>3 throw(ErrorException("This is an error."))
4 y = y*(y + z)
5 return y
6 end

About to run: (throw)(ErrorException("This is an error."))

跟前面的想法差不多,只是這次是在 REPL 下操作。

這次就不用急著進入除錯模式,可以直接在 REPL 下設定在拋出例外前的斷點,可以用 break_on(:error)

@run 這個 macro 就等同於 @enter + c,所以他會執行直到斷點發生。

查看變數的值

我們停在斷點之後可以做什麼事情呢?除錯就是要在執行環境中去檢查執行時期的變數是不是符合預期。

這時候我們可以把 backtrace 給列出來。

1
2
3
4
5
6
7
1|debug> bt
[1] foo(x, y) at REPL[10]:3
| x::Int64 = 3
| y::Int64 = 5
| z::Int64 = 5
[2] bar(x) at REPL[2]:1
| x::Int64 = 3

輸入 bt 可以列出停在目前的斷點,在 foobar 函式中存有的區域變數以及他的值有哪些。

我們可以發現我們首先呼叫 barbar 會去呼叫 foo。我們知道函式呼叫會在堆疊(stack)中,推入一個新的呼叫框(frame),呼叫框中包含著函式呼叫的引數(arguments)、區域變數等等資訊。

foo 的呼叫會在整個堆疊的最上面,也就是第一個 frame,所以看到他標示 [1],而 bar 的呼叫就是位於第二個 frame。

叫出指定 frame 中的變數

我們可以叫出指定的 frame 的區域變數,只要在除錯模式中輸入 fr [i::Int]i 則是指定第幾個 frame。

1
2
3
4
5
1|debug> fr 1
[1] foo(x, y) at REPL[10]:1
| x::Int64 = 3
| y::Int64 = 5
| z::Int64 = 5

我們就可以看到在這個階段第一個 frame 的 xyz 變數的值各是多少。

我們可以再試試看第二個 frame 的內容。

1
2
3
1|debug> fr 2
[2] bar(x) at REPL[2]:1
| x::Int64 = 3

切換目前的 frame

剛進入斷點的話,我們都會是位於第一個 frame 中。有時候我們會想要知道在其他 frame 的狀況,這時候我們就需要切換 frame 的位置。

這時候只要輸入 f [i::Int] 就可以切換不同的 frame,例如,想要切換到第二個 frame 就可以輸入 f 2

1
2
3
4
5
6
1|debug> f 2
In bar(x) at REPL[2]:1
>1 bar(x) = foo(x, 5)

About to run: (foo)(3, 5)
2|debug>

還可以發現在除錯模式的 prompt 2|debug>,最開頭的數字改變了,一開始是 1,後來變成 2

這就表示目前所在第幾個 frame。

切換成 Julia REPL 模式

在除錯模式輸入 `(鍵盤左上),就可以切換到 Julia REPL 模式,並且存取變數。

1
2
3
4
5
6
7
8
9
10
1|debug> fr 2
[2] bar(x) at REPL[2]:1
| x::Int64 = 3
1|debug> f 2
In bar(x) at REPL[2]:1
>1 bar(x) = foo(x, 5)

About to run: (foo)(3, 5)
2|julia> x
3

切換之後,你會看到 2|debug> 切換成 2|julia,並且可以直接存取該變數 x。如果要切換回除錯模式,只要按 backspace 鍵,把 ` 刪除即可。

1
2
2|julia> x
3

也可以對該變數做運算。

1
2
2|julia> x + 2
5

但是沒辦法存新的變數。

1
2
3
4
5
6
7
8
2|julia> y = x + 2
5

2|julia> y
ERROR: UndefVarError: y not defined
Stacktrace:
[1] top-level scope
@ none:1

結論

我們有了這些工具跟方法,可以讓我們在不同的 frame 之間做跳躍,去看在上層或是下層的 frame 中變數的數值各是多少。

同時,也可以測試你自己寫的函式是否正常執行。

我們就可以用這些方法來除錯程式碼。