Русский
Русский
English
Статистика
Реклама

Отладчик

Использование бюджетных JTAG-отладчиков в PlatformIO

09.05.2021 16:23:04 | Автор: admin

В этом туториале я хотел бы рассказать о том, как использовать ультрабюджетные JTAG-отладчики CJMCU FT232H и RV-Debugger-Lite в PlatformIO для прошивки и отладки устройств на платформах ESP32 и GD32. Полноценной инструкции на просторах интернета я не нашел, и в процессе настройки столкнулся со многими проблемами, поэтому этот туториал появляется здесь для вашего удобства. Оговорюсь сразу, что настройка прописана для Linux, но для Windows принципиальной разницы нет за исключением танцев с Zadig.

CJMCU FT232H + ESR32 Rev1 aka ESP32Dev

Имеется в виду вот этот дебаггер на основе чипа FT232H:

И вот эта плата на основе ESP-WROOM-32:

Настройки platformio.ini:

[env:esp32dev]platform = espressif32framework = arduinoboard = esp32devmonitor_speed = 115200upload_speed = 921600debug_tool = minimoduleupload_protocol = minimodule

Здесь у нас CJMCU FT232H за 700 рублей будет прикидываться отладчиком FT2232H MINI MODULE за 4500. Для этого необходимо внести следующие изменения в файл (закомментированные мной строки начинаются с ##):

/home/<username>/.platformio/packages/tool-openocd-esp32/share/openocd/scripts/interface/ftdi/minimodule.cfg

## FTDI MiniModule## http://www.ftdichip.com/Support/Documents/DataSheets/Modules/DS_FT2232H_Mini_Module.pdf#interface ftdi##ftdi_device_desc "FT2232H MiniModule"##ftdi_vid_pid 0x0403 0x6010ftdi_vid_pid 0x0403 0x6014# Every pin set as high impedance except TCK, TDI, TDO and TMSftdi_layout_init 0x0008 0x000b# nSRST defined on pin CN2-13 of the MiniModule (pin ADBUS5 [AD5] on the FT2232H chip)# This choice is arbitrary. Use other GPIO pin if desired.##ftdi_layout_signal nSRST -data 0x0020 -oe 0x0020## added# interface 1 is the uartftdi_channel 0reset_config none

Немаловажно закомментировать описание отладчика minimodule"ftdi_device_desc", иначеCJMCU FT232H не получит прав доступа. В том же файле мы меняем пару VID/PID, и ее же прописываем в:

/etc/udev/rules.d/99-platformio-udev.rules

SUBSYSTEM=="usb", ATTR{idVendor}=="0403", ATTR{idProduct}=="6014",GROUP="plugdev", MODE="0666"

чтобы отладчик определялся.

Подключение следующее:

Функция JTAG

CJMCU FT232H

ESP32Dev

TDO

AD2

GPIO15

TDI

AD1

GPIO12

TCK

AD0

GPIO13

TMS

AD3

GPIO14

+3.3V

3V3

GND

GND

CJMCU FT232H + Sipeed Longan Nano

Имеется в виду вот такая вот плата на основе чипа GD32VF103CBT6:

Настройки platformio.ini:

[env:sipeed-longan-nano]platform = gd32vframework = gd32vf103-sdkboard = sipeed-longan-nanomonitor_speed = 115200debug_tool = um232hupload_protocol = um232h

Здесь у нас CJMCU FT232H будет прикидываться отладчиком UM232H за 2000 рублей. Для этого необходимо внести следующие изменения в файл:

/home/<username>/.platformio/packages/tool-openocd-gd32v/share/openocd/scripts/interface/ftdi/um232h.cfg

## FTDI UM232H as a JTAG interface## http://www.ftdichip.com/Products/Modules/DevelopmentModules.htm#UM232H## This should also work with a UM232H-B, but that has not been tested.# Note that UM232H and UM232H-B are 3.3V only.#interface ftdi#ftdi_device_desc "UM232H"ftdi_vid_pid 0x0403 0x6014##ftdi_layout_init 0xfff8 0xfffb##ftdi_layout_signal nTRST -data 0x0100 -oe 0x0100##ftdi_layout_signal nSRST -data 0x0200 -oe 0x0200# interface 1 is the uartftdi_channel 0# just TCK TDI TDO TMS, no resetftdi_layout_init 0x0008 0x000b#ftdi_layout_init 0x0c08 0x0f1breset_config none

Здесь у нас чип совпадает, поэтому только закомментируем ftdi_device_desc, и не забываем внести описанные ранее изменения в 99-platformio-udev.rules.

Возможно вам придется поставить недостающую библиотеку libhidapi-hidraw0:

sudo apt-get install -y libhidapi-hidraw0

Подключение следующее:

Функция JTAG

CJMCU FT232H

Sipeed Longan Nano

TDO

AD2

JTDO

TDI

AD1

JTDI

TCK

AD0

JTCK

TMS

AD3

JTMS

+3.3V

3V3

GND

GND

RV-Debugger-Lite + Sipeed Longan Nano

И напоследок я хочу описать, как использовать ультрабюджетный отладчик RV-Debugger-Lite за 200 рублей.

Настройки platformio.ini:

[env:sipeed-longan-nano]platform = gd32vframework = gd32vf103-sdkboard = sipeed-longan-nanomonitor_speed = 115200debug_tool = sipeed-rv-debuggerupload_protocol = sipeed-rv-debugger

Здесь мы будем прикидываться его старшим братом Sipeed RV-Debugger. Сейчас его в продаже нет, но, если мне не изменяет память, он стоил в пределах 1000 рублей. Для этого мы редактируем файл:

/home/<username>/.platformio/packages/tool-openocd-gd32v/share/openocd/scripts/interface/ftdi/sipeed-rv-debugger.cfg

interface ftdi##ftdi_device_desc "Dual RS232"ftdi_vid_pid 0x0403 0x6010#autoexit true#interface cmsis-daptransport select jtagftdi_layout_init 0x0008 0x001bftdi_layout_signal nSRST -oe 0x0020 -data 0x0020

Здесь мы только комментируем ftdi_device_desc, так как чип почти совпадает: у RV-Debugger FT2232D, а у RV-Debugger-Lite определяется как FT2232C, хотя на самом деле он CH552T. Пара VID/PID у них совпадает.

Подключение следующее:

Функция JTAG

RV-Debugger-Lite

Sipeed Longan Nano

TDO

TDO

JTDO

TDI

TDI

JTDI

TCK

TCK

JTCK

TMS

TMS

JTMS

3V3

3V3

GND

GND

Хочу подчеркнуть, что вся собранная информация была проверена мной лично на моих устройствах. Надеюсь, что данный туториал будет полезен многим для экономии времени.

Литература

PIO (platformio) JTAG FT232H ESP32

Бюджетный отладчик к ESP-32 и его настройка

Debugging Longan Nano

Подробнее..

Особенности структурной обработки исключений в Win64

13.01.2021 14:06:06 | Автор: admin

Введение

В процессе перевода своих средств программирования на платформу x86-64 потребовалось перевести и встроенный интерактивный отладчик. В отличие от подключаемых отладчиков, например, WinDbg, данный отладчик находится, так сказать, непосредственно на борту каждой исполняемой программы. При этом он имеет сравнительно небольшие размеры (около 44 Кбайт, большую часть которых занимает дизассемблер). Я так привык к этому отладчику, что уже совершенно не могу без него обходиться, и поэтому его перевод в 64-разрядную среду стал настоятельно необходимым.

Однако формальный перевод отладчика с Win32 в Win64 дал такие странные результаты, что пришлось потратить много сил и времени, чтобы разобраться, почему то, что ранее работало в Windows-XP, перестало нормально работать в Windows 7/10. Виной всему оказалась структурная обработка исключений, мало задействованная ранее в среде Win32.

В процессе разбирательства я прочел множество обсуждений на эту тему на компьютерных форумах. Задаваемые вопросы (и особенно ответы) показали, что многие программисты смутно представляют конкретную реализацию структурной обработки исключений в Win64. Поэтому кажется полезным пояснить некоторые особенности поведения системы на примере конкретной задачи, в результате чего и появилась данная статья.

Проблема при переводе отладчика на Win64

Внешне проблема после перевода в среду Win64 выглядела так:

Интерактивный встроенный отладчик, основанный на установке обработчика с помощью UnhandledExceptionFilter, заработал в Windows 7/10, но, например, в пошаговом режиме при выполнении очередной, и, казалось бы, безобидной команды отцеплялся, после чего программа заканчивалась с надоедливым предложением отправить отчет в Microsoft, причем отчет содержал сообщение об исключении с кодом 80000004 (т.е. как раз об исключении пошаговой отладки, которое, по сути, и генерировал сам отладчик).

Кроме этого, перестали меняться отладочные регистры DR0-DR7. Все остальное работало так же как в Win32.

Особенно загадочным выглядел крах программы при записи в стек подряд двух одинаковых значений.

Пришлось анализировать коды библиотеки NTDLL.DLL в части работы диспетчера исключений. К счастью, объем кода оказался небольшим.

Общий алгоритм обработки исключения в Windows

В целом процесс обработки исключения и в среде Win32 и в среде Win64 реализован одинаково. При возникновении исключения в программе, управление, естественно, попадает в ядро Windows. Ядро опять возвращает управление на уровень пользователя и вызывает подпрограмму KiUserExceptionDispatcher, которая является маленькой оболочкой для подпрограммы собственно диспетчера исключений. Диспетчер исключений пытается найти в программе пользователя подходящий внутрипоточный обработчик для данного исключения. Если таковой находится и после своего запуска возвращает ответ, что исключение обработано, диспетчер оканчивает свою работу. В этом случае управление уже больше не ныряет в ядро, а просто восстанавливается контекст с помощью обращения к RtlRestoreContext. В конце этой подпрограммы стоит команда iret (iretq для Win64), по которой управление возвращается в прерванное место или в то место, которое обработчик установил в EIP (RIP для Win64) в измененном контексте.

Если же диспетчер так и не нашел в программе нужных внутрипоточных обработчиков, исключение опять взводится с помощью вызова NtRaiseException и, тем самым, передается на обработку уже самой Windows.

Отличия в обработке исключения Win32 и Win64

Вот на этом этапе и начинаются отличия Win32 и Win64. В Win32 для обработки исключения в программе пользователя еще остается последний шанс в виде UnhandledExceptionFilter. Если программа установила обработчик, обращаясь к SetUnhandledExceptionFilter, то из ядра Win32 опять происходит переход на уровень пользователя и вызывается этот обработчик, в моем случае интерактивный отладчик, который может изменить контекст потока, включая отладочные регистры DR0-DR7. Измененный (или не измененный) контекст устанавливается опять-таки через ядро с помощью SetThreadContext, и затем управление, наконец, попадает в прерванное место или в место, назначенное обработчиком. Если такой финальный обработчик в программе не установлен или сообщил, что не может обработать исключение с данным кодом только тогда в Win32 вызывается всем знакомое окно с предложением потревожить Microsoft отчетом об ошибке.

В Win64 если диспетчер исключений не нашел внутрипоточного обработчика, то никакого последнего шанса нет, и сразу выдается системное сообщение об ошибке с прекращением работы программы.

Идея и реализация структурной обработки исключений

Но позвольте, как же тогда в Win64 вообще вызывается обработчик, установленный по SetUnhandledExceptionFilter? Именно здесь и вступает в игру структурная обработка исключений.

Сама идея структурной обработки исключений, заключающаяся в попытке выдать информацию об исключении и обработать исключение на возможно более высоком уровне, весьма здрава. В самом деле, ведь Windows имеет адрес команды, где случилось исключение. Если заранее сообщить ей какая подпрограмма занимает диапазон адресов, в который попало место исключения, и как в этом случае обрабатывать исключения, то можно автоматически вызывать именно нужный в данный момент обработчик, а не какой-то универсальный и всеобъемлющий, но безликий и неинформативный.

Вот только реализация этой идеи в Win64 вызывает массу нареканий.

Причем первый шаг обработки в Win64 вполне нормален. Диспетчер по адресу прерванной команды пытается узнать подпрограмму с помощью вызова RtlLookupFunctionEntry. Если информация о подпрограмме имеется во внутренней таблице (а зарегистрировать ее можно, указав прямо внутри заголовка EXE-файла или обратившись к RtlAddFunctionTable), то дело в шляпе, становится известен обработчик исключений именно для данной подпрограммы и диспетчер вызывает его. Но вот, если по адресу исключения подпрограмма не опознана

Здесь начинаются совсем неочевидные вещи. Не найдя в какой из известных ему подпрограмм произошло исключение, диспетчер не сдается и начинает анализировать текущий стек программы дальше. В Microsoft назвали это раскруткой стека.

Диспетчер берет следующие 8 байт вглубь стека, надеясь, что это очередной адрес возврата, и пытается по ним все же определить, какая из известных, т.е. зарегистрированных подпрограмм выполнялась. Но это очень шаткая и сомнительная технология.

Во-первых, исключения бывают в результате разных ошибок. При некоторых ошибках далеко не факт, что в вершине стека вообще содержит что-то разумное.

Во-вторых, при этом начисто забыли о пошаговом режиме отладки. Например, при вызове какой-нибудь процедуры API Windows подготовка к вызову начинается с команды типа sub rsp,20h. При выполнении этой команды в режиме трассировки, т.е. при очередном пошаговом исключении в вершине стека временно оказываются четыре совершенно случайных значения, не имеющих прямого отношения к программе.

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

Несмотря на это, анализ стека, проводимый в диспетчере исключений, включает даже чтение кода команды (в предположении, что очередные 8 байт это адрес возврата), дизассемблирование этого кода и моделирование (!) работы этой команды с точки зрения изменения стека. Да-да, большую часть диспетчера исключений занимает дизассемблер (как и в моем интерактивном отладчике). Кстати, замечу, что постепенно дизассемблирование превращается в кошмар по моим подсчетам в современном процессоре x86 более 670 разных команд, да еще со всевозможными префиксами.

Особенности обработки исключения

Вернемся к рассматриваемому случаю интерактивного отладчика. Поскольку сначала я не знал о структурной обработке исключений и принудительном анализе стека в Win64, я, конечно, не регистрировал никаких внутрипоточных обработчиков, интуитивно считая, что уже установленный UnhandledExceptionFilter будет вызван в любом случае (как в Win32). В результате этого непонимания отладчик начинал работу пошаговой отладки со случайным содержимым стека. Впрочем, один адрес в самой глубине стека был не случайным. Это был адрес возврата, установленный в результате работы подпрограммы RtlUserThreadStart, которая собственно и запускала мою программу. Именно эта подпрограмма оказалась зарегистрирована в Windows и именно ее обработчик все-таки вызывал UnhandledExceptionFilter, и в конце-концов мой встроенных отладчик.

Таким образом, вся работа отладчика, переделанного для Win64, висела на тоненькой ниточке процесса исследования стека. Если диспетчер при анализе стека доходил до адреса, входящего в RtlUserThreadStart, очередное обращение RtlLookupFunctionEntry давало успех и на данном шаге мой отладчик вызывался.

Но стоило, например, в трассировке выполнить команду dec rsp, как диспетчер немедленно прекращал поиск в стеке адресов возврата (считая, что они должны лежать исключительно по адресам, кратным 8) и я вместо очередного шага получал на экране стандартное сообщение от системы об исключении именно пошагового режима (что сделало бы честь самому Капитану Очевидность).

Кроме не кратности указателя стека 8, много крови мне испортил вот этот фрагмент диспетчера исключений (напомню, сам диспетчер находится в NTDLL.DLL):

76dddda9 4c8b842478010000 mov     r8,qword ptr [rsp+178h]76ddddb1 498b00           mov     rax,qword ptr [r8]76ddddb4 4c3be0           cmp     r12,rax76ddddb7 0f8445120600     je      76e3f00276ddddbd 4983c008         add     r8,876ddddc1 48898424d8010000 mov     qword ptr [rsp+1D8h],rax76ddddc9 4c89842478010000 mov     qword ptr [rsp+178h],r876ddddd1 e92bb30000       jmp     76de9101

Здесь достается очередное значение из стека (моделируемое значение указателя стека находится в R8) и сравнивается с предыдущим анализируемым значением (оно находится в R12). Причем в это место диспетчера управление попадает, если для очередного значения стека вызов RtlLookupFunctionEntry ничего не нашел. Т.е. если для очередного значения в стеке ничего не найдено, а следующее значение точно такое же, диспетчер прекращает исследование стека вообще. Смысл этой проверки для меня непонятен. В моем случае два одинаковых значения это были просто два нуля, т.е. уж точно не адреса возврата, тем не менее, два подряд любых одинаковых значения в стеке являются для диспетчера сигналом, что он заблудился, и анализ стека надо прекратить. Тем самым, объяснилось странное поведение программы при записи двух одинаковых значений в стек подряд.

Но все же самым неприятным для меня оказался другой фрагмент диспетчера. Вот этот:

76de90d2 40f6c507        test    bpl,776de90d6 0f85225f0500    jne     76e3effe76de90dc 483b6c2478      cmp     rbp,qword ptr [rsp+78h]76de90e1 0f82175f0500    jb      76e3effe76de90e7 483b6c2468      cmp     rbp,qword ptr [rsp+68h]76de90ec 0f830c5f0500    jae     76e3effe

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

Но вот дополнительная проверка указателя стека на кратность 8 это безобразие. Какое собственно Windows дело кратен или не кратен указатель стека 8 в программе, раз обработчик все равно уже найден и стек дальше исследовать не надо? Пусть обработчик сам и разбирается с кратностью указателя стека. Из-за этой маленькой и вредной команды test bpl,7 мне пришлось сильно переделывать все свои библиотеки.

Обидно, что сам процессор и программы нормально работают с любым указателем стека. И только при отладке встроенным отладчиком невозможно пошагово пройти команды, делающие (хотя бы даже временно) стек не кратным 8. В Win32 я привык свободно обращаться с указателем стека. Конечно, есть доводы насчет эффективности выполнения по кратным адресам. Но вот простой контрпример: в стеке лежит текстовая строка, от которой нужно отрезать слева число байт, записанное, скажем, в EAX. В Win32 я мог делать это единственной командой:

add esp,eax

А теперь нужно еще двигать всю строку в стеке, что бы ее новое начало легло по адресу, кратному 8. Причем в общем случае может потребоваться и сдвиг обрезанной строки вправо и сдвиг влево. Разве это эффективно? И все лишь для того, чтобы встроенный отладчик вызывался при пошаговом режиме! Справедливости ради, следует отметить, что подключаемый отладчик не имеет таких проблем (и WinDbg тому доказательство). Но мне нужен не подключаемый, а собственный встроенный отладчик!

Доработки отладчика для Win64

Поскольку все прояснилось, можно было, наконец, правильно дорабатывать встроенный отладчик для Win64.

С помощью RtlAddFunctionTable я установил свой обработчик (по сути, просто вызов отладчика) для всего диапазона адресов 0-FFFFFFFF, тем самым полностью отключив вмешательство всяких обработчиков от RtlUserThreadStart и попутно отключив попытки любого анализа стека (кроме проклятой проверки кратности 8) в диспетчере. Я предпочитаю, чтобы при работе моих программ диспетчер исключений не пытался дизассемблировать команды по непонятно какому содержимому стека. Кстати, эффективность обработки исключения при этом тоже возрастает.

Немного отвлекаясь, хочу заметить, что собственная обработка исключений нужна была не только из-за отладчика. В используемом мною языке PL/1 структурная обработка исключений c успехом применялась лет так на 40 раньше, чем в Win64. Поэтому работающая PL-программа просто обязана перехватывать все исключительные ситуации, генерируемые и аппаратурой и Windows. После этого собственный диспетчер исключений языка PL/1 определяет, что делать в программе при той или иной ситуации. Например, при исключениях с кодами 80000003 и 80000004 нужно вызывать тот самый встроенный отладчик.

Использование отладочных регистров

Однако я рано радовался. Отладчик-то заработал и если стек оставался кратен 8, работал, как положено, но регистры DR0-DR7 он изменять не мог, и они по-прежнему оставались нулевыми. А ведь это у нас один из основных механизмов поиска ошибок. Например, моя программа ведет себя ненормально. Я исследую ее переменные и вижу, что в переменной, скажем, X1 вдруг находится неверное значение, скажем, 5. Откуда оно там взялось? Я заново запускаю программу и задаю директиву встроенному отладчику:

K .X1 W1 =5

Что означает поставить аппаратную контрольную точку по записи байта в переменную X1. Если эта точка срабатывает, уже сам отладчик проверяет, стало ли равно значение 5. Если нет отладчик возвращается обратно в программу, не снимая этой аппаратной контрольной точки. Когда значение становится равным 5, отладчик останавливается, не возвращаясь в программу. Поэтому внешне для меня программа запускается и останавливается в отладчике, когда X1 стала равна 5, показывая в программе точку записи в заданную переменную заданного значения (на самом деле начало следующей команды). И я сразу вижу, кто посмел испортить переменную X1 значением 5.

Для этого мощного и универсального способа поиска ошибок и нужны отладочные регистры. В Win32 все работало нормально. Что же в Win64 опять не так? И здесь причина в новой структурной обработке исключений. Поскольку ядро больше не вызывает UnhandledExceptionFilter, все сводится к работе диспетчера исключений и в случае успеха к вызову RtlRestoreContext. Легко убедится, что эта подпрограмма восстанавливает большинство регистров, но не DR0-DR7. На уровне привилегий пользователя она просто не может этого сделать.

Пришлось внутри отладчика внести еще одну доработку. В Win64 перед возвратом в диспетчер исключений отладчику с помощью обращения к SetThreadContext нужно отдельно установить требуемые значения DR0-DR7 для текущего потока. В общем случае устанавливать контекст для текущего потока, конечно, нельзя. Но это как раз тот частный случай, когда устанавливаются безвредные регистры. Для этого флаг контекста задается равным 100010H, т.е. требуется изменить только регистры DR0-DR7, не трогая остальных. Остальные изменит последующее выполнение RtlRestoreContext.

При этой доработке я, подобно многим другим бедолагам, наступил на все грабли и, конечно же, получил пресловутое сообщение об ошибке 998. И как многие, я тоже сначала воспринял его как какое-то нарушение прав доступа. Хотя оно сообщает всего-навсего о том, что адрес контекста при вызове SetThreadContext был не кратен 16, а нужно обязательно кратно, иначе и выдается ошибка 998.

Заключение

Подведем итоги. После всех мытарств отладчик заработал в Win64, наконец, так же как в Win32, кроме случаев, когда указатель стека в момент исключения был не кратен 8, такое исключение не перехватывается. Между прочим, это означает, что не все ошибки можно перехватить в самой программе. Конечно, и в Win32 может быть ситуация, когда вместо вызова обработчика выдается системное сообщение об ошибке. Но в Win64 из-за ограничения на указатель стека таких ситуаций стало больше. Т.е. из-за введения структурной обработки исключений может не получиться никакая обработка исключения в программе вообще.

Для того чтобы правильно организовать обработку исключений в Win64 пришлось исследовать код диспетчера исключений. Хотя в целом, информации и документации по структурной обработке исключений довольно много, например [1].

Но ни в какой документации я не нашел, что в Win64 обработчик, установленный с помощью UnhandledExceptionFilter теперь вызывается через обработчик, указанный для RtlUserThreadStart, и при этом он не может автоматически изменить DR0-DR7.

Нигде не написано, что два одинаковых 8-байтных значения в стеке подряд приводят к прекращению анализа стека в диспетчере исключений.

Предполагаю, что в исходном тексте Windows вызов RtlUserThreadStart просто обрамили какими-нибудь __try и __except, применив структурную обработку к самой Windows. Поэтому и возникли такие отличия Win64 от Win32 в обработке исключений. Однако разработчики не приняли во внимание, что не все программы пишутся на C++, и может потребоваться совсем другая обработка исключений, например, для целей отладки. В этом смысле более простая обработка в Win32, в частности не требующая обязательного выравнивания стека, оказалась и более универсальной, т.е. позволяющей по-разному организовывать работу программ.

Литература

1. Обработка исключений Win32 для программистов на ассемблере. Оригинал на англ., http://www.jorgon.freeserve.co.uk/ExceptFrame.htm

Подробнее..

IDA Pro каким не должен быть SDK

05.07.2020 22:23:42 | Автор: admin

Приветствую,



Эта статья будет о том, как не нужно делать, когда разрабатываешь SDK для своего продукта. А примером, можно даже сказать, самым ярким, будет IDA Pro. Те, кто хоть раз что-то разрабатывал под неё и старался поддерживать, при чтении этих строк, наверняка, сейчас вздрогнули и покрылись холодным потом. Здесь я собрал опыт сопровождения проектов, начиная с IDA v6.5, и заканчивая последней на момент написания статьи версии v7.5. В общем, погнали.


Краткое описание


SDK для IDA Pro позволяет вам разрабатывать следующие типы приложений:


  • Загрузчики различных форматов
  • Процессорные модули
  • Плагины, расширяющие функционал (процессорных модулей, интерфейса и т.п.)
  • IDC-скрипты (свой внутренний язык) и Python-скрипты (вторые использует стороннюю разработку IDAPython, которая стала неотъемлемой частью IDA)

По информации с сайта Hex-Rays, стоимость плана поддержки SDK 10000 USD. На практике же если у вас есть лицензия, вам даётся код доступа к Support-зоне, в которой вы его скачиваете и работаете с ним. Стоимость же указана на тот случай, если у вас будут появляться вопросы и вы захотите задать их разработчикам: без плана поддержки вам скажут, мол, напишите это сами; с поддержкой же, как я понимаю, отказать вам не могут. К сожалению, я не знаю ни одного человека (фирмы), который купил данный план.


Немного подробнее


С того момента, как у вас появляется желание написать что-то под IDA, и вы скачиваете SDK, вы ступаете на достаточно скользкую дорожку, на которой, к тому же, очень легко сойти с ума. И вот почему:


1) В современной разработке вы, скорее всего, уже привыкли к тому, что у любого более-менее серьёзного проекта есть либо doxygen-сгенерированная справка по API, либо вручную написанная хорошая справка, которая вам говорит что куда передаётся, что нужно заполнять, чтобы работало, и т.п., с кучей примеров.
В IDA практически в большинстве доступных пользователю API указаны лишь имена параметров и их типы, а что туда передавать можно отгадывать лишь по именам аргументов. В последнее время, конечно, со справкой у IDA стало получше, но не то что бы значительно.


