В моих предыдущих публикациях 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.

Комментарии (0)