EternalWindows
デバッグ API / デバッグ時の通知

マネージアプリケーションのデバッグを行いたい場合、アンマネージAPIの1つであるデバッグ APIは非常に役に立ちます。 マネージアプリケーションには、クラスやメソッドのデータがメタデータに記録されているため、 デバッグAPIを使用すれば、そうした名前を基にブレークポイントを設定する事などができるからです。 次に、今回作成するサンプルの画面を示します。

メニューの「デバッグ」から「デバッグ開始」を選択することで、デバッグ対象のプロセスを起動します。 プロセス内で起こった動作はデバッガに通知されることになっており、 その通知の内容をリストボックスに表示しています。 ブレークポイントにヒットした場合や例外が発生した場合はコードの進行が停止することになり、 そのときの情報が下のリストビューに表示されます。 左のリストビューには現在呼ばれているメソッドの引数の中身が表示され、 右のリストビューにはスタックフレームが列挙されます。 スタックフレームとは簡単に言えば、関数の引数やローカル変数、リターンアドレスなどをまとめたものです。 最上位に表示されているスタックフレームが現在アクティブなフレームであり、 現在そのメソッドが呼ばれていることを意味します。 下にあるメソッドは上のメソッドを呼び出していることを意味します。

デバッグ APIとプロファイルAPIは、どちらも特定のアプリケーションの情報を取得するためのものですが、 具体的にはどのように使い分ければよのでしょうか。 デバッグ APIは、その名前の通りアプリケーションをデバッグするためのものですから、 特定のメソッドにブレークポイントを設定し、その時の引数やローカル変数を確認するというのが主な目的になるでしょう。 デバッグAPIでメソッド呼び出しを検出する場合は、 ブレークポイントがヒットするか例外が発生するかしないため、 全てのメソッド呼び出しを検出したい場合はプロファイルAPIが向いているといえます。 また、メモリの使用量やパフォーマンスを測定したい場合も、プロファイルAPIを使用するでしょう。 デバッグ APIを使用する場合は単純に1つのアプリケーションを開発するだけで済みますが、 プロファイルAPIを使用する場合はプロファイラ(DLL)とクライアントの2つを開発することにります。

デバッグAPIの使用は、ICorDebugを取得するところから始まります。 これには、CreateDebuggingInterfaceFromVersionを呼び出します。

CreateDebuggingInterfaceFromVersion(CorDebugVersion_2_0, L"v2.0.50727", &pUnknown);
pUnknown->QueryInterface(IID_PPV_ARGS(&pDebug));
pUnknown->Release();

pDebug->Initialize();

pManagedCallback = new CManagedCallback();
pDebug->SetManagedHandler(pManagedCallback);

.NET 2.0のプロセスを対象としたい場合は、CreateDebuggingInterfaceFromVersionの引数を上記のようにします。 逆に.NET 4のプロセスを対象にする場合は、第1引数に4を指定し、第2引数にv4.0.30319を指定するでしょう。 返されるインターフェースの型はIUnknownであるため、 QueryInterfaceでICorDebugを照会するようにします。 ICorDebug::Initializeでデバッグサービスを初期化したら、 ICorDebug::SetManagedHandlerでイベントハンドラオブジェクトを設定します。 このオブジェクトの型は、次のようにICorDebugManagedCallbackとICorDebugManagedCallback2を継承していなければなりません。

class CManagedCallback : public ICorDebugManagedCallback, public ICorDebugManagedCallback2
{
public:
	STDMETHODIMP QueryInterface(REFIID riid, void **ppvObject);
	STDMETHODIMP_(ULONG) AddRef();
	STDMETHODIMP_(ULONG) Release();
	
	STDMETHODIMP Breakpoint(ICorDebugAppDomain *pAppDomain, ICorDebugThread *pThread, ICorDebugBreakpoint *pBreakpoint);
	STDMETHODIMP StepComplete(ICorDebugAppDomain *pAppDomain, ICorDebugThread *pThread, ICorDebugStepper *pStepper, CorDebugStepReason reason);
	・
	・
	・	
};

デバッグサービスは、デバッグ対象のプロセスで何らかのイベントが発生した場合に、ICorDebugManagedCallback(2)のメソッドを呼び出します。 たとえば、デバッグ対象のプロセスがブレークポイントにヒットした場合は、Breakpointが呼ばれます。

