EternalWindows
デバッグ API / メソッド引数とスタックフレーム

多くのデバッガは、ブレークポイントがヒットした場合や例外が発生した場合に、 そのときのパラメータなどをGUIを表示しています。 ブレークポイントがヒットした場合は、ICorDebugManagedCallback::Breakpointが呼ばれます。

STDMETHODIMP CManagedCallback::Breakpoint(ICorDebugAppDomain *pAppDomain, ICorDebugThread *pThread, ICorDebugBreakpoint *pBreakpoint)
{
	ICorDebugFunction *pFunction;
	ICorDebugFrame    *pFrame;
	WCHAR             szName[256], szBuf[512];

	EnumValueAndFrame(pThread);
	
	pThread->GetActiveFrame(&pFrame);
	pFrame->GetFunction(&pFunction);

	GetFullMethodName(pFunction, szName);
	wsprintfW(szBuf, L"Breakpoint : %s", szName);
	ShowString(szBuf);
	
	pFunction->Release();
	pFrame->Release();

	return S_OK;
}

EnumValueAndFrameという独自メソッドは、メソッドの引数やスタックフレームをGUIに表示します。 その後の処理は、Breakpointが呼ばれたことを示す文字列をGUIに表示しようとしています。 この際には、どのメソッドでブレークポイントにヒットしたかを表示したいため、 ICorDebugFrame::GetFunctionでICorDebugFunctionを取得し、 GetFullMethodNameでメソッドの名前を取得しています。

例外が発生した場合は、ICorDebugManagedCallback::Exceptionが呼ばれます

STDMETHODIMP CManagedCallback::Exception(ICorDebugAppDomain *pAppDomain, ICorDebugThread *pThread, BOOL unhandled)
{
	ICorDebugValue       *pValue;
	ICorDebugObjectValue *pObjectValue;
	ICorDebugClass       *pClass;
	WCHAR                szError[256], szBuf[512];

	EnumValueAndFrame(pThread);

	pThread->GetCurrentException(&pValue);
	ObjectDereference(&pValue);
	pValue->QueryInterface(IID_PPV_ARGS(&pObjectValue));
	
	pObjectValue->GetClass(&pClass);
	GetClassName(pClass, szError);

	wsprintfW(szBuf, L"Exception : %s %s", szError, unhandled ? L"例外未処理" : L"例外処理");
	ShowString(szBuf);

	pClass->Release();
	pObjectValue->Release();
	pValue->Release();

	return S_OK;
}

例外の発生時においても、そのときの引数やスタックフレームをGUIに表示したいため、EnumValueAndFrameを呼び出しています。 スローされた例外はICorDebugThread::GetCurrentExceptionで取得でき、 それはICorDebugValueとして返されます。 ICorDebugValueは、何らかの1つの値を識別するためのインターフェースですが、 これでは例外の内容を示す文字列を取得できないため、 別途ICorDebugObjectValueを照会しています。 この直前に呼び出しているObjectDereferenceについては後述します。 ICorDebugObjectValue::GetClassを呼び出せば、値の型を示すICorDebugClassを取得でき、 GetClassNameという自作メソッドを通じて型の名前を取得できます。 この名前というのは本来ならばcharやintのようになりますが、 例外の値については例外の内容を示す文字列になります。 後はこれをShowStringでGUIに表示します。

EnumValueAndFrameの実装は次のようになっています。

void CManagedCallback::EnumValueAndFrame(ICorDebugThread *pThread)
{
	EnumArguments(pThread);

	EnumFrame(pThread);

	DebugStop();
}

EnumArgumentsは、現在のメソッドの引数の情報をGUIに表示するメソッドです。 一方、EnumFrameは現在のスタックフレームをGUIに表示するメソッドです。 DebugStopは、コードの進行が現在停止していることをGUIスレッドに伝えるための関数です。 前節で述べたように、ExceptionとBreakpointはICorDebugAppDomain::Continueを呼び出していませんでしたから、 コードの進行は停止することになります。 これを再開させる際には、GUIスレッドがICorDebugProcess::Continueを呼び出すことになります。 EnumArgumentsの実装は次のようになっています。

