Перевод. Берём на вооружение: Основы отладчика GDB на примере PostgreSQL

PostgreSQLНесколько месяцев назад я уже пробовал поработать с отладчиком GDB, собрать PostgreSQL с отладочными символами, но тогда у меня была какая-то ошибка, так что не получилось и я отложил это на будущее.

Будущее наступило.

Прислали ссылку на статью Unlock Your Arsenals: GDB Debugging Essentials with PostgreSQL. В тот же день я не мог остановиться и просидел почти до часа ночи и успешно выполнил всё то, что описано в статье. Ну а на следующий день просидел до двух ночи, всё это перевёл и еще лучше разобрался в том, что написано в статье.

К этой статье я буду часто добавлять свои комментарии, начинать их я буду вот так:

Примечание Транслирующего (ПТ), ну а еще потому что Павел Толмачев 🙂

И писать курсивом.

Также я для некоторых функций добавил ссылки на github. Рекомендую туда перейти и посмотреть что происходит непосредственно в коде и осознать откуда и куда прыгает GDB при работе.

И еще некоторые термины я попытался перевести (например бектрейс), но бросил это дело — я не помню чтобы их переводили на русский, да и сходу перевод нормальный не нашел. То же относится к фрейму (frame). Решил использовать слово frame всё-таки.

Ссылка на оригинал.


Как опытный разработчик C/C++, я всегда считал GDB (GNU Debugger) своим «лучшим другом» в разработке программного обеспечения из-за его незаменимой роли в процессе разработки и отладки. С помощью этого мощного инструмента разработчик может:

  1. Искать и устранять проблемы в программах C/C++, таких как ошибки сегментации и логические ошибки;
  2. Отслеживать и понимать поток выполнения, переменных, содержимого памяти, сигналов и системных вызовов вашей программы C/C++.

Больше всего времени, связанного с GDB, я трачу на второй пункт: я хочу выяснить, как работает сложное программное обеспечение, прежде чем я смогу уверенно добавлять или улучшать его функции.

В этой статье я буду использовать PostgreSQL 16 в качестве практического примера, чтобы продемонстрировать некоторые возможности GDB в командной строке Ubuntu 18.04. Я также продемонстрирую, как использую отладку GDB, чтобы изучить внутреннюю логику выполнения PostgreSQL.

Содержание:

  1. Включение отладки и отключение оптимизации
  2. Подключение с помощью GDB к работающей программе (Attach)
  3. Запуск программы с помощью GDB (Run)
  4. Настройка GDB для трассировки
    1. Точка останова (Break Point)
    2. Стек вызовов (Back Trace)
    3. Кадр (Frame)
  5. Управление точками останова
  6. Что можно посмотреть в точке останова
    1. Вывод переменной или структуры (Print)
    2. Исследование блока памяти (Examine)
    3. Отображение исходного кода (List)
  7. Контроль выполнения
    1. Команды Next и Step
    2. Продолжение выполнения (Continue)
    3. Присмотр за порождённым процессом (Fork)
    4. POSIX-сигналы
  8. GDB Debugging. Итоги
  9. Ссылки
  10. Об авторе

(ПТ): я выполнял эту «практику» в Ubuntu 23.04 и PostgreSQL 16.0. Результат получился точно такой же как в статье.

 

1. Включение отладки и отключение оптимизации

Программу нужно скомпилировать в режиме отладки, чтобы она включала отладочные символы, понятные GDB. Я бы также рекомендовал отключить оптимизацию компилятора, чтобы гарантировать, что отладчик GDB может отслеживать поток выполнения и правильно печатать переменные. При включенной оптимизации некоторые переменные могут быть «оптимизированы», а некоторые блоки кода могут быть пропущены при выполнении. Это приводит к путанице в результатах с GDB.

В PostgreSQL 16 вы можете включить отладку, отключить оптимизацию и создать отладочную версию программы с помощью любой из приведенных ниже команд:

Традиционный ./configure и Makefile:

CFLAGS=-O0 ./configure --prefix=$PWD/mypg --enable-debug
make
make install

С помощью meson (если используете её):

meson setup build --prefix=$PWD/mypg -Dbuildtype=debug -Doptimization=0
ninja
ninja install

(ПТ) Я пользовался традиционными .configure и Makefile

2. Подключение с помощью GDB к работающей программе (Attach)

PostgreSQL — это многопроцессная программа, которая обычно запускается утилитой pg_ctl. Утилита порождает процесс postmaster, который, в свою очередь, запускает другие процессы, каждый из которых выполняет свою собственную задачу. В этой статье мы подключим GDB к внутренниму процессу, который отвечает за обслуживание SQL-запросов клиента psql.

# инициализируем кластер баз данных по адресу debugdb (PGDATA)
mypg/bin/initdb -D debugdb
# запускаем PostgreSQL
mypg/bin/pg_ctl -D debugdb -l logfile start
# подключаемся к PostgreSQL к БД postgres под системным пользователем
mypg/bin/psql -d postgres

(ПТ) Я устанавливал и запускал PostgreSQL следующим образом:

git clone https://github.com/postgres/postgres
git checkout REL_16_STABLE
CFLAGS=-O0 ./configure --prefix=/home/pavel/mypostgres --with-pgport=5001 --enable-debug
make -j 8
sudo make install
cd mypostgres
mkdir data
cd bin
./initdb -D ../data
./pg_ctl -D ../data -l logfile start
./psql -p 5001 -d postgres
\conninfo 
You are connected to database "postgres" as user "pavel" via socket in "/tmp" at port "5001".

И сразу выполните подготовительное действие — создайте таблицу, которую далее автор будет использовать:

CREATE TABLE mytable (id integer, val text);

В отдельном терминале мы можем проверить все запущенные процессы PostgreSQL с помощью команды ps -ef.

