EJTAG: аттракцион для хакеров-2

от автора


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

ссылка на оригинал статьи http://geektimes.ru/post/261092/


Комментарии

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *