В моих предыдущих публикациях EJTAG: аттракцион для хакеров и Black Swift: использование EJTAG рассматривался самый простой сценарий применения EJTAG — загрузка в ОЗУ и запуск на исполнение программы пользователя. Однако, возможности EJTAG этим не ограничиваются. В публикации рассказывается как организовать несложную отладку кода при помощи EJTAG, используя свободно-распространяемые программные средства openocd и GDB.
На написание данной публикации меня подтолкнуло письмо пожелавшего остаться неизвестным читателя, который обратился ко мне за помощью — устройство на базе AR9344 не грузится (виснет на этапе инициализации U-boot) — как при помощи EJTAG разобраться в чём проблема?
Так как под рукой у меня устройства на базе AR9344 не оказалось, а оказалась плата Black Swift Pro на базе родственного чипа AR9331, то повествование будет вестись с оглядкой на неё. Думаю, что изменения, которые придётся сделать для AR9344 несущественны.
Перейдём к постановке задачи:
ИМЕЕТСЯ плата Black Swift Pro, к которой мы подключились при помощи openocd по EJTAG и перевели процессор в режим останова.
ТРЕБУЕТСЯ последовательно выполнить несколько десятков инструкций процессора, останавливаясь после выполнения каждой инструкции и, при необходимости проверяя содержимое ОЗУ, загрузочного ПЗУ, регистров периферийных контроллеров или регистров процессора.
Для решения задачи вижу по крайней мере два подхода:
- простой — только при помощи openocd — базовая функциональность для выполнения требуемых действий уже есть в openocd. Надо лишь суметь ей воспользоваться;
- сложный — при помощи связки openocd + GDB — при этом пользователь будет управлять процессом исполнения инструкций процессора через GDB, а openocd станет конвертировать запросы GDB в команды EJTAG.
Теперь рассмотрим оба решения подробнее.
Дальнейшее изложение построено в предположении, что читатель ознакомился с публикацией Black Swift: использование EJTAG.
Решение 1: используем только openocd
Те кто читал мои предыдущие публикации про EJTAG должны помнить, что openocd предстаёт в них как тупой исполнитель скриптов (конфигурационных файлов), который как будто бы работает в пакетном режиме и не предусматривает взаимодействия с пользователем. Однако это не так. На самом деле, пока ПО openocd запущено есть возможность «попросить» его выполнить ту или иную команду при помощи интерфейса командной строки. Для доступа к интерфейсу командной строки openocd запускает telnet-сервер.
По умолчанию, для telnet-сервера будет использован TCP-порт 4444. При необходимости номер TCP-порта можно поменять при помощи опции
telnet_port
(см. пример ниже).Попробуем потрассировать загрузчик платы Black Swift при помощи openocd.
Пример конфигурационного файла
black-swift-trace.cfg
для openocd, который заставляет openocd для telnet-сервера использовать порт 4455:source [find interface/ftdi/tumpa.cfg] adapter_khz 6000 source [find black-swift.cfg] telnet_port 4455 init halt
Запуск openocd 0.9.0 от имени пользователя root выглядит так:
# openocd -f black-swift-trace.cfg Open On-Chip Debugger 0.9.0 (2015-05-28-17:08) Licensed under GNU GPL v2 For bug reports, read http://openocd.org/doc/doxygen/bugs.html none separate adapter speed: 6000 kHz Info : auto-selecting first available session transport "jtag". To override use 'transport select <transport>'. Error: no device found Error: unable to open ftdi device with vid 0403, pid 8a98, description '*' and serial '*' Info : clock speed 6000 kHz Info : JTAG tap: ar9331.cpu tap/device found: 0x00000001 (mfg: 0x000, part: 0x0000, ver: 0x0) target state: halted target halted in MIPS32 mode due to debug-request, pc: 0xbfc00000 target state: halted target halted in MIPS32 mode due to single-step, pc: 0xbfc00404
Теперь мы можем открыть ещё одно окно терминала и подключиться к telnet-серверу openocd можно при помощи программы
telnet
:$ telnet localhost 4455 Trying ::1... Trying 127.0.0.1... Connected to localhost. Escape character is '^]'. Open On-Chip Debugger >
Список всех команд openocd легко получить при помощи команды
help
.Для пошагового исполнения инструкций процессора нам пригодится команда
step
:step [address] выполнить одну инструкцию по адресу, определяемому регистром счётчика команд (PC). Если указан параметр address, то будет выполнена инструкция начиная с адреса address.
Пошаговое выполнение инструкций процессора в консоли выглядит так:
> step 0xbfc00400 target state: halted target halted in MIPS32 mode due to single-step, pc: 0xbfc00404 > step target state: halted target halted in MIPS32 mode due to single-step, pc: 0xbfc00408 > step target state: halted target halted in MIPS32 mode due to single-step, pc: 0xbfc0040c
Также полезными могут быть следующие команды openocd:
reg [(register_number|register_name) [(value|'force')]] прочитать или записать значение регистра процессора. Вызов reg без параметров приводит к выводу значения всех регистров. Если использован параметр 'force', производится принудительное вычитывание регистра из процессора (вместо выдачи закэшированного значения). mwb ['phys'] address value [count] записать по адресу address байт, значение которого в параметре value. Если указан параметр phys, то адрес address — физический адрес, в противном случае — виртуальный. Если задан параметр count то по адресу address будет произведена запись массива байт длины count, причём каждый элемент массива имеет значений value. mwh ['phys'] address value [count] команда аналогична mwb, но вместо байта записывается 16-разрядное слово. mww ['phys'] address value [count] команда аналогична mwb, но вместо байта записывается 32-разрядное слово. mdb ['phys'] address [count] Прочитать и вывести на печать байт по адресу address. Если указан параметр phys, то адрес address — физический адрес, в противном случае — виртуальный. Если задан параметр count то будет произведены чтение и вывод на печать массива по адресу address длины count байт. mdh ['phys'] address [count] команда аналогична mdb, но вместо байта читается 16-разрядное слово. mdw ['phys'] address [count] команда аналогична mdb, но вместо байта читается 32-разрядное слово.
Как видно, к сожалению, новейшая (на момент написания публикации) версия openocd 0.9.0 не умеет дизассемблировать инструкции процессоров с архитектурой MIPS, хотя для процессоров с архитектурой ARM такой дизассемблер имеется.
Отсутствие дизассемблера делает пошаговое исполнение инструкций процессора непосредственно при помощи openocd не слишком комфортным. Повысить уровень комфорта можно, если использовать GDB.
Решение 2: используем связку openocd + GDB
В связке openocd + GDB роли распределены так: пользователь общается с GDB, который обеспечивает удобный интерфейс именно для отладки абстрагируясь от того, при помощи какого механизма осуществляется управление выполнением инструкций, а openocd берёт на себя задачу непосредственного управления процессором по указаниям GDB.
Использование GDB для управления исполнением инструкций на процессоре MIPS через EJTAG имеет ряд преимуществ, по сравнению с openocd:
- как было сказано выше, в GDB встроен дизассемблер для архитектуры MIPS;
- возможно использовать отладочную информацию из исходных текстов; к примеру, если вы отлаживаете собственную C-программу, то GDB сможет показывать какая строка C-кода сейчас исполняется и детализировать состояние именно переменных программы, а не ячеек памяти с загадочными адресами;
- для взаимодействия между openocd и GDB используется протокол GDB Remote Serial Protocol; эмулятор qemu также поддерживает этот протокол, так что если вы перемежаете запуск своей отлаживаемой программы на реальном процессоре с запуском под эмулятором, то в обоих случаях удастся отлаживаться используя интерфейс одного и того же инструмента — GDB;
- наконец, интерфейс командной строки GDB поддерживает дополнение команд при помощи TAB.
Следует иметь в виду, что GDB оперирует высокоуровневыми понятиями, а openocd вынужден работать с той аппаратурой, которая есть и далеко не всегда хотелки GDB могут быть эффективно реализованы при помощи EJTAG.
Например, пользователь даёт указание GDB установить точку останова по указанному адресу, это указание поступает в openocd, но для процессоров архитектуры MIPS у openocd по крайней мере два способа установить точку останова:
- заменить инструкцию по адресу точки останова на инструкцию sdbbp, при достижении этой инструкции возникнет прерывание процесса исполнения, openocd заменит sdbbp на оригинальную инструкцию, но зато установит sdbbp вместо следующую за прерванной инструкцией, и запустит исполнение. Такой метод называется software breakpoint. Понятное дело, его нельзя использовать если происходит непосредственное исполнение кода из ПЗУ.
- настроить специальный блок контроля счётчика инструкций процессора на адрес останова. При достижении счётчиком инструкции указанного адреса также произойдёт прерывание исполнения. Однако количество таких точек останова, hardware breakpoint, ограничено.
Хотя внутри протокола GDB Remote Serial Protocol есть различение hardware breakpoint и software breakpoint (см. пакеты z и z0 в описании протокола), а в GDB предусмотрены соответствующие опции для выбора типа точек останова, может оказаться, что на конкретном процессоре есть ограничения на использования точек останова того или иного типа. Соответственно у openocd имеется опция
gdb_breakpoint_override
, которая позволяет принудительно выбрать один из двух описанных методов организации точек останова.Для подключения отладчика GDB в openocd реализован GDB-сервер, который по умолчанию использует TCP-порт 3333. При необходимости номер TCP-порта можно поменять при помощи опции
gdb_port
.Для подключения к GDB-серверу openocd, который управляет процессором MIPS, нам потребуется специальный вариант GDB с поддержкой MIPS. Я предполагаю, что для запуска openocd и GDB читатель использует ЭВМ на базе x86/amd64 под управлением Debian Linux, используется mips-linux-gnu-gdb из пакета Sourcery CodeBench. О том, как установить этот пакет написано тут, следует только ввести поправку, что на момент написания этих строк последней является версия Sourcery CodeBench mips-2015.05-18, выпущенная в мае 2015 года, скачать архив можно вот по этой ссылке.
Попробуем использовать связку openocd + GDB на практике. Запустим openocd:
# openocd -f run-u-boot_mod-trace.cfg > -c "gdb_breakpoint_override hard" -c "step 0xbfc00400"
Последние две команды для openocd обеспечат следующее:
- будут использованы hardware breakpoints, независимо от того, что там себе возомнил GDB (адреса 0xbfc0xxxx соответствуют ПЗУ, так что software breakpoints работать не будут);
- будет исполнена одна инструкция с адреса 0xbfc00400, после чего процессор вновь остановится.
Открываем ещё одно окно терминала и запускаем GDB:
$ /opt/mips-2015.05/bin/mips-linux-gnu-gdb GNU gdb (Sourcery CodeBench Lite 2015.05-18) 7.7.50.20140217-cvs Copyright (C) 2014 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 "--host=i686-pc-linux-gnu --target=mips-linux-gnu". Type "show configuration" for configuration details. For bug reporting instructions, please see: <https://sourcery.mentor.com/GNUToolchain/>. 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". (gdb)
Теперь объясним GDB тип процессора с которым будем работать, попросим дизассемблировать очередную исполняемую инструкцию процессор и, наконец, подключимся к GDB-серверу openocd:
(gdb) set architecture mips:isa32r2 The target architecture is assumed to be mips:isa32r2 (gdb) set endian big The target is assumed to be big endian (gdb) set disassemble-next-line on (gdb) target remote :3333 Remote debugging using :3333 0xbfc00404 in ?? () => 0xbfc00404: 40 80 08 00 mtc0 zero,c0_random
Для выполнения одной инструкции процессора в GDB используется команда
stepi
. Попробуем выполнить несколько инструкций процессора и пусть вас не смущают предупреждения GDB об отсутствии отладочной информации (can't find the start of the function), взять эту информацию в данной ситуации неоткуда.(gdb) stepi warning: GDB can't find the start of the function at 0xbfc00408. GDB is unable to find the start of the function at 0xbfc00408 and thus can't determine the size of that function's stack frame. This means that GDB may be unable to access that stack frame, or the frames below it. This problem is most likely caused by an invalid program counter or stack pointer. However, if you think GDB should simply search farther back from 0xbfc00408 for code which looks like the beginning of a function, you can increase the range of the search using the `set heuristic-fence-post' command. 0xbfc00408 in ?? () => 0xbfc00408: 40 80 10 00 mtc0 zero,c0_entrylo0 (gdb) stepi warning: GDB can't find the start of the function at 0xbfc0040c. 0xbfc0040c in ?? () => 0xbfc0040c: 40 80 18 00 mtc0 zero,c0_entrylo1 (gdb) stepi warning: GDB can't find the start of the function at 0xbfc00410. 0xbfc00410 in ?? () => 0xbfc00410: 40 80 20 00 mtc0 zero,c0_context (gdb)
Теперь прочитаем 32-разрядное слово по адресу 0xbfc00408:
(gdb) p /x *0xbfc00408 $1 = 0x40801000
Для печати состояния регистров процессора используется команда
info registers
:(gdb) info registers zero at v0 v1 a0 a1 a2 a3 R0 00000000 37c688e2 22b15a00 28252198 0c12d319 4193c014 84e49102 06193640 t0 t1 t2 t3 t4 t5 t6 t7 R8 00000002 9f003bc0 92061301 1201c163 31d004a0 92944911 ac031248 b806001c s0 s1 s2 s3 s4 s5 s6 s7 R16 8bc81985 402da011 c94d2454 88d5a554 81808e0d cc445151 4401a826 50020402 t8 t9 k0 k1 gp sp s8 ra R24 01c06b30 01000000 10000004 fffffffe 9f003bc0 54854eab 329d626b bfc004b4 status lo hi badvaddr cause pc 00400004 00244309 b9ca872c ed6a1f00 60808350 bfc00410 fcsr fir 00000000 00000000
GDB — это развитый инструмент с большим число команд и опций; для более близкого знакомства с GDB я отсылаю читателя к официльной документации GDB.
Инициализация контроллера ОЗУ AR9331
Посмотрим, как можно при помощи GDB решить специфическую задачу: протрассировав исполнение U-boot на плате Black Swift, выявить последовательность записей в регистры контроллера ОЗУ, которая приводит к его инициализации. Выявление такой последовательности чрезвычайно полезно, если мы хотим запускать программы на Black Swift при помощи openocd минуя U-boot. Также эта инициализационная последовательность пригодится при создании альтернативного загрузчика для Black Swift.
Запустим openocd (точно также, как в предыдущем разделе):
# openocd -f run-u-boot_mod-trace.cfg > -c "gdb_breakpoint_override hard" -c "step 0xbfc00400"
Для запуска GDB составим скрипт
bs-u-boot-trace-gdb.conf
:set architecture mips:isa32r2 set endian big set disassemble-next-line on target remote :3333 set pagination off set logging file bs_gdb.log set logging on while $pc != (void (*)()) 0x9f002ab0 stepi info registers end detach quit
По сравнению с примером в предыдущем разделе данный скрипт заставляет GDB дублировать вывод в файл
bs_gdb.log
, и отключает pagination (подтверждение листания страниц пользователем), а затем начинает циклически выполнять инструкции процессора и выводить состояние регисров процессора после выполнеия каждой инструкции. По достижения регистром PC (регистр адреса следующей инструкции) значения 0x9f002ab0 GDB отключается от openocd и прекращает работу. Таким образом по окончании работы GDB будет создан файл bs_gdb.log
с полной трассой исполнения инструкций процессора.Запуск GDB будет осуществляться так:
$ /opt/mips-2015.05/bin/mips-linux-gnu-gdb -x bs-u-boot-trace-gdb.conf
Замечание: скриптbs-u-boot-trace-gdb.conf
, скорее всего, не сработает непосредственно после подачи питания на плату, так как u-boot_mod для борьбы с таинственными ошибками AR9331 производит дополнительный сброс, отчего исполнение скрипта остановится. В этом случае следует остановить openocd и GDB, а затем запустить openocd и GDB заново.
Теперь дело за малым — надо выбрать из файла
bs_gdb.log
все инструкции записи sw
(store word, то есть запись 32-разрядного значения). Регистры контроллера памяти AR9331 32-разрядные, так, что другие инструкции из семейства store можно смело игнорировать.Так как дизассемблер выдаёт только имена регистров-аргументов инструкции
sw
=> 0xbfc004ec: ad f9 00 00 sw t9,0(t7)
но не их значения, то недостаточно из файла
bs_gdb.log
выбрать все строки, содержащие инструкцию sw. Для определения того, какое значение по какому адресу при помощи sw было записано надо подвергнуть файл bs_gdb.log
дополнительной обработке. Обработку можно сделать при помощи скрипта parse_gdb_output.pl: #!/usr/bin/perl -w
my %r;
foreach $i (qw(zero at v0 v1 a0 a1 a2 a3 t0 t1 t2 t3 t4 t5 t6 t7
s0 s1 s2 s3 s4 s5 s6 s7 t8 t9 k0 k1 gp sp s8 ra)) {
$r{$i} = "none";
}
sub parse_reg($)
{
$_ = $_[0];
if (/^ R/) {
my @fields = split m'\s+';
my $f = 2;
my @rgs;
@rgs = qw(zero at v0 v1 a0 a1 a2 a3) if (/^ R0/);
@rgs = qw(t0 t1 t2 t3 t4 t5 t6 t7) if (/^ R8/);
@rgs = qw(s0 s1 s2 s3 s4 s5 s6 s7) if (/^ R1/);
@rgs = qw(t8 t9 k0 k1 gp sp s8 ra) if (/^ R2/);
foreach $i (@rgs) {
$r{$i} = $fields[$f];
$f = $f + 1;
}
}
}
while (<>) {
if (/^=>([^s]*)\tsw\t([^,]*),(\d+)\(([^)]*)\)/) {
my $rs = $2;
my $offset = $3;
my $rd = $4;
parse_reg(<>);
parse_reg(<>);
parse_reg(<>);
parse_reg(<>);
print("$1 sw $rs={0x$r{$rs}}, $offset($rd={0x$r{$rd}})\n");
}
}
Запуск
parse_gdb_output.pl
производится так:$ grep "^=\|^ R" bs_gdb.log | ./parse_gdb_output.pl
Вот фрагмент вывода
parse_gdb_output.pl
(отметки '<<< PLL
' и '<<< DDR
' внесены вручную позднее):... 0x9f002700: ad cf 00 00 sw t7={0x00dbd860}, 0(t6={0xb8116248}) 0x9f00271c: ad f9 00 00 sw t9={0x000fffff}, 0(t7={0xb800009c}) 0x9f0027a0: ad f9 00 00 sw t9={0x00018004}, 0(t7={0xb8050008}) <<< PLL 0x9f0027dc: ad f9 00 00 sw t9={0x00000352}, 0(t7={0xb8050004}) <<< 0x9f002840: ad f9 00 00 sw t9={0x40818000}, 0(t7={0xb8050000}) <<< 0x9f002898: ad f9 00 00 sw t9={0x001003e8}, 0(t7={0xb8050010}) <<< 0x9f0028f4: ad f9 00 00 sw t9={0x00818000}, 0(t7={0xb8050000}) <<< 0x9f002970: ad cf 00 00 sw t7={0x00800000}, 0(t6={0xb8116248}) ... 0x9f002994: ad cf 00 00 sw t7={0x40800700}, 0(t6={0xb8116248}) 0x9f002a54: ad f9 00 00 sw t9={0x00008000}, 0(t7={0xb8050008}) 0x9f00309c: af 38 00 00 sw t8={0x7fbc8cd0}, 0(t9={0xb8000000}) <<< DDR 0x9f0030b0: af 38 00 00 sw t8={0x9dd0e6a8}, 0(t9={0xb8000004}) <<< 0x9f0030dc: af 38 00 00 sw t8={0x00000a59}, 0(t9={0xb800008c}) <<< 0x9f0030ec: af 38 00 00 sw t8={0x00000008}, 0(t9={0xb8000010}) <<< ...
Так как адреса регистров формирователя тактового сигнала (PLL) и адреса регистров контроллера памяти типа DDR известны, то легко сообразить какие числа по каким адресам надо записать, чтобы правильно инициализировать контроллер ОЗУ.
Итоги
Как видно вести отладку через JTAG при помощи openocd и GDB совсем не сложно, а описанные приёмы работы годятся не только для AR9331, но, после некоторой адаптации, и даже для процессоров с другой архитектурой, для которых есть поддержка в openocd и GDB.