ps -ef | grep postgres
caryh    3269353       1  0 18:13 ?        00:00:00 /home/caryh/postgres/mydb/bin/postgres -D debugdb
caryh    3269354 3269353  0 18:13 ?        00:00:00 postgres: checkpointer
caryh    3269355 3269353  0 18:13 ?        00:00:00 postgres: background writer
caryh    3269357 3269353  0 18:13 ?        00:00:00 postgres: walwriter
caryh    3269358 3269353  0 18:13 ?        00:00:00 postgres: autovacuum launcher
caryh    3269359 3269353  0 18:13 ?        00:00:00 postgres: logical replication launcher
caryh    3271568 3176393  0 18:15 pts/0    00:00:00 psql -d postgres -p 5432
caryh    3271569 3269353  0 18:15 ?        00:00:00 postgres: caryh postgres [local] idle      # это обслуживающий psql процесс
caryh    3271762 3238353  0 18:15 pts/1    00:00:00 grep --color=auto postgres

(ТП) Соответственно, у меня эта строка выглядит так:

pavel 419621 332974 0 14:38 ? 00:00:00 postgres: pavel postgres [local] idle

Обратите внимание что сначала идёт «процесс: база данных роль».

Но можно сделать проще — в psql’e выполните команду:

SELECT pg_backend_pid();
 pg_backend_pid 
----------------
         419621
(1 row)

Получите ровно то, что нужно — номер обслуживающего процесса.

Серверный процесс PostgreSQL, отвечающий за обслуживание клиента psql, имеет описание postgres: caryh postgres [local]idle с идентификатором процесса 3271569. Этот идентификатор нужен GDB чтобы присоединения нему. Во время подключения GDB загружает отладочные символы из обслуживающего процесса и из сторонних библиотек, которые тот может использовать. Некоторые библиотеки имеют отладочные символы, а некоторые нет. Если вы выполняете трассировку сторонней библиотеки без отладочных символов, это означает, что GDB не сможет интерпретировать информацию о памяти или получить стек вызовов (back trace). Это нормально, потому что сейчас мы будем отслеживать процесс PostgreSQL.

Процесс приостановится, как только он будет подключен к GDB, и будет ожидать дальнейших команд GDB.

# запускаем GDB
sudo gdb highgo/bin/postgres
(gdb) attach 3271569
# GDB загружает отладочные символы при подключении к процессу
Attaching to process 3271569
Reading symbols from /home/caryh/postgres/mydb/bin/postgres...done.
Reading symbols from /lib/x86_64-linux-gnu/libm.so.6...Reading symbols from /usr/lib/debug//lib/x86_64-linux-gnu/libm-2.27.so...done.
Reading symbols from /lib/x86_64-linux-gnu/libdl.so.2...Reading symbols from /usr/lib/debug//lib/x86_64-linux-gnu/libdl-2.27.so...done.
Reading symbols from /lib/x86_64-linux-gnu/librt.so.1...Reading symbols from /usr/lib/debug//lib/x86_64-linux-gnu/librt-2.27.so...done.
Reading symbols from /usr/lib/x86_64-linux-gnu/libgssapi_krb5.so.2...(no debugging symbols found)...done.
Reading symbols from /usr/lib/x86_64-linux-gnu/libicuuc.so.60...(no debugging symbols found)...done.
Reading symbols from /usr/lib/x86_64-linux-gnu/libicui18n.so.60...(no debugging symbols found)...done.
...
...
Reading symbols from /lib/x86_64-linux-gnu/libpthread.so.0...Reading symbols from /usr/lib/debug/.build-id/1f/06001733b9be9478b105faf0dac6bdf36c85de.debug...done.
[Thread debugging using libthread_db enabled]
Reading symbols from /usr/lib/x86_64-linux-gnu/libffi.so.6...(no debugging symbols found)...done.
Reading symbols from /lib/x86_64-linux-gnu/libnss_files.so.2...Reading symbols from /usr/lib/debug//lib/x86_64-linux-gnu/libnss_files-2.27.so...done.
# программа приостанавливается здесь при подключении GDB
0x00007fa18ed71907 in epoll_wait (epfd=7, events=0x556537600ad0, maxevents=1, timeout=-1)
    at ../sysdeps/unix/sysv/linux/epoll_wait.c:30
30      ../sysdeps/unix/sysv/linux/epoll_wait.c: No such file or directory.
# GDB ждет вашей команды
(gdb)

(ТП) Для запуска GDB я выполнил:

sudo gdb ~/mypostgres/bin/./postgres

3. Запуск программы с помощью GDB (Run)

Также можно запустить программу с помощью GDB. В основном это используется в однопроцессных программах или в таких, которые не должны работать постоянно. Например, pg_waldump — это утилита PostgreSQL для выгрузки WAL в текст, после чего утилита заканчивает свою работу. Лучше использовать GDB для запуска pg_waldump вместо присоединения к процессу:

# используйте GDB для запуска pg_waldump с аргументами
sudo gdb --args mypg/bin/pg_waldump debugdb/pg_wal/000000010000000000000001
Reading symbols from highgo/bin/pg_waldump...done.
# обычно мы указываем несколько точек останова перед «запуском». (См. следующий раздел)
(gdb) b main
# используйте команду run для запуска pg_waldump
(gdb) run

4. Настройка GDB для трассировки

4.1. Точка останова (Break Point)

Точка останова относится к месту в исходном коде программы, где GDB приостанавливает выполнение. Её можно установить с помощью команды b, обычно она устанавливается:

  • перед запуском подозрительной функции или кода (если вы устраняете ошибку);
  • на нужную функциональности для дальнейшей обратной или прямой трассировки (если вы отслеживаете большое программное обеспечение и изучаете, как оно выполняется).