void CManagedCallback::EnumArguments(ICorDebugThread *pThread)
{
	ICorDebugFrame     *pFrame;
	ICorDebugILFrame   *pILFrame;
	ICorDebugValueEnum *pValueEnum;
	ICorDebugValue     *pValue;
	ICorDebugModule    *pModule;
	ICorDebugFunction  *pFunction;
	IMetaDataImport    *pMetaDataImport;
	mdMethodDef        methodDef;
	HCORENUM           hCorEnum;
	ULONG              uCount;
	mdParamDef         paramDefs[2048];
	WCHAR              szName[256], szType[256], szValue[256];
	int                i;

	pThread->GetActiveFrame(&pFrame);
	pFrame->QueryInterface(IID_PPV_ARGS(&pILFrame));
	pILFrame->EnumerateArguments(&pValueEnum);

	pFrame->GetFunction(&pFunction);
	pFunction->GetToken(&methodDef);
	pFunction->GetModule(&pModule);
	pModule->GetMetaDataInterface(IID_IMetaDataImport, (IUnknown **)&pMetaDataImport);

	hCorEnum = NULL;
	pMetaDataImport->EnumParams(&hCorEnum, methodDef, paramDefs, sizeof(paramDefs), &uCount);
	
	i = 0;
	while (pValueEnum->Next(1, &pValue, NULL) == S_OK) {
		pMetaDataImport->GetParamProps(paramDefs[i], NULL, NULL, szName, 256, NULL, NULL, NULL, NULL, NULL);
		GetValue(pValue, szValue);
		GetElementTypeName(pValue, szType);
		SetValueItem(szName, szValue, szType, i);
		pValue->Release();
		i++;
	}
	
	pMetaDataImport->CloseEnum(hCorEnum);
	pMetaDataImport->Release();
	pModule->Release();
	pFunction->Release();
	pValueEnum->Release();
	pILFrame->Release();
	pFrame->Release();
}

メソッドの引数は、現在のスタックフレームに格納されています。 このスタックフレームはICorDebugThread::GetActiveFrameで取得可能であり、 QueryInterfaceを通じてICorDebugILFrameを照会するようにします。 そうすれば、ICorDebugILFrame::EnumerateArgumentsで引数の値を列挙できます。 引数の情報をGUIへ表示するにあたり、引数の名前も表示すれば分かりやすいため、 メタデータのParamDefトークンを取得するようにしています。 具体的には、ICorDebugModule::GetMetaDataInterfaceでIMetaDataImportを取得し、 IMetaDataImport::EnumParamsに現在のメソッドのMethodDefトークンを指定することで、 ParamDefトークンの配列を取得できます。 実際に引数の列挙を開始するには、ICorDebugValueEnum::Nextを呼び出します。 第2引数に返されるICorDebugValueは1つの値(この場合引数)を表しており、 GetElementTypeNameに指定することで引数の型が分かり、 GetValueに指定することで引数の値が分かります。 また、引数の名前はIMetaDataImport->GetParamPropsで取得可能です。 取得した名前、値、型は、SetValueItemを通じてGUIに表示されます。

EnumFrameの実装は次のようになっています。

void CManagedCallback::EnumFrame(ICorDebugThread *pThread)
{
	ICorDebugChain     *pChain;
	ICorDebugFrame     *pFrame;
	ICorDebugFrameEnum *pFrameEnum;
	ICorDebugFunction  *pFunction;
	WCHAR              szModule[256], szName[256], szBuf[512];
	ULONG32            uLine;
	int                i;

	pThread->GetActiveChain(&pChain);
	pChain->EnumerateFrames(&pFrameEnum);

	i = 0;
	while (pFrameEnum->Next(1, &pFrame, NULL) == S_OK) {
		pFrame->GetFunction(&pFunction);
		GetFullMethodName(pFunction, szName);
		GetModuleNameFromFunction(pFunction, szModule);

		uLine = GetSourceLine(pThread);
		if (i == 0 && uLine > 0)
			wsprintfW(szBuf, L"%s!%s %d行", szModule, szName, uLine);
		else
			wsprintfW(szBuf, L"%s!%s", szModule, szName);

		SetFrameItem(szBuf, i);
	
		pFunction->Release();
		pFrame->Release();
		i++;
	}

	pFrameEnum->Release();
	pChain->Release();
}

