Отладка Go-кода с помощью GDB
Приведённые ниже инструкции применимы к стандартной toolchain
(компилятор gc и инструменты Go).
Gccgo имеет нативную поддержку gdb.
Обратите внимание, что
Delve — лучшая альтернатива GDB
при отладке Go-программ, собранных с использованием стандартной toolchain.
Delve лучше понимает среду выполнения Go, структуры данных и выражения, чем GDB.
Delve в настоящее время поддерживает Linux, OSX и Windows на amd64.
Для получения актуального списка поддерживаемых платформ, пожалуйста, обратитесь
к
документации Delve.
GDB не очень хорошо понимает Go-программы. Управление стеком, потоки и среда выполнения содержат особенности, которые отличаются достаточно от модели выполнения, ожидаемой GDB, что может сбивать отладчик с толку и приводить к некорректным результатам даже при компиляции программы с помощью gccgo. В результате, хотя GDB может быть полезен в некоторых ситуациях (например, для отладки Cgo-кода или отладки самой среды выполнения), он не является надежным отладчиком для Go-программ, особенно сильно параллелизированных. Кроме того, улучшение поддержки GDB в Go — не приоритет проекта Go, так как эти проблемы сложны.
Короче говоря, инструкции ниже следует рассматривать только как руководство по использованию GDB, когда он работает, а не как гарантию успеха. Помимо этого обзора, вы можете обратиться к руководству GDB.
Введение
Когда вы компилируете и связываете свои Go-программы с помощью toolchain gc
на Linux, macOS, FreeBSD или NetBSD, получаемые исполняемые файлы содержат отладочную
информацию DWARFv4, которую современные версии (≥7.5) отладчика GDB могут использовать
для анализа работающего процесса или дампа памяти.
Передайте флаг '-w' компоновщику, чтобы исключить отладочную информацию
(например, go build -ldflags=-w prog.go).
Код, генерируемый компилятором gc, включает инлайнинг вызовов функций и
регистризацию переменных. Эти оптимизации иногда могут усложнить отладку с помощью
gdb.
Если вы обнаружили, что необходимо отключить эти оптимизации,
соберите программу с помощью go build -gcflags=all="-N -l".
Если вы хотите использовать gdb для анализа дампа памяти, можно вызвать дамп
при сбое программы, если система позволяет, установив переменную окружения
GOTRACEBACK=crash (подробнее см. в
документации пакета runtime).
Общие операции
-
Показать файл и номер строки кода, установить точки останова и выполнить дизассемблирование:
(gdb) <b>list</b> (gdb) <b>list <i>line</i></b> (gdb) <b>list <i>file.go</i>:<i>line</i></b> (gdb) <b>break <i>line</i></b> (gdb) <b>break <i>file.go</i>:<i>line</i></b> (gdb) <b>disas</b>
-
Показать трассировку стека и раскрутить кадры стека:
(gdb) <b>bt</b> (gdb) <b>frame <i>n</i></b>
-
Показать имя, тип и расположение локальных переменных, аргументов и возвращаемых значений на кадре стека:
(gdb) <b>info locals</b> (gdb) <b>info args</b> (gdb) <b>p variable</b> (gdb) <b>whatis variable</b>
-
Показать имя, тип и расположение глобальных переменных:
(gdb) <b>info variables <i>regexp</i></b>
Расширения Go
Недавнее расширение GDB позволяет загружать скрипты расширений для заданного двоичного файла. Инструментарий использует это для расширения GDB набором команд для inspect (проверки) внутреннего состояния кода среды выполнения (например, горутин) и для красивого вывода встроенных типов карт, срезов и каналов.
-
Красивый вывод строки, среза, карты, канала или интерфейса:
(gdb) <b>p <i>var</i></b>
-
Функции $len() и $cap() для строк, срезов и карт:
(gdb) <b>p $len(<i>var</i>)</b>
-
Функция для приведения интерфейсов к их динамическим типам:
(gdb) <b>p $dtype(<i>var</i>)</b> (gdb) <b>iface <i>var</i></b>
Известная проблема: GDB не может автоматически найти динамический тип значения интерфейса, если его длинное имя отличается от короткого (досадно при печати трассировок стека, красивый принтер возвращается к печати короткого имени типа и указателя).
-
Инспектирование горутин:
(gdb) <b>info goroutines</b> (gdb) <b>goroutine <i>n</i> <i>cmd</i></b> (gdb) <b>help goroutine</b>
Например:(gdb) <b>goroutine 12 bt</b>
Можно инспектировать все горутины, передавallвместо конкретного ID горутины. Например:(gdb) <b>goroutine all bt</b>
Если вы хотите увидеть, как это работает, или хотите расширить его, обратитесь к src/runtime/runtime-gdb.py в дистрибутиве исходного кода Go. Этот файл зависит от некоторых специальных магических типов (hash<T,U>) и переменных (runtime.m и runtime.g), которые компоновщик (src/cmd/link/internal/ld/dwarf.go) гарантирует, что они описаны в коде DWARF.
Если вас интересует, как выглядит отладочная информация, запустите objdump -W a.out и пролистайте разделы .debug_*.
Известные проблемы
- Красивая печать строк срабатывает только для типа string, но не для типов, производных от него.
- Информация о типах отсутствует для частей библиотеки среды выполнения, написанных на C.
- GDB не понимает квалификации имён Go и рассматривает
"fmt.Print"как неструктурированный литерал с символом".", который нужно экранировать. Он еще сильнее возражает против имен методов видаpkg.(*MyType).Meth. - Начиная с Go 1.11, отладочная информация сжимается по умолчанию.
Более старые версии gdb, такие как та, которая по умолчанию доступна в MacOS,
не понимают сжатие.
Вы можете генерировать несжатую отладочную информацию, используя
go build -ldflags=-compressdwarf=false. (Для удобства вы можете поместить опцию-ldflagsв переменную окруженияGOFLAGS, чтобы не указывать её каждый раз.)
Руководство
В этом руководстве мы рассмотрим двоичный файл модуля
regexp для его модульных тестов. Чтобы собрать двоичный файл,
перейдите в каталог $GOROOT/src/regexp и выполните команду go test -c.
Это должно привести к созданию исполняемого файла с именем regexp.test.
Начало работы
Запустите GDB, отлаживая regexp.test:
$ <b>gdb regexp.test</b> 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)
Сообщение "Loading Go Runtime support" означает, что GDB загрузил расширение из $GOROOT/src/runtime/runtime-gdb.py.
Чтобы помочь GDB найти исходные файлы среды выполнения Go и сопутствующий скрипт поддержки,
передайте ваш $GOROOT с флагом '-d':
$ <b>gdb regexp.test -d $GOROOT</b>
Если по какой-либо причине GDB по-прежнему не может найти этот каталог или скрипт, вы можете загрузить его вручную,
сообщив gdb (предполагая, что исходные коды Go находятся в ~/go/):
(gdb) <b>source ~/go/src/runtime/runtime-gdb.py</b> Loading Go Runtime support.
Исследование исходного кода
Используйте команду "l" или "list" для просмотра исходного кода.
(gdb) <b>l</b>
Отобразите определенную часть исходного кода, передав имя функции в команду "list" (оно должно быть квалифицировано именем пакета).
(gdb) <b>l main.main</b>
Отобразите конкретный файл и номер строки:
(gdb) <b>l regexp.go:1</b> (gdb) <i># Нажмите Enter для повторения последней команды. Здесь отображаются следующие 10 строк.</i>
Именование
Имена переменных и функций должны быть квалифицированы именем пакета, к которому они принадлежат.
Функция Compile из пакета regexp известна GDB как 'regexp.Compile'.
Методы должны быть квалифицированы именем типа получателя. Например, метод String типа *Regexp известен как
'regexp.(*Regexp).String'.
Переменные, которые скрывают другие переменные, магическим образом получают числовую суффиксацию в отладочной информации. Переменные, на которые ссылаются замыкания, будут отображаться как указатели, магическим образом префиксированные символом '&'.
Установка точек останова
Установите точку останова в функции TestFind:
(gdb) <b>b 'regexp.TestFind'</b> Breakpoint 1 at 0x424908: file /home/user/go/src/regexp/find_test.go, line 148.
Запустите программу:
(gdb) <b>run</b>
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) {
Выполнение приостановлено на точке останова. Просмотрите, какие горутины работают, и что они делают:
(gdb) <b>info goroutines</b> 1 waiting runtime.gosched * 13 running runtime.goexit
Та, которая отмечена символом *, является текущей горутиной.
Проверка стека
Просмотрите трассировку стека, где программа была приостановлена:
(gdb) <b>bt</b> <i># backtrace</i> #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 0x0000000000f8404a89c0 in ?? () #4 0x0000000000573720 in ?? () #5 0x0000000000000000 in ?? ()
Другая горутина, номер 1, заблокирована в runtime.gosched, ожидая получение данных из канала:
(gdb) <b>goroutine 1 bt</b>
#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 ?? ()
Фрейм стека показывает, что в данный момент выполняется функция regexp.TestFind, как и ожидалось.
(gdb) <b>info frame</b> Stack level 0, frame at 0x7ffff7f9ff88: rip = 0x425530 in regexp.TestFind (/home/user/go/src/regexp/find_test.go:1148); 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
Команда info locals выводит все локальные переменные функции и их значения, но её использование несколько опасно,
поскольку она также попытается вывести неинициализированные переменные. Неинициализированные срезы могут привести к тому,
что gdb попытается вывести произвольно большие массивы.
Аргументы функции:
(gdb) <b>info args</b> t = 0xf840688b60
При печати аргумента обратите внимание, что это указатель на значение Regexp. Обратите внимание, что GDB
неправильно разместил символ * справа от имени типа и добавил ключевое слово 'struct' в традиционном стиле C.
(gdb) <b>p re</b>
(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>
Эта структура hchan<*testing.T> является внутренним представлением канала в среде выполнения. В данный момент она пуста, иначе GDB бы отобразил её содержимое в удобочитаемом виде.
Переход к следующей строке:
(gdb) <b>n</b> <i># выполнить следующую строку</i>
149 for _, test := range findTests {
(gdb) <i># нажатие Enter повторяет предыдущую команду</i>
150 re := MustCompile(test.pat)
(gdb) <b>p test.pat</b>
$4 = ""
(gdb) <b>p re</b>
$5 = (struct regexp.Regexp *) 0xf84068d070
(gdb) <b>p *re</b>
$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) <b>p *re->prog</b>
$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}
Можно войти в вызов функции String с аргументом "s":
(gdb) <b>s</b>
regexp.(*Regexp).String (re=0xf84068d070, noname=void) at /home/user/go/src/regexp/regexp.go:97
97 func (re *Regexp) String() string {
Получим трассировку стека, чтобы увидеть текущее положение:
(gdb) <b>bt</b> #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 ....
Посмотрим на исходный код:
(gdb) <b>l</b>
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,
Pretty Printing
Механизм pretty printing в GDB активируется при совпадении регулярных выражений с именами типов. Пример для срезов:
(gdb) <b>p utf</b>
$22 = []uint8 = {0 '\000', 0 '\000', 0 '\000', 0 '\000'}
Поскольку срезы, массивы и строки не являются указателями на C, GDB не может самостоятельно интерпретировать операцию индексирования, но вы можете просмотреть внутреннее представление в среде выполнения, чтобы сделать это (автозавершение с помощью TAB здесь помогает):
(gdb) <b>p slc</b>
$11 = []int = {0, 0}
(gdb) <b>p slc-></b><i><TAB></i>
array slc len
(gdb) <b>p slc->array</b>
$12 = (int *) 0xf84057af00
(gdb) <b>p slc->array[1]</b>
$13 = 0
Функции расширения $len и $cap работают со строками, массивами и срезами:
(gdb) <b>p $len(utf)</b> $23 = 4 (gdb) <b>p $cap(utf)</b> $24 = 4
Каналы и карты являются типами «ссылочного» типа, которые GDB отображает как указатели на типы, похожие на C++ hash<int,string>*. Разыменование приведёт к активации prettyprinting
Интерфейсы в среде выполнения представлены в виде указателя на дескриптор типа и указателя на значение. Расширение среды выполнения Go для GDB расшифровывает эту информацию и автоматически активирует pretty printing для типа среды выполнения. Функция расширения $dtype расшифровывает динамический тип (примеры взяты из точки останова в файле regexp.go на строке 293.)
(gdb) <b>p i</b>
$4 = {str = "cbb"}
(gdb) <b>whatis i</b>
type = regexp.input
(gdb) <b>p $dtype(i)</b>
$26 = (struct regexp.inputBytes *) 0xf8400b4930
(gdb) <b>iface i</b>
regexp.input: struct regexp.inputBytes *