Лабораторная работа №9. Методы отладки программ
9.1 Цель и задачи
Целью работы является освоение способов отладки программ на примере RISC-V.
Для достижения поставленной цели требуется решить следующие задачи:
- Изучить флаги компиляции для отладки программ на С.
- Ознакомиться с основными командами отладчика gdb.
- Рассмотреть типовые приемы отладки.
Презентация к блоку "Оптимизации в компиляторах и отладка"
9.2. Основные теоретические сведения
Отладка программ является неотъемлемой частью работы программиста. Существует несколько подходов к отладке:
- инструментирование (отладочный вывод, санитайзеры, assertion),
- использование отладчика (в нативном и удаленном варианте),
- применение JTAG.
В идеальных условиях, все три подхода должны применяться на различных стадиях жизненного цикла ПО, так как каждый из них обладает своими сильными сторонами: инструментирование позволяет в автоматическом режиме на ранних этапах обнаружить большое количество ошибок, отладчики обеспечивают эффективное воспроизведение и отладку ошибок времени выполнения, а JTAG является способом отладить ошибки для уже развернутых встраиваемых систем.
Наиболее важным и востребованным инструментом поиска ошибок в работе программ выступает отладчик. Как правило, графические среды разработки содержат специальные интерфейсы для его запуска, позволяющие визуализировать работу программы. Однако при этом, чаще всего, в основе подобных наглядных инструментов лежит открытый отладчик gdb, реализующий типовые операции отладки бинарных файлов программ с помощью интерактивной оболочки командной строки:
- точки останова,
- просмотр и изменение значений переменных,
- просмотр отладочной информации (исходный код программы),
- просмотр ассемблерного кода,
- трассировка выполнения программы (пошаговое выполнение инструкций).
Рассмотрим пример сеанса отладки программы a.out в gdb. Для примера будем считать, что наша программа ожидает ввода целого числа и после ввода выполнит деление константы на данное число, а также что она собрана с флагом -g. Для начала запустим gdb с указанием бинарного файла для отладки:
$ gdb a.out GNU gdb (Ubuntu 8.2-0ubuntu1~16.04.1) 8.2 Copyright (C) 2018 Free Software Foundation, Inc. License GPLv3+: GNU GPL version 3 or later <http://gnu.org/licenses/gpl.html> This is free software: you are free to change and redistribute it. There is NO WARRANTY, to the extent permitted by law. Type "show copying" and "show warranty" for details. This GDB was configured as "x86_64-linux-gnu". Type "show configuration" for configuration details. For bug reporting instructions, please see: <http://www.gnu.org/software/gdb/bugs/>. Find the GDB manual and other documentation resources online at: <http://www.gnu.org/software/gdb/documentation/>. For help, type "help". Type "apropos word" to search for commands related to "word"... Reading symbols from a.out...(no debugging symbols found)...done. (gdb)
После этого открылась оболочка (Интерфейс командной строки) отладчика gdb. Строка (gdb) является приглашением ввода команд. Обратите внимание, что сразу после запуска отладчика программа не начинает свою работу - для ее запуска необходимо вызвать команду run.
(gdb) run Starting program: /home/user/a.out
В данной точке начинается выполнение бинарного файла программы - a.out ожидает пользовательского ввода. Введем число 0, чтобы спровоцировать ошибку:
Starting program: /home/user/ws/a.out 0 Program received signal SIGFPE, Arithmetic exception. 0x0000555555554717 in main () at example.c:9 9 a = a / b; (gdb)
Мы увидим сообщение отладчика о типе ошибки (Arithmetic exception), указание на стек вызова и проблемную строку из исходного кода. В данный момент исполнение программы (в данном случае - аварийное завершение работы) приостановлено и мы можем изучить обстоятельства, приведшие к ошибке. Для печати более широкого фрагмента исходного кода используем команду l (list):
(gdb) l 4 int a=0; 5 int b; 6 7 scanf("%d", &b); 8 9 a = a / b; 10 11 return 0; 12 } (gdb)
Для печати текущих (на момент сбоя) значений переменных используем команду p (print) :
(gdb) p a $1 = 0 (gdb) p b $2 = 0
Для изменения значений переменных можно использовать команду set:
(gdb) set variable a=20 (gdb) p a $3 = 20
Для продолжения выполнения программы используем c (continue):
(gdb) c Continuing. Program terminated with signal SIGFPE, Arithmetic exception. The program no longer exists.
Мы можем также искусственно остановить работу программы в определенной точке, не дожидаясь сбоя, используя команду break (создание точки останова):
(gdb) break 8 Breakpoint 1 at 0x555555554710: file example.c, line 9. Запустим программу заново командой run и снова введем 0: (gdb) run Starting program: /home/vood/ws/a.out 0 Breakpoint 1, main () at example.c:9 9 a = a / b; (gdb)
Выполнение программы снова остановлено, однако это произошло ДО ошибки деления на ноль, поэтому в данной точке у нас есть возможность подробнее изучить причины сбоя или вмешаться в выполнение программы для проверки гипотез. После данных манипуляций можно возобновить работу программы командой continue.
9.3. Задание к курсовой работе
Вам дан бинарный исполняемый файл, скомпилированный с отладочной информацией (флаг -g). Данный файл реализует логику программы расчета контрольных сумм для строк по оригинальному (но не практичному) алгоритму. Исходная строка вводится в программу через поток stdin. Программа аварийно завершает работу при вводе строки "sdasdasz".
Вам необходимо провести отладку программы с помощью gdb и определить:
- номер строки в исходном коде для строки, где происходит ошибка;
- вмешаться в поведение программы и, преодолев участок с ошибкой, определить значение контрольной суммы (для этого необходимо зафиксировать команды GDB, которые нужно для этого выполнить).
Пример программы:
#include <stdio.h> int main(void) { char data[5] ; scanf("%s", data); int sum = 0; int i; int *pointer = NULL; for(i = 0; data[i] != '\0' && data[i + 1] != '\0'; i += 2) { int value = ((unsigned char)data[i] << 8) | (unsigned char)data[i + 1]; if (data[i] == 'z' ) pointer = 4 ; sum = (sum + value ) & 0xffff; } printf("Sum is %04x\n", sum); return 0; }
Примечание для преподавателей: Выше приведен пример исходного кода программы, расчитаный на начинающих студентов. Мы рекомендуем дополнительно модифицировать этот файл в тайне от студентов так, чтобы логика и требуемые значения были изменены - в противном случае, студенты могут скопировать часть ответа из кода выше. При необходимости, данное задание можно усложнить с помощью нескольких способов:
- Обфусцировать исходный код программы.
- Пересобрать бинарный файл без флага -g. Тогда студентам потребуется анализировать поведение ПО с опорой только на ассемблерный код.
- Использовать другие виды ошибок времени выполнения.
- Изменить требования задания в сторону большего уклона в реверс-инжиниринг (выявление промежуточных значений переменных или изменение работы программы).