一連のスタックフレームはスタックチェーンと呼ばれ、ICorDebugThread::GetActiveChainで取得できます。 これのEnumerateFramesを呼び出せば、スタックフレームを列挙するためのICorDebugFrameEnumを取得でき、 ICorDebugFrameEnum::NextによってICorDebugFrameを取得できます。 ループ内ではICorDebugFrame::GetFunctionでICorDebugFunctionを取得し、 これを基にメソッド名とモジュール名を取得しています。 これらを1つの文字列として整形したら、SetFrameItemでGUIに表示することになります。 GetSourceLineは、現在の命令ポインタがソース上で何行目に相当するかを調べる関数ですが、 これについては後の節で説明します。

ICorDebugValueから型を取得する処理は、GetElementTypeNameで行われています。

void CManagedCallback::GetElementTypeName(ICorDebugValue *pValue, LPWSTR lpszBuf)
{
	CorElementType type;
	HRESULT        hr;

	pValue->GetType(&type);

	if (type == ELEMENT_TYPE_VALUETYPE || type == ELEMENT_TYPE_CLASS) {
		ICorDebugValue2 *pValue2;
		ICorDebugType   *pType;
		ICorDebugClass  *pClass;
		
		pValue->QueryInterface(IID_PPV_ARGS(&pValue2));
		pValue2->GetExactType(&pType);
		pType->GetClass(&pClass);

		GetClassName(pClass, lpszBuf);

		pClass->Release();
		pType->Release();
		pValue2->Release();
		
		return;
	}
	else if (type == ELEMENT_TYPE_STRING)
		lstrcpyW(lpszBuf, L"string");
	else if (type == ELEMENT_TYPE_SZARRAY) {
		ICorDebugArrayValue *pArrayValue;
		WCHAR               szType[256];

		ObjectDereference(&pValue);
		hr = pValue->QueryInterface(IID_PPV_ARGS(&pArrayValue));
		if (SUCCEEDED(hr)) {
			ICorDebugValue *p;
			hr = pArrayValue->GetElementAtPosition(0, &p);
			if (SUCCEEDED(hr)) {
				GetElementTypeName(p, szType);
				wsprintfW(lpszBuf, L"%s[]", szType);
				pArrayValue->Release();
				p->Release();
			}
			else
				lstrcpyW(lpszBuf, L"?");
		}
		else
			lstrcpyW(lpszBuf, L"?");
		return;
	}
	else
		GetGenericType(type, lpszBuf);
}

ICorDebugValue::GetTypeを呼び出せば、型を表すCorElementType列挙型を取得できます。 これがELEMENT_TYPE_VALUETYPEかELEMENT_TYPE_CLASSである場合は、 型が構造体かクラスであることを意味し、 ICorDebugClassから型の名前を取得できます。 ICorDebugClassを取得するには、ICorDebugValueからICorDebugValue2を照会し、 ICorDebugValue2::GetExactTypeからICorDebugTypeを取得、 そしてICorDebugType::GetClassを呼び出します。 typeがELEMENT_TYPE_STRINGである場合はstring型であるため、 この場合は単純にstringという文字列を指定しています。 typeがELEMENT_TYPE_SZARRAYである場合は一次元配列であることを意味し、 この場合はICorDebugArrayValueを照会できるはずです。 ICorDebugArrayValue::GetElementAtPositionを呼び出せば、第1引数のインデックスで識別できる要素を取得できるため、 これをGetElementTypeName(再帰呼び出し)に指定することで、配列が何型であるかを特定しています。 else文で呼び出しているGetGenericTypeは、charやintなどの単純型を処理します。

void CManagedCallback::GetGenericType(CorElementType type, LPWSTR lpszBuf)
{
	WCHAR szType[256];

	if (type == ELEMENT_TYPE_BOOLEAN)
		lstrcpyW(szType, L"bool");
	else if (type == ELEMENT_TYPE_CHAR)
		lstrcpyW(szType, L"char");
	else if (type == ELEMENT_TYPE_I1)
		lstrcpyW(szType, L"sbyte");
	else if (type == ELEMENT_TYPE_U1)
		lstrcpyW(szType, L"byte");
	else if (type == ELEMENT_TYPE_I2)
		lstrcpyW(szType, L"short");
	else if (type == ELEMENT_TYPE_U2)
		lstrcpyW(szType, L"ushort");
	else if (type == ELEMENT_TYPE_I4)
		lstrcpyW(szType, L"int");
	else if (type == ELEMENT_TYPE_U4)
		lstrcpyW(szType, L"uint");
	else if (type == ELEMENT_TYPE_I8)
		lstrcpyW(szType, L"long");
	else if (type == ELEMENT_TYPE_U8)
		lstrcpyW(szType, L"ulong");
	else if (type == ELEMENT_TYPE_R4)
		lstrcpyW(szType, L"float");
	else if (type == ELEMENT_TYPE_R8)
		lstrcpyW(szType, L"double");
	else
		wsprintfW(szType, L"unknown", type);

	lstrcpyW(lpszBuf, szType);
}

