Несколько месяцев назад я уже пробовал поработать с отладчиком GDB, собрать PostgreSQL с отладочными символами, но тогда у меня была какая-то ошибка, так что не получилось и я отложил это на будущее.
Будущее наступило.
Прислали ссылку на статью Unlock Your Arsenals: GDB Debugging Essentials with PostgreSQL. В тот же день я не мог остановиться и просидел почти до часа ночи и успешно выполнил всё то, что описано в статье. Ну а на следующий день просидел до двух ночи, всё это перевёл и еще лучше разобрался в том, что написано в статье.
К этой статье я буду часто добавлять свои комментарии, начинать их я буду вот так:
Примечание Транслирующего (ПТ), ну а еще потому что Павел Толмачев 🙂
И писать курсивом.
Также я для некоторых функций добавил ссылки на github. Рекомендую туда перейти и посмотреть что происходит непосредственно в коде и осознать откуда и куда прыгает GDB при работе.
И еще некоторые термины я попытался перевести (например бектрейс), но бросил это дело — я не помню чтобы их переводили на русский, да и сходу перевод нормальный не нашел. То же относится к фрейму (frame). Решил использовать слово frame всё-таки.
Ссылка на оригинал.
Как опытный разработчик C/C++, я всегда считал GDB (GNU Debugger) своим «лучшим другом» в разработке программного обеспечения из-за его незаменимой роли в процессе разработки и отладки. С помощью этого мощного инструмента разработчик может:
- Искать и устранять проблемы в программах C/C++, таких как ошибки сегментации и логические ошибки;
- Отслеживать и понимать поток выполнения, переменных, содержимого памяти, сигналов и системных вызовов вашей программы C/C++.
Больше всего времени, связанного с GDB, я трачу на второй пункт: я хочу выяснить, как работает сложное программное обеспечение, прежде чем я смогу уверенно добавлять или улучшать его функции.
В этой статье я буду использовать PostgreSQL 16 в качестве практического примера, чтобы продемонстрировать некоторые возможности GDB в командной строке Ubuntu 18.04. Я также продемонстрирую, как использую отладку GDB, чтобы изучить внутреннюю логику выполнения PostgreSQL.
Содержание:
- Включение отладки и отключение оптимизации
- Подключение с помощью GDB к работающей программе (Attach)
- Запуск программы с помощью GDB (Run)
- Настройка GDB для трассировки
- Управление точками останова
- Что можно посмотреть в точке останова
- Контроль выполнения
- GDB Debugging. Итоги
- Ссылки
- Об авторе
(ПТ): я выполнял эту «практику» в 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. Об авторе
Привет, это Кэри, ваш дружелюбный энтузиаст технологий, педагог и автор. Мне нравится упрощать сложные концепции, погружаться в сложности программирования, разгадывать тайны программного обеспечения и, что наиболее важно, делиться и обучать других всем технологиям.
Ещё раз ссылка на оригинал.
Я в восторге от статьи!
Leave a Reply