Например, чтобы узнать, как PostgreSQL обрабатывает операцию INSERT, я бы установил точку останова на функцию heap_insert(). Есть несколько способов установить точку останова; ниже приведены наиболее распространенные из них, которые я использую:

  • Устанавить точку останова в функции heap_insert():
    (gdb) b heap_insert
    (ТП) Хм, а что, в Постгресе не бывает двух одинаково названных функций в разных местах?
  • Приостановить программу в строке 1836 исходного файла heapam.c:
    (gdb) b heapam.c:1836
  • Приостанавить программу в функции heap_insert() при выполнении условия:
    (gdb) b heap_insert if IsCatalogRelation(relation) != 1

(ТП) У меня отработало вот так:

(gdb) b heapam.c:1836
Breakpoint 1 at 0x557052a602e3: file heapam.c, line 1839.

Как только точка останова установлена, мы можем указать GDB продолжить выполнение программы с помощью команды continue (c). Если вам нужно установить больше точек останова или выполнить другие команды GDB, вы можете использовать Ctrl-C, чтобы прервать работу GDB и снова получить приглашение GDB.

# продолжим программу
(gdb) c 
Continuing.
# снова получим приглашение GDB — оно приостановит программу в текущем месте выполнения.
^C
Program received signal SIGINT, Interrupt.
0x00007fa18ed71907 in epoll_wait (epfd=7, events=0x556537600ad0, maxevents=1, timeout=-1) at ../sysdeps/unix/sysv/linux/epoll_wait.c:30
30      in ../sysdeps/unix/sysv/linux/epoll_wait.c
(gdb)

(ТП) Не знаю почему, но у меня нужно нажать комбинацию Ctrl+Shift+C чтобы вернуть управление в GDB.

4.2. Стек вызовов (Back Trace)

Мы можем перейти к созданной ранее точке останова с помощью простой команды вставки, например INSERT INTO mytable VALUES (1, ‘test’). После её срабатывания мы сможем получить стек вызовов с помощью команды bt.

Breakpoint 1, heap_insert (relation=0x7f11005e7280, tup=0x562602e1d878, cid=0, options=0, bistate=0x0) at heapam.c:1828
(gdb) bt
#0  heap_insert (relation=0x7f11005e7280, tup=0x562602e1d878, cid=0, options=0, bistate=0x0) at heapam.c:1828
#1  0x0000562600ba3eb7 in heapam_tuple_insert (relation=0x7f11005e7280, slot=0x562602e1d770, cid=0, options=0, bistate=0x0) at heapam_handler.c:252
#2  0x0000562600e20c5e in table_tuple_insert (rel=0x7f11005e7280, slot=0x562602e1d770, cid=0, options=0, bistate=0x0) at ../../../src/include/access/tableam.h:1400
#3  0x0000562600e22976 in ExecInsert (context=0x7ffdca3e51f0, resultRelInfo=0x562602e1cc40, slot=0x562602e1d770, canSetTag=true, inserted_tuple=0x0, insert_destrel=0x0) at nodeModifyTable.c:1133
#4  0x0000562600e268fe in ExecModifyTable (pstate=0x562602e1ca38) at nodeModifyTable.c:3790
#5  0x0000562600deb512 in ExecProcNodeFirst (node=0x562602e1ca38) at execProcnode.c:464
#6  0x0000562600ddfb64 in ExecProcNode (node=0x562602e1ca38) at ../../../src/include/executor/executor.h:273
#7  0x0000562600de2327 in ExecutePlan (estate=0x562602e1c7f0, planstate=0x562602e1ca38, use_parallel_mode=false, operation=CMD_INSERT, sendTuples=false, numberTuples=0, direction=ForwardScanDirection, dest=0x562602e01230, execute_once=true)
    at execMain.c:1670
#8  0x0000562600de00d6 in standard_ExecutorRun (queryDesc=0x562602e318c0, direction=ForwardScanDirection, count=0, execute_once=true) at execMain.c:365
#9  0x0000562600ddff6d in ExecutorRun (queryDesc=0x562602e318c0, direction=ForwardScanDirection, count=0, execute_once=true) at execMain.c:309
#10 0x000056260105d7a7 in ProcessQuery (plan=0x562602e010e0, sourceText=0x562602d39220 "insert into mytable values(1, 'fff');", params=0x0, queryEnv=0x0, dest=0x562602e01230, qc=0x7ffdca3e5650) at pquery.c:160
#11 0x000056260105f0c3 in PortalRunMulti (portal=0x562602daeec0, isTopLevel=true, setHoldSnapshot=false, dest=0x562602e01230, altdest=0x562602e01230, qc=0x7ffdca3e5650) at pquery.c:1277
#12 0x000056260105e68e in PortalRun (portal=0x562602daeec0, count=9223372036854775807, isTopLevel=true, run_once=true, dest=0x562602e01230, altdest=0x562602e01230, qc=0x7ffdca3e5650) at pquery.c:791
#13 0x0000562601057d78 in exec_simple_query (query_string=0x562602d39220 "insert into mytable values(1, 'fff');") at postgres.c:1274
#14 0x000056260105c789 in PostgresMain (dbname=0x562602d33c50 "postgres", username=0x562602d6ee50 "caryh") at postgres.c:4637
#15 0x0000562600f9a479 in BackendRun (port=0x562602d624b0) at postmaster.c:4464
#16 0x0000562600f99d69 in BackendStartup (port=0x562602d624b0) at postmaster.c:4192
#17 0x0000562600f9655e in ServerLoop () at postmaster.c:1782
#18 0x0000562600f95e59 in PostmasterMain (argc=3, argv=0x562602d31ba0) at postmaster.c:1466
#19 0x0000562600e5ff1f in main (argc=3, argv=0x562602d31ba0) at main.c:198

Обратная трассировка здесь очень важна, потому что она буквально сообщает вам, как программа добралась до точки останова (из какой функции, из какого исходного файла и номера строки) с момента запуска программы.