2) Во многих заполняемых структурах требуется задавать callback-функции, при этом некоторые указаны как необязательные, мол, не укажешь (передашь NULL) и ладно. В действительности крэши приложения при попытке запуска вашего плагина. И, т.к. колбэков много (пример плагин-отладчик), ты начинаешь поочерёдно задавать все, которые "можно не задавать". В итоге это очень сильно утомляет, ты открываешь x64dbg/ollyDbg, в нём idaq.exe/ida.exe, грузишь плагин, ставишь точки остановки, и пытаешься словить момент, когда управление передаётся в 0x00000000.


Эх, помню те времена, когда так много папок с проектами были забиты 200MB dmp-файлами, которые создавались при крэше IDA Но мне они ничем не помогали.


3) Самая болезненная тема для IDA Pro обратная совместимость. В принципе, это достаточно тяжёлая для любого разработчика задача наперёд продумывать интерфейсы, структуры, модульность и т.п. Поэтому здесь и возникает два пути:


  • Хранить обратную совместимость со всеми старыми версиями
  • Не заниматься обратной совместимостью

В первом случае очень быстро накапливается legacy-код, от которого потом становится невозможно избавиться. Вычистить его редко кому удаётся. Потому как, фактически, нужно бросать все силы не на разработку нового функционала, а на выпиливание/перенос старого.


