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

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.

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.mruntime.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

  1. 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ó.
  2. Thiếu thông tin kiểu cho các phần C của thư viện runtime.
  3. 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ạng pkg.(*MyType).Meth.
  4. 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 -ldflags trong biến môi trường GOFLAGS để 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 *