Let's build .NET profiler

For FUN

... and Profit

3...2...1...start

Disclaimer

Backend

ICorProfilerCallbackX*

* X - 1 do 10

::Initialize

auto hr = pICorProfilerInfoUnk->QueryInterface(
	IID_ICorProfilerInfo5, 
    (void**)&pInfo);

::Initialize

auto hr = pInfo->SetEventMask2(
	COR_PRF_ALL | 
	COR_PRF_MONITOR_ALL | 
	COR_PRF_ENABLE_STACK_SNAPSHOT | 
	COR_PRF_MONITOR_THREADS, COR_PRF_HIGH_MONITOR_NONE);

🤙

RESULT __stdcall OctoProfiler::AssemblyLoadStarted(AssemblyID assemblyId)
{
	auto assemblyName = nameResolver->ResolveAssemblyName(assemblyId);
	Logger::DoLog(std::format(L"OctoProfiler::AssemblyLoadStarted: {0}", 
    		assemblyName.value_or(L"<<no info>>")));
	return S_OK;
}

HRESULT __stdcall OctoProfiler::AssemblyLoadFinished(AssemblyID assemblyId, HRESULT hrStatus)
{
	auto assemblyName = nameResolver->ResolveAssemblyName(assemblyId);
	Logger::DoLog(std::format(L"OctoProfiler::AssemblyLoadFinished: {0}", 
    assemblyName.value_or(L"<<no info>>")));
	return S_OK;
}

HRESULT __stdcall OctoProfiler::AssemblyUnloadStarted(AssemblyID assemblyId)
{
	return E_NOTIMPL;
}

👣

hr = pInfo->DoStackSnapshot(NULL, &StackSnapshotInfo, COR_PRF_SNAPSHOT_DEFAULT, 
		reinterpret_cast<void *>(nameResolver.get()), NULL, 0);

HRESULT __stdcall StackSnapshotInfo(
			FunctionID funcId, UINT_PTR ip, COR_PRF_FRAME_INFO frameInfo, 
			ULONG32 contextSize, BYTE context[], void* clientData)
{	
	if (!funcId)
	{
		Logger::DoLog(std::format("OctoProfiler::Native frame {0:x}", ip));
	}
	else
	{
		NameResolver* nameResolver = reinterpret_cast<NameResolver*>(clientData);
		auto functionName = nameResolver->ResolveFunctionName(funcId);
		Logger::DoLog(std::format(L"OctoProfiler::Managed frame {0} {1:x}", 
        		functionName.value_or(L"<<no info>>"), ip));
	}

	return S_OK;
}

Function Enter & Leave

hr = pInfo->SetEventMask2(COR_PRF_MONITOR_ENTERLEAVE, 
						  COR_PRF_HIGH_MONITOR_NONE);

this->pInfo->SetFunctionIDMapper2(&MapFunctionId, 
	reinterpret_cast<void*>(nameResolver.get()));
this->pInfo->SetEnterLeaveFunctionHooks2(
	reinterpret_cast<FunctionEnter2*>(FuncEnterCallback),
	reinterpret_cast<FunctionLeave2*>(FuncLeaveCallback), nullptr);

Back to C++-land

void FuncEnterCallback(
	FunctionID funId,
	UINT_PTR clientData,
	COR_PRF_FRAME_INFO frameInfo,
	COR_PRF_FUNCTION_ARGUMENT_INFO * argInfo);

void FuncLeaveCallback(
	FunctionID funId,
	UINT_PTR clientData,
	COR_PRF_FRAME_INFO frameInfo,
	COR_PRF_FUNCTION_ARGUMENT_INFO* argInfo);

Back to C++-land

UINT_PTR __stdcall MapFunctionId(
		FunctionID funcId, 
        void *clientData, 
        BOOL *pbHookFunction)
{	
  auto nameResolver = reinterpret_cast<NameResolver*>(clientData);
  auto functionName = nameResolver->ResolveFunctionName(funcId);
  *pbHookFunction = false;

  if (functionName.has_value())
  {
	auto c_ptr = new std::wstring(
    	functionName.value_or(L"<<unknown>>"));
	*pbHookFunction = true;
	return reinterpret_cast<UINT_PTR>(c_ptr->c_str());
  }
  return NULL;
}

x64 ASM

FuncEnterCallback proc frame
	; rcx - funcId, rdx - clientData, 
    ; r8  - frameInfo, r9  - argInfo		
	push rax
	.allocstack 8
	sub rsp, 20h
	.allocstack 20h
	.endprolog

	call FuncEnterStub

	add rsp, 20h
	; restore
	pop rax

	ret
FuncEnterCallback endp

Frontend

How to run this?

SET DOTNET_EnableDiagnostics=1
SET COR_ENABLE_PROFILING=1
SET CORECLR_ENABLE_PROFILING=1
SET COR_PROFILER={8A8CC829-CCF2-49FE-BBAE-0F022228071A}
SET CORECLR_PROFILER={8A8CC829-CCF2-49FE-BBAE-0F022228071A}
SET COR_PROFILER_PATH=.\x64\Release\OctoProfiler.dll
SET CORECLR_PROFILER_PATH_64=.\x64\Release\OctoProfiler.dll

Troubleshooting

Demo

⭐⭐⭐

Feedback?

Lets build a .NET profiler

By Pawel Lukasik

Lets build a .NET profiler

  • 100