Лабораторный практикум по технологиям RISC-V

Лабораторная работа №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. Тогда студентам потребуется анализировать поведение ПО с опорой только на ассемблерный код.
  • Использовать другие виды ошибок времени выполнения.
  • Изменить требования задания в сторону большего уклона в реверс-инжиниринг (выявление промежуточных значений переменных или изменение работы программы).
  • 9.3.1. Описание последовательности выполнения работы

    Первым шагом для выполнения задания необходимо воспроизвести описанную неисправность вне отладчика путем запуска программы в RISC-V ОС и передачи ей строки, вызывающей ошибку. Это необходимо для того, чтобы в дальнейшем вести отладку с учетом максимально достоверной картины - в ряде случаев использование отладчиков может само по себе изменять поведение программы.

    Далее, программу необходимо запустить в gdb и зафиксировать момент ее аварийной остановки. После этого необходимо визуализировать исходный код программы через команды gdb и путем изучения значений переменных, определить - какая именно операция привела к ошибке. Затем завершить выполнение программы.

    Далее, необходимо установить точку останова до момента сбоя так, чтобы на момент остановки у вас была возможность вмешаться в ее работу путем изменения значений переменных. Далее запустите программу через gdb и введите данные, вызывающие ошибку. После остановки на точке останова, измените значения нужных переменных в программе и продолжите ее выполнение. Убедитесь, что после данного вмешательства не происходит ошибок и программа корректно завершает работу. Зафиксируйте ее вывод в отчете.

    9.3.2. Пример выполнения задания на защиту

    Для примера рассмотрим измененный пример программы из теоретической части:

    
    #include <stdio.h>
    
    int main(){
      int a=0;
      int b;
      int c= 13;
    
      scanf("%d", b);
      a = a / b;
      printf("%d", b + c);
    
      return 0;
    }
    

    Мы знаем, что программа должна выводить некоторую контрольную сумму, но при вводе значения 0 происходит ошибка. Воспроизводим ошибку в gdb:

    $ gdb a.out
    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;
    

    Последнее сообщение содержит номер строки, в которой произошла ошибка - строка №9. Выведем часть исходного кода через отладчик

    
    (gdb) l
    4	  int a=0;
    5	  int b;
    6	  int c= 13;
    7	
    8	  scanf("%d", b);
    9	  a = a / b;
    10	  printf("%d", b + c);
    11	
    12	  return 0;
    13	}
    

    Таким образом мы видим проблему в исходном коде - результат ввода пользователя в переменной b используется для деления. В случае если b=0 происходит ошибка деления на ноль. При этом результат деления никак не используется. Поэтому, при использовании точек останова и редактирования значений переменных мы можем вмешаться в поведение программы и преодолеть сбой:

    (gdb) continue
    Continuing.
    
    Program terminated with signal SIGFPE, Arithmetic exception.
    The program no longer exists.
    (gdb) break 9
    Breakpoint 1 at 0x555555554757: file example.c, line 9.
    (gdb) break 10
    Breakpoint 2 at 0x555555554763: file example.c, line 10.
    (gdb) run
    Starting program: /home/vood/ws/a.out 
    0
    
    Breakpoint 1, main () at example.c:9
    9	  a = a / b;
    (gdb) set variable b=1
    (gdb) continue
    Continuing.
    
    Breakpoint 2, main () at example.c:10
    10	  printf("%d", b + c);
    (gdb) set variable b=0
    (gdb) continue
    Continuing.
    13[Inferior 1 (process 26853) exited normally]
    

    9.4. Вопросы для контроля

  • Приведите примеры кода на языке С, который может вызвать ошибку времени выполнения.
  • Что необходимо сделать в процессе сборки программы компилятором gcc, чтобы ее бинарный файл включал отладочную информацию?
  • Объясните, как с точки зрения программиста выглядит работа механизма точек останова.
  • Страницы:

    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. Тогда студентам потребуется анализировать поведение ПО с опорой только на ассемблерный код.
  • Использовать другие виды ошибок времени выполнения.
  • Изменить требования задания в сторону большего уклона в реверс-инжиниринг (выявление промежуточных значений переменных или изменение работы программы).
  • 9.3.1. Описание последовательности выполнения работы

    Первым шагом для выполнения задания необходимо воспроизвести описанную неисправность вне отладчика путем запуска программы в RISC-V ОС и передачи ей строки, вызывающей ошибку. Это необходимо для того, чтобы в дальнейшем вести отладку с учетом максимально достоверной картины - в ряде случаев использование отладчиков может само по себе изменять поведение программы.

    Далее, программу необходимо запустить в gdb и зафиксировать момент ее аварийной остановки. После этого необходимо визуализировать исходный код программы через команды gdb и путем изучения значений переменных, определить - какая именно операция привела к ошибке. Затем завершить выполнение программы.

    Далее, необходимо установить точку останова до момента сбоя так, чтобы на момент остановки у вас была возможность вмешаться в ее работу путем изменения значений переменных. Далее запустите программу через gdb и введите данные, вызывающие ошибку. После остановки на точке останова, измените значения нужных переменных в программе и продолжите ее выполнение. Убедитесь, что после данного вмешательства не происходит ошибок и программа корректно завершает работу. Зафиксируйте ее вывод в отчете.

    9.3.2. Пример выполнения задания на защиту

    Для примера рассмотрим измененный пример программы из теоретической части:

    
    #include <stdio.h>
    
    int main(){
      int a=0;
      int b;
      int c= 13;
    
      scanf("%d", b);
      a = a / b;
      printf("%d", b + c);
    
      return 0;
    }
    

    Мы знаем, что программа должна выводить некоторую контрольную сумму, но при вводе значения 0 происходит ошибка. Воспроизводим ошибку в gdb:

    $ gdb a.out
    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;
    

    Последнее сообщение содержит номер строки, в которой произошла ошибка - строка №9. Выведем часть исходного кода через отладчик

    
    (gdb) l
    4	  int a=0;
    5	  int b;
    6	  int c= 13;
    7	
    8	  scanf("%d", b);
    9	  a = a / b;
    10	  printf("%d", b + c);
    11	
    12	  return 0;
    13	}
    

    Таким образом мы видим проблему в исходном коде - результат ввода пользователя в переменной b используется для деления. В случае если b=0 происходит ошибка деления на ноль. При этом результат деления никак не используется. Поэтому, при использовании точек останова и редактирования значений переменных мы можем вмешаться в поведение программы и преодолеть сбой:

    (gdb) continue
    Continuing.
    
    Program terminated with signal SIGFPE, Arithmetic exception.
    The program no longer exists.
    (gdb) break 9
    Breakpoint 1 at 0x555555554757: file example.c, line 9.
    (gdb) break 10
    Breakpoint 2 at 0x555555554763: file example.c, line 10.
    (gdb) run
    Starting program: /home/vood/ws/a.out 
    0
    
    Breakpoint 1, main () at example.c:9
    9	  a = a / b;
    (gdb) set variable b=1
    (gdb) continue
    Continuing.
    
    Breakpoint 2, main () at example.c:10
    10	  printf("%d", b + c);
    (gdb) set variable b=0
    (gdb) continue
    Continuing.
    13[Inferior 1 (process 26853) exited normally]
    

    9.4. Вопросы для контроля

  • Приведите примеры кода на языке С, который может вызвать ошибку времени выполнения.
  • Что необходимо сделать в процессе сборки программы компилятором gcc, чтобы ее бинарный файл включал отладочную информацию?
  • Объясните, как с точки зрения программиста выглядит работа механизма точек останова.
  • Вернуться к учебному плану