Инфраструктура: простой отладчик

Инфраструктура — повышение эффективности разработки проекта

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

На самом деле вы уже испытали, что инфраструктура может для вас сделать. Наш каркасный код уже даёт Makefile для компиляции NEMU в один клик. Предположим, однокликовой компиляции нет, и вам нужно вручную набирать команду gcc, чтобы компилировать исходные файлы: предположим, вручную набрать одну команду gcc у вас занимает 10 секунд (нужно вводить много опций компиляции, и 10 секунд — это уже очень быстро), а в проекте NEMU 30 исходных файлов, сколько времени вам понадобится, чтобы скомпилировать исполняемый файл NEMU? Чтобы скомпилировать исполняемый файл NEMU, сколько времени нужно потратить? Однако при разработке NEMU нужно ещё много раз перекомпилировать: допустим, чтобы выполнить PA, нужно скомпилировать NEMU 500 раз; за семестр сколько времени вы только на набор команд компиляции потратите?

Некоторые проекты даже с инструментами собираются дольше. Например, IDE vivado/quartus может занимать от получаса до часа, чтобы сгенерировать bit stream, то есть после того как вы написали код, возможно, придётся ждать до часа, прежде чем проверить, что код верный. Потому что процесс не так прост, как компиляция программы, и нужно разбирать много алгоритмических NPC-задач. Чтобы получить приличный bit stream, инструменты аппаратной разработки платят большую цену, чем gcc, чтобы решать эти NPC-задачи. Здесь инфраструктура ещё важнее: если есть инструменты, которые помогут за раз проверить несколько сторон, они сэкономят вам бесчисленные «часы».

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

В будущем вы точно будете участвовать в проектах больше PA, и вопрос, как повысить эффективность разработки проекта, тоже важен. Надеемся, в ходе выполнения PA вы получите новое понимание инфраструктуры: где есть код, там есть инфраструктура. С накоплением знаний вы, возможно, тоже войдёте в эти неизведанные области и внесете свой вклад для разработчиков всего мира.

Реальные истории

В группе yzh был случай, когда из-за плохой инфраструктуры качество научной работы к дедлайну подачи статьи оказалось неудовлетворительным.

Работа требовала запускать разные тесты, чтобы проверить результаты, и прогнать все тесты на двух 4-ядерных 8-поточных PC занимало около 24 часов. Каждый раз после изменения дизайна все тесты нужно было запускать заново, так что после изменения одной строки кода результат приходил через 24 часа. На самом деле за три месяца до дедлайна подачи мы узнали, что доступен 112-ядерный сервер. Развернуть тестовую среду на этом сервере ожидалось сократить общее время тестов до 1/5 исходного (1/5 — комплексная цифра, учитывая, что у многоядерных серверов частота CPU вдвое ниже, чем у PC, и архитектура отстаёт на два поколения).

Но студент, ведущий проект, не осознал важность инфраструктуры: он всё время тестировал на PC. На самом деле сократить общее время тестов до 1/5 исходного значило, что шансов улучшить дизайн стало в пять раз больше. В итоге к дедлайну дизайн всё ещё правили, тесты всё ещё гоняли снова и снова, и пришлось сдать статью с версией дизайна, которую ещё нужно было улучшать.

Simple Debugger (sdb) — очень важная инфраструктура в NEMU. Мы знаем, что NEMU — программа, которая используется, чтобы выполнять другие гостевые программы, а это значит, что NEMU в любой момент знает всю информацию о выполнении гостевой программы. Однако эта информация внешним отладчикам (например, GDB) нелегко доступна. Например, когда отлаживаете NEMU через GDB, вам будет трудно ставить точки останова в гостевой программе, работающей в NEMU, а для NEMU это менее трудная задача.

Чтобы повысить эффективность отладки, а также как упражнение освоиться с каркасным кодом, нужно реализовать в monitor простой отладчик со следующими функциями (соответствующая часть кода в каталоге nemu/src/monitor/sdb/); если формат и функции команд неясны, смотрите следующую таблицу:

КомандаФорматПримерПояснение
Справка(1)helphelpПечатает справочную информацию по команде
Продолжить выполнение(1)ccВозобновить выполнение приостановленной программы
Выход(1)qqВыйти из NEMU
Пошаговое выполнениеsi [N]si 10Даёт программе приостановиться после выполнения N инструкций в пошаговом режиме,
если N не дан, по умолчанию 1
Печать состояния программыinfo SUBCMDinfo r
info w
Печатать состояние регистров
Печатать информацию о точках наблюдения
Сканировать память(2)x N EXPRx 10 $espНаходит значение выражения EXPR, использует результат как начальный адрес
памяти и выводит подряд N по 4 байта в шестнадцатеричном виде.
Вычисление выраженийp EXPRp $eax + 1Найти значение выражения EXPR, поддерживаемые для EXPR
операции
см. главу Вычисление выражений при отладке.
Поставить точку наблюденияw EXPRw *0x2000Приостановить выполнение программы, когда значение выражения EXPR меняется.
Удаление точки наблюденияd Nd 2Удаляет точку наблюдения с ID N.

Замечания:

  • (1) Команда уже реализована.
  • (2) По сравнению с GDB мы здесь упростили, изменив формат команды.

Баги, которые однажды сами вас найдут