このメソッドでは、CorElementTypeに関連する文字列をlpszBufに格納するだけです。

.NETの型には値型と参照型がありますが、参照型を表すICorDebugValueはICorDebugReferenceValueを実装しています。 ICorDebugReferenceValue::Dereferenceを呼び出せば、参照先の値を示すICorDebugValueを取得できます。

BOOL CManagedCallback::ObjectDereference(ICorDebugValue **ppDebugValue)
{
	ICorDebugValue          *pDebugValueOld = *ppDebugValue;
	ICorDebugValue          *pDebugValueNew;
	ICorDebugReferenceValue *pDebugReferenceValue;
	HRESULT                 hr;
	BOOL                    bNull;

	hr = pDebugValueOld->QueryInterface(IID_PPV_ARGS(&pDebugReferenceValue));
	if (FAILED(hr)) {
		pDebugReferenceValue->Release();
		return FALSE;
	}
	
	hr = pDebugReferenceValue->IsNull(&bNull);
	if (FAILED(hr) || bNull) {
		pDebugReferenceValue->Release();
		return FALSE;
	}

	pDebugValueOld->Release();
	pDebugReferenceValue->Dereference(&pDebugValueNew);
	*ppDebugValue = pDebugValueNew;

	return TRUE;
}

ICorDebugReferenceValue::Dereferenceを呼び出す前に、参照先がNULLであるかをICorDebugReferenceValue::IsNullで確認します。 NULLでない場合はICorDebugReferenceValue::Dereferenceを呼び出して、参照先の値を取得します。

メソッドの引数を列挙するEnumArgumentsでは、引数の型を取得するGetElementTypeNameだけでなく、 引数の値を取得するGetValueも呼び出していました。 このメソッドの実装は次のようになっています。

void CManagedCallback::GetValue(ICorDebugValue *pValue, LPWSTR lpszBuf)
{
	CorElementType type;
	HRESULT        hr;

	pValue->GetType(&type);
	
	if (type == ELEMENT_TYPE_VALUETYPE) {
		ICorDebugObjectValue *pObjectValue;

		hr = pValue->QueryInterface(IID_PPV_ARGS(&pObjectValue));
		if (SUCCEEDED(hr)) {
			lstrcpyW(lpszBuf, L"[struct]");
			pObjectValue->Release();
		}
		else
			lstrcpyW(lpszBuf, L"?");
	}
	else if (type == ELEMENT_TYPE_CLASS) {
		ICorDebugObjectValue *pObjectValue;

		ObjectDereference(&pValue);
		
		hr = pValue->QueryInterface(IID_PPV_ARGS(&pObjectValue));
		if (SUCCEEDED(hr)) {
			lstrcpyW(lpszBuf, L"[class]");
			pObjectValue->Release();
		}
		else {
			ICorDebugBoxValue    *pBoxValue;
			ICorDebugObjectValue *p;
			
			hr = pValue->QueryInterface(IID_PPV_ARGS(&pBoxValue));
			if (SUCCEEDED(hr)) {
				pBoxValue->GetObject(&p);
				GetValue(p, lpszBuf);
				p->Release();
			}
			else
				lstrcpyW(lpszBuf, L"null");
		}
	}
	else if (type == ELEMENT_TYPE_STRING) {
		ICorDebugStringValue *pStringValue;
		ULONG32              uSize;

		ObjectDereference(&pValue);

		hr = pValue->QueryInterface(IID_PPV_ARGS(&pStringValue));
		if (SUCCEEDED(hr)) {
			pStringValue->GetString(256, &uSize, lpszBuf);
			pStringValue->Release();
		}
		else
			lstrcpyW(lpszBuf, L"null");
	}
	else if (type == ELEMENT_TYPE_SZARRAY) {
		ICorDebugArrayValue *pArrayValue;

		ObjectDereference(&pValue);

		hr = pValue->QueryInterface(IID_PPV_ARGS(&pArrayValue));
		if (SUCCEEDED(hr)) {
			lstrcpyW(lpszBuf, L"[array]");
			pArrayValue->Release();
		}
		else
			lstrcpyW(lpszBuf, L"?");
	}
	else {
		ICorDebugGenericValue *pGenericValue;

		hr = pValue->QueryInterface(IID_PPV_ARGS(&pGenericValue));
		if (SUCCEEDED(hr)) {
			BYTE   byte[100];
			LPBYTE lpValue = byte;
			
			pGenericValue->GetValue(lpValue);
			GetGenericValue(lpValue, type, lpszBuf);

			pGenericValue->Release();
		}
		else
			lstrcpyW(lpszBuf, L"?");
	}
}

