EternalWindows
デバッグ API / シンボル ストア診断 APIの使用

Visual Studioを使用してアプリケーションの開発を行っていると、拡張子が.pdbであるpdbファイル(シンボルファイル)が作成されることがあります。 このファイルには、アプリケーションの元になったソースコードの情報などが格納されており、 ローカル変数の名前や例外発生時のソース上の行などを取得できます。 もし、デバッグ対象のアプリケーションがpdbファイルを用意している場合は シンボル ストア診断 APIを使用することで、デバッグ時に表示できる情報を増やすことができます。

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) {
		ISymUnmanagedBinder *pSymBinder;
		ULONG32             uOffset = 0;

		CoInitialize(0);

		HRESULT hr = CoCreateInstance(CLSID_CorSymBinder_deprecated, NULL, CLSCTX_INPROC_SERVER, IID_PPV_ARGS(&pSymBinder));
		if (SUCCEEDED(hr)) {
			IUnknown *pUnknown;
			
			pModule->GetMetaDataInterface(IID_IUnknown, &pUnknown);
			hr = pSymBinder->GetReaderForFile(pUnknown, szModule, NULL, &g_pSymReader);
			if (SUCCEEDED(hr)) {
				ShowString(L"pdbファイル参照");
				uOffset = GetOffsetFromSource(L"C:\\sample\\Program.cs", 66, 9);
			}
			pUnknown->Release();
		}
		
		SetBreakpoint(pModule, L"Program", L"ShowClass", uOffset);

		pSymBinder->Release();
	}

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

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

	return S_OK;
}

シンボル ストア診断 APIを使用するには、まずISymUnmanagedBinderを取得しなければなりません。 これは、CoCreateInstanceにCLSID_CorSymBinder_deprecatedを指定することで可能です。 実際にpdbファイルへアクセスするためには、ISymUnmanagedReaderという別のインターフェースが必要であり、 これはISymUnmanagedBinder::GetReaderForFileで取得できます。 第1引数は、アクセスしたいpdbファイルに関連するモジュールのメタデータであり、 ICorDebugModule::GetMetaDataInterfaceで取得できます。 第2引数は、pdbファイルに関連するモジュールのフルパスを指定します。 これにより、モジュールと同じフォルダに存在するpdbファイルが参照されます。 モジュールのファイル名がsample.exeである場合は、pdbファイルの名前がsample.pdbになっている必要があります。 pdbファイルが存在してISymUnmanagedReaderを取得したら、GetOffsetFromSourceというメソッドを呼び出しています。 このメソッドは、指定したソースファイルの指定した行と列からオフセットを取得であり、 これをSetBreakpointに指定することで、任意の位置にブレークポイントを設定できます。 逆に、pdbファイルが存在しない場合やGetOffsetFromSourceの引数が不正な場合は、 メソッドの先頭位置にブレークポイントが設定されることになるでしょう。 GetOffsetFromSourceの実装は、次のようになっています。

ULONG32 CManagedCallback::GetOffsetFromSource(LPWSTR lpszSource, ULONG32 uLine, ULONG32 uColumn)
{
	ISymUnmanagedDocument *pSymDocument;
	ISymUnmanagedMethod   *pSymMethod;
	HRESULT               hr;
	GUID                  guid = {0};
	ULONG32               uOffset, uRangeArraySize, *pRangeArray;

	hr = g_pSymReader->GetDocument(lpszSource, guid, guid, guid, &pSymDocument);
	if (FAILED(hr))
		return 0;

	hr = g_pSymReader->GetMethodFromDocumentPosition(pSymDocument, uLine, uColumn, &pSymMethod);
	if (FAILED(hr)) {
		pSymDocument->Release();
		return 0;
	}

	uRangeArraySize = 0;
	pSymMethod->GetRanges(pSymDocument, uLine, uColumn, 0, &uRangeArraySize, NULL);
	pRangeArray = (ULONG32 *)HeapAlloc(GetProcessHeap(), 0, sizeof(ULONG32) * uRangeArraySize);
	pSymMethod->GetRanges(pSymDocument, uLine, uColumn, uRangeArraySize, &uRangeArraySize, pRangeArray);
	uOffset = pRangeArray[0];
	
	pSymMethod->Release();
	pSymDocument->Release();
	HeapFree(GetProcessHeap(), 0, pRangeArray);

	return uOffset;
}