#0 — это установленная нами точка останова. #19 — это первый кадр (frame) всего стека вызовов. Благодаря обратной трассировке у нас есть возможность узнать, как работает программа PostgreSQL, потому что мы точно знаем, куда смотреть.

(ТП) Вот так выглядит функция heapam_tuple_insert — первая точка входа функции heap_insert(), она находится в файле heapam_handler.c, как раз строка с номером #1 об этом говорит:

/* ----------------------------------------------------------------------------
 *  Functions for manipulations of physical tuples for heap AM.
 * ----------------------------------------------------------------------------
 */

static void
heapam_tuple_insert(Relation relation, TupleTableSlot *slot, CommandId cid,
					int options, BulkInsertState bistate)
{
	bool		shouldFree = true;
	HeapTuple	tuple = ExecFetchSlotHeapTuple(slot, true, &shouldFree);

	/* Update the tuple with table oid */
	slot->tts_tableOid = RelationGetRelid(relation);
	tuple->t_tableOid = slot->tts_tableOid;

	/* Perform the insertion, and copy the resulting ItemPointer */
	heap_insert(relation, tuple, cid, options, bistate);
	ItemPointerCopy(&tuple->t_self, &slot->tts_tid);

	if (shouldFree)
		pfree(tuple);
}

А вызов функции heap_insert(), на которую мы ранее и поставили точку останова — находится в строке 252 в файле heapam_handler.c. Посередине функции heapam_tuple_insert(), текст которой я выше привёл.

В общем, всё это написано в трейсе, просто и понятно 🙂

4.3. Кадр (Frame)