まず、ICorDebugValue::GetTypeで型を取得し、その型に応じて使用するインターフェースを使い分けます。 たとえば、構造体やクラスではICorDebugObjectValueを使用しますが、 文字列の場合はICorDebugStringValueを使用します。 ただし、pValueから直接それらを照会していいわけではなく、 クラスや文字列、配列といった参照型の場合はObjectDereferenceを事前に呼び出しておきます。 値を表示するにあたってint型やstring型は簡単ですが、 クラスや配列では複数のフィールドや要素が必要になるため、上記では止む無く[class]や[array]というように記述しています。 .NETの型にはobjectという型もありますが、この場合にELEMENT_TYPE_OBJECTが指定されることはありません。 typeの値はELEMENT_TYPE_CLASSであり、ICorDebugBoxValue::GetObjectで値を取得することになります。 この値のtypeは、ELEMENT_TYPE_VALUETYPEになっていると思われます。 else文で使用しているICorDebugGenericValueは、charやintなどの単純型の値を取得する場合に使用します。 ICorDebugGenericValue::GetValueを呼び出せば、指定したバッファに値が格納されるため、 これをGetGenericValueで処理します。

void CManagedCallback::GetGenericValue(LPVOID lpValue, CorElementType type, LPWSTR lpszBuf)
{
	if (type == ELEMENT_TYPE_BOOLEAN) {
		if (*(bool *)lpValue == true)
			lstrcpyW(lpszBuf, L"true");
		else
			lstrcpyW(lpszBuf, L"false");
	}
	else if (type == ELEMENT_TYPE_CHAR)
		wsprintfW(lpszBuf, L"%c", *(char *)lpValue);	
	else if (type == ELEMENT_TYPE_I1)
		wsprintfW(lpszBuf, L"%d", *(char *)lpValue);
	else if (type == ELEMENT_TYPE_U1)
		wsprintfW(lpszBuf, L"%u", *(unsigned char *)lpValue);
	else if (type == ELEMENT_TYPE_I2)
		wsprintfW(lpszBuf, L"%d", *(short *)lpValue);
	else if (type == ELEMENT_TYPE_U2)
		wsprintfW(lpszBuf, L"%u", *(unsigned short *)lpValue);
	else if (type == ELEMENT_TYPE_I4)
		wsprintfW(lpszBuf, L"%d", *(int *)lpValue);
	else if (type == ELEMENT_TYPE_U4)
		wsprintfW(lpszBuf, L"%u", *(unsigned int *)lpValue);
	else if (type == ELEMENT_TYPE_I8)
		wsprintfW(lpszBuf, L"%d", *(LONGLONG *)lpValue);
	else if (type == ELEMENT_TYPE_U8)
		wsprintfW(lpszBuf, L"%u", *(ULONGLONG *)lpValue);
	else if (type == ELEMENT_TYPE_R4)
		lstrcpyW(lpszBuf, L"[float]");
	else if (type == ELEMENT_TYPE_R8)
		lstrcpyW(lpszBuf, L"[double]");
	else
		lstrcpyW(lpszBuf, L"?");
}

基本的には、typeに関連する型で値をキャストするだけです。 wsprintfでは小数点を扱えないため、floatとdoubleは値を参照していません。


戻る