ВУЗ: Не указан
Категория: Не указан
Дисциплина: Не указана
Добавлен: 23.11.2023
Просмотров: 118
Скачиваний: 2
ВНИМАНИЕ! Если данный файл нарушает Ваши авторские права, то обязательно сообщите нам.
Работа с точками наблюдения. В отличие от точек останова точка наблюдения (англ. watchpoint) позволяет отслеживать изменение зна- чения переменной или целого выражения. Для установки точки наблюдения предназначена команда watch: watch [-l | -location] <выражение>
54
Исполнение программы будет прервано, если изменится значение указанного выражения. Выражение может быть именем переменной, выражением с участием имен переменных и арифметических и логиче- ских операций или адресом, приведенным к нужному типу (например,
*(int*)0x12345678). Опциональный аргумент -location позволяет наблюдать за изменением значения не самого выражения, а места в памяти, на которое выражение ссылается. Так как переменные принад- лежат определенной области видимости в программе, перед установ- кой точки наблюдения рекомендуется установить точку останова в нужном месте в программе.
Для отслеживания только чтения или чтения / записи переменных применяются команды rwatch и awatch соответственно: rwatch [-l | -location] <выражение> awatch [-l | -location] <выражение>
Просмотр и удаление точек наблюдения выполняется теми же ко- мандами, что и для точек останова.
Просмотр исходного кода. Для вывода на экран части исходного кода (например, для получения имен функций и номеров строк) ис- пользуется команда list
[
l
]. Вызов команды list без аргументов
(gdb) list выводит на экран 10 (по умолчанию) строк текущего исходного файла.
Указание номера строки или имени функции
(gdb) list [<имя файла>:]<номер строки>
(gdb) list [<имя файла>:]<имя функции> позволяет вывести строки вокруг указанной строки или заголовка функции.
При выводе запоминается текущая позиция в файле, поэтому по- следовательные вызовы команды list позволяют выводить большие участки файлов. Для возврата к предыдущему блоку строк нужно пе- редать дефис в качестве аргумента:
(gdb) list -
55
Число строк для вывода может быть изменено с помощью команд:
(gdb) set listsize <число>
(gdb) set listsize unlimited
Работа с данными программы. При достижении точки останова или места ошибки полезной является возможность просмотра значений интересующих нас данных и переменных программы. Для вывода зна- чений переменных и выражений используется команда print [p]:
(gdb) print <имя переменной>
(gdb) print <выражение>
В качестве аргумента команда принимает имя одной переменной или выражение, содержащее имена разных переменных, арифметиче- ские и логические операции, а также вызовы функций. Имя перемен- ной может быть задано как локальное (по отношению к текущему ме- сту в программе), так и в расширенной форме в виде <имя фай- ла>::<имя переменной> или <имя функции>::<имя перемен- ной>.
GDB позволяет изменять значения переменных с помощью коман- ды set:
(gdb) set var <имя переменной>=<выражение>
Можно также создавать вспомогательные переменные для сеанса от- ладки:
(gdb) set $<имя вспомогательной переменной>=<выражение>
При обращении к вспомогательной переменной перед ее именем также ставится знак $.
Пошаговое исполнение. После достижении точки останова и вы- полнения нужных действий (анализ и изменение значения переменных и т. д.) исполнение программы можно продолжить как в обычном ре- жиме, так и в пошаговом. Команда continue [c] продолжает обычное исполнение программы до ее завершения или возникновения следую- щего события останова (достижение следующей точки останова, воз- никновение исключения).
(gbd) continue
56
Команда step [s] продолжает выполнение программы до достиже- ния следующей строки кода, после чего исполнение снова приостанав- ливается. При встрече вызова функции происходит заход в эту функ- цию и исполнение приостанавливается перед первой строкой тела функции. Опциональный числовой аргумент позволяет повторить ко- манду заданное число раз.
(gdb) step [<число>]
Команда next [n], как и команда step, продолжает выполнение программы до достижения следующей строки кода, но заход в вызыва- емые функции не производится. Опциональный числовой аргумент позволяет повторить команду заданное число раз.
(gdb) next [<число>]
Работа со стеком и фреймами. Для каждого вызова функции GDB хранит информацию о вызове: место вызова функции, значения ее ар- гументов и локальных переменных. Блок данных с такой информацией для каждого вызова называется фреймом (кадром) стека, сокращенно – просто фрейм. Фреймы формируют стек вызовов, по одному фрейму на элемент стека.
Для просмотра стека вызовов используется команда backtrace [bt]. Для каждого фрейма выводится его номер, место вы- зова функции и значения ее фактических аргументов. Фрейму с номе- ром 0 соответствует самый «вложенный» уровень (место останова), фрейм с наибольшим номером обычно соответствует вызову функции main.
(gdb) backtrace
Команда frame [f] позволяет перейти на фрейм с указанным номе- ром. Команды up и down сдвинутся на указанное число фреймов вверх
(по направлению к внешним фреймам) или вниз (по направлению к вложенным фреймам) по стеку соответственно.
(gdb) frame <номер фрейма>
(gdb) up <число>
(gdb) down <число>
57
Для вывода информации о фрейме используется команда frame без аргументов
(gdb) frame или команда info с аргументами: frame – для вывода подробного описания фрейма;
(gdb) info frame args – для вывода значений аргументов фрейма;
(gdb) info args locals – для вывода значений локальных переменных.
(gdb) info locals
Отладка многопоточных программ. Для многопоточных про- грамм GDB хранит информацию о созданных потоках. Каждому пото- ку присваивается уникальный номер. Для просмотра информации о потоках используется команда
(gdb) info threads
Команда thread позволяет переключиться на поток с указанным номером:
(gdb) thread <номер потока>
Чтобы установить точку останова для определенного потока, ко- манде break передается дополнительный аргумент:
(gdb) break <место точки останова> thread <номер потока>
Имеется возможность исполнить некоторую команду для заданного списка потоков либо для всех потоков сразу:
(gbd) thread apply [<список номеров потоков> | all] <команда>
Список потоков задается в виде перечисления их номеров через пробел, например, 1 2 4. Можно также указывать диапазоны, напри- мер, 1 3-5.
58
Существует два режима работы GDB при отладке многопоточных программ: all-stop и non-stop. В режиме all-stop (используется по умол- чанию) при остановке исполнения на точке останова все потоки про- граммы приостанавливают свое исполнение. Команды продолжения исполнения (continue, step, next) также запускают все потоки.
В режиме non-stop при остановке исполнения приостанавливается только текущий поток (в котором была точка останова), в то время как остальные потоки продолжают исполнение. Команды продолжения исполнения действуют только на текущий поток.
Для включения режима non-stop в GDB перед запуском программы нужно использовать следующие команды:
(gdb) set pagination off
(gdb) set non-stop on
Для выключения режима используется следующая команда:
(gdb) set non-stop off
Режим non-stop может не поддерживаться на некоторых архитекту- рах. В этом случае будет использоваться режим all-stop.
Завершение работы. Для завершения сеанса отладки используется команда quit
[
q
].
(gdb) quit
П
РИМЕРЫ СЕАНСОВ РАБОТЫ С
GDB
Рассмотрим программу из примера 8, в которую специально внесе- на ошибка, приводящая к зависанию программы.
Пример 8. Программа с ошибкой
#include
#include int a = 0; pthread_mutex_t m; void* thread_func(void*) { pthread_mutex_lock(&m); a++;
59
// return 0;
} int main() { pthread_t t[2];
// pthread_mutex_init(&m, NULL); pthread_create(&t[0], NULL, thread_func, NULL); pthread_create(&t[1], NULL, thread_func, NULL); pthread_join(t[0], NULL); pthread_join(t[1], NULL); pthread_mutex_destroy(&m);
// return 0;
}
Пусть эта программа находится в файле error.cpp. Скомпилируем и запустим ее на исполнение:
$ g++ -pthread -g -O0 error.cpp
$ ./a.out
В результате программа зависнет. Используем отладчик GDB, чтобы обнаружить ошибку.
Способ 1. Анализ стека вызовов
Переключимся на другой терминал (например, нажатием комбина- ции клавиш Alt+F2) и выполним команду ps, чтобы узнать номер ра- ботающего (зависшего) процесса:
$ ps ax | grep a.out
7621 pts/19 Sl+ 0:00 ./a.out
7634 pts/5 S+ 0:00 grep --color=auto a.out
Нужный нам номер – 7621. Подключимся к процессу из отладчика:
$ sudo gdb ./a.out 7621
Attaching to program: /home/gdb/a.out, process 7621
60
[New LWP 7623]
[Thread debugging using libthread_db enabled]
Using host libthread_db library "/lib/x86_64-linux- gnu/libthread_db.so.1".
0x00007fca656bf98d in pthread_join (threadid=140507249293056, thread_return=0x0) at pthread_join.c:92 92 lll_wait_tid (pd->tid);
Главный поток сейчас находится в функции pthread_join. Узнаем информацию обо всех потоках:
(gdb) info threads
Id Target Id Frame
* 1 Thread 0x7fca65acd700 (LWP 7621) "a.out" 0x00007fca656bf98d in pthread_join (threadid=140507249293056, thread_return=0x0) at pthread_join.c:92 2 Thread 0x7fca64aeb700 (LWP 7623) "a.out" __lll_lock_wait () at ../sysdeps/unix/sysv/linux/x86_64/lowlevellock.S:135
Второй поток находится в низкоуровневой функции ожидания. Пере- ключимся на него:
(gdb) thread 2
[Switching to thread 2 (Thread 0x7fca64aeb700 (LWP 7623))]
#0 __lll_lock_wait () at ../sysdeps/unix/sysv/linux/x86_64/lowlevellock.S:135 135 2: movl %edx, %eax
Посмотрим стек вызовов:
(gdb) bt
#0 __lll_lock_wait () at ../sysdeps/unix/sysv/linux/x86_64/lowlevellock.S:135
#1 0x00007fca656c0dbd in __GI___pthread_mutex_lock (mutex=0x6010a0) at ../nptl/pthread_mutex_lock.c:80
#2 0x00000000004007cc in thread_func () at error.cpp:8
#3 0x00007fca656be6ba in start_thread (arg=0x7fca64aeb700) at pthread_create.c:333
#4 0x00007fca653f43dd in clone () at ../sysdeps/unix/sysv/linux/x86_64/clone.S:109
61
Нас интересуют второй и третий фреймы как относящиеся к нашему коду. Переключимся на второй фрейм:
(gdb) frame 2
#2 0x00000000004007cc in thread_func () at error.cpp:8 8 pthread_mutex_lock(&m);
Видно, что исполнение потока остановилось на ожидании мьютекса
(строка 8). Просмотрим участок кода, где это произошло:
(gdb) list
3 4 int a = 0;
5 pthread_mutex_t m;
6 7 void* thread_func(void*) {
8 pthread_mutex_lock(&m);
9 a++;
10 //
11 return 0;
12 }
После визуального анализа программного кода становится очевидно, что после доступа к разделяемому ресурсу (строка 9) пропущена ко- манда освобождения мьютекса. Для решения проблемы необходимо добавить после строки 9 команду pthread_mutex_unlock(&m). По- вторная компиляция и запуск программы позволят убедиться, что за- висание исчезло и программа работает корректно.
Способ 2. Использование точек останова
Попробуем альтернативный подход к обнаружению проблемы. От- кроем программу в отладчике
$ gdb ./a.out и установим точку останова на строке 8, чтобы убедиться, что до мо- мента обращения к разделяемому ресурсу программа работает кор- ректно:
(gdb) break 8
Breakpoint 1 at 0x4007c2: file error.cpp, line 8.
62
Можно посмотреть информацию по точкам останова:
(gdb) info breakpoints
Num Type Disp Enb Address What
1 breakpoint keep y 0x00000000004007c2 in thread_func(void*) at error.cpp:8
Запустим программу:
(gdb) r
Starting program: /home/gdb/a.out
[Thread debugging using libthread_db enabled]
Using host libthread_db library "/lib/x86_64-linux-gnu/libthread_db.so.1".
[New Thread 0x7ffff77ef700 (LWP 7908)]
[New Thread 0x7ffff6fee700 (LWP 7909)]
[Switching to Thread 0x7ffff77ef700 (LWP 7908)]
Thread 2 "a.out" hit Breakpoint 1, thread_func () at error.cpp:8 8 pthread_mutex_lock(&m);
Видно, что породилось два дочерних потока. Посмотрим информацию о них более подробно:
(gdb) info threads
Id Target Id Frame
1 Thread 0x7ffff7fcc700 (LWP 7904) "a.out" clone () at ../sysdeps/unix/sysv/linux/x86_64/clone.S:81
* 2 Thread 0x7ffff77ef700 (LWP 7908) "a.out" thread_func () at error.cpp:8 3 Thread 0x7ffff6fee700 (LWP 7909) "a.out" thread_func () at error.cpp:8
Добавим точку останова перед выходом из функции потока
(gdb) break 11
Breakpoint 2 at 0x4007db: file error.cpp, line 11. и выведем еще раз список точек останова:
(gdb) info breakpoints
Num Type Disp Enb Address What
1 breakpoint keep y 0x00000000004007c2 in thread_func(void*) at error.cpp:8
63 breakpoint already hit 1 time
2 breakpoint keep y 0x00000000004007db in thread_func(void*) at error.cpp:11
Продолжаем исполнение:
(gdb) c
Continuing.
[Switching to Thread 0x7ffff6fee700 (LWP 7909)]
Thread 3 "a.out" hit Breakpoint 1, thread_func () at error.cpp:8 8 pthread_mutex_lock(&m);
Поток номер 3 достиг первой точки останова на строке 8. Продолжаем исполнение:
(gdb) c
Continuing.
[Switching to Thread 0x7ffff77ef700 (LWP 7908)]
Thread 2 "a.out" hit Breakpoint 2, thread_func () at error.cpp:11 11 return 0;
Поток номер 2 достиг второй точки останова. Посмотрим состояние потока 3:
(gdb) thread 3
[Switching to thread 3 (Thread 0x7ffff6fee700 (LWP 7909))]
#0 __lll_lock_wait () at ../sysdeps/unix/sysv/linux/x86_64/lowlevellock.S:135 135 2: movl %edx, %eax
Посмотрим стек вызовов:
(gdb) bt
#0 __lll_lock_wait () at ../sysdeps/unix/sysv/linux/x86_64/lowlevellock.S:135
#1 0x00007fca656c0dbd in __GI___pthread_mutex_lock (mutex=0x6010a0) at ../nptl/pthread_mutex_lock.c:80
#2 0x00000000004007cc in thread_func () at error.cpp:8
#3 0x00007fca656be6ba in start_thread (arg=0x7fca64aeb700)
64 at pthread_create.c:333
#4 0x00007fca653f43dd in clone () at ../sysdeps/unix/sysv/linux/x86_64/clone.S:109
Переключимся на второй фрейм:
(gdb) frame 2
#2 0x00000000004007cc in thread_func () at error.cpp:8 8 pthread_mutex_lock(&m);
Поток 3 находится на ожидании мьютекса. Проверим мьютекс:
(gdb) p pthread_mutex_trylock(&m)
$1 = 16
GDB вычисляет указанное выражение (в данном случае – вызывает функцию pthread_mutex_trylock()) и возвращает результат (в дан- ном случае – код ошибки). Найдем для этого кода строковое описание:
(gdb) p strerror(16)
$2 = 0x7ffff797d05e "Device or resource busy"
Код ошибки, возвращенный pthread_mutex_trylock(), говорит, что мьютекс занят. В то же время поток 2 уже завершил свое исполне- ние, т. е. освободить мьютекс некому. Следовательно, поток 3 будет ждать его вечно.
Проверим эту догадку, для чего создадим точку наблюдения
(gdb) watch pthread_mutex_trylock(&m)
Watchpoint 3: pthread_mutex_trylock(&m) и запустим программу на дальнейшее исполнение:
(gdb) c
Continuing.
В результате точка наблюдения ни разу не срабатывает, т. е. мьютекс не освобождается, из-за чего программа зависает.
65
Попробуем добавить в функцию thread_func() из примера 8 вы- зов функции pthread_mutex_unlock(&m) после доступа к разделяе- мому ресурсу, так что функция thread_func() примет вид void* thread_func(void*) { pthread_mutex_lock(&m); a++; pthread_mutex_unlock(&m);
// return 0;
}
После этого пройдем сценарий отладки еще раз:
$ gdb ./a.out
(gdb) break 8
Breakpoint 1 at 0x400812: file error.cpp, line 8.
(gdb) break 10
Breakpoint 2 at 0x40082b: file error.cpp, line 10.
(gdb) r
Starting program: /home/gdb/a.out
[Thread debugging using libthread_db enabled]
Using host libthread_db library "/lib/x86_64-linux-gnu/libthread_db.so.1".
[New Thread 0x7ffff77ef700 (LWP 8345)]
[New Thread 0x7ffff6fee700 (LWP 8346)]
[Switching to Thread 0x7ffff77ef700 (LWP 8345)]
Thread 2 "a.out" hit Breakpoint 1, thread_func () at error.cpp:8 8 pthread_mutex_lock(&m);
(gdb) c
Continuing.
[Switching to Thread 0x7ffff6fee700 (LWP 8346)]
Thread 3 "a.out" hit Breakpoint 1, thread_func () at error.cpp:8 8 pthread_mutex_lock(&m);
(gdb) c
Continuing.
[Switching to Thread 0x7ffff77ef700 (LWP 8345)]
Thread 2 "a.out" hit Breakpoint 2, thread_func () at error.cpp:10 10 pthread_mutex_unlock(&m);
66
(gdb) c
Continuing.
[Thread 0x7ffff77ef700 (LWP 8345) exited]
[Switching to Thread 0x7ffff6fee700 (LWP 8346)]
Thread 3 "a.out" hit Breakpoint 2, thread_func () at error.cpp:10 10 pthread_mutex_unlock(&m);
(gdb) c
Continuing.
[Thread 0x7ffff6fee700 (LWP 8346) exited]
[Inferior 1 (process 8341) exited normally]
Ошибка устранена, и программа успешно завершается.
Д
ОПОЛНИТЕЛЬНАЯ ЛИТЕРАТУРА
1. Кулямин В. В. Методы верификации программного обеспечения [Элек- тронный ресурс] // Портал «Информационно-коммуникационные технологии в образовании». – URL: http://www.ict.edu.ru/ft/005645/62322e1-st09.pdf (дата обращения: 08.11.2017).
2. Rahul P., George B. Concurrency: Tools And Techniques to Identify Con- currency Issues [Electronic resource] // MSDN Magazine. – URL: http://down- load.microsoft.com/download/3/a/7/3a7fa450-1f33-41f7-9e6d-3aa95b5a6aea/
MSDNMagazine2008_06en-us.chm (accessed: 08.11.2017).
3. Кудрин М.Ю., Прокопенко А.С., Тормасов А.Г. Метод нахождения со- стояний гонки в потоках, работающих на разделяемой памяти // Труды
МФТИ. – URL: https://mipt.ru/upload/e08/f_edv8-arphcxl1tgs.pdf (дата обраще- ния: 08.11.2017).
4. Колосов А.П. Методы отладки параллельных программ // Известия
Тульского государственного университета. – URL: https://tidings.tsu.tula.ru/ tidings/pdf/web/preview_therest_ru.php?x=tsu_izv_technical_sciences_2010_02_ part_2&year=2010 (дата обращения: 08.11.2017).
5. Krishna K., Alireza M., Deng-jyi Chen. Modeling multithreaded applications using Petri nets [Electronic resource] // International Journal of Parallel Program- ming. – URL: http://csrl.cse.unt.edu/kavi/Research/IJPP.pdf (accessed:
08.11.2017).
6. GDB: The GNU Project Debugger [Electronic resource] // Операционная система GNU. – URL: https://www.gnu.org/software/gdb/documentation/ (ac- cessed: 08.11.2017).
54
Исполнение программы будет прервано, если изменится значение указанного выражения. Выражение может быть именем переменной, выражением с участием имен переменных и арифметических и логиче- ских операций или адресом, приведенным к нужному типу (например,
*(int*)0x12345678). Опциональный аргумент -location позволяет наблюдать за изменением значения не самого выражения, а места в памяти, на которое выражение ссылается. Так как переменные принад- лежат определенной области видимости в программе, перед установ- кой точки наблюдения рекомендуется установить точку останова в нужном месте в программе.
Для отслеживания только чтения или чтения / записи переменных применяются команды rwatch и awatch соответственно: rwatch [-l | -location] <выражение> awatch [-l | -location] <выражение>
Просмотр и удаление точек наблюдения выполняется теми же ко- мандами, что и для точек останова.
Просмотр исходного кода. Для вывода на экран части исходного кода (например, для получения имен функций и номеров строк) ис- пользуется команда list
[
l
]. Вызов команды list без аргументов
(gdb) list выводит на экран 10 (по умолчанию) строк текущего исходного файла.
Указание номера строки или имени функции
(gdb) list [<имя файла>:]<номер строки>
(gdb) list [<имя файла>:]<имя функции> позволяет вывести строки вокруг указанной строки или заголовка функции.
При выводе запоминается текущая позиция в файле, поэтому по- следовательные вызовы команды list позволяют выводить большие участки файлов. Для возврата к предыдущему блоку строк нужно пе- редать дефис в качестве аргумента:
(gdb) list -
55
Число строк для вывода может быть изменено с помощью команд:
(gdb) set listsize <число>
(gdb) set listsize unlimited
Работа с данными программы. При достижении точки останова или места ошибки полезной является возможность просмотра значений интересующих нас данных и переменных программы. Для вывода зна- чений переменных и выражений используется команда print [p]:
(gdb) print <имя переменной>
(gdb) print <выражение>
В качестве аргумента команда принимает имя одной переменной или выражение, содержащее имена разных переменных, арифметиче- ские и логические операции, а также вызовы функций. Имя перемен- ной может быть задано как локальное (по отношению к текущему ме- сту в программе), так и в расширенной форме в виде <имя фай- ла>::<имя переменной> или <имя функции>::<имя перемен- ной>.
GDB позволяет изменять значения переменных с помощью коман- ды set:
(gdb) set var <имя переменной>=<выражение>
Можно также создавать вспомогательные переменные для сеанса от- ладки:
(gdb) set $<имя вспомогательной переменной>=<выражение>
При обращении к вспомогательной переменной перед ее именем также ставится знак $.
Пошаговое исполнение. После достижении точки останова и вы- полнения нужных действий (анализ и изменение значения переменных и т. д.) исполнение программы можно продолжить как в обычном ре- жиме, так и в пошаговом. Команда continue [c] продолжает обычное исполнение программы до ее завершения или возникновения следую- щего события останова (достижение следующей точки останова, воз- никновение исключения).
(gbd) continue
56
Команда step [s] продолжает выполнение программы до достиже- ния следующей строки кода, после чего исполнение снова приостанав- ливается. При встрече вызова функции происходит заход в эту функ- цию и исполнение приостанавливается перед первой строкой тела функции. Опциональный числовой аргумент позволяет повторить ко- манду заданное число раз.
(gdb) step [<число>]
Команда next [n], как и команда step, продолжает выполнение программы до достижения следующей строки кода, но заход в вызыва- емые функции не производится. Опциональный числовой аргумент позволяет повторить команду заданное число раз.
(gdb) next [<число>]
Работа со стеком и фреймами. Для каждого вызова функции GDB хранит информацию о вызове: место вызова функции, значения ее ар- гументов и локальных переменных. Блок данных с такой информацией для каждого вызова называется фреймом (кадром) стека, сокращенно – просто фрейм. Фреймы формируют стек вызовов, по одному фрейму на элемент стека.
Для просмотра стека вызовов используется команда backtrace [bt]. Для каждого фрейма выводится его номер, место вы- зова функции и значения ее фактических аргументов. Фрейму с номе- ром 0 соответствует самый «вложенный» уровень (место останова), фрейм с наибольшим номером обычно соответствует вызову функции main.
(gdb) backtrace
Команда frame [f] позволяет перейти на фрейм с указанным номе- ром. Команды up и down сдвинутся на указанное число фреймов вверх
(по направлению к внешним фреймам) или вниз (по направлению к вложенным фреймам) по стеку соответственно.
(gdb) frame <номер фрейма>
(gdb) up <число>
(gdb) down <число>
57
Для вывода информации о фрейме используется команда frame без аргументов
(gdb) frame или команда info с аргументами: frame – для вывода подробного описания фрейма;
(gdb) info frame args – для вывода значений аргументов фрейма;
(gdb) info args locals – для вывода значений локальных переменных.
(gdb) info locals
Отладка многопоточных программ. Для многопоточных про- грамм GDB хранит информацию о созданных потоках. Каждому пото- ку присваивается уникальный номер. Для просмотра информации о потоках используется команда
(gdb) info threads
Команда thread позволяет переключиться на поток с указанным номером:
(gdb) thread <номер потока>
Чтобы установить точку останова для определенного потока, ко- манде break передается дополнительный аргумент:
(gdb) break <место точки останова> thread <номер потока>
Имеется возможность исполнить некоторую команду для заданного списка потоков либо для всех потоков сразу:
(gbd) thread apply [<список номеров потоков> | all] <команда>
Список потоков задается в виде перечисления их номеров через пробел, например, 1 2 4. Можно также указывать диапазоны, напри- мер, 1 3-5.
58
Существует два режима работы GDB при отладке многопоточных программ: all-stop и non-stop. В режиме all-stop (используется по умол- чанию) при остановке исполнения на точке останова все потоки про- граммы приостанавливают свое исполнение. Команды продолжения исполнения (continue, step, next) также запускают все потоки.
В режиме non-stop при остановке исполнения приостанавливается только текущий поток (в котором была точка останова), в то время как остальные потоки продолжают исполнение. Команды продолжения исполнения действуют только на текущий поток.
Для включения режима non-stop в GDB перед запуском программы нужно использовать следующие команды:
(gdb) set pagination off
(gdb) set non-stop on
Для выключения режима используется следующая команда:
(gdb) set non-stop off
Режим non-stop может не поддерживаться на некоторых архитекту- рах. В этом случае будет использоваться режим all-stop.
Завершение работы. Для завершения сеанса отладки используется команда quit
[
q
].
(gdb) quit
П
РИМЕРЫ СЕАНСОВ РАБОТЫ С
GDB
Рассмотрим программу из примера 8, в которую специально внесе- на ошибка, приводящая к зависанию программы.
Пример 8. Программа с ошибкой
#include
#include
59
// return 0;
} int main() { pthread_t t[2];
// pthread_mutex_init(&m, NULL); pthread_create(&t[0], NULL, thread_func, NULL); pthread_create(&t[1], NULL, thread_func, NULL); pthread_join(t[0], NULL); pthread_join(t[1], NULL); pthread_mutex_destroy(&m);
// return 0;
}
Пусть эта программа находится в файле error.cpp. Скомпилируем и запустим ее на исполнение:
$ g++ -pthread -g -O0 error.cpp
$ ./a.out
В результате программа зависнет. Используем отладчик GDB, чтобы обнаружить ошибку.
Способ 1. Анализ стека вызовов
Переключимся на другой терминал (например, нажатием комбина- ции клавиш Alt+F2) и выполним команду ps, чтобы узнать номер ра- ботающего (зависшего) процесса:
$ ps ax | grep a.out
7621 pts/19 Sl+ 0:00 ./a.out
7634 pts/5 S+ 0:00 grep --color=auto a.out
Нужный нам номер – 7621. Подключимся к процессу из отладчика:
$ sudo gdb ./a.out 7621
Attaching to program: /home/gdb/a.out, process 7621
60
[New LWP 7623]
[Thread debugging using libthread_db enabled]
Using host libthread_db library "/lib/x86_64-linux- gnu/libthread_db.so.1".
0x00007fca656bf98d in pthread_join (threadid=140507249293056, thread_return=0x0) at pthread_join.c:92 92 lll_wait_tid (pd->tid);
Главный поток сейчас находится в функции pthread_join. Узнаем информацию обо всех потоках:
(gdb) info threads
Id Target Id Frame
* 1 Thread 0x7fca65acd700 (LWP 7621) "a.out" 0x00007fca656bf98d in pthread_join (threadid=140507249293056, thread_return=0x0) at pthread_join.c:92 2 Thread 0x7fca64aeb700 (LWP 7623) "a.out" __lll_lock_wait () at ../sysdeps/unix/sysv/linux/x86_64/lowlevellock.S:135
Второй поток находится в низкоуровневой функции ожидания. Пере- ключимся на него:
(gdb) thread 2
[Switching to thread 2 (Thread 0x7fca64aeb700 (LWP 7623))]
#0 __lll_lock_wait () at ../sysdeps/unix/sysv/linux/x86_64/lowlevellock.S:135 135 2: movl %edx, %eax
Посмотрим стек вызовов:
(gdb) bt
#0 __lll_lock_wait () at ../sysdeps/unix/sysv/linux/x86_64/lowlevellock.S:135
#1 0x00007fca656c0dbd in __GI___pthread_mutex_lock (mutex=0x6010a0
#2 0x00000000004007cc in thread_func () at error.cpp:8
#3 0x00007fca656be6ba in start_thread (arg=0x7fca64aeb700) at pthread_create.c:333
#4 0x00007fca653f43dd in clone () at ../sysdeps/unix/sysv/linux/x86_64/clone.S:109
61
Нас интересуют второй и третий фреймы как относящиеся к нашему коду. Переключимся на второй фрейм:
(gdb) frame 2
#2 0x00000000004007cc in thread_func () at error.cpp:8 8 pthread_mutex_lock(&m);
Видно, что исполнение потока остановилось на ожидании мьютекса
(строка 8). Просмотрим участок кода, где это произошло:
(gdb) list
3 4 int a = 0;
5 pthread_mutex_t m;
6 7 void* thread_func(void*) {
8 pthread_mutex_lock(&m);
9 a++;
10 //
11 return 0;
12 }
После визуального анализа программного кода становится очевидно, что после доступа к разделяемому ресурсу (строка 9) пропущена ко- манда освобождения мьютекса. Для решения проблемы необходимо добавить после строки 9 команду pthread_mutex_unlock(&m). По- вторная компиляция и запуск программы позволят убедиться, что за- висание исчезло и программа работает корректно.
Способ 2. Использование точек останова
Попробуем альтернативный подход к обнаружению проблемы. От- кроем программу в отладчике
$ gdb ./a.out и установим точку останова на строке 8, чтобы убедиться, что до мо- мента обращения к разделяемому ресурсу программа работает кор- ректно:
(gdb) break 8
Breakpoint 1 at 0x4007c2: file error.cpp, line 8.
62
Можно посмотреть информацию по точкам останова:
(gdb) info breakpoints
Num Type Disp Enb Address What
1 breakpoint keep y 0x00000000004007c2 in thread_func(void*) at error.cpp:8
Запустим программу:
(gdb) r
Starting program: /home/gdb/a.out
[Thread debugging using libthread_db enabled]
Using host libthread_db library "/lib/x86_64-linux-gnu/libthread_db.so.1".
[New Thread 0x7ffff77ef700 (LWP 7908)]
[New Thread 0x7ffff6fee700 (LWP 7909)]
[Switching to Thread 0x7ffff77ef700 (LWP 7908)]
Thread 2 "a.out" hit Breakpoint 1, thread_func () at error.cpp:8 8 pthread_mutex_lock(&m);
Видно, что породилось два дочерних потока. Посмотрим информацию о них более подробно:
(gdb) info threads
Id Target Id Frame
1 Thread 0x7ffff7fcc700 (LWP 7904) "a.out" clone () at ../sysdeps/unix/sysv/linux/x86_64/clone.S:81
* 2 Thread 0x7ffff77ef700 (LWP 7908) "a.out" thread_func () at error.cpp:8 3 Thread 0x7ffff6fee700 (LWP 7909) "a.out" thread_func () at error.cpp:8
Добавим точку останова перед выходом из функции потока
(gdb) break 11
Breakpoint 2 at 0x4007db: file error.cpp, line 11. и выведем еще раз список точек останова:
(gdb) info breakpoints
Num Type Disp Enb Address What
1 breakpoint keep y 0x00000000004007c2 in thread_func(void*) at error.cpp:8
63 breakpoint already hit 1 time
2 breakpoint keep y 0x00000000004007db in thread_func(void*) at error.cpp:11
Продолжаем исполнение:
(gdb) c
Continuing.
[Switching to Thread 0x7ffff6fee700 (LWP 7909)]
Thread 3 "a.out" hit Breakpoint 1, thread_func () at error.cpp:8 8 pthread_mutex_lock(&m);
Поток номер 3 достиг первой точки останова на строке 8. Продолжаем исполнение:
(gdb) c
Continuing.
[Switching to Thread 0x7ffff77ef700 (LWP 7908)]
Thread 2 "a.out" hit Breakpoint 2, thread_func () at error.cpp:11 11 return 0;
Поток номер 2 достиг второй точки останова. Посмотрим состояние потока 3:
(gdb) thread 3
[Switching to thread 3 (Thread 0x7ffff6fee700 (LWP 7909))]
#0 __lll_lock_wait () at ../sysdeps/unix/sysv/linux/x86_64/lowlevellock.S:135 135 2: movl %edx, %eax
Посмотрим стек вызовов:
(gdb) bt
#0 __lll_lock_wait () at ../sysdeps/unix/sysv/linux/x86_64/lowlevellock.S:135
#1 0x00007fca656c0dbd in __GI___pthread_mutex_lock (mutex=0x6010a0
#2 0x00000000004007cc in thread_func () at error.cpp:8
#3 0x00007fca656be6ba in start_thread (arg=0x7fca64aeb700)
64 at pthread_create.c:333
#4 0x00007fca653f43dd in clone () at ../sysdeps/unix/sysv/linux/x86_64/clone.S:109
Переключимся на второй фрейм:
(gdb) frame 2
#2 0x00000000004007cc in thread_func () at error.cpp:8 8 pthread_mutex_lock(&m);
Поток 3 находится на ожидании мьютекса. Проверим мьютекс:
(gdb) p pthread_mutex_trylock(&m)
$1 = 16
GDB вычисляет указанное выражение (в данном случае – вызывает функцию pthread_mutex_trylock()) и возвращает результат (в дан- ном случае – код ошибки). Найдем для этого кода строковое описание:
(gdb) p strerror(16)
$2 = 0x7ffff797d05e "Device or resource busy"
Код ошибки, возвращенный pthread_mutex_trylock(), говорит, что мьютекс занят. В то же время поток 2 уже завершил свое исполне- ние, т. е. освободить мьютекс некому. Следовательно, поток 3 будет ждать его вечно.
Проверим эту догадку, для чего создадим точку наблюдения
(gdb) watch pthread_mutex_trylock(&m)
Watchpoint 3: pthread_mutex_trylock(&m) и запустим программу на дальнейшее исполнение:
(gdb) c
Continuing.
В результате точка наблюдения ни разу не срабатывает, т. е. мьютекс не освобождается, из-за чего программа зависает.
65
Попробуем добавить в функцию thread_func() из примера 8 вы- зов функции pthread_mutex_unlock(&m) после доступа к разделяе- мому ресурсу, так что функция thread_func() примет вид void* thread_func(void*) { pthread_mutex_lock(&m); a++; pthread_mutex_unlock(&m);
// return 0;
}
После этого пройдем сценарий отладки еще раз:
$ gdb ./a.out
(gdb) break 8
Breakpoint 1 at 0x400812: file error.cpp, line 8.
(gdb) break 10
Breakpoint 2 at 0x40082b: file error.cpp, line 10.
(gdb) r
Starting program: /home/gdb/a.out
[Thread debugging using libthread_db enabled]
Using host libthread_db library "/lib/x86_64-linux-gnu/libthread_db.so.1".
[New Thread 0x7ffff77ef700 (LWP 8345)]
[New Thread 0x7ffff6fee700 (LWP 8346)]
[Switching to Thread 0x7ffff77ef700 (LWP 8345)]
Thread 2 "a.out" hit Breakpoint 1, thread_func () at error.cpp:8 8 pthread_mutex_lock(&m);
(gdb) c
Continuing.
[Switching to Thread 0x7ffff6fee700 (LWP 8346)]
Thread 3 "a.out" hit Breakpoint 1, thread_func () at error.cpp:8 8 pthread_mutex_lock(&m);
(gdb) c
Continuing.
[Switching to Thread 0x7ffff77ef700 (LWP 8345)]
Thread 2 "a.out" hit Breakpoint 2, thread_func () at error.cpp:10 10 pthread_mutex_unlock(&m);
66
(gdb) c
Continuing.
[Thread 0x7ffff77ef700 (LWP 8345) exited]
[Switching to Thread 0x7ffff6fee700 (LWP 8346)]
Thread 3 "a.out" hit Breakpoint 2, thread_func () at error.cpp:10 10 pthread_mutex_unlock(&m);
(gdb) c
Continuing.
[Thread 0x7ffff6fee700 (LWP 8346) exited]
[Inferior 1 (process 8341) exited normally]
Ошибка устранена, и программа успешно завершается.
Д
ОПОЛНИТЕЛЬНАЯ ЛИТЕРАТУРА
1. Кулямин В. В. Методы верификации программного обеспечения [Элек- тронный ресурс] // Портал «Информационно-коммуникационные технологии в образовании». – URL: http://www.ict.edu.ru/ft/005645/62322e1-st09.pdf (дата обращения: 08.11.2017).
2. Rahul P., George B. Concurrency: Tools And Techniques to Identify Con- currency Issues [Electronic resource] // MSDN Magazine. – URL: http://down- load.microsoft.com/download/3/a/7/3a7fa450-1f33-41f7-9e6d-3aa95b5a6aea/
MSDNMagazine2008_06en-us.chm (accessed: 08.11.2017).
3. Кудрин М.Ю., Прокопенко А.С., Тормасов А.Г. Метод нахождения со- стояний гонки в потоках, работающих на разделяемой памяти // Труды
МФТИ. – URL: https://mipt.ru/upload/e08/f_edv8-arphcxl1tgs.pdf (дата обраще- ния: 08.11.2017).
4. Колосов А.П. Методы отладки параллельных программ // Известия
Тульского государственного университета. – URL: https://tidings.tsu.tula.ru/ tidings/pdf/web/preview_therest_ru.php?x=tsu_izv_technical_sciences_2010_02_ part_2&year=2010 (дата обращения: 08.11.2017).
5. Krishna K., Alireza M., Deng-jyi Chen. Modeling multithreaded applications using Petri nets [Electronic resource] // International Journal of Parallel Program- ming. – URL: http://csrl.cse.unt.edu/kavi/Research/IJPP.pdf (accessed:
08.11.2017).
6. GDB: The GNU Project Debugger [Electronic resource] // Операционная система GNU. – URL: https://www.gnu.org/software/gdb/documentation/ (ac- cessed: 08.11.2017).