Во втором случае у нас не стоит проблемы обратной совместимости, но возникает другая: т.к. у нас имеется публично доступный SDK, требуется регулярно следить за тем, какие изменения происходят, и документировать их, чтобы все те, кто начал разработку под предыдущую версию, могли безболезненно перейти на новую.


Что же в случае Hex-Rays? Вы удивитесь, но они пошли двумя путями одновременно! Известно, что проект развивается с очень и очень бородатых времён, когда основной целевой платформой был лишь MS-DOS (и, следовательно, реверс-инжиниринг написанных под него приложений). Нужно было поддерживать сегментные регистры, селекторы, параграфы и другую подобную атрибутику. Шло время, в IDA начали появляться другие платформы, процессорные модули и загрузчики, где модель памяти уже была плоской (flat), но пережиток в виде перечисленных мной MS-DOS "фич" сохраняется до сих пор! Весь интерфейс IDA пронизан этим. При разработке процессорных модулей, в который только flat, вам всё равно придётся указываться сегментные регистры (правда уже виртуальные).


А вот с SDK ни о какой обратной совместимости речи идти не может вовсе. В каждой новой версии (даже внутри минорных билдов 6.x и 7.x) что-то да ломается: у колбэков появляются новые аргументы, у старых структур переименовываются и теряются поля, функции API, которые раньше делали одну задачу, теперь делают другую. И таких примеров много.


И ладно бы это всё хоть как-то сопровождалось разработчиком, мол, в этой версии поменялось то и это, теперь нужно так и так. Так нет же! Гайд есть, да: IDA 7.0 SDK: Porting from IDA 4.9-6.x API to IDA 7.0 API, но это всё. Более того, по нему вам не удастся перевести свой проект на новую версию, т.к. он не включает очень многих, но мелких, изменений, о которых, конечно же, вам никто не сообщит. К тому же, это последний гайд для C/C++ разработчика, а с тех пор вышло ещё где-то 5-6 версий SDK.


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


Реальный пример


Когда-то я взял на себя смелость попытаться разработать свой первый плагин-отладчик Motorola 68000 под IDA. В поставляемом SDK был пример отладчика (который, фактически, используется в IDA Pro и сейчас в качестве локального и удалённого), но он был выполнен настолько плохо, что пытаться по нему сделать свой было невозможно. Тогда я полез в интернет и нашёл единственный плагин-отладчик для PS3, который, что забавно, был выполнен на базе того самого кода из SDK.


Чтобы вы понимали, почему код из SDK я так и не решился использовать, вот вам скриншот участка кода, на котором я уже тогда понял, что будет несладко:



Видите cpp-файлы, которые включены через #include? И так по всему исходнику. Тем не менее, тщательно изучив исходный код отладчика PS3, мне удалось вычленить из него что-то рабочее и сделать свой для Sega Mega Drive.


Была одна загвоздка: исходников процессорного модуля, так необходимого для написания отладчика, у меня не было. А нужно было обращаться к регистрам, знать, какие индексы и флаги у каких опкодов установлены, заполнять структуры, отображающие вычисляемые ссылки...



Для этого пришлось снова отлаживать IDA, процессорный модуль m68k, и делать исправления для последнего (об этом я писал в "Модернизация IDA Pro. Исправляем косяки процессорных модулей").


Несмотря на все трудности, мне удалось написать хороший отладчик! К сожалению, вышла новая версия SDK В ней изменилась структура debugger_t, отвечающая за отладчик и его колбэки, и всё, что я пытался сделать, приводило к крэшам самой IDA. Спустя время я и с этим справился.


Но вышла новая версия SDK x64, без совместимости с x86! А эмулятор Gens, на базе которого я делал отладчик, не умел в x64, и проект заглох на много лет. Когда же я нашёл эмулятор, который был способен работать в x64, вышло так много версий SDK, что снова пытаться понять, почему мой плагин не работает, я не решился.


Выводы


Проблемы SDK для IDA Pro в отсутствии нормальной документации об изменениях в каждой из версий; в том, что изменения выходят скопом их много, они кардинальные, и за ними очень тяжело угнаться.


Если уж ваша фирма/команда выкладывает какой-то публичный SDK, и у него есть пользователи, которые платят деньги будьте добры, думайте и о них тоже! Их разработки могут помочь вашему продукту стать лучше и популярнее, как это произошло с IDAPython. Понятно, что хранить обратную совместимость очень сложно, но, если уж решились не поддерживать старые версии, постарайтесь документировать все изменения, которые вы делаете.


Я видел на Github большое количество полезных проектов, которые так и остались непортированными на IDA v7.x. Можно подумать, что их функционал стал ненужным в новых версиях? Может и так, но, как по мне, это усталость и нежелание бороться с постоянно меняющимся API в совокупности с хоббийностью проекта.


IDA Pro Book


Ещё хотелось бы вспомнить об одной бесценной книге, которая мне когда-то очень помогла, но которая сейчас абсолютно бесполезна для разработчика плагинов к IDA IDA Pro Book от Chris Eagle. Всё описанное в ней относится к версии 6.x (ориентировочно v6.5-v6.8). С тех пор изменилось практически всё.


Спасибо.

Подробнее..

Пишем плагин отладки для SNES игр в IDA v7

16.04.2021 04:04:08 | Автор: admin


Приветствую,


Моя очень старая мечта сбылась я написал модуль-отладчик, с помощью которого можно отлаживать SNES (Super Nintendo) игры прямо в IDA! Если интересно узнать, как я это сделал, "прошу под кат" (как тут принято говорить).


Введение


Я давно увлекаюсь реверс-инжинирингом. Сначала это было просто хобби, затем стало работой (и при этом хобби никуда не делось). Только на работе "всё серьёзно", а дома это баловство в виде обратной разработки игр под ретро-приставки: Sega Mega Drive / Genesis, PS1, AmigaOS. Задача обычно стоит следующая: понять как работает игра, если есть сжатие победить его, понять как строится уровень, как размещаются враги на уровне и т.д.


За то время, которое прошло с момента, как я начал этим заниматься, мною было написано несколько удобных и полезных инструментов для тех, кто хотел реверсить игры на Сегу, Соньку, и другие платформы.


Мне удалось разреверсить один очень крутой shoot'em-up: Thunder Force 3 (а именно благодаря этой игре я и познакомился с Идой). Я написал редактор уровней, разреверсил игру до исходников на ассемблере, и всё это попутно создавая и улучшая инструмент, который в последствии и облегчал данную работу плагин-отладчик сеговских ромов для IDA, который я назвал просто Gensida (т.к. в основе лежал один очень популярный эмулятор этой платформы GENS, а точнее его модификация).



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

Со временем я узнал, что у Thunder Force 3 есть и версия для SNES Thunder Spirits, которая имеет несколько новых уровней и некоторые изменения в интерфейсе. Так вот, мне захотелось портировать всё это на Сегу, дополнив игру. Но, знаний как о самой Super Nintendo, так и о том, как её реверсить, у меня не было. Я пошёл гуглить и понял, что как-то всё плохо с отладкой у "сеги подороже". На данный момент существует всего ДВА (!) эмулятора SNES с отладкой, и у одного нет исходников, а второй второй имеет настолько убогий исходный код, что я боялся даже с ним работать.


Тем не менее, овладев некоторыми знаниями и умениями, и переборов желание не ввязываться в такой ужасный код (эмулятора), я смог написать и Snesida отладчик SNES ромов для под IDA. И, я считаю, что теперь то уж настал тот момент, когда я готов рассказать о том, как создать более-менее полноценный отладчик для этого ревёрсерского инструмента.


Что нам потребуется


Для того, чтобы создать свой плагин-отладчик под Иду, нам потребуется:


  1. IDA v7.x
  2. IDA SDK
  3. Эмулятор-отладчик (можно и без отладки, главное с исходниками, которые захочется допилить)
  4. Thrift (да, я выбрал его за сериализацию и RPC прямо "из коробки")
  5. Умение писать на C++

Думаю, список достаточно простой и понятный. Если чего-то из этого у вас нет, то плагин не получится, увы.


А теперь пишем код


Прежде чем начать, советую ознакомиться со статьёй "Модернизация IDA Pro. Отладчик для Sega Mega Drive (часть 2)", т.к. многие моменты здесь будут повторяться, но будут и некоторые новые (т.к. SDK Иды обновляется, и то, что работало раньше, теперь не применимо).


Собственно, написание любого плагина для IDA всегда начинается с создания кода-шаблона. Я использую для этого Visual Studio (на данный момент самой свежей является версия 2019).


Открываем Студию, создаём новый проект DLL, и прописываем в следующие пути к библиотекам в свойствах Linker для проекта:


  • d:\idasdk76\lib\x64_win_vc_32\ это для плагина, который будет работать с 32-битными приложениями (открываться в ida.exe)
  • d:\idasdk76\lib\x64_win_vc_64\ это для плагина, который будет работать с 64-битными приложениями (открываться в ida64.exe)
  • Если у вас не Windows и компилятор не Visual Studio, посмотрите другие имеющиеся папки в d:\idasdk76\lib\

В линкуемые библиотеки добавляем ida.lib. Теперь создаём пустой cpp-файл, чтобы VS показала свойства C/C++ компилятора и указываем:


  • d:\idasdk76\include\ в спискок путей к инклудам
  • Меняем /MDd и /MD на /MTd и /MT соответственно в свойствах Code Generation просто, чтобы не зависеть от лишних библиотек, которые не всегда установлены
  • __NT__;__IDP__;__X64__; в Preprocessor Definitions компилятора
  • __EA64__; дополнительно к предыдущим флагам, если плагин будет работать с 64-битными приложениями
  • Убираем SDL Checks с ним будет сложнее писать код

С подготовкой вроде бы всё. Теперь начнём писать код.


Плагин


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


ida_plugin.cpp
#include <ida.hpp>#include <idp.hpp>#include <dbg.hpp>#include <loader.hpp>#include "ida_plugin.h"extern debugger_t debugger;static bool plugin_inited;static bool init_plugin(void) {    return (ph.id == PLFM_65C816);}static void print_version(){    static const char format[] = NAME " debugger plugin v%s;\nAuthor: DrMefistO [Lab 313] <newinferno@gmail.com>.";    info(format, VERSION);    msg(format, VERSION);}static plugmod_t* idaapi init(void) {    if (init_plugin()) {        dbg = &debugger;        plugin_inited = true;        print_version();        return PLUGIN_KEEP;    }    return PLUGIN_SKIP;}static void idaapi term(void) {    if (plugin_inited) {        plugin_inited = false;    }}static bool idaapi run(size_t arg) {    return false;}char comment[] = NAME " debugger plugin by DrMefistO.";char help[] =    NAME " debugger plugin by DrMefistO.\n"    "\n"    "This module lets you debug SNES roms in IDA.\n";plugin_t PLUGIN = {    IDP_INTERFACE_VERSION,    PLUGIN_PROC | PLUGIN_DBG,    init,    term,    run,    comment,    help,    NAME " debugger plugin",    ""};

Здесь мы описываем наш плагин, инициализируем структуру dbg, т.к. мы отладчик, и указываем, что работаем мы только с платформой PLFM_65C816 (в моём случае). Более подробно в статье про отладчик для Сеги.


Следом идёт ida_plugin.h. Тут всё просто константы для cpp-файла плагина:


#pragma once#define NAME "snesida"#define VERSION "1.0"

Код самого отладчика


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