Когда программа приостановлена, мы можем проверять локальные переменные внутри текущей функции (в #0) или проверять переменные в глобальной области видимости. Однако мы не можем проверять локальную переменную в других фреймах. Для этого нам нужно изменить кадр с помощью команды f. Например, чтобы напечатать локальную переменную cmdtagname, объявленную внутри функции exec_simple_query (#13), мы можем:

(gdb) p cmdtagname
No symbol "cmdtagname" in current context.
(gdb) f 13
#13 0x0000557052f3bd57 in exec_simple_query (query_string=0x5570552b3010 "INSERT INTO mytable VALUES (1, 'test');") at postgres.c:1274
1274			(void) PortalRun(portal,
(gdb) p cmdtagname
$1 = 0x5570532b6734 "INSERT"
(gdb) 

(ТП) Хоть автор и написал, но всё же уточню. Откуда взялась переменная cmdtagname? Смотрим на всю эту строку, там написано postgres.c:1274. Идём туда, там PortalRun (в трейсе эта функция упоминается в строке #12). Но в тексте статьи речь идёт о переменной cmdtagname, значит она должна быть в файле postgres.c. Поиском её легко можно найти. Но где именно искать? А в трейсе и это тоже написано, в самом начале строки #13 — в функции exec_simple_query то функция, из которой вызывается PortalRun). Где-то там ниже появляется и cmdtagname и другие переменные, используемые в этой функции. Можете для интереса посмотреть значение переменной cmdtaglen:

(gdb) p cmdtaglen
$33 = 6

Обратите внимание, что в кадре 13 мы не можем использовать команду next или step into, чтобы заставить программу выполняться дальше, начиная с кадра 13, поскольку она уже была выполнена. Команда f (Frame) просто позволяет вам взглянуть на каждую точку трассировки стека.

5. Управление точками останова

Чтобы управлять всеми созданными на данный момент точками останова, вы можете использовать команду info b:

W(gdb) info b
Num     Type           Disp Enb Address            What
1       breakpoint     keep y   0x000055a847e4e9f2 in heap_insert at heapam.c:1828
        breakpoint already hit 1 time
2       breakpoint     keep y   0x000055a847e4ea14 in heap_insert at heapam.c:1836
3       breakpoint     keep y   0x000055a847e4e9f2 in heap_insert at heapam.c:1828
        stop only if IsCatalogRelation(relation) != 1

Мы можем отключить, включить или удалить точки останова:

  • # отключить точку останова 2 и 3
    disable 2 3
  • # включить точку останова 3
    enable 3
  • # удалить точку останова 2
    delete 2

6. Что можно посмотреть в точке останова

6.1. Вывод переменной или структуры (Print)

Вы можете отобразить любые переменные или структуры в пределах текущего кадра, включая локальные и глобальные. Например, в функцию heap_insert передается структура отношений (ТП: heap_insert(relation, tuple, cid, options, bistate)). Мы можем использовать команду print (p), чтобы изучить эту структуру. Вы можете использовать команду set print beautiful, чтобы напечатать структуру в более красивой форме. Обратите внимание: поскольку структура передается как указатель, вам необходимо использовать перед ней оператор разыменования *, чтобы просмотреть ее содержимое, иначе GDB напечатает только адрес памяти этого указателя.

Вы также можете использовать операторы -> или . для доступа к другой переменной внутри этой структуры.

Continuing.
Breakpoint 1, heap_insert (relation=0x7f3c1d6ca280, tup=0x5653ffa489d8, cid=0, options=0, bistate=0x0)
    at heapam.c:1828
1828    {
# вывести адрес указателя отношения
(gdb) p relation
$1 = (Relation) 0x7f3c1d6ca280
# разыменовать указатель отношения
(gdb) p *relation
$2 = {rd_locator = {spcOid = 1663, dbOid = 5, relNumber = 16388}, rd_smgr = 0x0, rd_refcnt = 1, rd_backend = -1,
  rd_islocaltemp = false, rd_isnailed = false, rd_isvalid = true, rd_indexvalid = false, rd_statvalid = false,
  rd_createSubid = 0, rd_newRelfilelocatorSubid = 0, rd_firstRelfilelocatorSubid = 0, rd_droppedSubid = 0,
  rd_rel = 0x7f3c1d6ca488, rd_att = 0x7f3c1d6ca590, rd_id = 16388, rd_lockInfo = {lockRelId = {relId = 16388,
      dbId = 5}}, rd_rules = 0x0, rd_rulescxt = 0x0, trigdesc = 0x0, rd_rsdesc = 0x0, rd_fkeylist = 0x0,
  rd_fkeyvalid = false, rd_partkey = 0x0, rd_partkeycxt = 0x0, rd_partdesc = 0x0, rd_pdcxt = 0x0,
  rd_partdesc_nodetached = 0x0, rd_pddcxt = 0x0, rd_partdesc_nodetached_xmin = 0, rd_partcheck = 0x0,
  rd_partcheckvalid = false, rd_partcheckcxt = 0x0, rd_indexlist = 0x0, rd_pkindex = 0, rd_replidindex = 0,
  rd_statlist = 0x0, rd_attrsvalid = false, rd_keyattr = 0x0, rd_pkattr = 0x0, rd_idattr = 0x0,
  rd_hotblockingattr = 0x0, rd_summarizedattr = 0x0, rd_pubdesc = 0x0, rd_options = 0x0, rd_amhandler = 3,
  rd_tableam = 0x5653ff75aaa0 <heapam_methods>, rd_index = 0x0, rd_indextuple = 0x0, rd_indexcxt = 0x0,
  rd_indam = 0x0, rd_opfamily = 0x0, rd_opcintype = 0x0, rd_support = 0x0, rd_supportinfo = 0x0,
  rd_indoption = 0x0, rd_indexprs = 0x0, rd_indpred = 0x0, rd_exclops = 0x0, rd_exclprocs = 0x0,
  rd_exclstrats = 0x0, rd_indcollation = 0x0, rd_opcoptions = 0x0, rd_amcache = 0x0, rd_fdwroutine = 0x0,
  rd_toastoid = 0, pgstat_enabled = true, pgstat_info = 0x0}
# разыменовать определенный члена в структуре
(gdb) set print pretty
(gdb) p *relation->rd_rel
$3 = {
  oid = 16388,
  relname = {
    data = "mytable", '\000' <repeats 56 times>
  },
  relnamespace = 2200,
  reltype = 16390,
  reloftype = 0,
  relowner = 10,
  relam = 2,
…

6.2. Исследование блока памяти (Examine)

Помимо отображения (p), мы также можем просмотреть блок памяти с помощью команды examine (x). Например, мы можем сначала отобразить содержимое структуры HeapTuple (которая представляет собой строку данных), чтобы узнать ее размер, а затем использовать команду x для проверки ее поля данных, используя этот размер. Команда examine (x) лучше, чем команда print (p), потому что HeapTuple содержит массив, и текущие пользовательские данные начинаются в конце определения структуры.

Следующий пример показывает, что с помощью команды examine (x) я могу легко увидеть содержимое строки в команде INSERT (insert into mytable values(1, ‘fff’)). 0x66 0x66 0x66 соответствует входным данным «fff».

# узнать размер HeapTuple (t_len, равен 32)
(gdb) p *tup
$4 = {
  t_len = 32,
  t_self = {
    ip_blkid = {
      bi_hi = 65535,
      bi_lo = 65535
    },
    ip_posid = 0
  },
  t_tableOid = 16388,
  t_data = 0x5653ffa489f0
}
# проверьте содержимое памяти tup->t_data
(gdb) x/32bx tup->t_data
0x5653ffa489f0: 0x80    0x00    0x00    0x00    0xff    0xff    0xff    0xff
0x5653ffa489f8: 0xc9    0x08    0x00    0x00    0xff    0xff    0xff    0xff
0x5653ffa48a00: 0x00    0x00    0x02    0x00    0x02    0x00    0x18    0x00
0x5653ffa48a08: 0x01    0x00    0x00    0x00    0x09    0x66    0x66    0x66

(ТП) Я добавил другую строку, поэтому у меня было так:

0x55705539e598: 0x01 0x00 0x00 0x00 0x0b 0x74 0x65 0x73
0x55705539e5a0: 0x74

Берём последние четыре символа, открываем любую ASCII-таблицу и можно перевести эти символы в текст (ну или онлайн-переводчиком). Прикольно 🙂

А если используете Ubuntu — тогда выполните команду man ascii, там же и поиск будет.

6.3. Отображение исходного кода (List)

Вы можете использовать команду list (l), чтобы просмотреть исходный код вокруг текущего фрейма. На первый взгляд эта команда кажется удобной, но я редко использую её для просмотра исходного кода. Лучше просмотреть весь исходный код в вашей любимой IDE (например, Eclipse или VSCode) с правильной подсветкой синтаксиса.

Код вокруг текущего фрейма:

(gdb) l
1823     * reflected into *tup.
1824     */
1825    void
1826    heap_insert(Relation relation, HeapTuple tup, CommandId cid,
1827                            int options, BulkInsertState bistate)
1828    {
1829            TransactionId xid = GetCurrentTransactionId();
1830            HeapTuple       heaptup;
1831            Buffer          buffer;
1832            Buffer          vmbuffer = InvalidBuffer;

Код вокруг строки 2000:

(gdb) list 2000
1995            pgstat_count_heap_insert(relation, 1);
1996
1997            /*
1998             * If heaptup is a private copy, release it.  Don't forget to copy t_self
1999             * back to the caller's image, too.
2000             */
2001            if (heaptup != tup)
2002            {
2003                    tup->t_self = heaptup->t_self;
2004                    heap_freetuple(heaptup);

7. Контроль выполнения

7.1. Команды Next и Step

Команды Next (n) и Step (s) самые часто используемые для управления выполнением программы и чрезвычайно удобны для отладки и трассировки. Вы также можете использовать команду print (p) для наблюдения за значениями переменных по мере продвижения программы.

Breakpoint 1, heap_insert (relation=0x7f3c1d6a5c60, tup=0x5653ffa4a4a8, cid=0, options=0, bistate=0x0) at heapam.c:1828
1828    {
# используйте команду «n» для выполнения одной строки кода. 
(gdb) n
1829        TransactionId xid = GetCurrentTransactionId();
# Продолжайте нажимать клавишу «enter», чтобы выполнить ту же команду, что и в прошлый раз
(gdb)      
1832        Buffer        vmbuffer = InvalidBuffer;
(gdb) 
1833        bool        all_visible_cleared = false;
(gdb) 
1845        heaptup = heap_prepare_insert(relation, tup, xid, cid, options);
# когда дошли до вызова функции, используйте команду «s», чтобы войти в функцию
(gdb) s     
heap_prepare_insert (relation=0x7f3c1d6a5c60, tup=0x5653ffa4a4a8, xid=751, cid=0, options=0) at heapam.c:2024
2024        if (IsParallelWorker())
# затем используйте команду «n» для пошагового выполнения кода внутри этой функции
(gdb) n    
2029        tup->t_data->t_infomask &= ~(HEAP_XACT_MASK);
(gdb) 
2030        tup->t_data->t_infomask2 &= ~(HEAP2_XACT_MASK);
(gdb) 
2031        tup->t_data->t_infomask |= HEAP_XMAX_INVALID;
# если команда «n» запускается для функции, GDB автоматически выполняет всю функцию
(gdb)      
2032        HeapTupleHeaderSetXmin(tup->t_data, xid);
(gdb) 
2033        if (options & HEAP_INSERT_FROZEN)
(gdb) 
2036        HeapTupleHeaderSetCmin(tup->t_data, cid);
(gdb) 
2037        HeapTupleHeaderSetXmax(tup->t_data, 0); /* for cleanliness */
(gdb) 
2038        tup->t_tableOid = RelationGetRelid(relation);
(gdb) 
2044        if (relation->rd_rel->relkind != RELKIND_RELATION &&
(gdb) 
2051        else if (HeapTupleHasExternal(tup) || tup->t_len > TOAST_TUPLE_THRESHOLD)
(gdb) 
2054            return tup;
(gdb) 
2055    }
(gdb) 
# как только вы вернётесь из функции, в которую зашли по команде s, GDB перенесет вас на следующую строку после её вызова
heap_insert (relation=0x7f3c1d6a5c60, tup=0x5653ffa4a4a8, cid=0, options=0, bistate=0x0) at heapam.c:1851
1851        buffer = RelationGetBufferForTuple(relation, heaptup->t_len,
(gdb) 
1871        CheckForSerializableConflictIn(relation, NULL, InvalidBlockNumber);
(gdb) 
1874        START_CRIT_SECTION();
(gdb)

(ТП) На счёт последнего комментария в коде выше — обратите внимание на номер строк, который слева отображается. Мы зашли в функцию heap_prepare_insert по номеру строки 1845 (странно что в гитхабе другой номер немного) — перешли на номер heapam.c:2024. Дальше пробежались пошагово без заходов в функции и цикло-ветвления до строки 2055 и вернулись обратно, на следующую строку кода после 1845 — это строка 1851. Строки не подряд потому что комментарии и переносы одной строки не считаются.

7.2. Продолжение выполнения (Continue)

Вместо использования команд n или s для выполнения программы по одной строке за раз, вы можете использовать команду continue (c) для продолжения выполнения до достижения следующей точки останова или до тех пор, пока программа не будет прервана сигналом (например, нажатием Ctrl-C) или когда программа завершит работу.

# используйте команду «c», чтобы продолжить выполнение программы
(gdb) c
Continuing.

7.3. Присмотр за порождённым процессом (Fork)

Вполне возможно, что текущий процесс породит новый процесс для выполнения некоторых задач с помощью команды fork(). В этом случае GDB проигнорирует дочерний процесс и продолжит выполнение родительского процесса. Что, если мы хотим, чтобы GDB отслеживал дочерний процесс, а не родительский? Для этого вы можете использовать команду set follow-fork-mode child.

Чтобы продемонстрировать это на PostgreSQL, нам нужно подключить GDB к процессу postmaster, который отвечает за создание новых процессов для различных целей. Мы предварительно заполним таблицу миллионом записей и выполним для нее SELECT COUNT(*); по умолчанию это заставит postmaster создавать параллельные рабочие процессы, чтобы ускорить выполнение этого запроса.

ps -ef | grep postgres
caryh    3269353       1  0 18:13 ?        00:00:00 /home/caryh/postgres/mydb/bin/postgres -D debugdb   # это основной процесс postgres
caryh    3269354 3269353  0 18:13 ?        00:00:00 postgres: checkpointer
caryh    3269355 3269353  0 18:13 ?        00:00:00 postgres: background writer
caryh    3269357 3269353  0 18:13 ?        00:00:00 postgres: walwriter
caryh    3269358 3269353  0 18:13 ?        00:00:00 postgres: autovacuum launcher
caryh    3269359 3269353  0 18:13 ?        00:00:00 postgres: logical replication launcher
caryh    3271568 3176393  0 18:15 pts/0    00:00:00 psql -d postgres -p 5432
caryh    3271569 3269353  0 18:15 ?        00:00:00 postgres: caryh postgres [local] idle      # это обслуживающий psql процесс
caryh    3271762 3238353  0 18:15 pts/1    00:00:00 grep --color=auto postgres

Давайте заполним таблицу данными. Если ваш сеанс отладки GDB все ещё подключен к обслуживающему процессу, вы можете выйти из GDB с помощью команды quit, чтобы он не запускал постоянно точку останова на heap_insert. После вставки мы можем использовать explain analyze, чтобы убедиться, что PostgreSQL создаст параллельные рабочие процессы для выполнения запроса SELECT COUNT(*).

postgres=# insert into mytable values(generate_series(1,1000000), 'fff');
INSERT 0 1000000
postgres=# explain analyze select count(*) from mytable;
                                        QUERY PLAN                                                                 
------------------------------------------------------------------------------------------
 Finalize Aggregate  (cost=8352.17..8352.18 rows=1 width=8) (actual time=4083.773..4084.069 rows=1 loops=1)
   ->  Gather  (cost=8351.95..8352.16 rows=2 width=8) (actual time=4083.737..4084.045 rows=3 loops=1)
         Workers Planned: 2
         Workers Launched: 2
         ->  Partial Aggregate  (cost=7351.95..7351.96 rows=1 width=8) (actual time=1470.671..1470.673 rows=1 loops=3)
               ->  Parallel Seq Scan on mytable  (cost=0.00..6766.56 rows=234156 width=0) (actual time=0.064..1433.957 rows=333336 loops=3)
 Planning Time: 0.583 ms
 Execution Time: 4084.143 ms
(8 rows)

Теперь мы можем запустить еще один сеанс GDB, привязать его к PID 3269353 и выполнить по порядку следующие действия:

  • (gdb) установить точку останова на fork_process
  • (gdb) продолжить выполнение (c)
  • (psql) выполнить SELECT COUNT(*) FROM mytable;
  • (gdb) опять продолжить выполнение (c) если было прервано получением сигнала (подробнее позже)
  • (gdb) переходим к точке останова fork_process
  • (gdb) устанавливаем follow-fork-mode child
  • (gdb) устанавливаем точку останова на ParallelWorkerMain
  • (gdb) продолжаем выполнение
(gdb) attach 3269353
Attaching to program: /home/caryh/highgo/git/postgres.community/highgo/bin/postgres, process 1768640
Reading symbols from /usr/lib/x86_64-linux-gnu/libssl.so.1.1...(no debugging symbols found)...done.
Reading symbols from /usr/lib/x86_64-linux-gnu/libcrypto.so.1.1...(no debugging symbols found)...done.
Reading symbols from /lib/x86_64-linux-gnu/libz.so.1...(no debugging symbols found)...done.
...
0x00007f3c28d0d907 in epoll_wait (epfd=10, events=0x5653ff965910, maxevents=3, timeout=60000) at ../sysdeps/unix/sysv/linux/epoll_wait.c:30
30    ../sysdeps/unix/sysv/linux/epoll_wait.c: No such file or directory.
# установить точку останова на fork_process
(gdb) b fork_process
Breakpoint 1 at 0x5653fef9c308: file fork_process.c, line 33.
(gdb) c
Continuing.

Program received signal SIGUSR1, User defined signal 1.
0x00007f3c28d0d907 in epoll_wait (epfd=10, events=0x5653ff965910, maxevents=3, timeout=60000) at ../sysdeps/unix/sysv/linux/epoll_wait.c:30
30    in ../sysdeps/unix/sysv/linux/epoll_wait.c
(gdb) c
Continuing.

# (psql) SELECT COUNT(*) FROM mutable;

Breakpoint 1, fork_process () at fork_process.c:33
33    {
# set follow-fork-mode to child — установим переход на порождённый процесс
(gdb) set follow-fork-mode child
(gdb) b ParallelWorkerMain
Breakpoint 2 at 0x5653fec06128: file parallel.c, line 1263.
(gdb) c
Continuing.

# создается новый процесс с PID 1277703, и теперь GDB подключается к нему
[New process 3279353]
[Thread debugging using libthread_db enabled]
Using host libthread_db library "/lib/x86_64-linux-gnu/libthread_db.so.1".
[Switching to Thread 0x7f3c2ad7b240 (LWP 3279353)]

# GDB теперь находится в ParallelWorkerMain дочернего (порождённого) процесса
Thread 2.1 "postgres" hit Breakpoint 2, ParallelWorkerMain (main_arg=385426468) at parallel.c:1263
1263    {
(gdb) n
1291        InitializingParallelWorker = true;
(gdb) l
1286        char       *session_dsm_handle_space;
1287        Snapshot    tsnapshot;
1288        Snapshot    asnapshot;
1289    
1290        /* Set flag to indicate that we're initializing a parallel worker. */
1291        InitializingParallelWorker = true;
1292    
1293        /* Establish signal handlers. */
1294        pqsignal(SIGTERM, die);
1295        BackgroundWorkerUnblockSignals();

(ТП) Номер постмастера поможно посмотреть и проще, обращением к файлу postmaster.pid в вашем каталоге с данными PGDATA:

sudo head -n 1 ~/mypostgres/data/postmaster.pid

Ну и мне пришлось несколько раз continue выполнить чтобы попасть в порождённый процесс.

Следуя приведенным выше командам, GDB теперь подключился к дочернему процессу с PID 3279353, и мы можем продолжить отслеживать поведение параллельного рабочего процесса. Мы также можем наблюдать за этим процессом с помощью команды ps -ef.

ps -ef | grep postgres
caryh    3269353       1  0 18:13 ?        00:00:00 /home/caryh/postgres/mydb/bin/postgres -D debugdb   # это основной процесс postgres
caryh    3269354 3269353  0 18:13 ?        00:00:00 postgres: checkpointer
caryh    3269355 3269353  0 18:13 ?        00:00:00 postgres: background writer
caryh    3269357 3269353  0 18:13 ?        00:00:00 postgres: walwriter
caryh    3269358 3269353  0 18:13 ?        00:00:00 postgres: autovacuum launcher
caryh    3269359 3269353  0 18:13 ?        00:00:00 postgres: logical replication launcher
caryh    3271568 3176393  0 18:15 pts/0    00:00:00 psql -d postgres -p 5432
caryh    3271569 3269353  0 18:15 ?        00:00:00 postgres: caryh postgres [local] idle      # это обслуживающий psql процесс
caryh    3271762 3238353  0 18:15 pts/1    00:00:00 grep --color=auto postgres
caryh    3279353 3269353  0 11:18 ?        00:00:00 postgres: parallel worker for PID 3271569  # это порождённый параллельный процесс

7.4. POSIX-сигналы

PostgreSQL использует сигналы POSIX, чтобы прерывать другие процессы для выполнения некоторых задач если это требуется. Например, процесс psql может отправить сигнал SIGUSR1 постмастеру, чтобы запустить параллельный рабочий процесс, который поможет параллельно обрабатывать пользовательский запрос SELECT COUNT(*). По умолчанию подобные сигналы могут прерывать GDB и позволяют вам отслеживать код обработчика сигналов программы.

В зависимости от ваших требований можно настроить GDB на остановку или игнорирование определенных сигналов. Чтобы просмотреть список типов сигналов и то, как GDB их обрабатывает, вы можете использовать команду info signal, где:

  • stop: означает, что GDB должен приостановить работу при получении сигнала;
  • print: GDB должен напечатать сообщение о получении сигнала;
  • pass to program: GDB должен переслать этот сигнал программе.

Обратите внимание, что по умолчанию SIGINT (Ctrl-C) и SIGTRAP (для точек останова) не пересылаются программе после их получения GDB. Это потому, что GDB тоже их использует. Другими словами, если вы сконфигурируете GDB на игнорирование SIGINT и SIGTRAP, это будет означать, что GDB больше не сможет приостанавливать программу в точке останова или Ctrl-C. Если вы это сделаете, GDB выдаст вам предупреждение.

(gdb) info signals
Signal        Stop    Print    Pass to program    Description
SIGHUP        Yes    Yes    Yes        Hangup
SIGINT        Yes    Yes    No        Interrupt
SIGQUIT       Yes    Yes    Yes        Quit
SIGILL        Yes    Yes    Yes        Illegal instruction
SIGTRAP       Yes    Yes    No        Trace/breakpoint trap
SIGABRT       Yes    Yes    Yes        Aborted
SIGEMT        Yes    Yes    Yes        Emulation trap
SIGFPE        Yes    Yes    Yes        Arithmetic exception
SIGKILL       Yes    Yes    Yes        Killed
SIGBUS        Yes    Yes    Yes        Bus error
SIGSEGV       Yes    Yes    Yes        Segmentation fault
SIGSYS        Yes    Yes    Yes        Bad system call
SIGPIPE       Yes    Yes    Yes        Broken pipe
SIGALRM       No    No    Yes        Alarm clock
SIGTERM       Yes    Yes    Yes        Terminated
SIGURG        No    No    Yes        Urgent I/O condition
SIGSTOP       Yes    Yes    Yes        Stopped (signal)
SIGTSTP       Yes    Yes    Yes        Stopped (user)
SIGCONT       Yes    Yes    Yes        Continued
SIGCHLD       No    No    Yes        Child status changed
...
(gdb) handle SIGINT nostop noprint nopass
SIGINT is used by the debugger.
Are you sure you want to change it? (y or n) n
Not confirmed, unchanged.

Для всех остальных сигналов вы можете настроить поведение при их получении GDB.

(gdb) handle SIGPIPE nostop noprint pass
Signal        Stop    Print    Pass to program    Description
SIGPIPE       No    No    Yes        Broken pipe
(gdb) handle SIGUSR1 nostop noprint pass
Signal        Stop    Print    Pass to program    Description
SIGUSR1       No    No    Yes        User defined signal 1
(gdb) handle SIGUSR1 stop print nopass
Signal        Stop    Print    Pass to program    Description
SIGUSR1       Yes    Yes    No        User defined signal 1

8. GDB Debugging. Итоги

GDB — отличный инструмент с множеством мощных функций. В этой статье собраны некоторые из самых полезных команд, которые вы можете использовать для отладки или трассировки программ с помощью отладчика GDB. В таблице ниже представлены все команды GDB, которые мы упомянули сегодня:

Команда Описание
list (l) отобразить исходный код вокруг текущего местоположения
backtrace (bt) отобразить обратную трассировка к текущей точке останова/местоположению
next (n) выполнить одну строку кода
step (s) зайти в функцию
frame (f) изменить текущий кадр на один из кадров, напечатанных bt
continue (c)
продолжить выполнение программы до следующей точки останова, или сигнала, или завершения программы
breakpoint (b) установить точку останова в указанном месте исходного кода или имени функции с условием или без него
info b показать информацию обо всех созданных точках останова
delete x удалить точку останова, где x — идентификатор точки останова, возвращаемый info b
print (p) отобразить значение или адрес переменной или ссылки
examine (x) отобразить блок памяти и вывести его в указанном пользователем формате
run (r) запустить программу с помощью GDB
set follow-fork-mode x
сообщить GDB, за каким процессом следовать, когда текущий процесс разветвляется. X может быть parent, child или ask. По умолчанию — parent
info signals показать информацию о том, как GDB обрабатывает все виды сигналов
handle w x y z указать, как GDB будет обрабатывает сигнал, где
w = имя сигнала, возвращаемого info signals
x = stop or nostop
y = print or noprint
z = pass or nopass
quit выйти GDB

9. Ссылки

10. Об авторе

Кэри Хуанг (Cary Huang)
Кэри Хуанг (Cary Huang)

Привет, это Кэри, ваш дружелюбный энтузиаст технологий, педагог и автор. Мне нравится упрощать сложные концепции, погружаться в сложности программирования, разгадывать тайны программного обеспечения и, что наиболее важно, делиться и обучать других всем технологиям.


Ещё раз ссылка на оригинал.

Я в восторге от статьи!


Be the first to comment

Leave a Reply

Ваш Mail не будет опубликован.


*