Этими возможностями вам нужно будет пользоваться в будущих PA, чтобы помогать отлаживать NEMU. Если реализация с ошибками, вы можете оказаться в таком сценарии: вы реализовали новую возможность, тестируете её, сканируете участок памяти и видите, что вывод не тот, что ожидали. Вы думаете, что дело в только что реализованной новой возможности, и отлаживаете её. После дней и ночей отладки вы со слезами на глазах понимаете, что баг в функции сканирования памяти!

Если хотите избежать такой муки, после реализации возможности её нужно полноценно тестировать. Со временем цена обнаружения одного и того же бага становится всё выше.

Разбор команд

Чтобы простым отладчиком было легко пользоваться, NEMU взаимодействует с пользователем через библиотеку readline, которая использует функцию readline(), чтобы читать команды с клавиатуры. В отличие от gets(), readline() даёт возможность «редактирования строки», чаще всего — листать историю клавишами вверх и вниз. На самом деле программы оболочки читают команды через readline(). Про функцию и возвращаемое значение readline() см.

man readline

После чтения команды с клавиатуры NEMU нужно разобрать команду и затем выполнить соответствующее действие. Цель разбора команды — распознать аргументы в команде: например, в команде si 10 распознать si и 10 и тем самым понять, что это команда пошагово выполнить 10 инструкций. Разбор команд делается через ряд функций обработки строк, например strtok() в каркасном коде. strtok() — стандартная библиотечная функция в C. Если вы никогда не пользовались strtok() и планируете дальше разбирать команды через strtok() в каркасном коде, обязательно посмотрите

man strtok

Также функция cmd_help() даёт пример использования strtok(). На самом деле функций обработки строк много, введите следующее.

man 3 str<TAB><TAB>

где <TAB> — клавиша TAB на клавиатуре. Вы увидите много функций, которые начинаются с str, среди них вам будут знакомы strlen(), strcpy() и т.д. Имеет смысл сначала посмотреть manual page этих функций обработки строк, чтобы узнать, что они делают, потому что некоторые из них вам, скорее всего, понадобятся, чтобы разбирать команды. Разумеется, вы можете написать и свою функцию обработки строк, чтобы разбирать команды.

Как тестировать функцию обработки строк?

Вас может не удержать порыв кодить: вместо RTFM написать свою. Если так, подумайте: как вы будете тестировать свою функцию обработки строк?

Если вы готовы RTFM, тоже подумайте об этом вопросе, потому что с похожим столкнётесь в PA2.

Ещё одна рекомендуемая функция обработки строк — sscanf(): она очень похожа на scanf(), отличие в том, что sscanf() читает форматированное содержимое из строки, и ею иногда удобно разбирать строки. Если вы ими никогда не пользовались, RTFM или STFW.

Пошаговое выполнение

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

Печать регистров

Печать регистров ещё проще. Но поскольку структура регистров зависит от ISA, мы хотели скрыть различия ISA для простого отладчика. Каркасный код подготовил для вас следующий API:

// nemu/src/isa/$ISA/reg.c
void isa_reg_display(void);

После выполнения info r вызовите isa_reg_display(), внутри просто через printf() выведите значения всех регистров. Если вы никогда не пользовались printf(), RTFM или STFW. Если не знаете, что выводить, можете ориентироваться на вывод в GDB.

Сканирование памяти

Сканирование памяти реализовать тоже не слишком трудно. После разбора команды сначала вычислите значение выражения. Но вычисление выражений вы ещё не реализовали, поэтому пока можно сделать простую версию: задайте, что выражение EXPR может быть только шестнадцатеричным числом, например

x 10 0x80000000

Такое упрощение позволяет пока не увязать в деталях вычисления выражений. Когда разберёте начальный адрес памяти для сканирования, можно циклом вывести данные памяти заданной длины в шестнадцатеричном виде. Если не знаете, как выводить, снова можете ориентироваться на вывод в GDB. Вопрос в том, как нам обращаться к памяти гостевого компьютера? (Ответ уже был.)

Когда реализуете сканирование памяти, можете напечатать память около 0x80000000 или 0x100000: вы должны увидеть код программы; сравните с содержимым встроенной гостевой программы и проверьте, верна ли реализация.

реализовать пошаговое выполнение, печать регистров и сканирование памяти.

Когда освоитесь с каркасом NEMU, эти функции реализовать просто, и жёстких правил по формату вывода у нас нет, так что считайте это упражнением освоиться с программированием в GNU/Linux.

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

Не знаете, как подступиться? Что ж, похоже, нужно ещё раз прочитать раздел RTFSC. Если вы забыли некоторые замечания, перечитать тоже стоит.

Боюсь, код напишется неправильно, что делать?

Michael Stonebrakeropenоткрыть в новом окне, лауреат премии Тьюринга 2014, в одном интервью упоминал, что пять лет разрабатывал первую в мире реляционную СУБД (relational database system) Ingres. Ingres, 90% из которых ушло на то, чтобы её запустить. Другими словами, 90% времени разработки система не работала, была с багами и нуждалась в отладке.

Так что примите реальность: ошибки в коде — это нормально, и нужно проснуться от иллюзии «код можно один раз скомпилировать и успешно запустить», которую вы чувствовали на лабораторных по программированию. Важно, что нужны правильные методы и инструменты, чтобы тестировать и отлаживать и в итоге запустить программу. Пример — инструмент контроля версий git: он может отслеживать изменения кода, чтобы понять, когда внесён баг, и при необходимости откатиться к последней рабочей версии программы.

Короче, только освоив правильные методы и инструменты, можно по-настоящему развеять страх перед багами.

Подсказка

На этом заканчивается этап 1 PA1.