ソースファイルのパスをISymUnmanagedReader::GetDocumentに指定すれば、ISymUnmanagedDocumentを取得できます。 このインターフェースと行と列をISymUnmanagedReader::GetMethodFromDocumentPositionに指定すれば、 その指定位置に相当するメソッドが第4引数に返ります。 これはISymUnmanagedMethodで表すことができ、GetRangesを呼び出すことによって、 指定した行と列からオフセットを取得できます。

前節ではスタックフレームを列挙するEnumFrameを説明しましたが、 そこではGetSourceLineというメソッドを呼び出していました。 このメソッドは、命令ポインタ(IP)から現在ソース上の何行目を参照しているかを取得します。

ULONG32 CManagedCallback::GetSourceLine(ICorDebugThread *pThread)
{
	ICorDebugFrame      *pFrame;
	ICorDebugILFrame    *pILFrame;
	ICorDebugFunction   *pFunction;
	ISymUnmanagedMethod *pSymMethod;
	mdMethodDef         methodDef;
	BOOL                bSym = g_pSymReader != NULL;
	ULONG32             uCount, uIP, i, uLine;
	ULONG32             *pOffsets, *pLines;

	if (!bSym)
		return 0;
	
	pThread->GetActiveFrame(&pFrame);
	pFrame->GetFunction(&pFunction);
	pFunction->GetToken(&methodDef);
	
	g_pSymReader->GetMethod(methodDef, &pSymMethod);
	pSymMethod->GetSequencePointCount(&uCount);
	
	pOffsets = (ULONG32 *)HeapAlloc(GetProcessHeap(), 0, uCount * sizeof(ULONG32));
	pLines = (ULONG32 *)HeapAlloc(GetProcessHeap(), 0, uCount * sizeof(ULONG32));
	
	pSymMethod->GetSequencePoints(uCount, NULL, pOffsets, NULL, pLines, NULL, NULL, NULL);
	pFrame->QueryInterface(IID_PPV_ARGS(&pILFrame));
	pILFrame->GetIP(&uIP, NULL);
	
	uLine = 0;
	for (i = 0; i < uCount; i++) {
		if (pOffsets[i] == uIP) {
			uLine = pLines[i];
			break;
		}
	}

	HeapFree(GetProcessHeap(), 0, pOffsets);
	HeapFree(GetProcessHeap(), 0, pLines);

	pILFrame->Release();
	pSymMethod->Release();
	pFunction->Release();
	pFrame->Release();
	
	return uLine;
}

まずは現在実行されているメソッドを確認するために、 ICorDebugFunction::GetTokenでMethodDefトークンを取得します。 これがあれば、ISymUnmanagedReader::GetMethodを通じて、 メソッドを表すISymUnmanagedMethodを取得できます。 シーケンスポイントとは、ブレークポイントを設定する場合など、デバッガが一意に参照できることを求めるILコード内の位置のことであり、 その位置の数を返すのがISymUnmanagedMethod::GetSequencePointCountです。 ISymUnmanagedMethod::GetSequencePointsでは、メソッドの先頭からシーケンスポイントまでのオフセットの配列と、 シーケンスポイントが配置されている行の配列を取得しています。 pOffsets[i]に関連する行はpLine[i]に格納されているため、 pOffsets[i]とuIPが一致した場合にpLine[i]を参照することで、現在参照している行が分かります。 uIP(命令ポインタ)を取得するには、ICorDebugILFrame::GetIPを呼び出します。

前節のEnumArgumentsはメソッドの引数を列挙していましたが、 引数だけでなくローカル変数も列挙したい場合があるかしれません。 次に示すEnumLocalVariablesは、ローカル変数の列挙を行っています。

void CManagedCallback::EnumLocalVariables(ICorDebugThread *pThread)
{
	ICorDebugFrame     *pFrame;
	ICorDebugILFrame   *pILFrame;
	ICorDebugValueEnum *pValueEnum;
	ICorDebugValue     *pValue;
	WCHAR              szName[256], szValue[256], szType[256];
	int                i;

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

	i = 0;
	while (pValueEnum->Next(1, &pValue, NULL) == S_OK) {
		lstrcpyW(szName, L"?");
		GetValue(pValue, szValue);
		GetElementTypeName(pValue, szType);
		SetValueItem(szName, szValue, szType, i);
		pValue->Release();
		i++;
	}

	pValueEnum->Release();
	pILFrame->Release();
	pFrame->Release();
}

引数の列挙時にはICorDebugILFrame::EnumerateLocalVariablesを呼び出していましたが、 ローカル変数の列挙時にはICorDebugILFrame::EnumerateLocalVariablesを呼び出します。 引数の際には引数の名前をメタデータから解決できましたが、 ローカル変数の場合はこれができないため、上記では変数名が?になっています。 ただし、ISymUnmanagedReaderを取得している場合はpdbファイルから変数名を解決できるため、 この方法について考えていきます。