実際にデバッグを開始するには、デバッグ対象のプロセスを示すICorDebugProcessを取得しなければなりません。 新しく作成したプロセスをデバッグしたい場合は、ICorDebugProcess::CreateProcessを呼び出します。

STARTUPINFOW        startupInfo;
PROCESS_INFORMATION processInformation;
		
ZeroMemory(&startupInfo, sizeof(STARTUPINFO));
startupInfo.cb = sizeof(STARTUPINFO);
pDebug->CreateProcess(NULL, g_szModule, NULL, NULL, FALSE, 0, NULL, NULL, &startupInfo, &processInformation, DEBUG_NO_SPECIAL_OPTIONS, &pProcess);			

第10引数までは、Win32のCreateProces関数と同じ意味になります。 第11引数はデバッグオプションを指定できますが、オプションが不要な場合はDEBUG_NO_SPECIAL_OPTIONSを指定します。 メソッドが成功すると、第12引数にICorDebugProcessが返ります。

ICorDebug::CreateProcessでプロセスの作成に成功すると、ICorDebugManagedCallback::CreateProcessが呼ばれます。

STDMETHODIMP CManagedCallback::CreateProcess(ICorDebugProcess *pProcess)
{
	ShowString(L"CreateProcess");
	
	pProcess->Continue(FALSE);

	return S_OK;
}

ICorDebugManagedCallback::CreateProcessに限ったことではありませんが、 多くのコールバック関数ではICorDebugProcess::Continue(またはICorDebugAppDomain::Continue)を呼び出すことになります。 理由は、このメソッドを呼び出すことによってコードの進行が継続するからです。 ただし、ブレークポイントのヒットを示すICorDebugManagedCallback::Breakpointと、 例外の発生を示すICorDebugManagedCallback::Exceptionでは、コードの進行が停止した方がよいため、 この場合はContinueを呼び出さないことになるでしょう。

ICorDebugManagedCallback::CreateProcessの後には様々なICorDebugManagedCallbackのメソッドが呼ばれますが、 今回はその中のICorDebugManagedCallback::LoadModuleを特別に処理しています。 このメソッドで渡されるICorDebugModuleは、ブレークポイントを設定するために必要です。

STDMETHODIMP CManagedCallback::LoadModule(ICorDebugAppDomain *pAppDomain, ICorDebugModule *pModule)
{
	ULONG32 uSize;
	WCHAR   szName[256], szModule[256], szBuf[512];

	pModule->GetName(256, &uSize, szModule);
	if (lstrcmpW(szModule, g_szModule) == 0)
		SetBreakpoint(pModule, L"Program", L"ShowClass", 0);

	pModule->GetName(256, &uSize, szName);
	wsprintfW(szBuf, L"LoadModule : %s", szName);

	ShowString(szBuf);
	pAppDomain->Continue(FALSE);

	return S_OK;
}

LoadModuleに渡されるモジュールは、デバッグ対象アプリケーションのexe以外に、mscorlib.dllなども含まれます。 ブレークポイントを設定するにあたって、アプリケーションのexeを対象にする必要がありますから、 それを見極めるためにICorDebugModule::GetNameでモジュールのフルパスを取得します。 g_szModuleはexeのフルパスが格納されていますから、これと一致したならばexeのモジュールであることが分かります。 SetBreakpointは、第2引数のクラスに存在する第3引数のメソッドにブレークポイントを設定します。 設定する位置は第4引数によって決定され、これが0である場合はメソッドが呼ばれたと同時にヒットします。 指定するクラス名とメソッド名については、ildasmなどでモジュールのメタデータから調べておくことになるでしょう。 SetBreakpointの実装は次のようになっています。

