Skip to content

MchKosticyn/ManagedCorProfiler

 
 

Repository files navigation

ManagedCorProfiler

A prototype .NET profiler written in C# leveraging NativeAOT + ComWrappers.

Warning Work in progress

///
/// A sample profiler callback that prints the loaded modules
///
[ProfilerCallback("090B7720-6605-462B-86A0-C4D4C444D3F5")]
internal unsafe class MyProfiler : ICorProfilerCallback2
{
    private ICorProfilerInfo4* _profilerInfo;
    
    int ICorProfilerCallback.Initialize(CorProf.Bindings.IUnknown* unknown)
    {
        var guid_ = CorProfConsts.IID_ICorProfilerInfo4;

        var hr = Marshal.QueryInterface((nint)unknown, ref guid_, out var pinfo);

        if (hr < 0)
        {
            return HResult.E_FAIL;
        }

        var eventMask = COR_PRF_MONITOR.COR_PRF_MONITOR_MODULE_LOADS;

        _profilerInfo = (ICorProfilerInfo4*)pinfo;

        hr = _profilerInfo->SetEventMask((uint)eventMask);
        
        if (hr < 0)
        {
            return HResult.E_FAIL;
        }
    }
    
    int ICorProfilerCallback.ModuleLoadFinished(ulong moduleId, int hrStatus)
    {
        var pbBaseLoadAddr = (byte*)null;
        uint pcchName = 0;
        ulong assemblyId = 0;
        var szName = (ushort*)NativeMemory.Alloc(sizeof(ushort) * 300);

        var hr = _profilerInfo->GetModuleInfo(
            moduleId,
            &pbBaseLoadAddr,
            300,
            &pcchName,
            szName,
            &assemblyId);

        if (hr < 0)
        {
            return HResult.E_FAIL;
        }

        var module = Marshal.PtrToStringUni((nint)szName);

        Console.WriteLine($"Loaded Moudle -> '{module}'");

        NativeMemory.Free(szName);

        return HResult.S_OK;
    }
    
    int ICorProfilerCallback.Shutdown() 
    {
        return HResult.S_OK; 
    }
}

Overview

Sample

Sample The sample produces a native DLL that can be loaded as a CLR Profiler.

To test the concept, the example does a few basic things:

  1. Exposes DllGetClassObject DLL entry point
  2. Implements ICorProfilerCallback + ICorProfilerCallback2 interfaces
  3. hooks to ICorProfilerCallback::ModuleLoadFinished and ICorProfilerInfo2::SetEnterLeaveFunctionHooks2
  4. logs the name of every loaded module

Sample output

C:\ManagedCorProfiler\ManagedCorProfiler> .\run.cmd
DllMain(reason=DLL_PROCESS_ATTACH)
DllGetClassObject(reason=DLL_THREAD_ATTACH)
        RCLSID = cf0d821e-299b-5307-a3d8-b283c03916dd
        RIID   = 00000001-0000-0000-c000-000000000046
MyProfiler!Initialize:::ICorProfilerCallback!Initialize()
Loaded Moudle -> 'C:\Users\user\..\runtime\artifacts\bin\coreclr\windows.x64.Debug\System.Private.CoreLib.dll'
Loaded Moudle -> 'C:\Users\user\..\SampleApp\bin\Debug\net7.0\SampleApp.dll'
Loaded Moudle -> 'C:\Program Files\dotnet\shared\Microsoft.NETCore.App\7.0.0\system.runtime.dll'
Loaded Moudle -> 'C:\Program Files\dotnet\shared\Microsoft.NETCore.App\7.0.0\system.console.dll'
Loaded Moudle -> 'C:\Program Files\dotnet\shared\Microsoft.NETCore.App\7.0.0\system.threading.dll'
Loaded Moudle -> 'C:\Program Files\dotnet\shared\Microsoft.NETCore.App\7.0.0\system.text.encoding.extensions.dll'
Loaded Moudle -> 'C:\Program Files\dotnet\shared\Microsoft.NETCore.App\7.0.0\system.runtime.interopservices.dll'
[... OMITTED ...]
=> GetPinnableReference()
=> get_Length()
=> WriteFile()
=> SetLastSystemError()
Hello World!
=> GetLastSystemError()
=> Flush()
=> Flush()
Hello World!
=> OnProcessExit()
[... OMITTED ...]
C:\ManagedCorProfiler\ManagedCorProfiler> █

output is actually interleaved, formatted and trimmed for clarity

Other approaches or variations

not tested, just off the top of my head

  • DNNE + managed assembly implementing the profiler itself
  • A native (C++) library using nethost to host an instance of the CRL + a managed library implementing the profiler
  • do not use ComWrappers but directly mess with UnmanagedCallersOnly, function pointers and co to manually implement COM ABI vtables.

Dumpbin of the profiler DLL

C:\ManagedCorProfiler\ManagedCorProfiler> dumpbin.exe /EXPORTS bin\Debug\net7.0\win-x64\native\ManagedCorProfiler.dll
[...OMITTED FOR BREVITY...]
    ordinal hint RVA      name
          1    0 00232660 DllCanUnloadNow = DllCanUnloadNow
          2    1 002323A0 DllGetClassObject = DllGetClassObject
          3    2 002326A0 DllMain = DllMain
          4    3 00497430 DotNetRuntimeDebugHeader = DotNetRuntimeDebugHeader
[...OMITTED FOR BREVITY...]
C:\ManagedCorProfiler\ManagedCorProfiler> █

Contributing

Any contribution is welcome. I'm actually working on porting the tests at dotnet/runtime/tree/main/src/tests/profiler, this is a good place to start contributing.

Codegen

  • code contained in the *.Bindings projects is generated using ClangSharpPInvokeGenerator
  • at this point, some manual tweaks are needed to make the generated bindings for an header compile (e.g fixing callconvs)

Resources

Misc

COM / COM Interop

Profiling

Native AOT

About

Prototype NET (CLR) Profiler written in C#

Resources

License

Stars

Watchers

Forks

Releases

No releases published

Packages

No packages published

Languages

  • C# 98.4%
  • Other 1.6%