ida_debug.cpp
#include <ida.hpp>#include <dbg.hpp>#include <auto.hpp>#include <deque>#include <mutex>#include "ida_plugin.h"#include "ida_debmod.h"#include "ida_registers.h"static ::std::mutex list_mutex;static eventlist_t events;static const char* const p_reg[] ={    "CF",    "ZF",    "IF",    "DF",    "XF",    "MF",    "VF",    "NF",};static register_info_t registers[] = {    {"A", 0, RC_CPU, dt_word, NULL, 0},    {"X", 0, RC_CPU, dt_word, NULL, 0},    {"Y", 0, RC_CPU, dt_word, NULL, 0},    {"D", 0, RC_CPU, dt_word, NULL, 0},    {"DB", 0, RC_CPU, dt_byte, NULL, 0},    {"PC", REGISTER_IP | REGISTER_ADDRESS, RC_CPU, dt_dword, NULL, 0},  {"S", REGISTER_SP | REGISTER_ADDRESS, RC_CPU, dt_word, NULL, 0},    {"P", REGISTER_READONLY, RC_CPU, dt_byte, p_reg, 0xFF},  {"m", REGISTER_READONLY, RC_CPU, dt_byte, NULL, 0},  {"x", REGISTER_READONLY, RC_CPU, dt_byte, NULL, 0},    {"e", REGISTER_READONLY, RC_CPU, dt_byte, NULL, 0},};static const char* register_classes[] = {    "General Registers",    NULL};static drc_t idaapi init_debugger(const char* hostname, int portnum, const char* password, qstring* errbuf){  return DRC_OK;}static drc_t idaapi term_debugger(void){  return DRC_OK;}static drc_t s_get_processes(procinfo_vec_t* procs, qstring* errbuf) {  process_info_t info;  info.name.sprnt("bsnes");  info.pid = 1;  procs->add(info);  return DRC_OK;}static drc_t idaapi s_start_process(const char* path,  const char* args,  const char* startdir,  uint32 dbg_proc_flags,  const char* input_path,  uint32 input_file_crc32,  qstring* errbuf = NULL){  ::std::lock_guard<::std::mutex> lock(list_mutex);  events.clear();  return DRC_OK;}static drc_t idaapi prepare_to_pause_process(qstring* errbuf){  return DRC_OK;}static drc_t idaapi emul_exit_process(qstring* errbuf){  return DRC_OK;}static gdecode_t idaapi get_debug_event(debug_event_t* event, int timeout_ms){  while (true)  {    ::std::lock_guard<::std::mutex> lock(list_mutex);    // are there any pending events?    if (events.retrieve(event))    {      return events.empty() ? GDE_ONE_EVENT : GDE_MANY_EVENTS;    }    if (events.empty())      break;  }  return GDE_NO_EVENT;}static drc_t idaapi continue_after_event(const debug_event_t* event){  dbg_notification_t req = get_running_notification();  switch (event->eid())  {  case PROCESS_SUSPENDED:    break;  case PROCESS_EXITED:    break;  }  return DRC_OK;}static drc_t idaapi s_set_resume_mode(thid_t tid, resume_mode_t resmod) // Run one instruction in the thread{  switch (resmod)  {  case RESMOD_INTO:    ///< step into call (the most typical single stepping)    break;  case RESMOD_OVER:    ///< step over call    break;  }  return DRC_OK;}static drc_t idaapi read_registers(thid_t tid, int clsmask, regval_t* values, qstring* errbuf){  if (clsmask & RC_CPU)  {    }    return DRC_OK;}static drc_t idaapi write_register(thid_t tid, int regidx, const regval_t* value, qstring* errbuf){  if (regidx >= static_cast<int>(SNES_REGS::SR_PC) && regidx <= static_cast<int>(SNES_REGS::SR_EFLAG)) {    }    return DRC_OK;}static drc_t idaapi get_memory_info(meminfo_vec_t& areas, qstring* errbuf){  memory_info_t info;  info.start_ea = 0x0000;  info.end_ea = 0x01FFF;  info.sclass = "STACK";  info.bitness = 0;  info.perm = SEGPERM_READ | SEGPERM_WRITE;  areas.push_back(info);  // Don't remove this loop  for (int i = 0; i < get_segm_qty(); ++i)  {    segment_t* segm = getnseg(i);    info.start_ea = segm->start_ea;    info.end_ea = segm->end_ea;    qstring buf;    get_segm_name(&buf, segm);    info.name = buf;    get_segm_class(&buf, segm);    info.sclass = buf;    info.sbase = get_segm_base(segm);    info.perm = segm->perm;    info.bitness = segm->bitness;    areas.push_back(info);  }  // Don't remove this loop    return DRC_OK;}static ssize_t idaapi read_memory(ea_t ea, void* buffer, size_t size, qstring* errbuf){  return size;}static ssize_t idaapi write_memory(ea_t ea, const void* buffer, size_t size, qstring* errbuf){  return size;}static int idaapi is_ok_bpt(bpttype_t type, ea_t ea, int len){  switch (type)  {  case BPT_EXEC:  case BPT_READ:  case BPT_WRITE:  case BPT_RDWR:    return BPT_OK;  }  return BPT_BAD_TYPE;}static drc_t idaapi update_bpts(int* nbpts, update_bpt_info_t* bpts, int nadd, int ndel, qstring* errbuf){  for (int i = 0; i < nadd; ++i)  {    ea_t start = bpts[i].ea;    ea_t end = bpts[i].ea + bpts[i].size - 1;    bpts[i].code = BPT_OK;  }  for (int i = 0; i < ndel; ++i)  {    ea_t start = bpts[nadd + i].ea;    ea_t end = bpts[nadd + i].ea + bpts[nadd + i].size - 1;    bpts[nadd + i].code = BPT_OK;  }  *nbpts = (ndel + nadd);  return DRC_OK;}static ssize_t idaapi idd_notify(void*, int msgid, va_list va) {  drc_t retcode = DRC_NONE;  qstring* errbuf;  switch (msgid)  {  case debugger_t::ev_init_debugger:  {    const char* hostname = va_arg(va, const char*);    int portnum = va_arg(va, int);    const char* password = va_arg(va, const char*);    errbuf = va_arg(va, qstring*);    QASSERT(1522, errbuf != NULL);    retcode = init_debugger(hostname, portnum, password, errbuf);  }  break;  case debugger_t::ev_term_debugger:    retcode = term_debugger();    break;  case debugger_t::ev_get_processes:  {    procinfo_vec_t* procs = va_arg(va, procinfo_vec_t*);    errbuf = va_arg(va, qstring*);    retcode = s_get_processes(procs, errbuf);  }  break;  case debugger_t::ev_start_process:  {    const char* path = va_arg(va, const char*);    const char* args = va_arg(va, const char*);    const char* startdir = va_arg(va, const char*);    uint32 dbg_proc_flags = va_arg(va, uint32);    const char* input_path = va_arg(va, const char*);    uint32 input_file_crc32 = va_arg(va, uint32);    errbuf = va_arg(va, qstring*);    retcode = s_start_process(path,      args,      startdir,      dbg_proc_flags,      input_path,      input_file_crc32,      errbuf);  }  break;  case debugger_t::ev_get_debapp_attrs:  {    debapp_attrs_t* out_pattrs = va_arg(va, debapp_attrs_t*);    out_pattrs->addrsize = 3;    out_pattrs->is_be = false;    out_pattrs->platform = "bsnes";    out_pattrs->cbsize = sizeof(debapp_attrs_t);    retcode = DRC_OK;  }  break;  case debugger_t::ev_rebase_if_required_to:  {    ea_t new_base = va_arg(va, ea_t);    retcode = DRC_OK;  }  break;  case debugger_t::ev_request_pause:    errbuf = va_arg(va, qstring*);    retcode = prepare_to_pause_process(errbuf);    break;  case debugger_t::ev_exit_process:    errbuf = va_arg(va, qstring*);    retcode = emul_exit_process(errbuf);    break;  case debugger_t::ev_get_debug_event:  {    gdecode_t* code = va_arg(va, gdecode_t*);    debug_event_t* event = va_arg(va, debug_event_t*);    int timeout_ms = va_arg(va, int);    *code = get_debug_event(event, timeout_ms);    retcode = DRC_OK;  }  break;  case debugger_t::ev_resume:  {    debug_event_t* event = va_arg(va, debug_event_t*);    retcode = continue_after_event(event);  }  break;  case debugger_t::ev_thread_suspend:  {    thid_t tid = va_argi(va, thid_t);    retcode = DRC_OK;  }  break;  case debugger_t::ev_thread_continue:  {    thid_t tid = va_argi(va, thid_t);    retcode = DRC_OK;  }  break;  case debugger_t::ev_set_resume_mode:  {    thid_t tid = va_argi(va, thid_t);    resume_mode_t resmod = va_argi(va, resume_mode_t);    retcode = s_set_resume_mode(tid, resmod);  }  break;  case debugger_t::ev_read_registers:  {    thid_t tid = va_argi(va, thid_t);    int clsmask = va_arg(va, int);    regval_t* values = va_arg(va, regval_t*);    errbuf = va_arg(va, qstring*);    retcode = read_registers(tid, clsmask, values, errbuf);  }  break;  case debugger_t::ev_write_register:  {    thid_t tid = va_argi(va, thid_t);    int regidx = va_arg(va, int);    const regval_t* value = va_arg(va, const regval_t*);    errbuf = va_arg(va, qstring*);    retcode = write_register(tid, regidx, value, errbuf);  }  break;  case debugger_t::ev_get_memory_info:  {    meminfo_vec_t* ranges = va_arg(va, meminfo_vec_t*);    errbuf = va_arg(va, qstring*);    retcode = get_memory_info(*ranges, errbuf);  }  break;  case debugger_t::ev_read_memory:  {    size_t* nbytes = va_arg(va, size_t*);    ea_t ea = va_arg(va, ea_t);    void* buffer = va_arg(va, void*);    size_t size = va_arg(va, size_t);    errbuf = va_arg(va, qstring*);    ssize_t code = read_memory(ea, buffer, size, errbuf);    *nbytes = code >= 0 ? code : 0;    retcode = code >= 0 ? DRC_OK : DRC_NOPROC;  }  break;  case debugger_t::ev_write_memory:  {    size_t* nbytes = va_arg(va, size_t*);    ea_t ea = va_arg(va, ea_t);    const void* buffer = va_arg(va, void*);    size_t size = va_arg(va, size_t);    errbuf = va_arg(va, qstring*);    ssize_t code = write_memory(ea, buffer, size, errbuf);    *nbytes = code >= 0 ? code : 0;    retcode = code >= 0 ? DRC_OK : DRC_NOPROC;  }  break;  case debugger_t::ev_check_bpt:  {    int* bptvc = va_arg(va, int*);    bpttype_t type = va_argi(va, bpttype_t);    ea_t ea = va_arg(va, ea_t);    int len = va_arg(va, int);    *bptvc = is_ok_bpt(type, ea, len);    retcode = DRC_OK;  }  break;  case debugger_t::ev_update_bpts:  {    int* nbpts = va_arg(va, int*);    update_bpt_info_t* bpts = va_arg(va, update_bpt_info_t*);    int nadd = va_arg(va, int);    int ndel = va_arg(va, int);    errbuf = va_arg(va, qstring*);    retcode = update_bpts(nbpts, bpts, nadd, ndel, errbuf);  }  break;  default:    retcode = DRC_NONE;  }  return retcode;}debugger_t debugger{    IDD_INTERFACE_VERSION,    NAME,    0x8000 + 6581, // (6)    "65816",    DBG_FLAG_NOHOST | DBG_FLAG_CAN_CONT_BPT | DBG_FLAG_SAFE | DBG_FLAG_FAKE_ATTACH | DBG_FLAG_NOPASSWORD |    DBG_FLAG_NOSTARTDIR | DBG_FLAG_NOPARAMETERS | DBG_FLAG_ANYSIZE_HWBPT | DBG_FLAG_DEBTHREAD | DBG_FLAG_PREFER_SWBPTS,    DBG_HAS_GET_PROCESSES | DBG_HAS_REQUEST_PAUSE | DBG_HAS_SET_RESUME_MODE | DBG_HAS_THREAD_SUSPEND | DBG_HAS_THREAD_CONTINUE | DBG_HAS_CHECK_BPT,    register_classes,    RC_CPU,    registers,    qnumber(registers),    0x1000,    NULL,    0,    0,    DBG_RESMOD_STEP_INTO | DBG_RESMOD_STEP_OVER,    NULL,    idd_notify};

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


Вторым важным изменением стало введением "стандартизированных" кодов возврата у функций отладчика drc_t. Тут всё просто: если функция отработала без ошибок, возвращаем DRC_OK, иначе DRC_FAILED.


Остальные инклуды:


ida_registers.h
#pragma once#define RC_CPU (1 << 0)#define RC_PPU (1 << 1)enum class SNES_REGS : uint8_t{    SR_A,    SR_X,    SR_Y,    SR_D,    SR_DB,    SR_PC,    SR_S,    SR_P,    SR_MFLAG,    SR_XFLAG,    SR_EFLAG,};

