Gỡ lỗi mã Go với GDB
Các hướng dẫn sau áp dụng cho bộ công cụ chuẩn
(trình biên dịch và công cụ Go gc).
Gccgo có hỗ trợ gdb gốc.
Lưu ý rằng
Delve là lựa chọn tốt hơn
GDB khi gỡ lỗi các chương trình Go được xây dựng bằng bộ công cụ chuẩn.
Delve hiểu runtime, cấu trúc dữ liệu và biểu thức của Go tốt hơn GDB.
Hiện tại Delve hỗ trợ Linux, OSX và Windows trên amd64.
Để biết danh sách các nền tảng được hỗ trợ mới nhất, vui lòng xem
tài liệu Delve.
GDB không hiểu tốt các chương trình Go. Việc quản lý stack, phân luồng và runtime có những điểm khác biệt đáng kể so với mô hình thực thi mà GDB kỳ vọng, có thể gây nhầm lẫn cho trình gỡ lỗi và tạo ra kết quả sai ngay cả khi chương trình được biên dịch bằng gccgo. Do đó, mặc dù GDB có thể hữu ích trong một số tình huống (ví dụ: gỡ lỗi mã Cgo, hoặc gỡ lỗi bản thân runtime), nhưng nó không phải là trình gỡ lỗi đáng tin cậy cho các chương trình Go, đặc biệt là những chương trình có độ đồng thời cao. Hơn nữa, việc giải quyết những vấn đề này không phải là ưu tiên của dự án Go vì chúng rất khó.
Tóm lại, các hướng dẫn bên dưới chỉ nên được xem như hướng dẫn về cách sử dụng GDB khi nó hoạt động được, không phải là sự đảm bảo thành công. Ngoài phần tổng quan này, bạn có thể tham khảo thêm tài liệu GDB.
Giới thiệu
Khi bạn biên dịch và liên kết các chương trình Go bằng bộ công cụ gc
trên Linux, macOS, FreeBSD hoặc NetBSD, các tệp nhị phân kết quả chứa thông tin
gỡ lỗi DWARFv4 mà các phiên bản gần đây (≥7.5) của trình gỡ lỗi GDB có thể
sử dụng để kiểm tra một tiến trình đang chạy hoặc một core dump.
Truyền cờ '-w' cho linker để bỏ qua thông tin gỡ lỗi
(ví dụ: go build -ldflags=-w prog.go).
Mã do trình biên dịch gc tạo ra bao gồm inlining các lời gọi hàm
và registerization các biến. Những tối ưu hóa này đôi khi làm cho việc gỡ lỗi
bằng gdb khó hơn.
Nếu cần tắt các tối ưu hóa này, hãy xây dựng chương trình bằng
go build -gcflags=all="-N -l".
Nếu bạn muốn dùng gdb để kiểm tra một core dump, bạn có thể kích hoạt tạo dump
khi chương trình crash, trên các hệ thống cho phép điều đó, bằng cách đặt
GOTRACEBACK=crash trong môi trường (xem
tài liệu gói runtime
để biết thêm thông tin).
Các thao tác phổ biến
-
Hiển thị tệp và số dòng của mã, đặt breakpoint và disassemble:
(gdb) list (gdb) list line (gdb) list file.go:line (gdb) break line (gdb) break file.go:line (gdb) disas
-
Hiển thị backtrace và unwind stack frame:
(gdb) bt (gdb) frame n
-
Hiển thị tên, kiểu và vị trí trên stack frame của các biến cục bộ,
tham số và giá trị trả về:
(gdb) info locals (gdb) info args (gdb) p variable (gdb) whatis variable
-
Hiển thị tên, kiểu và vị trí của các biến toàn cục:
(gdb) info variables regexp
Phần mở rộng Go
Cơ chế mở rộng gần đây của GDB cho phép tải các script mở rộng cho một tệp nhị phân nhất định. Bộ công cụ sử dụng cơ chế này để mở rộng GDB bằng một số lệnh để kiểm tra các thành phần nội bộ của mã runtime (như goroutine) và để in đẹp các kiểu map, slice và channel tích hợp sẵn.
-
In đẹp một string, slice, map, channel hoặc interface:
(gdb) p var
-
Hàm $len() và $cap() cho string, slice và map:
(gdb) p $len(var)
-
Hàm để chuyển đổi interface về kiểu động của chúng:
(gdb) p $dtype(var) (gdb) iface var
Vấn đề đã biết: GDB không thể tự động tìm kiểu động của một giá trị interface nếu tên đầy đủ khác với tên rút gọn (gây phiền toái khi in stacktrace, pretty printer sẽ in tên kiểu rút gọn và một con trỏ).
-
Kiểm tra goroutine:
(gdb) info goroutines (gdb) goroutine n cmd (gdb) help goroutine
Ví dụ:(gdb) goroutine 12 bt
Bạn có thể kiểm tra tất cả goroutine bằng cách truyềnallthay vì ID của một goroutine cụ thể. Ví dụ:(gdb) goroutine all bt
Nếu bạn muốn xem cách thức hoạt động, hoặc muốn mở rộng nó, hãy xem src/runtime/runtime-gdb.py trong
bản phân phối mã nguồn Go. Nó phụ thuộc vào một số kiểu ma thuật đặc biệt
(hash<T,U>) và biến (runtime.m và
runtime.g) mà linker
(src/cmd/link/internal/ld/dwarf.go) đảm bảo được mô tả trong
mã DWARF.
Nếu bạn muốn xem thông tin gỡ lỗi trông như thế nào, hãy chạy
objdump -W a.out và duyệt qua các section .debug_*.
Các vấn đề đã biết
- In đẹp string chỉ kích hoạt cho kiểu string, không kích hoạt cho các kiểu dẫn xuất từ nó.
- Thiếu thông tin kiểu cho các phần C của thư viện runtime.
- GDB không hiểu qualifier tên của Go và xử lý
"fmt.Print"như một literal không có cấu trúc với một dấu"."cần phải được đặt trong dấu ngoặc kép. Nó còn phản đối mạnh hơn với tên phương thức dạngpkg.(*MyType).Meth. - Từ Go 1.11, thông tin gỡ lỗi được nén mặc định.
Các phiên bản gdb cũ hơn, chẳng hạn như phiên bản có sẵn mặc định trên MacOS,
không hiểu định dạng nén.
Bạn có thể tạo thông tin gỡ lỗi không nén bằng cách dùng
go build -ldflags=-compressdwarf=false. (Để thuận tiện, bạn có thể đặt tùy chọn-ldflagstrong biến môi trườngGOFLAGSđể khỏi phải chỉ định mỗi lần.)
Hướng dẫn
Trong hướng dẫn này, chúng ta sẽ kiểm tra tệp nhị phân của
các bài kiểm thử đơn vị trong gói regexp. Để xây dựng tệp nhị phân,
hãy chuyển đến $GOROOT/src/regexp và chạy go test -c.
Thao tác này sẽ tạo ra một tệp thực thi có tên regexp.test.
Bắt đầu
Khởi động GDB, gỡ lỗi regexp.test:
$ gdb regexp.test GNU gdb (GDB) 7.2-gg8 Copyright (C) 2010 Free Software Foundation, Inc. License GPLv 3+: GNU GPL version 3 or later <http://gnu.org/licenses/gpl.html> Type "show copying" and "show warranty" for licensing/warranty details. This GDB was configured as "x86_64-linux". Reading symbols from /home/user/go/src/regexp/regexp.test... done. Loading Go Runtime support. (gdb)
Thông báo "Loading Go Runtime support" có nghĩa là GDB đã tải
extension từ $GOROOT/src/runtime/runtime-gdb.py.
Để giúp GDB tìm mã nguồn runtime Go và script hỗ trợ đi kèm,
hãy truyền $GOROOT của bạn với cờ '-d':
$ gdb regexp.test -d $GOROOT
Nếu vì lý do nào đó GDB vẫn không tìm được thư mục hoặc script đó, bạn có thể
tải thủ công bằng cách nói với gdb (giả sử bạn có mã nguồn go trong
~/go/):
(gdb) source ~/go/src/runtime/runtime-gdb.py Loading Go Runtime support.
Kiểm tra mã nguồn
Dùng lệnh "l" hoặc "list" để kiểm tra mã nguồn.
(gdb) l
Liệt kê một phần cụ thể của mã nguồn bằng cách tham số hóa "list"
với tên hàm (phải được đủ điều kiện bằng tên gói của nó).
(gdb) l main.main
Liệt kê theo tên tệp và số dòng cụ thể:
(gdb) l regexp.go:1 (gdb) # Hit enter to repeat last command. Here, this lists next 10 lines.
Đặt tên
Tên biến và hàm phải được đủ điều kiện bằng tên của gói chứa chúng.
Hàm Compile từ gói regexp
được GDB biết đến với tên 'regexp.Compile'.
Phương thức phải được đủ điều kiện bằng tên kiểu receiver. Ví dụ,
phương thức String của kiểu *Regexp được biết đến là
'regexp.(*Regexp).String'.
Các biến che khuất biến khác được tự động thêm hậu tố số trong thông tin gỡ lỗi. Các biến được tham chiếu bởi closure sẽ xuất hiện dưới dạng con trỏ có tiền tố '&'.
Đặt breakpoint
Đặt breakpoint tại hàm TestFind:
(gdb) b 'regexp.TestFind' Breakpoint 1 at 0x424908: file /home/user/go/src/regexp/find_test.go, line 148.
Chạy chương trình:
(gdb) run
Starting program: /home/user/go/src/regexp/regexp.test
Breakpoint 1, regexp.TestFind (t=0xf8404a89c0) at /home/user/go/src/regexp/find_test.go:148
148 func TestFind(t *testing.T) {
Thực thi đã tạm dừng tại breakpoint. Xem goroutine nào đang chạy và chúng đang làm gì:
(gdb) info goroutines 1 waiting runtime.gosched * 13 running runtime.goexit
Goroutine được đánh dấu * là goroutine hiện tại.
Kiểm tra stack
Xem stack trace tại vị trí chương trình đã tạm dừng:
(gdb) bt # backtrace #0 regexp.TestFind (t=0xf8404a89c0) at /home/user/go/src/regexp/find_test.go:148 #1 0x000000000042f60b in testing.tRunner (t=0xf8404a89c0, test=0x573720) at /home/user/go/src/testing/testing.go:156 #2 0x000000000040df64 in runtime.initdone () at /home/user/go/src/runtime/proc.c:242 #3 0x000000f8404a89c0 in ?? () #4 0x0000000000573720 in ?? () #5 0x0000000000000000 in ?? ()
Goroutine còn lại, số 1, đang bị kẹt trong runtime.gosched, bị block khi nhận từ channel:
(gdb) goroutine 1 bt
#0 0x000000000040facb in runtime.gosched () at /home/user/go/src/runtime/proc.c:873
#1 0x00000000004031c9 in runtime.chanrecv (c=void, ep=void, selected=void, received=void)
at /home/user/go/src/runtime/chan.c:342
#2 0x0000000000403299 in runtime.chanrecv1 (t=void, c=void) at/home/user/go/src/runtime/chan.c:423
#3 0x000000000043075b in testing.RunTests (matchString={void (struct string, struct string, bool *, error *)}
0x7ffff7f9ef60, tests= []testing.InternalTest = {...}) at /home/user/go/src/testing/testing.go:201
#4 0x00000000004302b1 in testing.Main (matchString={void (struct string, struct string, bool *, error *)}
0x7ffff7f9ef80, tests= []testing.InternalTest = {...}, benchmarks= []testing.InternalBenchmark = {...})
at /home/user/go/src/testing/testing.go:168
#5 0x0000000000400dc1 in main.main () at /home/user/go/src/regexp/_testmain.go:98
#6 0x00000000004022e7 in runtime.mainstart () at /home/user/go/src/runtime/amd64/asm.s:78
#7 0x000000000040ea6f in runtime.initdone () at /home/user/go/src/runtime/proc.c:243
#8 0x0000000000000000 in ?? ()
Stack frame cho thấy chúng ta đang thực thi hàm regexp.TestFind, đúng như kỳ vọng.
(gdb) info frame
Stack level 0, frame at 0x7ffff7f9ff88:
rip = 0x425530 in regexp.TestFind (/home/user/go/src/regexp/find_test.go:148);
saved rip 0x430233
called by frame at 0x7ffff7f9ffa8
source language minimal.
Arglist at 0x7ffff7f9ff78, args: t=0xf840688b60
Locals at 0x7ffff7f9ff78, Previous frame's sp is 0x7ffff7f9ff88
Saved registers:
rip at 0x7ffff7f9ff80
Lệnh info locals liệt kê tất cả biến cục bộ của hàm cùng giá trị của chúng, nhưng hơi
nguy hiểm khi sử dụng, vì nó cũng sẽ cố in các biến chưa được khởi tạo. Các slice chưa khởi tạo có thể khiến gdb cố
in các mảng với kích thước tùy ý.
Các tham số của hàm:
(gdb) info args t = 0xf840688b60
Khi in tham số, lưu ý rằng đó là con trỏ tới một
giá trị Regexp. Lưu ý rằng GDB đã đặt nhầm *
ở phía bên phải của tên kiểu và tự thêm từ khóa 'struct' theo phong cách C truyền thống.
(gdb) p re
(gdb) p t
$1 = (struct testing.T *) 0xf840688b60
(gdb) p t
$1 = (struct testing.T *) 0xf840688b60
(gdb) p *t
$2 = {errors = "", failed = false, ch = 0xf8406f5690}
(gdb) p *t->ch
$3 = struct hchan<*testing.T>
struct hchan<*testing.T> là
biểu diễn nội bộ của runtime cho một channel. Nó hiện đang rỗng,
nếu không thì gdb đã in đẹp nội dung của nó.
Tiến thêm một bước:
(gdb) n # execute next line
149 for _, test := range findTests {
(gdb) # enter is repeat
150 re := MustCompile(test.pat)
(gdb) p test.pat
$4 = ""
(gdb) p re
$5 = (struct regexp.Regexp *) 0xf84068d070
(gdb) p *re
$6 = {expr = "", prog = 0xf840688b80, prefix = "", prefixBytes = []uint8, prefixComplete = true,
prefixRune = 0, cond = 0 '\000', numSubexp = 0, longest = false, mu = {state = 0, sema = 0},
machine = []*regexp.machine}
(gdb) p *re->prog
$7 = {Inst = []regexp/syntax.Inst = {{Op = 5 '\005', Out = 0, Arg = 0, Rune = []int}, {Op =
6 '\006', Out = 2, Arg = 0, Rune = []int}, {Op = 4 '\004', Out = 0, Arg = 0, Rune = []int}},
Start = 1, NumCap = 2}
Chúng ta có thể bước vào lời gọi hàm String bằng "s":
(gdb) s
regexp.(*Regexp).String (re=0xf84068d070, noname=void) at /home/user/go/src/regexp/regexp.go:97
97 func (re *Regexp) String() string {
Lấy stack trace để xem chúng ta đang ở đâu:
(gdb) bt
#0 regexp.(*Regexp).String (re=0xf84068d070, noname=void)
at /home/user/go/src/regexp/regexp.go:97
#1 0x0000000000425615 in regexp.TestFind (t=0xf840688b60)
at /home/user/go/src/regexp/find_test.go:151
#2 0x0000000000430233 in testing.tRunner (t=0xf840688b60, test=0x5747b8)
at /home/user/go/src/testing/testing.go:156
#3 0x000000000040ea6f in runtime.initdone () at /home/user/go/src/runtime/proc.c:243
....
Xem mã nguồn:
(gdb) l
92 mu sync.Mutex
93 machine []*machine
94 }
95
96 // String returns the source text used to compile the regular expression.
97 func (re *Regexp) String() string {
98 return re.expr
99 }
100
101 // Compile parses a regular expression and returns, if successful,
In đẹp
Cơ chế in đẹp của GDB được kích hoạt bằng cách khớp regexp với tên kiểu. Ví dụ với slice:
(gdb) p utf
$22 = []uint8 = {0 '\000', 0 '\000', 0 '\000', 0 '\000'}
Vì slice, mảng và string không phải là con trỏ C, GDB không thể giải thích thao tác chỉ số cho bạn, nhưng bạn có thể xem bên trong biểu diễn runtime để làm điều đó (tab completion hữu ích ở đây):
(gdb) p slc
$11 = []int = {0, 0}
(gdb) p slc-><TAB>
array slc len
(gdb) p slc->array
$12 = (int *) 0xf84057af00
(gdb) p slc->array[1]
$13 = 0
Các hàm mở rộng $len và $cap hoạt động với string, mảng và slice:
(gdb) p $len(utf) $23 = 4 (gdb) p $cap(utf) $24 = 4
Channel và map là các kiểu 'tham chiếu', gdb hiển thị chúng như con trỏ tới các kiểu C++ như hash<int,string>*. Dereference sẽ kích hoạt prettyprinting.
Interface được biểu diễn trong runtime dưới dạng con trỏ tới một type descriptor và một con trỏ tới giá trị. Extension runtime Go GDB giải mã điều này và tự động kích hoạt in đẹp cho kiểu runtime. Hàm mở rộng $dtype giải mã kiểu động cho bạn (ví dụ được lấy từ một breakpoint tại dòng 293 của regexp.go).
(gdb) p i
$4 = {str = "cbb"}
(gdb) whatis i
type = regexp.input
(gdb) p $dtype(i)
$26 = (struct regexp.inputBytes *) 0xf8400b4930
(gdb) iface i
regexp.input: struct regexp.inputBytes *