Как подружить .NET и IDA Pro (о дружбе C# и C++)
Приветствую,
Сначала о проблеме/задаче:
Начав писать очередной плагин-отладчик для ретро-платформы под IDA Pro (по секрету: это будет SNES), я столкнулся со следующим набором, который требовалось подружить:
- Ядро эмулятора написано на C++ и компилируется в DLL
- GUI эмулятора написано на C# и использует DLL-ку ядра для управления эмуляцией
- IDA Pro, которая использует плагины либо на питоне, либо на C++ в виде DLL (а отладчики только на C++)
Мне нужно было как-то обращаться из IDA к ядру эмуляции (пошаговая отладка, регистры, память и т.п.) так, чтобы и IDA и графический интерфейс имели одно общее состояние ядра.
Вариантов решения проблемы на старте виделось несколько (не все из них правильные), но о каждом по порядку:
1) Сделать LoadLibrary("emu_core.dll")
, затем GetProcAddress(core, "Pause")
и pause()
, чтобы, например, сделать в эмуляторе паузу.
Я тогда думал: вот оно — идеальное решение с наименьшим количеством изменений в код (и от ядра, и от GUI исходники у меня имеются)! Но, проблемой здесь стало то, что состояние ядра, опть же, хранится в DLL-ке, которая, в свою очередь, загружена в процесс UI, и только там и происходят изменения.
В итоге, загрузив DLL-ку в плагине для IDA, я получил непроинициализированное состояние ядра. Нужно было придумывать другое решение.
2) Сделать что-то типа хост-процесса, который бы и работал с единственным экземпляром ядра, а все остальные — Ида и GUI, обращались бы к этому процессу через RPC-протокол.
Идея вполне здравая, только имелось несколько препятствий в её использовании:
- IDA должна реагировать на события эмуляции (брейкоинты, пауза, reset), т.е. хостовое приложение должно быть активным, а отладчик должен всё время ожидать сообщения от ядра, через хост, себе.
- Колбэки. На самом деле, это уже заложенный в ядре эмулятора функционал, позволяющий решить первый пункт, но, как решать его через RPC я не знал.
Решение
Тогда мне посоветовали взглянуть в сторону COM. Ведь у него, действительно, уже имеется своя замечательная реализация RPC с колбэками и интерфейсами. Которую, к тому же, можно использовать через WinAPI (т.е. C/C++) и нативно — в C#.
А т.к. GUI эмулятора был на шарпе, дело было за малым. Оставалось только воспользоваться его возможностями по работе с COM-интерфейсами.
Схема получается следующей:
1) Классы и типы данных, которые могут быть использованы через COM, помечаем как [ComVisible(true)]
. К счастью, если вдруг какой-то тип вы всё же забудете пометить как видимый, используемые далее инструменты вам об этом скажут (иногда явно, иногда нет, но скажут)
2) Создаю интерфейс, в котором будут описаны все методы, необходимые внешнему пользователю (отладчику в IDA). Помечаю его ещё двумя тегами (к уже имеющемуся ComVisible
):
[Guid("7710855F-D37B-464B-B645-972BAB2706D4")] // Visual Studio умеет генерировать новые GUID-ы из коробки
[InterfaceType(ComInterfaceType.InterfaceIsIUnknown)]
[ComVisible(true)]
public interface IDebugCOM
3) Реализую этот интерфейс, в виде класса DebugCOM
:
[Guid("6AF9F5DD-6626-4E35-BEF4-29A73E18A739")]
[ComVisible(true)]
public class DebugCOM : IDebugCOM
4) Выношу отладочный интерфейс (без реализации) и все его зависимости в отдельный проект (Class Library
), компилирую. Получаю DebugCOM.dll
5) С помощью утилиты tlbexp.exe из поставки Visual Studio (доступна через Developer Command Prompt
) конвертирую DLL в TLB. В этом файле содержится полное описание интерфейсов, используемых типов данных и т.д.
Командная строка:
tlbexp DebugCOM.dll /out:DebugCOM.tlb
Если всё сделано правильно и все типы данных помечены как видимые для COM, то мы получим красивое сообщение:
Assembly exported to 'DebugCOM.tlb'
Иначе, нас будет ждать ад. Ад зависимостей:
TlbExp : warning TX801311B0 : Type library exporter warning processing 'AddressInfo.Type, DebugCOM'. Warning: Non COM visible value type 'SnesMemoryType' is being referenced either from the type currently being exported or from one of its base types.
TlbExp : warning TX801311B0 : Type library exporter warning processing 'InteropBreakpoint.MemoryType, DebugCOM'. Warning: Non COM visible value type 'SnesMemoryType' is being referenced either from the type currently being exported or from one of its base types.
TlbExp : warning TX801311B0 : Type library exporter warning processing 'IDebugCOM.GetMemorySize(type), DebugCOM'. Warning: Non COM visible value type 'SnesMemoryType' is being referenced either from the type currently being exported or from one of its base types.
TlbExp : warning TX801311B0 : Type library exporter warning processing 'IDebugCOM.GetMemoryValue(type), DebugCOM'. Warning: Non COM visible value type 'SnesMemoryType' is being referenced either from the type currently being exported or from one of its base types.
TlbExp : warning TX801311B0 : Type library exporter warning processing 'IDebugCOM.SetMemoryValue(type), DebugCOM'. Warning: Non COM visible value type 'SnesMemoryType' is being referenced either from the type currently being exported or from one of its base types.
TlbExp : warning TX801311B0 : Type library exporter warning processing 'IDebugCOM.SetMemoryValues(type), DebugCOM'. Warning: Non COM visible value type 'SnesMemoryType' is being referenced either from the type currently being exported or from one of its base types.
TlbExp : warning TX801311B0 : Type library exporter warning processing 'IDebugCOM.SetMemoryState(type), DebugCOM'. Warning: Non COM visible value type 'SnesMemoryType' is being referenced either from the type currently being exported or from one of its base types.
TlbExp : warning TX801311B0 : Type library exporter warning processing 'IDebugCOM.GetMemoryState(type), DebugCOM'. Warning: Non COM visible value type 'SnesMemoryType' is being referenced either from the type currently being exported or from one of its base types.
TlbExp : warning TX801311B0 : Type library exporter warning processing 'IDebugCOM.GetMemoryAccessCounts(type), DebugCOM'. Warning: Non COM visible value type 'SnesMemoryType' is being referenced either from the type currently being exported or from one of its base types.
TlbExp : warning TX801311B0 : Type library exporter warning processing 'IDebugCOM.GetMemoryAccessCounts2(type), DebugCOM'. Warning: Non COM visible value type 'SnesMemoryType' is being referenced either from the type currently being exported or from one of its base types.
Видим, что тип данных SnesMemoryType
не помечен как ComVisible(true)
. Исправляем.
6) Получившийся TLB-файл открываем (File->View TypeLib...
) в ComView. Утилита находится здесь:
c:\Windows\Microsoft.NET\Framework\v4.0.30319\ComView.exe
Нам открывается окно просмотра нашего файла в виде уже сконвертированного IDL:
Жмём File->Save as...
и сохраняем как IDL-файл.
7) Сгенерированный на предыдущем шаге IDL-файл добавляем в C++ проект, в котором мы, собственно, и будем обращаться к DebugCOM
:
8) Теперь обращаемся к контекстному меню IDL-файла и жмём Compile Ctrl+F7
. Если всё хорошо, мы получим два файла: DebugCOM_h.h
и DebugCOM_i.c
. Если же нет (у меня именно так и случилось), придётся решать ещё одну проблему.
Дело в том, что в C# порядок объявления типов и мест где они используются, совершенно не важен. Чего не скажешь про C/C++. Здесь пришлось упорядочивать объявления типов для C#.
9) Наконец-то получив долгожданные .h
и .c
файлы, инклудим их в наш код, и обращаемся к нужным методам отладочного интерфейса. Пример кода:
#include
#include
#include "DebugCOM_h.h"
#include "DebugCOM_i.c"
int main(int argc, char* argv[]) {
HRESULT hr;
hr = ::CoInitializeEx(0, COINITBASE_MULTITHREADED);
if (FAILED(hr))
{
std::cout << "CoInitializeEx failure: " << std::hex << std::showbase << hr << std::endl;
return EXIT_FAILURE;
}
CLSID CLSID_server;
hr = ::CLSIDFromString(L"{6AF9F5DD-6626-4E35-BEF4-29A73E18A739}", &CLSID_server);
if (FAILED(hr))
{
std::cout << "CLSIDFromString failure: " << std::hex << std::showbase << hr << std::endl;
return EXIT_FAILURE;
}
CComPtr server;
hr = ::CoCreateInstance(CLSID_server, nullptr, CLSCTX_LOCAL_SERVER, __uuidof(IDebugCOM), (void**)&server);
if (FAILED(hr))
{
std::cout << "CoCreateInstance failure: " << std::hex << std::showbase << hr << std::endl;
return EXIT_FAILURE;
}
DebugState state = {};
hr = server->GetState(&state);
if (FAILED(hr))
{
std::cout << "GetState failure: " << std::hex << std::showbase << hr << std::endl;
return EXIT_FAILURE;
}
std::cout << "PC Address: " << std::hex << std::showbase << state.Cpu.PC << std::endl;
::CoUninitialize();
return EXIT_SUCCESS;
return 0;
}
И здесь мы плавно подошли к особенностям использования COM.
Они бывают двух типов:
- Inprocess
- Out-of-process
Первый тип используется, если требуется использовать COM в том же приложении, где уже есть реализация интерфейса. Например, я мог бы управлять отладчиком по COM из GUI, но не из IDA.
Данный тип использует реализацию COM в виде DLL.
Второй тип используется, если нужно иметь доступ к COM-интерфейсу извне приложения. В этом случае регистрируется исполняемый файл программы, который, к тому же, будет являться тем самым хост-приложением. Т.е. оно будет хранить состояние ядра, а COM поможет нам транслировать запросы.
Осталось найти описание того, как правильно регистрировать out-of-process COM. Оказалось, на GitHub есть самый что ни на есть «из первых рук» пример: https://github.com/dotnet/samples/tree/master/core/extensions/OutOfProcCOM. Он легко адаптируется и используется под нужды ваших проектов.
Выводы
В итоге, мне удалось подружить IDA Pro и .NET Framework приложение средствами Windows, не городя велосипеды из собственных RPC-протоколов. К тому же, я решил проблему наличия всего одной копии ядра эмулятора и шаринга его состояния между несколькими приложениями.