ida_debmod.h
#pragma once#include <deque>#include <ida.hpp>#include <idd.hpp>//--------------------------------------------------------------------------// Very simple class to store pending eventsenum queue_pos_t{    IN_FRONT,    IN_BACK};struct eventlist_t : public std::deque<debug_event_t>{private:    bool synced;public:    // save a pending event    void enqueue(const debug_event_t &ev, queue_pos_t pos)    {        if (pos != IN_BACK)            push_front(ev);        else            push_back(ev);    }    // retrieve a pending event    bool retrieve(debug_event_t *event)    {        if (empty())            return false;        // get the first event and return it        *event = front();        pop_front();        return true;    }};

В ida_registers.h мы просто перечисляем список регистров для удоства обращений к ним в коде, а в ida_debmod.h описан формат eventlist_t, который мы будем использовать для хранения событий, с которыми будет работать IDA.


Подготовка завершена


Теперь, когда код шаблона у нас имеется, стоит понять, что мы будем делать дальше. А дальше нам нужно соорудить модель, по которой между IDA и эмулятором будет происходить общение. Для этого нужно держать в голове следующее:


  1. Эмулятор с функцией отладки должен уметь реагировать на запросы Иды "добавить/убрать брейкпоинт", "прочитать/записать память", "получить/изменить регистры"
  2. Эмулятор также должен: уведомлять IDA о том, что: "брейкпоинт сработал", "шаг при пошаговой отладке выполнен", или "процесс отладки начат или завершён"
  3. Ида должна уметь сообщать эмулятору о том, что есть необходимость: "добавить/убрать брейкпоинт", "прочитать/записать память", "получить/изменить регистры"
  4. Ида должна реагировать на сообщения от эмулятора о том, что: "брейкпоинт сработал", "шаг при пошаговой отладке выполнен", или "процесс отладки начат или завершён"

Исходя из перечисленного понимаем, что понадобятся два канала, т.к. каждому может захотеться "пообщаться" в любой момент, асинхронно:


  1. IDA => эмулятор
  2. Эмулятор => IDA

Учитывая это, можно, опять же, пойти по стопам предыдущей статьи про сеговский отладчик, а можно захотеть использовать "модные и современные" технологии для реализации RPC и сериализации любых данных. Мой выбор пал в сторону Thrift, т.к. с ним работать гораздо удобнее, и он практически не требует дополнительной подготовки (как, например, доклеивание RPC в protobuf, но тут, скорее, на любителя). Единственная сложность, это компиляция сего зверя, но, я оставлю это за рамками данной статьи.


Thrift пишем прототип RPC


Давайте ещё раз посмотрим на те 4 пункта, которые я описал выше, и которые мы всё ещё держим в голове, откроем блокнот, и напишем что-то вроде этого:


service IdaClient {  oneway void start_event(),  oneway void add_visited(1:set<i32> visited, 2:bool is_step),  oneway void pause_event(1:i32 address),  oneway void stop_event(),}

Как видим, в Thrift нету ничего сложного. Здесь мы описали сервис IdaClient, которым будет пользоваться эмулятор, и обработчик которого будет располагаться в IDA. Все эти методы помечены ключевым словом oneway, т.к., по сути, нам не нужно дожидаться их выполнения, и в принципе ожидать, что их обработают.


start_event() будет сообщать Иде о том, что ром выбрал и его эмуляция началась.


add_visited() метод, с помощью которого мы будем сообщать в Иду о том коде, который был выполнен эмулятором. Это полезно при отладке как раз таки ретро-платформ, т.к. в ромах для них код часто перемежается с данными. Если таковой функции в выбранном вами эмуляторе нет, её можно также пропустить и в протоколе.


pause_event() этим методом мы будем сообщать Иде о том, что произошла пауза эмуляции по какой-либо причине: будь то брейкпоинт, завершился шаг при StepInto или StepOver или какой-то другой причине. В качестве нагрузки данный метод будет также передавать адрес, где именно произошла остановка.


stop_event() думаю, тут всё понятно. Эмуляция завершилась, например, по причине завершения процесса эмуляции.


С этим разобрались, теперь часть посложнее отладочный RPC:


service BsnesDebugger {  i32 get_cpu_reg(1:BsnesRegister reg),  BsnesRegisters get_cpu_regs(),  void set_cpu_reg(1:BsnesRegister reg, 2:i32 value),  binary read_memory(1:DbgMemorySource src, 2:i32 address, 3:i32 size),  void write_memory(1:DbgMemorySource src, 2:i32 address, 3:binary data),  void add_breakpoint(1:DbgBreakpoint bpt),  void del_breakpoint(1:DbgBreakpoint bpt),  void pause(),  void resume(),  void start_emulation(),  void exit_emulation(),  void step_into(),  void step_over(),}

Здесь у нас описана серверная часть, которая будет крутиться в эмуляторе, и к которой Ида время от времени будет приставать. Давайте разберём её более детально:


  i32 get_cpu_reg(1:BsnesRegister reg),  void set_cpu_reg(1:BsnesRegister reg, 2:i32 value),

Эти методы мы будем использовать тогда, когда нам потребуется прочитать или записать один регистр. Использованный enum BsnesRegister выглядит так:


enum BsnesRegister {  pc,  a,  x,  y,  s,  d,  db,  p,  mflag,  xflag,  eflag,}

Фактически, это те регистры, значения которых мы хотим видеть во время отладки, у вас они могут быть другими.


Т.к. IDA сама никогда не запрашивает по одному регистру, а требует все сразу, напишем метод, который будет их все сразу и отдавать:


struct BsnesRegisters {  1:i32 pc,  2:i32 a,  3:i32 x,  4:i32 y,  5:i32 s,  6:i32 d,  7:i16 db,  8:i16 p,  9:i8 mflag,  10:i8 xflag,  11:i8 eflag,}service BsnesDebugger {  ...  BsnesRegisters get_cpu_regs(),  ...}

Здесь я завёл одну общую структуру под регистры, указав их размеры и указал её в качестве возвращаемого значения для метода get_cpu_regs().


Теперь работа с памятью:


enum DbgMemorySource {  CPUBus,  APUBus,  APURAM,  DSP,  VRAM,  OAM,  CGRAM,  CartROM,  CartRAM,  SA1Bus,  SFXBus,  SGBBus,  SGBROM,  SGBRAM,}service BsnesDebugger {  ...  binary read_memory(1:DbgMemorySource src, 2:i32 address, 3:i32 size),  void write_memory(1:DbgMemorySource src, 2:i32 address, 3:binary data),  ...}

Здесь мы использовали встроенный в Thrift тип данных binary, и указали различные области памяти, которые могут быть прочитаны (взято из эмулятора).


Теперь пришла очередь брейкпоинтов:


enum BpType {  BP_PC = 1,  BP_READ = 2,  BP_WRITE = 4,}enum DbgBptSource {  CPUBus,  APURAM,  DSP,  VRAM,  OAM,  CGRAM,  SA1Bus,  SFXBus,  SGBBus,}struct DbgBreakpoint {  1:BpType type,  2:i32 bstart,  3:i32 bend,  4:bool enabled,  5:DbgBptSource src,}service BsnesDebugger {  ...  void add_breakpoint(1:DbgBreakpoint bpt),  void del_breakpoint(1:DbgBreakpoint bpt),  ...}

Т.к. список областей памяти, которые можно читать, и на которые можно ставить брейкпоинты отличаются, заводим отдельный список DbgBptSource. Также указываем тип брейкпоинта BpType и адрес его начала/конца bstart/bend. Ещё нам может понадобиться включать брейкпоинт не сразу enabled.


С основными сложными частями протокола закончили, теперь можно описать более простые:


service BsnesDebugger {  ...  void pause(),  void resume(),  void start_emulation(),  void exit_emulation(),  void step_into(),  void step_over(),  ...}

Метод pause() будет приостанавливать процесс отладки по запросу от IDA, resume() продолжать.


start_emulation() нужен для того, чтобы IDA могла сообщить эмулятору, что она начала процесс отладки, и ожидает от него какие-либо события. Фактически, используется в качестве синхронизации начала эмуляции между плагином-отладчиком и собственно эмулятором.


exit_emulation() на случай, если мы захотим остановить отладку из IDA, а не из эмулятора.


step_into() и step_over() пошаговая отладка.


Итоговый debug_proto.thrift
enum BsnesRegister {  pc,  a,  x,  y,  s,  d,  db,  p,  mflag,  xflag,  eflag,}struct BsnesRegisters {  1:i32 pc,  2:i32 a,  3:i32 x,  4:i32 y,  5:i32 s,  6:i32 d,  7:i16 db,  8:i16 p,  9:i8 mflag,  10:i8 xflag,  11:i8 eflag,}enum BpType {  BP_PC = 1,  BP_READ = 2,  BP_WRITE = 4,}enum DbgMemorySource {  CPUBus,  APUBus,  APURAM,  DSP,  VRAM,  OAM,  CGRAM,  CartROM,  CartRAM,  SA1Bus,  SFXBus,  SGBBus,  SGBROM,  SGBRAM,}enum DbgBptSource {  CPUBus,  APURAM,  DSP,  VRAM,  OAM,  CGRAM,  SA1Bus,  SFXBus,  SGBBus,}struct DbgBreakpoint {  1:BpType type,  2:i32 bstart,  3:i32 bend,  4:bool enabled,  5:DbgBptSource src,}service BsnesDebugger {  i32 get_cpu_reg(1:BsnesRegister reg),  BsnesRegisters get_cpu_regs(),  void set_cpu_reg(1:BsnesRegister reg, 2:i32 value),  binary read_memory(1:DbgMemorySource src, 2:i32 address, 3:i32 size),  void write_memory(1:DbgMemorySource src, 2:i32 address, 3:binary data),  void add_breakpoint(1:DbgBreakpoint bpt),  void del_breakpoint(1:DbgBreakpoint bpt),  void pause(),  void resume(),  void start_emulation(),  void exit_emulation(),  void step_into(),  void step_over(),}service IdaClient {  oneway void start_event(),  oneway void add_visited(1:set<i32> changed, 2:bool is_step),  oneway void pause_event(1:i32 address),  oneway void stop_event(),}

От RPC-прототипа к реализации


На этом процесс написания RPC-прототипа завершён. Чтобы сгенерировать из него код для языка C++, качаем Thrift-компилятор, выполняем из командной строки следующее:


thrift --gen cpp debug_proto.thrift

На выходе мы получим каталог gen-cpp, в котором нас будут ждать не только файлики, которые нужно будет компилировать вместе с проектом, но и шаблон кода каждого из сервисов IdaClient и BsnesDebugger.



Добавляем сгенерированные файлы в студийный проект (кроме файлов *_server.skeleton.cpp). Также необходимо слинковать наш проект плагина (и эмулятора) со скомпилированными статичными библиотеками thrift-а и libevent-а (мы будем использовать "nonblocking" вариант Thrift). У этих библиотек имеется CMake вариант сборки, который значительно упрощает процесс.


Код IdaClient хэндлера


Теперь давайте напишем шаблон кода, реализующий IdaClient-сервис:


Необходимые инклуды и адресные пространства
#include "gen-cpp/IdaClient.h"#include "gen-cpp/BsnesDebugger.h"#include <thrift/protocol/TBinaryProtocol.h>#include <thrift/transport/TSocket.h>#include <thrift/transport/TBufferTransports.h>#include <thrift/server/TNonblockingServer.h>#include <thrift/transport/TNonblockingServerSocket.h>#include <thrift/concurrency/ThreadFactory.h>using namespace ::apache::thrift;using namespace ::apache::thrift::protocol;using namespace ::apache::thrift::transport;using namespace ::apache::thrift::server;using namespace ::apache::thrift::concurrency;::std::shared_ptr<BsnesDebuggerClient> client;::std::shared_ptr<TNonblockingServer> srv;::std::shared_ptr<TTransport> cli_transport;

Реализация серверной части IdaClient
static void pause_execution(){  try {    if (client) {      client->pause();    }  }  catch (...) {  }}static void continue_execution(){  try {    if (client) {      client->resume();    }  }  catch (...) {  }}static void stop_server() {  try {    srv->stop();  }  catch (...) {  }}static void finish_execution(){  try {    if (client) {      client->exit_emulation();    }  }  catch (...) {  }  stop_server();}class IdaClientHandler : virtual public IdaClientIf {public:    void pause_event(const int32_t address) override {    ::std::lock_guard<::std::mutex> lock(list_mutex);    debug_event_t ev;    ev.pid = 1;    ev.tid = 1;    ev.ea = address | 0x800000;    ev.handled = true;    ev.set_eid(PROCESS_SUSPENDED);    events.enqueue(ev, IN_BACK);    }    void start_event() override {    ::std::lock_guard<::std::mutex> lock(list_mutex);    debug_event_t ev;    ev.pid = 1;    ev.tid = 1;    ev.ea = BADADDR;    ev.handled = true;    ev.set_modinfo(PROCESS_STARTED).name.sprnt("BSNES");    ev.set_modinfo(PROCESS_STARTED).base = 0;    ev.set_modinfo(PROCESS_STARTED).size = 0;    ev.set_modinfo(PROCESS_STARTED).rebase_to = BADADDR;    events.enqueue(ev, IN_BACK);    }    void stop_event() override {    ::std::lock_guard<::std::mutex> lock(list_mutex);    debug_event_t ev;    ev.pid = 1;    ev.handled = true;    ev.set_exit_code(PROCESS_EXITED, 0);    events.enqueue(ev, IN_BACK);    }  void add_visited(const std::set<int32_t>& changed, bool is_step) override {  }};