void CManagedCallback::SetBreakpoint(ICorDebugModule *pModule, LPWSTR lpszClass, LPWSTR lpszMethod, ULONG32 uOffset)
{
	IMetaDataImport             *pMetaDataImport;
	ICorDebugFunction           *pFunction;
	ICorDebugCode               *pCode;
	ICorDebugFunctionBreakpoint *pFunctionBreakpoint;
	HRESULT                     hr;
	mdTypeDef                   typeDef;
	mdMethodDef                 methodDef;

	pModule->GetMetaDataInterface(IID_IMetaDataImport, (IUnknown **)&pMetaDataImport);
	
	hr = pMetaDataImport->FindTypeDefByName(lpszClass, NULL, &typeDef);
	if (FAILED(hr)) {
		pMetaDataImport->Release();
		return;
	}

	hr = pMetaDataImport->FindMethod(typeDef, lpszMethod, NULL, 0, &methodDef);
	if (FAILED(hr)) {
		pMetaDataImport->Release();
		return;
	}
	
	pModule->GetFunctionFromToken(methodDef, &pFunction);
	pFunction->GetILCode(&pCode);
	pCode->CreateBreakpoint(uOffset, &pFunctionBreakpoint);
	pFunctionBreakpoint->Activate(TRUE);

	pFunctionBreakpoint->Release();
	pCode->Release();
	pFunction->Release();
	pMetaDataImport->Release();
}

ブレークポイントを設定するにあたり、その設定対象のメソッドを識別するICorDebugFunctionが必要です。 ICorDebugModule::GetFunctionFromTokenにMethodDefトークンを指定すればこれを取得できるため、 まずはメタデータAPIを使用してMethodDefトークンを取得します。 具体的には、IMetaDataImport::FindTypeDefByNameで型の名前からTypeDefトークンを取得し、 これとメソッド名をIMetaDataImport::FindMethodに指定するようにします。 ICorDebugFunctionを取得したらICorDebugFunction::GetILCodeでICorDebugCodeを取得し、 さらにICorDebugCode::CreateBreakpointにICorDebugFunctionBreakpointを取得します。 後はこれのActivateを呼び出せばブレークポイントが設定され、 ヒット時にはICorDebugManagedCallback::Breakpointが呼ばれるはずです。

ICorPublishについて

今回は新しく作成したプロセスをデバッグする方法について説明しましたが、 既存のプロセスをデバッグしたいことも多いと思われます。 このような場合は、既存のプロセスを列挙するためにICorPublishを使用することになるでしょう。

#include <windows.h>
#include <corpub.h>

#pragma comment (lib, "corguids.lib")

int WINAPI WinMain(HINSTANCE hinst, HINSTANCE hinstPrev, LPSTR lpszCmdLine, int nCmdShow)
{
	HRESULT                hr;
	ICorPublish            *pPublish;
	ICorPublishProcessEnum *pPublishProcessEnum;
	ICorPublishProcess     *pPublishProcess;
	WCHAR                  szName[256];
	ULONG32                uSize;

	CoInitialize(NULL);
	
	hr = CoCreateInstance(CLSID_CorpubPublish, 0, CLSCTX_INPROC_SERVER, IID_ICorPublish, (void **)&pPublish);
	if (FAILED(hr)) {
		CoUninitialize();
		return 0;
	}

	hr = pPublish->EnumProcesses(COR_PUB_MANAGEDONLY, &pPublishProcessEnum);
	if (FAILED(hr)) {
		pPublish->Release();
		CoUninitialize();
		return 0;
	}

	while (pPublishProcessEnum->Next(1, &pPublishProcess, NULL) == S_OK) {
		pPublishProcess->GetDisplayName(256, &uSize, szName);
		MessageBox(NULL, szName, TEXT("OK"), MB_OK);
		pPublishProcess->Release();
	}
	
	pPublishProcessEnum->Release();
	pPublish->Release();
	CoUninitialize();

	return 0;
}

CoCreateInstanceにCLSID_CorpubPublishを指定すれば、ICorPublishを取得できます。 EnumProcessesを呼び出せばプロセスを列挙するためのICorPublishProcessEnumを取得でき、 Nextを呼び出すことでプロセスを表すICorPublishProcessを取得できます。 後はICorPublishProcess::GetDisplayNameでプロセスのフルパスを取得してもよいですし、 ICorPublishProcess::GetProcessIDでプロセスIDを取得してもよいでしょう。 このプロセスIDをICorDebug::DebugActiveProcessに指定すれば、デバッグ対象のプロセスを表すICorDebugProcessを取得できます。 ちなみに、ICorPublishProcessEnum::Nextを列挙するプロセスは、マネージコードを持った(mscorlib.dllがロードされた)プロセスです。 マネージアプリケーションが列挙されるのは当然ですが、 アンマネージアプリケーションでも.NETの機能を使用している場合は列挙の対象になります。



戻る