シンボル ストア診断 APIでは、ローカル変数がスコープ単位で管理されています。 スコープとは、関数やif文における括弧のことだと思えばよいでしょう。 次に例を示します。

static void ShowParam(int n, long[] l, class1 c)
{
    int a = 1;
    int b = 2;

    if (n == 10)
    {
        string cur = System.IO.Directory.GetCurrentDirectory();
        MessageBox.Show(cur);

        if (a + b == 4)
        {
            string var = System.Environment.OSVersion.ToString();
            MessageBox.Show(var);
        }
    }

    if (l[0] == 2)
    {
        Random r = new Random();
        MessageBox.Show(r.Next(100).ToString());
    }
        
    if (c.s == "st")
    {
        DateTime date = DateTime.Now;
        MessageBox.Show(date.ToString());
    }
}

関数の括弧内に、2つのローカル変数と3つのスコープ(if文)が存在していると解釈してください。 上記の場合、if文のスコープは関数のスコープの子であると表現されます。 1つ目のスコープはその中にさらにスコープを持っていますが、これは1つ目のスコープの子になります。 スコープの取得をプログラムから行う例を次に示します。

pSymMethod->GetRootScope(&pScope);
pScope->GetChildren(1, &uCount, &pScope2); // 関数のスコープを取得

pScope2->GetLocalCount(&uCount); // 関数のスコープのローカル変数の数を取得
if (uCount > 0) {
	pScope2->GetLocals(NULL, &uCount, NULL);
	uSize = uCount * sizeof(ISymUnmanagedScope);
	ppVariables = (ISymUnmanagedVariable **)HeapAlloc(GetProcessHeap(), 0, uSize);
	pScope2->GetLocals(uSize, &uCount, ppVariables);
	// ppVariables[i]でローカル変数にアクセス
	HeapFree(GetProcessHeap(), 0, ppVariables);
}

pScope2->GetChildren(NULL, &uCount, NULL); // 関数内に存在する子スコープの数を取得
if (uCount > 0) {
	uSize = uCount * sizeof(ISymUnmanagedScope);
	ppScopes = (ISymUnmanagedScope **)HeapAlloc(GetProcessHeap(), 0, uSize);
	pScope2->GetChildren(uSize, &uCount, ppScopes);
	// ppScopes[i]で子スコープにアクセス
	HeapFree(GetProcessHeap(), 0, ppScopes);
}

まず、ISymUnmanagedMethod::GetRootScopeでルートスコープを取得します。 このルートスコープというものがどういうものかよく分からないのですが、 GetChildrenを呼び出せばルートスコープの子である関数のスコープを取得できます。 uCountは返されるスコープの数ですが、この場合は1になるはずです。 関数のスコープ(pScope2)を取得したらGetLocalCountを呼び出してローカル変数の数を取得しています。 関数内(if文内は含まれない)で定義していたローカル変数は2つであったため2が返ると思われますが、 場合によっては暗黙的に作成されたローカル変数も含まれることもあります。 GetChildrenは、スコープ内に存在する子スコープの数が返ります。 関数内にはif文が3つ存在していましたから、3という値が返ることになるでしょう。 ppScopes[0]は最初のif文を表しますが、この中にはさらにif文が存在していたため、 ppScopes[0]->GetChildrenは1を返すことになるでしょう。

上記の内容を考慮して、変数名を設定するようになったEnumLocalVariablesを示します。

void CManagedCallback::EnumLocalVariables(ICorDebugThread *pThread)
{
	ICorDebugFrame        *pFrame;
	ICorDebugILFrame      *pILFrame;
	ICorDebugValueEnum    *pValueEnum;
	ICorDebugValue        *pValue;
	ICorDebugFunction     *pFunction;
	ISymUnmanagedMethod   *pSymMethod;
	ISymUnmanagedScope    *pScope;
	ISymUnmanagedVariable *pVariable;
	mdMethodDef           methodDef;
	WCHAR                 szName[256], szValue[256], szType[256];
	BOOL                  bSym = g_pSymReader != NULL;
	HDPA                  hdpa = NULL;
	int                   i;

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

	if (bSym) {
		pFrame->GetFunction(&pFunction);
		pFunction->GetToken(&methodDef);
		g_pSymReader->GetMethod(methodDef, &pSymMethod);
		pSymMethod->GetRootScope(&pScope);
		hdpa = DPA_Create(1);
		CreateVariableArray(hdpa, pScope);

		pSymMethod->Release();
		pFunction->Release();
	}

	i = 0;
	while (pValueEnum->Next(1, &pValue, NULL) == S_OK) {
		if (bSym) {
			ULONG32 uSize;

			GetVariable(hdpa, i, &pVariable);
			if (pVariable != NULL)
				pVariable->GetName(256, &uSize, szName);
			else
				lstrcpyW(szName, L"?");
		}
		else
			lstrcpyW(szName, L"?");

		GetValue(pValue, szValue);
		GetElementTypeName(pValue, szType);
		SetValueItem(szName, szValue, szType, i);
		pValue->Release();
		i++;
	}

	if (bSym) {
		int n = DPA_GetPtrCount(hdpa);

		for (i = 0; i < n; i++) {
			pVariable = (ISymUnmanagedVariable *)DPA_GetPtr(hdpa, i);
			pVariable->Release();
		}

		DPA_Destroy(hdpa);
	}
	
	pValueEnum->Release();
	pILFrame->Release();
	pFrame->Release();
}