В этом коде мы реагируем на события эмуляции и сообщаем о них Иде, добавляя эти события в список. Более подробно о них можно прочитать в той же статье про отладчик для Сеги. Код add_visited() пока оставляем пустым. О нём позже.


Теперь напишем код, который будет отвечать за поднятие сервиса на стороне Иды (будем использовать порт 9091), и ожидание подключения к эмулятору:


init_ida_server и init_emu_client
static void init_ida_server() {    try {    ::std::shared_ptr<IdaClientHandler> handler(new IdaClientHandler());    ::std::shared_ptr<TProcessor> processor(new IdaClientProcessor(handler));    ::std::shared_ptr<TNonblockingServerTransport> serverTransport(new TNonblockingServerSocket(9091));    ::std::shared_ptr<TFramedTransportFactory> transportFactory(new TFramedTransportFactory());    ::std::shared_ptr<TProtocolFactory> protocolFactory(new TBinaryProtocolFactory());    srv = ::std::shared_ptr<TNonblockingServer>(new TNonblockingServer(processor, protocolFactory, serverTransport));    ::std::shared_ptr<ThreadFactory> tf(new ThreadFactory());    ::std::shared_ptr<Thread> thread = tf->newThread(srv);    thread->start();    } catch (...) {    }}static void init_emu_client() {  ::std::shared_ptr<TTransport> socket(new TSocket("127.0.0.1", 9090));  cli_transport = ::std::shared_ptr<TTransport>(new TFramedTransport(socket));  ::std::shared_ptr<TBinaryProtocol> protocol(new TBinaryProtocol(cli_transport));  client = ::std::shared_ptr<BsnesDebuggerClient>(new BsnesDebuggerClient(protocol));  show_wait_box("Waiting for BSNES-PLUS emulation...");  while (true) {    if (user_cancelled()) {      break;    }    try {      cli_transport->open();      break;    }    catch (...) {    }  }  hide_wait_box();}

Осталось дополнить имеющийся шаблон ida_debug.cpp кодом для работы со Thrift. Вот что получилось:


Полный код ida_debug.cpp
#include "gen-cpp/IdaClient.h"#include "gen-cpp/BsnesDebugger.h"#include <thrift/protocol/TBinaryProtocol.h>#include <thrift/transport/TSocket.h>#include <thrift/transport/TBufferTransports.h>#include <thrift/server/TNonblockingServer.h>#include <thrift/transport/TNonblockingServerSocket.h>#include <thrift/concurrency/ThreadFactory.h>using namespace ::apache::thrift;using namespace ::apache::thrift::protocol;using namespace ::apache::thrift::transport;using namespace ::apache::thrift::server;using namespace ::apache::thrift::concurrency;#include <ida.hpp>#include <dbg.hpp>#include <auto.hpp>#include <deque>#include <mutex>#include "ida_plugin.h"#include "ida_debmod.h"#include "ida_registers.h"::std::shared_ptr<BsnesDebuggerClient> client;::std::shared_ptr<TNonblockingServer> srv;::std::shared_ptr<TTransport> cli_transport;static ::std::mutex list_mutex;static eventlist_t events;static const char* const p_reg[] ={    "CF",    "ZF",    "IF",    "DF",    "XF",    "MF",    "VF",    "NF",};static register_info_t registers[] = {    {"A", 0, RC_CPU, dt_word, NULL, 0},    {"X", 0, RC_CPU, dt_word, NULL, 0},    {"Y", 0, RC_CPU, dt_word, NULL, 0},    {"D", 0, RC_CPU, dt_word, NULL, 0},    {"DB", 0, RC_CPU, dt_byte, NULL, 0},    {"PC", REGISTER_IP | REGISTER_ADDRESS, RC_CPU, dt_dword, NULL, 0},  {"S", REGISTER_SP | REGISTER_ADDRESS, RC_CPU, dt_word, NULL, 0},    {"P", REGISTER_READONLY, RC_CPU, dt_byte, p_reg, 0xFF},  {"m", REGISTER_READONLY, RC_CPU, dt_byte, NULL, 0},  {"x", REGISTER_READONLY, RC_CPU, dt_byte, NULL, 0},    {"e", REGISTER_READONLY, RC_CPU, dt_byte, NULL, 0},};static const char* register_classes[] = {    "General Registers",    NULL};static struct apply_codemap_req : public exec_request_t {private:  const std::set<int32_t>& _changed;  const bool _is_step;public:  apply_codemap_req(const std::set<int32_t>& changed, bool is_step) : _changed(changed), _is_step(is_step) {};  int idaapi execute(void) override {    auto m = _changed.size();    if (!_is_step) {      show_wait_box("Applying codemap: %d/%d...", 1, m);    }    auto x = 0;    for (auto i = _changed.cbegin(); i != _changed.cend(); ++i) {      if (!_is_step && user_cancelled()) {        break;      }      if (!_is_step) {        replace_wait_box("Applying codemap: %d/%d...", x, m);      }      ea_t addr = (ea_t)(*i | 0x800000);      auto_make_code(addr);      plan_ea(addr);      show_addr(addr);      x++;    }    if (!_is_step) {      hide_wait_box();    }    return 0;  }};static void apply_codemap(const std::set<int32_t>& changed, bool is_step){  if (changed.empty()) return;  apply_codemap_req req(changed, is_step);  execute_sync(req, MFF_FAST);}static void pause_execution(){  try {    if (client) {      client->pause();    }  }  catch (...) {  }}static void continue_execution(){  try {    if (client) {      client->resume();    }  }  catch (...) {  }}static void stop_server() {  try {    srv->stop();  }  catch (...) {  }}static void finish_execution(){  try {    if (client) {      client->exit_emulation();    }  }  catch (...) {  }  stop_server();}class IdaClientHandler : virtual public IdaClientIf {public:    void pause_event(const int32_t address) override {    ::std::lock_guard<::std::mutex> lock(list_mutex);    debug_event_t ev;    ev.pid = 1;    ev.tid = 1;    ev.ea = address | 0x800000;    ev.handled = true;    ev.set_eid(PROCESS_SUSPENDED);    events.enqueue(ev, IN_BACK);    }    void start_event() override {    ::std::lock_guard<::std::mutex> lock(list_mutex);    debug_event_t ev;    ev.pid = 1;    ev.tid = 1;    ev.ea = BADADDR;    ev.handled = true;    ev.set_modinfo(PROCESS_STARTED).name.sprnt("BSNES");    ev.set_modinfo(PROCESS_STARTED).base = 0;    ev.set_modinfo(PROCESS_STARTED).size = 0;    ev.set_modinfo(PROCESS_STARTED).rebase_to = BADADDR;    events.enqueue(ev, IN_BACK);    }    void stop_event() override {    ::std::lock_guard<::std::mutex> lock(list_mutex);    debug_event_t ev;    ev.pid = 1;    ev.handled = true;    ev.set_exit_code(PROCESS_EXITED, 0);    events.enqueue(ev, IN_BACK);    }  void add_visited(const std::set<int32_t>& changed, bool is_step) override {    apply_codemap(changed, is_step);  }};static void init_ida_server() {    try {    ::std::shared_ptr<IdaClientHandler> handler(new IdaClientHandler());    ::std::shared_ptr<TProcessor> processor(new IdaClientProcessor(handler));    ::std::shared_ptr<TNonblockingServerTransport> serverTransport(new TNonblockingServerSocket(9091));    ::std::shared_ptr<TFramedTransportFactory> transportFactory(new TFramedTransportFactory());    ::std::shared_ptr<TProtocolFactory> protocolFactory(new TBinaryProtocolFactory());    srv = ::std::shared_ptr<TNonblockingServer>(new TNonblockingServer(processor, protocolFactory, serverTransport));    ::std::shared_ptr<ThreadFactory> tf(new ThreadFactory());    ::std::shared_ptr<Thread> thread = tf->newThread(srv);    thread->start();    } catch (...) {    }}static void init_emu_client() {  ::std::shared_ptr<TTransport> socket(new TSocket("127.0.0.1", 9090));  cli_transport = ::std::shared_ptr<TTransport>(new TFramedTransport(socket));  ::std::shared_ptr<TBinaryProtocol> protocol(new TBinaryProtocol(cli_transport));  client = ::std::shared_ptr<BsnesDebuggerClient>(new BsnesDebuggerClient(protocol));  show_wait_box("Waiting for BSNES-PLUS emulation...");  while (true) {    if (user_cancelled()) {      break;    }    try {      cli_transport->open();      break;    }    catch (...) {    }  }  hide_wait_box();}static drc_t idaapi init_debugger(const char* hostname, int portnum, const char* password, qstring* errbuf){  return DRC_OK;}static drc_t idaapi term_debugger(void){  finish_execution();  return DRC_OK;}static drc_t s_get_processes(procinfo_vec_t* procs, qstring* errbuf) {  process_info_t info;  info.name.sprnt("bsnes");  info.pid = 1;  procs->add(info);  return DRC_OK;}static drc_t idaapi s_start_process(const char* path,  const char* args,  const char* startdir,  uint32 dbg_proc_flags,  const char* input_path,  uint32 input_file_crc32,  qstring* errbuf = NULL){  ::std::lock_guard<::std::mutex> lock(list_mutex);  events.clear();  init_ida_server();  init_emu_client();  try {    if (client) {      client->start_emulation();    }  }  catch (...) {    return DRC_FAILED;  }  return DRC_OK;}static drc_t idaapi prepare_to_pause_process(qstring* errbuf){  pause_execution();  return DRC_OK;}static drc_t idaapi emul_exit_process(qstring* errbuf){  finish_execution();  return DRC_OK;}static gdecode_t idaapi get_debug_event(debug_event_t* event, int timeout_ms){  while (true)  {    ::std::lock_guard<::std::mutex> lock(list_mutex);    // are there any pending events?    if (events.retrieve(event))    {      return events.empty() ? GDE_ONE_EVENT : GDE_MANY_EVENTS;    }    if (events.empty())      break;  }  return GDE_NO_EVENT;}static drc_t idaapi continue_after_event(const debug_event_t* event){  dbg_notification_t req = get_running_notification();  switch (event->eid())  {  case STEP:  case PROCESS_SUSPENDED:    if (req == dbg_null || req == dbg_run_to) {      continue_execution();    }    break;  case PROCESS_EXITED:    stop_server();    break;  }  return DRC_OK;}static drc_t idaapi s_set_resume_mode(thid_t tid, resume_mode_t resmod) // Run one instruction in the thread{  switch (resmod)  {  case RESMOD_INTO:    ///< step into call (the most typical single stepping)    try {      if (client) {        client->step_into();      }    }    catch (...) {      return DRC_FAILED;    }    break;  case RESMOD_OVER:    ///< step over call    try {      if (client) {        client->step_over();      }    }    catch (...) {      return DRC_FAILED;    }    break;  }  return DRC_OK;}static drc_t idaapi read_registers(thid_t tid, int clsmask, regval_t* values, qstring* errbuf){  if (clsmask & RC_CPU)  {        BsnesRegisters regs;    try {      if (client) {        client->get_cpu_regs(regs);                values[static_cast<int>(SNES_REGS::SR_PC)].ival = regs.pc | 0x800000;                values[static_cast<int>(SNES_REGS::SR_A)].ival = regs.a;                values[static_cast<int>(SNES_REGS::SR_X)].ival = regs.x;                values[static_cast<int>(SNES_REGS::SR_Y)].ival = regs.y;                values[static_cast<int>(SNES_REGS::SR_S)].ival = regs.s;                values[static_cast<int>(SNES_REGS::SR_D)].ival = regs.d;                values[static_cast<int>(SNES_REGS::SR_DB)].ival = regs.db;                values[static_cast<int>(SNES_REGS::SR_P)].ival = regs.p;        values[static_cast<int>(SNES_REGS::SR_MFLAG)].ival = regs.mflag;        values[static_cast<int>(SNES_REGS::SR_XFLAG)].ival = regs.xflag;                values[static_cast<int>(SNES_REGS::SR_EFLAG)].ival = regs.eflag;      }    }    catch (...) {      return DRC_FAILED;    }    }    return DRC_OK;}static drc_t idaapi write_register(thid_t tid, int regidx, const regval_t* value, qstring* errbuf){  if (regidx >= static_cast<int>(SNES_REGS::SR_PC) && regidx <= static_cast<int>(SNES_REGS::SR_EFLAG)) {    try {      if (client) {        client->set_cpu_reg(static_cast<BsnesRegister::type>(regidx), value->ival & 0xFFFFFFFF);      }    }    catch (...) {      return DRC_FAILED;    }    }    return DRC_OK;}static drc_t idaapi get_memory_info(meminfo_vec_t& areas, qstring* errbuf){  memory_info_t info;  info.start_ea = 0x0000;  info.end_ea = 0x01FFF;  info.sclass = "STACK";  info.bitness = 0;  info.perm = SEGPERM_READ | SEGPERM_WRITE;  areas.push_back(info);  // Don't remove this loop  for (int i = 0; i < get_segm_qty(); ++i)  {    segment_t* segm = getnseg(i);    info.start_ea = segm->start_ea;    info.end_ea = segm->end_ea;    qstring buf;    get_segm_name(&buf, segm);    info.name = buf;    get_segm_class(&buf, segm);    info.sclass = buf;    info.sbase = get_segm_base(segm);    info.perm = segm->perm;    info.bitness = segm->bitness;    areas.push_back(info);  }  // Don't remove this loop    return DRC_OK;}static ssize_t idaapi read_memory(ea_t ea, void* buffer, size_t size, qstring* errbuf){  std::string mem;  try {    if (client) {      client->read_memory(mem, DbgMemorySource::CPUBus, (int32_t)ea, (int32_t)size);      memcpy(&((unsigned char*)buffer)[0], mem.c_str(), size);    }  }  catch (...) {    return DRC_FAILED;  }  return size;}static ssize_t idaapi write_memory(ea_t ea, const void* buffer, size_t size, qstring* errbuf){  std::string mem((const char*)buffer);  try {    if (client) {      client->write_memory(DbgMemorySource::CPUBus, (int32_t)ea, mem);    }  }  catch (...) {    return 0;  }  return size;}static int idaapi is_ok_bpt(bpttype_t type, ea_t ea, int len){  DbgMemorySource::type btype = DbgMemorySource::CPUBus;  switch (btype) {  case DbgMemorySource::CPUBus:  case DbgMemorySource::APURAM:  case DbgMemorySource::DSP:  case DbgMemorySource::VRAM:  case DbgMemorySource::OAM:  case DbgMemorySource::CGRAM:  case DbgMemorySource::SA1Bus:  case DbgMemorySource::SFXBus:    break;  default:    return BPT_BAD_TYPE;  }  switch (type)  {  case BPT_EXEC:  case BPT_READ:  case BPT_WRITE:  case BPT_RDWR:    return BPT_OK;  }  return BPT_BAD_TYPE;}static drc_t idaapi update_bpts(int* nbpts, update_bpt_info_t* bpts, int nadd, int ndel, qstring* errbuf){  for (int i = 0; i < nadd; ++i)  {    ea_t start = bpts[i].ea;    ea_t end = bpts[i].ea + bpts[i].size - 1;    DbgBreakpoint bp;    bp.bstart = start;    bp.bend = end;    bp.enabled = true;    switch (bpts[i].type)    {    case BPT_EXEC:      bp.type = BpType::BP_PC;      break;    case BPT_READ:      bp.type = BpType::BP_READ;      break;    case BPT_WRITE:      bp.type = BpType::BP_WRITE;      break;    case BPT_RDWR:      bp.type = BpType::BP_READ;      break;    }    DbgMemorySource::type type = DbgMemorySource::CPUBus;    switch (type) {    case DbgMemorySource::CPUBus:      bp.src = DbgBptSource::CPUBus;      break;    case DbgMemorySource::APURAM:      bp.src = DbgBptSource::APURAM;      break;    case DbgMemorySource::DSP:      bp.src = DbgBptSource::DSP;      break;    case DbgMemorySource::VRAM:      bp.src = DbgBptSource::VRAM;      break;    case DbgMemorySource::OAM:      bp.src = DbgBptSource::OAM;      break;    case DbgMemorySource::CGRAM:      bp.src = DbgBptSource::CGRAM;      break;    case DbgMemorySource::SA1Bus:      bp.src = DbgBptSource::SA1Bus;      break;    case DbgMemorySource::SFXBus:      bp.src = DbgBptSource::SFXBus;      break;    default:      continue;    }    try {      if (client) {        client->add_breakpoint(bp);      }    }    catch (...) {      return DRC_FAILED;    }    bpts[i].code = BPT_OK;  }  for (int i = 0; i < ndel; ++i)  {    ea_t start = bpts[nadd + i].ea;    ea_t end = bpts[nadd + i].ea + bpts[nadd + i].size - 1;    DbgBreakpoint bp;    bp.bstart = start;    bp.bend = end;    bp.enabled = true;    switch (bpts[i].type)    {    case BPT_EXEC:      bp.type = BpType::BP_PC;      break;    case BPT_READ:      bp.type = BpType::BP_READ;      break;    case BPT_WRITE:      bp.type = BpType::BP_WRITE;      break;    case BPT_RDWR:      bp.type = BpType::BP_READ;      break;    }    DbgMemorySource::type type = DbgMemorySource::CPUBus;    switch (type) {    case DbgMemorySource::CPUBus:      bp.src = DbgBptSource::CPUBus;      break;    case DbgMemorySource::APURAM:      bp.src = DbgBptSource::APURAM;      break;    case DbgMemorySource::DSP:      bp.src = DbgBptSource::DSP;      break;    case DbgMemorySource::VRAM:      bp.src = DbgBptSource::VRAM;      break;    case DbgMemorySource::OAM:      bp.src = DbgBptSource::OAM;      break;    case DbgMemorySource::CGRAM:      bp.src = DbgBptSource::CGRAM;      break;    case DbgMemorySource::SA1Bus:      bp.src = DbgBptSource::SA1Bus;      break;    case DbgMemorySource::SFXBus:      bp.src = DbgBptSource::SFXBus;      break;    default:      continue;    }    try {      if (client) {        client->del_breakpoint(bp);      }    }    catch (...) {      return DRC_FAILED;    }    bpts[nadd + i].code = BPT_OK;  }  *nbpts = (ndel + nadd);  return DRC_OK;}static ssize_t idaapi idd_notify(void*, int msgid, va_list va) {  drc_t retcode = DRC_NONE;  qstring* errbuf;  switch (msgid)  {  case debugger_t::ev_init_debugger:  {    const char* hostname = va_arg(va, const char*);    int portnum = va_arg(va, int);    const char* password = va_arg(va, const char*);    errbuf = va_arg(va, qstring*);    QASSERT(1522, errbuf != NULL);    retcode = init_debugger(hostname, portnum, password, errbuf);  }  break;  case debugger_t::ev_term_debugger:    retcode = term_debugger();    break;  case debugger_t::ev_get_processes:  {    procinfo_vec_t* procs = va_arg(va, procinfo_vec_t*);    errbuf = va_arg(va, qstring*);    retcode = s_get_processes(procs, errbuf);  }  break;  case debugger_t::ev_start_process:  {    const char* path = va_arg(va, const char*);    const char* args = va_arg(va, const char*);    const char* startdir = va_arg(va, const char*);    uint32 dbg_proc_flags = va_arg(va, uint32);    const char* input_path = va_arg(va, const char*);    uint32 input_file_crc32 = va_arg(va, uint32);    errbuf = va_arg(va, qstring*);    retcode = s_start_process(path,      args,      startdir,      dbg_proc_flags,      input_path,      input_file_crc32,      errbuf);  }  break;  case debugger_t::ev_get_debapp_attrs:  {    debapp_attrs_t* out_pattrs = va_arg(va, debapp_attrs_t*);    out_pattrs->addrsize = 3;    out_pattrs->is_be = false;    out_pattrs->platform = "snes";    out_pattrs->cbsize = sizeof(debapp_attrs_t);    retcode = DRC_OK;  }  break;  case debugger_t::ev_rebase_if_required_to:  {    ea_t new_base = va_arg(va, ea_t);    retcode = DRC_OK;  }  break;  case debugger_t::ev_request_pause:    errbuf = va_arg(va, qstring*);    retcode = prepare_to_pause_process(errbuf);    break;  case debugger_t::ev_exit_process:    errbuf = va_arg(va, qstring*);    retcode = emul_exit_process(errbuf);    break;  case debugger_t::ev_get_debug_event:  {    gdecode_t* code = va_arg(va, gdecode_t*);    debug_event_t* event = va_arg(va, debug_event_t*);    int timeout_ms = va_arg(va, int);    *code = get_debug_event(event, timeout_ms);    retcode = DRC_OK;  }  break;  case debugger_t::ev_resume:  {    debug_event_t* event = va_arg(va, debug_event_t*);    retcode = continue_after_event(event);  }  break;  case debugger_t::ev_thread_suspend:  {    thid_t tid = va_argi(va, thid_t);    pause_execution();    retcode = DRC_OK;  }  break;  case debugger_t::ev_thread_continue:  {    thid_t tid = va_argi(va, thid_t);    continue_execution();    retcode = DRC_OK;  }  break;  case debugger_t::ev_set_resume_mode:  {    thid_t tid = va_argi(va, thid_t);    resume_mode_t resmod = va_argi(va, resume_mode_t);    retcode = s_set_resume_mode(tid, resmod);  }  break;  case debugger_t::ev_read_registers:  {    thid_t tid = va_argi(va, thid_t);    int clsmask = va_arg(va, int);    regval_t* values = va_arg(va, regval_t*);    errbuf = va_arg(va, qstring*);    retcode = read_registers(tid, clsmask, values, errbuf);  }  break;  case debugger_t::ev_write_register:  {    thid_t tid = va_argi(va, thid_t);    int regidx = va_arg(va, int);    const regval_t* value = va_arg(va, const regval_t*);    errbuf = va_arg(va, qstring*);    retcode = write_register(tid, regidx, value, errbuf);  }  break;  case debugger_t::ev_get_memory_info:  {    meminfo_vec_t* ranges = va_arg(va, meminfo_vec_t*);    errbuf = va_arg(va, qstring*);    retcode = get_memory_info(*ranges, errbuf);  }  break;  case debugger_t::ev_read_memory:  {    size_t* nbytes = va_arg(va, size_t*);    ea_t ea = va_arg(va, ea_t);    void* buffer = va_arg(va, void*);    size_t size = va_arg(va, size_t);    errbuf = va_arg(va, qstring*);    ssize_t code = read_memory(ea, buffer, size, errbuf);    *nbytes = code >= 0 ? code : 0;    retcode = code >= 0 ? DRC_OK : DRC_NOPROC;  }  break;  case debugger_t::ev_write_memory:  {    size_t* nbytes = va_arg(va, size_t*);    ea_t ea = va_arg(va, ea_t);    const void* buffer = va_arg(va, void*);    size_t size = va_arg(va, size_t);    errbuf = va_arg(va, qstring*);    ssize_t code = write_memory(ea, buffer, size, errbuf);    *nbytes = code >= 0 ? code : 0;    retcode = code >= 0 ? DRC_OK : DRC_NOPROC;  }  break;  case debugger_t::ev_check_bpt:  {    int* bptvc = va_arg(va, int*);    bpttype_t type = va_argi(va, bpttype_t);    ea_t ea = va_arg(va, ea_t);    int len = va_arg(va, int);    *bptvc = is_ok_bpt(type, ea, len);    retcode = DRC_OK;  }  break;  case debugger_t::ev_update_bpts:  {    int* nbpts = va_arg(va, int*);    update_bpt_info_t* bpts = va_arg(va, update_bpt_info_t*);    int nadd = va_arg(va, int);    int ndel = va_arg(va, int);    errbuf = va_arg(va, qstring*);    retcode = update_bpts(nbpts, bpts, nadd, ndel, errbuf);  }  break;  default:    retcode = DRC_NONE;  }  return retcode;}debugger_t debugger{    IDD_INTERFACE_VERSION,    NAME,    0x8000 + 6581, // (6)    "65816",    DBG_FLAG_NOHOST | DBG_FLAG_CAN_CONT_BPT | DBG_FLAG_SAFE | DBG_FLAG_FAKE_ATTACH | DBG_FLAG_NOPASSWORD |    DBG_FLAG_NOSTARTDIR | DBG_FLAG_NOPARAMETERS | DBG_FLAG_ANYSIZE_HWBPT | DBG_FLAG_DEBTHREAD | DBG_FLAG_PREFER_SWBPTS,    DBG_HAS_GET_PROCESSES | DBG_HAS_REQUEST_PAUSE | DBG_HAS_SET_RESUME_MODE | DBG_HAS_THREAD_SUSPEND | DBG_HAS_THREAD_CONTINUE | DBG_HAS_CHECK_BPT,    register_classes,    RC_CPU,    registers,    qnumber(registers),    0x1000,    NULL,    0,    0,    DBG_RESMOD_STEP_INTO | DBG_RESMOD_STEP_OVER,    NULL,    idd_notify};

Дабы не описывать весь этот код, здесь я опишу лишь типичный код для работы со Thrift со стороны IDA:


    try {      if (client) {        client->step_over();      }    }    catch (...) {      return DRC_FAILED;    }    return DRC_OK;

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


Код BsnesDebugger хэндлера


Теперь мы дошли до модификации непосредственно эмулятора. Как ни странно, изменений потребуется не так много. Для того, чтобы не вдаваться в подробности реализации конкретного эмулятора, и чтобы не бомбить о том, какая же здесь ужасная структура кода, я просто приведу шаблон cpp-файла, который я использовал при компиляции эмулятора.