bSymがTRUEの場合はg_pSymReaderを使用できるため、 ISymUnmanagedReader::GetMethodでISymUnmanagedMethodを取得し、 ISymUnmanagedMethod::GetRootScopeでISymUnmanagedScopeを取得します。 後はISymUnmanagedScope::GetLocalsでローカル変数を列挙すればよいのですが、 ここで列挙できるのはそのスコープに存在するローカル変数のみです。 プログラム的には、全てのローカル変数をまとめて列挙したいため、 全てのISymUnmanagedVariableを格納する配列が欲しいものです。 それを作成するのがCreateVariableArrayであり、 動的配列のhdpaへISymUnmanagedVariableを追加していきます。 CreateVariableArrayの実装は次のようになっています。

void CManagedCallback::CreateVariableArray(HDPA hdpa, ISymUnmanagedScope *pScope)
{
	ULONG32               i, uCount, uSize;
	ISymUnmanagedVariable **ppVariables;
	ISymUnmanagedScope    **ppScopes;

	pScope->GetLocalCount(&uCount);
	if (uCount > 0) {
		pScope->GetLocals(NULL, &uCount, NULL);
		uSize = uCount * sizeof(ISymUnmanagedScope);
		ppVariables = (ISymUnmanagedVariable **)HeapAlloc(GetProcessHeap(), 0, uSize);
		pScope->GetLocals(uSize, &uCount, ppVariables);
		for (i = 0; i < uCount; i++)
			DPA_InsertPtr(hdpa, 0, ppVariables[i]);
		HeapFree(GetProcessHeap(), 0, ppVariables);
	}
	
	pScope->GetChildren(NULL, &uCount, NULL);
	if (uCount > 0) {
		uSize = uCount * sizeof(ISymUnmanagedScope);
		ppScopes = (ISymUnmanagedScope **)HeapAlloc(GetProcessHeap(), 0, uSize);
		pScope->GetChildren(uSize, &uCount, ppScopes);
		for (i = 0; i < uCount; i++)
			CreateVariableArray(hdpa, ppScopes[i]);
		HeapFree(GetProcessHeap(), 0, ppScopes);
	}

	pScope->Release();
}

ISymUnmanagedScope::GetLocalsで取得したISymUnmanagedVariableを、DPA_InsertPtrで動的配列に追加します。 スコープの中にスコープが存在することもあるため、ISymUnmanagedScope::GetChildrenでISymUnmanagedScopeの配列を取得し、 その数だけCreateVariableArrayを再帰呼び出します。 全てが終わった際には、全てのスコープのローカル変数が動的配列に追加されているはずです。

EnumLocalVariablesのループ文では、指定したインデックスに対応するISymUnmanagedVariableを取得するために、 GetVariableを呼び出していました。

void CManagedCallback::GetVariable(HDPA hdpa, int nIndex, ISymUnmanagedVariable **ppVariable)
{
	int                   i;
	int                   n = DPA_GetPtrCount(hdpa);
	ISymUnmanagedVariable *pVariable;
	ULONG32               u;

	*ppVariable = NULL;

	for (i = 0; i < n; i++) {
		pVariable = (ISymUnmanagedVariable *)DPA_GetPtr(hdpa, i);
		pVariable->GetAddressField1(&u);
		if (u == (ULONG32)nIndex) {
			*ppVariable = pVariable;
			break;
		}
	}
}

DPA_InsertPtrで追加したデータは、DPA_GetPtrで取得できます。 ISymUnmanagedVariable::GetAddressField1がインデックスを返すようになっているため、 これとnIndexを比較します。


戻る