remote_debugger.cpp
#include "gen-cpp/IdaClient.h"#include "gen-cpp/BsnesDebugger.h"#include <thrift/protocol/TBinaryProtocol.h>#include <thrift/transport/TSocket.h>#include <thrift/transport/TBufferTransports.h>#include <thrift/server/TNonblockingServer.h>#include <thrift/transport/TNonblockingServerSocket.h>#include <thrift/concurrency/ThreadFactory.h>using namespace ::apache::thrift;using namespace ::apache::thrift::protocol;using namespace ::apache::thrift::transport;using namespace ::apache::thrift::server;using namespace ::apache::thrift::concurrency;#include "../ui-base.hpp"static ::std::shared_ptr<IdaClientClient> client;static ::std::shared_ptr<TNonblockingServer> srv;static ::std::shared_ptr<TTransport> cli_transport;static ::std::mutex list_mutex;::std::set<int32_t> visited;static void send_visited(bool is_step) {  const auto part = visited.size();  ::std::lock_guard<::std::mutex> lock(list_mutex);  try {    if (client) {      client->add_visited(visited, is_step);    }  }  catch (...) {  }  visited.clear();}static void stop_client() {  try {    if (client) {      send_visited(false);      client->stop_event();    }    cli_transport->close();  }  catch (...) {  }}static void init_ida_client() {  ::std::shared_ptr<TTransport> socket(new TSocket("127.0.0.1", 9091));  cli_transport = ::std::shared_ptr<TTransport>(new TFramedTransport(socket));  ::std::shared_ptr<TBinaryProtocol> protocol(new TBinaryProtocol(cli_transport));  client = ::std::shared_ptr<IdaClientClient>(new IdaClientClient(protocol));  while (true) {    try {      cli_transport->open();      break;    }    catch (...) {      Sleep(10);    }  }  atexit(stop_client);}static void toggle_pause(bool enable) {  application.debug = enable;  application.debugrun = enable;  if (enable) {    audio.clear();  }}class BsnesDebuggerHandler : virtual public BsnesDebuggerIf {public:  int32_t get_cpu_reg(const BsnesRegister::type reg) override {    switch (reg) {    case BsnesRegister::pc:    case BsnesRegister::a:    case BsnesRegister::x:    case BsnesRegister::y:    case BsnesRegister::s:    case BsnesRegister::d:    case BsnesRegister::db:    case BsnesRegister::p:      return SNES::cpu.getRegister((SNES::CPUDebugger::Register)reg);    case BsnesRegister::mflag:      return (SNES::cpu.usage[SNES::cpu.regs.pc] & SNES::CPUDebugger::UsageFlagM) ? 1 : 0;    case BsnesRegister::xflag:      return (SNES::cpu.usage[SNES::cpu.regs.pc] & SNES::CPUDebugger::UsageFlagX) ? 1 : 0;    case BsnesRegister::eflag:      return (SNES::cpu.usage[SNES::cpu.regs.pc] & SNES::CPUDebugger::UsageFlagE) ? 1 : 0;    }  }  void get_cpu_regs(BsnesRegisters& _return) override {    _return.pc = SNES::cpu.getRegister(SNES::CPUDebugger::Register::RegisterPC);    _return.a = SNES::cpu.getRegister(SNES::CPUDebugger::Register::RegisterA);    _return.x = SNES::cpu.getRegister(SNES::CPUDebugger::Register::RegisterX);    _return.y = SNES::cpu.getRegister(SNES::CPUDebugger::Register::RegisterY);    _return.s = SNES::cpu.getRegister(SNES::CPUDebugger::Register::RegisterS);    _return.d = SNES::cpu.getRegister(SNES::CPUDebugger::Register::RegisterD);    _return.db = SNES::cpu.getRegister(SNES::CPUDebugger::Register::RegisterDB);    _return.p = SNES::cpu.getRegister(SNES::CPUDebugger::Register::RegisterP);    _return.mflag = (SNES::cpu.usage[SNES::cpu.regs.pc] & SNES::CPUDebugger::UsageFlagM) ? 1 : 0;    _return.xflag = (SNES::cpu.usage[SNES::cpu.regs.pc] & SNES::CPUDebugger::UsageFlagX) ? 1 : 0;    _return.eflag = (SNES::cpu.usage[SNES::cpu.regs.pc] & SNES::CPUDebugger::UsageFlagE) ? 1 : 0;  }  void set_cpu_reg(const BsnesRegister::type reg, const int32_t value) override {    switch (reg) {    case BsnesRegister::pc:    case BsnesRegister::a:    case BsnesRegister::x:    case BsnesRegister::y:    case BsnesRegister::s:    case BsnesRegister::d:    case BsnesRegister::db:    case BsnesRegister::p:      SNES::cpu.setRegister((SNES::CPUDebugger::Register)reg, value);    }  }  void add_breakpoint(const DbgBreakpoint& bpt) override {    SNES::Debugger::Breakpoint add;    add.addr = bpt.bstart;    add.addr_end = bpt.bend;    add.mode = bpt.type;    add.source = (SNES::Debugger::Breakpoint::Source)bpt.src;    SNES::debugger.breakpoint.append(add);  }  void del_breakpoint(const DbgBreakpoint& bpt) override {    for (auto i = 0; i < SNES::debugger.breakpoint.size(); ++i) {      auto b = SNES::debugger.breakpoint[i];      if (b.source == (SNES::Debugger::Breakpoint::Source)bpt.src && b.addr == bpt.bstart && b.addr_end == bpt.bend && b.mode == bpt.type) {        SNES::debugger.breakpoint.remove(i);        break;      }    }  }  void read_memory(std::string& _return, const DbgMemorySource::type src, const int32_t address, const int32_t size) override {    _return.clear();    SNES::debugger.bus_access = true;    for (auto i = 0; i < size; ++i) {      _return += SNES::debugger.read((SNES::Debugger::MemorySource)src, address + i);    }    SNES::debugger.bus_access = false;  }  void write_memory(const DbgMemorySource::type src, const int32_t address, const std::string& data) override {    SNES::debugger.bus_access = true;    for (auto i = 0; i < data.size(); ++i) {      SNES::debugger.write((SNES::Debugger::MemorySource)src, address, data[i]);    }    SNES::debugger.bus_access = false;  }  void exit_emulation() override {    try {      if (client) {        send_visited(false);        client->stop_event();      }    }    catch (...) {    }    application.app->exit();  }  void pause() override {    step_into();  }  void resume() override {    toggle_pause(false);  }  void start_emulation() override {    init_ida_client();    try {      if (client) {        client->start_event();        visited.clear();        client->pause_event(SNES::cpu.getRegister(SNES::CPUDebugger::RegisterPC));      }    }    catch (...) {    }  }  void step_into() override {    SNES::debugger.step_type = SNES::Debugger::StepType::StepInto;    application.debugrun = true;    SNES::debugger.step_cpu = true;  }  void step_over() override {    SNES::debugger.step_type = SNES::Debugger::StepType::StepOver;    SNES::debugger.step_over_new = true;    SNES::debugger.call_count = 0;    application.debugrun = true;    SNES::debugger.step_cpu = true;  }};static void stop_server() {  srv->stop();}void init_dbg_server() {  ::std::shared_ptr<BsnesDebuggerHandler> handler(new BsnesDebuggerHandler());  ::std::shared_ptr<TProcessor> processor(new BsnesDebuggerProcessor(handler));  ::std::shared_ptr<TNonblockingServerTransport> serverTransport(new TNonblockingServerSocket(9090));  ::std::shared_ptr<TFramedTransportFactory> transportFactory(new TFramedTransportFactory());  ::std::shared_ptr<TProtocolFactory> protocolFactory(new TBinaryProtocolFactory());  srv = ::std::shared_ptr<TNonblockingServer>(new TNonblockingServer(processor, protocolFactory, serverTransport));  ::std::shared_ptr<ThreadFactory> tf(new ThreadFactory());  ::std::shared_ptr<Thread> thread = tf->newThread(srv);  thread->start();  atexit(stop_server);  SNES::debugger.breakpoint.reset();  SNES::debugger.step_type = SNES::Debugger::StepType::StepInto;  application.debugrun = true;  SNES::debugger.step_cpu = true;}void send_pause_event(bool is_step) {  try {    if (client) {      client->pause_event(SNES::cpu.getRegister(SNES::CPUDebugger::RegisterPC));      send_visited(is_step);    }  }  catch (...) {  }}

Фактически, я взял код, который уже был во встроенном отладчике, и скопировал его в реализацию каждого из требуемых методов интерфейса BsnesDebugger.


Часть объектов и методов я не делал статичными, т.к. к ним нам нужно будет обращаться из других участков кода эмулятора. Эти методы и объекты представлены в следующем списке:


  • ::std::set<int32_t> visited; сюда мы будем добавлять код, который выполнялся во время эмуляции, и который мы будем отправлять в Иду
  • void init_dbg_server() будем запускать RPC-сервер не при запуске эмулятора, а при запуске эмуляции выбранного рома
  • void send_pause_event(bool is_step) данный метод я использую не только для уведомления Иды о том, что эмуляция приостановлена, но и для отправки перед этим карты кода (codemap). Подробнее про параметр bool is_step и codemap я расскажу чуть позже

Теперь остаётся найти, где же эмулятору стоит сообщать о паузе, где начинается эмуляция, и где заполняется карта кода. Вот эти места:


Выполнение одной инструкции:


alwaysinline uint8_t CPUDebugger::op_readpc() {  extern std::set<int32_t> visited; // я решил не использовать отдельный header  visited.insert(regs.pc); // вставляем в карту кода текущее значение регистра PC  usage[regs.pc] |= UsageExec;  int offset = cartridge.rom_offset(regs.pc);  if (offset >= 0) cart_usage[offset] |= UsageExec;  // execute code without setting read flag  return CPU::op_read((regs.pc.b << 16) + regs.pc.w++);}

Открытие SNES рома:



Пошаговое исполнение:



Реакция на срабатывание брейкпоинта:



Хитрости применения codemap в Иде


Осталось рассказать о хитростях работы с функциями анализатора в IDA, и затем со спокойной (но переживающей "сомпилируется ли") душой нажать на Build Solution.


Оказалось, что просто так взять и в цикле выполнять функции, которые меняют IDB (файлы проектов в IDA) во время отладки нельзя будет вылетать через раз, и доводить своим непостоянством до сумасшествия. Нужно делать по-умному, например, вот так:


Как правильно менять IDB во время отладки
static struct apply_codemap_req : public exec_request_t {private:  const std::set<int32_t>& _changed;  const bool _is_step;public:  apply_codemap_req(const std::set<int32_t>& changed, bool is_step) : _changed(changed), _is_step(is_step) {};  int idaapi execute(void) override {    auto m = _changed.size();    if (!_is_step) {      show_wait_box("Applying codemap: %d/%d...", 1, m);    }    auto x = 0;    for (auto i = _changed.cbegin(); i != _changed.cend(); ++i) {      if (!_is_step && user_cancelled()) {        break;      }      if (!_is_step) {        replace_wait_box("Applying codemap: %d/%d...", x, m);      }      ea_t addr = (ea_t)(*i | 0x800000);      auto_make_code(addr);      plan_ea(addr);      show_addr(addr);      x++;    }    if (!_is_step) {      hide_wait_box();    }    return 0;  }};static void apply_codemap(const std::set<int32_t>& changed, bool is_step){  if (changed.empty()) return;  apply_codemap_req req(changed, is_step);  execute_sync(req, MFF_FAST);}

Если вкратце, то суть в использовании метода execute_sync() и реализации своего варианта структуры exec_request_t и её колбэка int idaapi execute(void). Это рекомендованный разработчиками способ.


Выводы и компиляция


Фактически, мы закончили писать свой собственный плагин-отладчик для IDA. Мне показалось, что как раз для реализации общения между Идой и эмулятором и создания отладчика Thrift подошёл как нельзя кстати. С минимальными усилиями мне удалось написать и серверную и клиентскую часть для обеих сущностей, не городя велосипеды в виде открытия сокетов по разному для разных платформ, и изобретения RPC реализации с нуля.


К тому же, получившийся протокол легко масштабируется под другие методы и структуры и легко переносим.


Всем спасибо!




Подробнее..

Категории

Последние комментарии

  • Имя: Макс
    24.08.2022 | 11:28
    Я разраб в IT компании, работаю на арбитражную команду. Мы работаем с приламы и сайтами, при работе замечаются постоянные баны и лаги. Пацаны посоветовали сервис по анализу исходного кода,https://app Подробнее..
  • Имя: 9055410337
    20.08.2022 | 17:41
    поможем пишите в телеграм Подробнее..
  • Имя: sabbat
    17.08.2022 | 20:42
    Охренеть.. это просто шикарная статья, феноменально круто. Большое спасибо за разбор! Надеюсь как-нибудь с тобой связаться для обсуждений чего-либо) Подробнее..
  • Имя: Мария
    09.08.2022 | 14:44
    Добрый день. Если обладаете такой информацией, то подскажите, пожалуйста, где можно найти много-много материала по Yggdrasil и его уязвимостях для написания диплома? Благодарю. Подробнее..
© 2006-2024, personeltest.ru