.NET CoreCLR Profiling API的使用
为了给一个 native/.NET 混合项目开发一个用户友好的崩溃追踪系统,需要结合 breakpad 进行栈调用 dump 。因为正常的 breakpad 只能识别出非托管的栈调用链,而托管端的栈调用链需要完整的内存空间才能分析出,所以需要另一种方式获取托管端的栈调用。最初的构想是托管端暴露一个函数给非托管代码,这个函数调用了 C# 的 StackTrace 以获取信息,而 breakpad 的 callback 则可以调用这个函数。经过测试,这种方法在 windows 上有效,而在 linux 上会出现问题。
所以为了实现这一功能,需要追踪每一个函数的调用,并实时维护每一个线程的调用栈。毫无疑问这种做法会带来运行时开销,所以需要谨慎使用。实现维护调用栈的方法其实除了使用 CoreCLR Profiling API 以外,还可以使用 Harmony 在 C# 端进行运行时Hook。但是考虑到潜在的性能问题,加上我自己也想尝试一下使用 Profiling API,所以没有采用这种方法。
CoreCLR Profiling API是 CoreCLR 暴露出的一套给开发者用于追踪性能的API,偏向底层,相关的文档也很少。本文大部分代码原型来自于 medium 上的一个系列教程,这是第一篇:Start a journey into the .NET Profiling APIs | by Christophe Nasarre | Medium
这篇文档写的较为详细,除了简单的函数追踪外还展开了更多的功能,推荐阅读。
此外,Microsoft 官方也提供了一些样例代码:clr-samples/ProfilingAPI/README.md
本文涉及到的代码可能会引入其他的头文件,请按需自行删除。
编译参数
这里使用 xmake 作为编译工具。
add_rules("mode.debug", "mode.release")
set_encodings("utf-8")
set_optimize("aggressive")
local DOTNET_PATH = os.getenv("DOTNET_PATH")
target("sw2tracer")
set_kind("shared")
set_languages("cxx23")
set_arch("x64")
add_files("src/*.cpp")
add_headerfiles("src/*.h")
add_rules("asm")
if is_plat("windows") then
add_files("src/*.def")
add_files("src/asm/windows/*")
else
add_files("src/asm/systemv/*")
end
if is_plat("windows") then
add_defines("WIN32")
add_defines("_WIN32")
else
add_defines("HOST_AMD64")
end
add_includedirs(path.join(DOTNET_PATH, "src/coreclr/pal/prebuilt/inc"))
if is_plat("linux") then
add_includedirs(path.join(DOTNET_PATH, "src/coreclr/pal/inc"))
add_includedirs(path.join(DOTNET_PATH, "src/coreclr/pal/inc/rt"))
end
add_includedirs(path.join(DOTNET_PATH, "src/coreclr/inc"))
add_includedirs(path.join(DOTNET_PATH, "src/coreclr"))
add_includedirs(path.join(DOTNET_PATH, "src/native"))
这份编译配置较为简洁,请按照你的需求自行进行更改。注意这里还添加了 asm 的支持,因为后面要用到。Linux 上特别导入了一些 pal 层的头文件,pal是其他平台特有的平台兼容层。
修复 Linux 宏定义问题
在 Linux 上使用时,你可能会遇到宏定义冲突问题,具体情况和解决方法请看:
GUID
在 Microsoft 的组件对象模型(COM)中,每个接口都有对应的 GUID 。同理,我们也要为自己的 Profiler 分配一个 GUID 。你可以在 PowerShell 中通过如下指令快速生成一个 GUID。
New-Guid在 CoreCLR 的 tests 测试代码中存在一个 GUID 定义文件,我们的项目开发也需要这个文件,来获取各种 CoreCLR Profiling API COM 接口的的 GUID(不知道为什么这个不在他导出的头文件里)。代码如下:
// "Guid.h"
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
#include "rpc.h"
#include "guiddef.h"
#ifndef EXTERN_C
#define EXTERN_C extern "C"
#endif // EXTERN_C
#ifdef DEFINE_GUID
#undef DEFINE_GUID
#endif
#ifndef _WIN32
#include <cstring>
bool minipal_guid_equals(const GUID *v1, const GUID *v2)
{
return memcmp(v1, v2, sizeof(GUID)) == 0;
}
#endif
#define DEFINE_GUID(name, l, w1, w2, b1, b2, b3, b4, b5, b6, b7, b8) \
EXTERN_C const GUID name = {l, w1, w2, {b1, b2, b3, b4, b5, b6, b7, b8}}
DEFINE_GUID(GUID_NULL, 0x00000000, 0x0000, 0x0000, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00);
DEFINE_GUID(IID_IUnknown, 0x00000000, 0x0000, 0x0000, 0xC0, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x46);
DEFINE_GUID(IID_IClassFactory, 0x00000001, 0x0000, 0x0000, 0xC0, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x46);
DEFINE_GUID(IID_ICorProfilerCallback, 0x176FBED1, 0xA55C, 0x4796, 0x98, 0xCA, 0xA9, 0xDA, 0x0E, 0xF8, 0x83, 0xE7);
DEFINE_GUID(IID_ICorProfilerCallback2, 0x8A8CC829, 0xCCF2, 0x49FE, 0xBB, 0xAE, 0x0F, 0x02, 0x22, 0x28, 0x07, 0x1A);
DEFINE_GUID(IID_ICorProfilerCallback3, 0x4FD2ED52, 0x7731, 0x4B8D, 0x94, 0x69, 0x03, 0xD2, 0xCC, 0x30, 0x86, 0xC5);
DEFINE_GUID(IID_ICorProfilerCallback4, 0x7B63B2E3, 0x107D, 0x4D48, 0xB2, 0xF6, 0xF6, 0x1E, 0x22, 0x94, 0x70, 0xD2);
DEFINE_GUID(IID_ICorProfilerCallback5, 0x8DFBA405, 0x8C9F, 0x45F8, 0xBF, 0xFA, 0x83, 0xB1, 0x4C, 0xEF, 0x78, 0xB5);
DEFINE_GUID(IID_ICorProfilerCallback6, 0xFC13DF4B, 0x4448, 0x4F4F, 0x95, 0x0C, 0xBA, 0x8D, 0x19, 0xD0, 0x0C, 0x36);
DEFINE_GUID(IID_ICorProfilerCallback7, 0xF76A2DBA, 0x1D52, 0x4539, 0x86, 0x6C, 0x2A, 0xA5, 0x18, 0xF9, 0xEF, 0xC3);
DEFINE_GUID(IID_ICorProfilerCallback8, 0x5BED9B15, 0xC079, 0x4D47, 0xBF, 0xE2, 0x21, 0x5A, 0x14, 0x0C, 0x07, 0xE0);
DEFINE_GUID(IID_ICorProfilerCallback9, 0x27583EC3, 0xC8F5, 0x482F, 0x80, 0x52, 0x19, 0x4B, 0x8C, 0xE4, 0x70, 0x5A);
DEFINE_GUID(IID_ICorProfilerCallback10, 0xCEC5B60E, 0xC69C, 0x495F, 0x87, 0xF6, 0x84, 0xD2, 0x8E, 0xE1, 0x6F, 0xFB);
DEFINE_GUID(IID_ICorProfilerCallback11, 0x42350846, 0xAAED, 0x47F7, 0xB1, 0x28, 0xFD, 0x0C, 0x98, 0x88, 0x1C, 0xDE);
DEFINE_GUID(IID_ICorProfilerInfo, 0x28B5557D, 0x3F3F, 0x48B4, 0x90, 0xB2, 0x5F, 0x9E, 0xEA, 0x2F, 0x6C, 0x48);
DEFINE_GUID(IID_ICorProfilerInfo2, 0xCC0935CD, 0xA518, 0x487D, 0xB0, 0xBB, 0xA9, 0x32, 0x14, 0xE6, 0x54, 0x78);
DEFINE_GUID(IID_ICorProfilerInfo3, 0xB555ED4F, 0x452A, 0x4E54, 0x8B, 0x39, 0xB5, 0x36, 0x0B, 0xAD, 0x32, 0xA0);
DEFINE_GUID(IID_ICorProfilerObjectEnum, 0x2C6269BD, 0x2D13, 0x4321, 0xAE, 0x12, 0x66, 0x86, 0x36, 0x5F, 0xD6, 0xAF);
DEFINE_GUID(IID_ICorProfilerFunctionEnum, 0xFF71301A, 0xB994, 0x429D, 0xA1, 0x0B, 0xB3, 0x45, 0xA6, 0x52, 0x80, 0xEF);
DEFINE_GUID(IID_ICorProfilerModuleEnum, 0xB0266D75, 0x2081, 0x4493, 0xAF, 0x7F, 0x02, 0x8B, 0xA3, 0x4D, 0xB8, 0x91);
DEFINE_GUID(IID_IMethodMalloc, 0xA0EFB28B, 0x6EE2, 0x4D7B, 0xB9, 0x83, 0xA7, 0x5E, 0xF7, 0xBE, 0xED, 0xB8);
DEFINE_GUID(IID_ICorProfilerFunctionControl, 0xF0963021, 0xE1EA, 0x4732, 0x85, 0x81, 0xE0, 0x1B, 0x0B, 0xD3, 0xC0, 0xC6);
DEFINE_GUID(IID_ICorProfilerInfo4, 0x0D8FDCAA, 0x6257, 0x47BF, 0xB1, 0xBF, 0x94, 0xDA, 0xC8, 0x84, 0x66, 0xEE);
DEFINE_GUID(IID_ICorProfilerInfo5, 0x07602928, 0xCE38, 0x4B83, 0x81, 0xE7, 0x74, 0xAD, 0xAF, 0x78, 0x12, 0x14);
DEFINE_GUID(IID_ICorProfilerInfo6, 0xF30A070D, 0xBFFB, 0x46A7, 0xB1, 0xD8, 0x87, 0x81, 0xEF, 0x7B, 0x69, 0x8A);
DEFINE_GUID(IID_ICorProfilerInfo7, 0x9AEECC0D, 0x63E0, 0x4187, 0x8C, 0x00, 0xE3, 0x12, 0xF5, 0x03, 0xF6, 0x63);
DEFINE_GUID(IID_ICorProfilerInfo8, 0xC5AC80A6, 0x782E, 0x4716, 0x80, 0x44, 0x39, 0x59, 0x8C, 0x60, 0xCF, 0xBF);
DEFINE_GUID(IID_ICorProfilerInfo9, 0x008170DB, 0xF8CC, 0x4796, 0x9A, 0x51, 0xDC, 0x8A, 0xA0, 0xB4, 0x70, 0x12);
DEFINE_GUID(IID_ICorProfilerInfo10, 0x2F1B5152, 0xC869, 0x40C9, 0xAA, 0x5F, 0x3A, 0xBE, 0x02, 0x6B, 0xD7, 0x20);
DEFINE_GUID(IID_ICorProfilerInfo11, 0x06398876, 0x8987, 0x4154, 0xB6, 0x21, 0x40, 0xA0, 0x0D, 0x6E, 0x4D, 0x04);
DEFINE_GUID(IID_ICorProfilerInfo12, 0x27B24CCD, 0x1CB1, 0x47C5, 0x96, 0xEE, 0x98, 0x19, 0x0D, 0xC3, 0x09, 0x59);
DEFINE_GUID(IID_ICorProfilerInfo13, 0x6E6C7EE2, 0x0701, 0x4EC2, 0x9D, 0x29, 0x2E, 0x87, 0x33, 0xB6, 0x69, 0x34);
DEFINE_GUID(IID_ICorProfilerInfo14, 0XF460E352, 0XD76D, 0X4FE9, 0X83, 0X5F, 0XF6, 0XAF, 0X9D, 0X6E, 0X86, 0X2D);
DEFINE_GUID(IID_ICorProfilerInfo15, 0XB446462D, 0XBD22, 0X41DD, 0X87, 0X2D, 0XDC, 0X71, 0X4C, 0X49, 0XEB, 0X56);
DEFINE_GUID(IID_ICorProfilerMethodEnum, 0xFCCEE788, 0x0088, 0x454B, 0xA8, 0x11, 0xC9, 0x9F, 0x29, 0x8D, 0x19, 0x42);
DEFINE_GUID(IID_ICorProfilerThreadEnum, 0x571194F7, 0x25ED, 0x419F, 0xAA, 0x8B, 0x70, 0x16, 0xB3, 0x15, 0x97, 0x01);
DEFINE_GUID(IID_ICorProfilerAssemblyReferenceProvider, 0x66A78C24, 0x2EEF, 0x4F65, 0xB4, 0x5F, 0xDD, 0x1D, 0x80, 0x38, 0xBF, 0x3C);你可能注意到我在这里为 Linux 定义了一个 minipal_guid_equals 函数。没错,不知道为什么 Linux 无法动态链接到这个函数。根据命名,这个函数理应是 PAL (平台兼容层)的一部分,并且源码中也存在这个函数,但是他就是无法链接上。所以如果你也遇到了这个问题,可以加上这个函数。
程序入口
Profiler 是一个动态链接的共享对象( DLL/so ),我们还需要定义一些导出的函数接口来让 CoreCLR 能够正确识别出我们的 Profiler 并挂载。
样例代码如下(请自行处理头文件):
// Copyright (c) .NET Foundation and contributors. All rights reserved.
// Licensed under the MIT license. See LICENSE file in the project root for full license information.
#include "Guid.h"
#include "ClassFactory.h"
#include "StackManager.h"
#include "Logger.h"
#include "Exports.h"
// {a2648b53-a560-486c-9e56-c3922a330182}
const IID CLSID_CorProfiler = {0xa2648b53, 0xa560, 0x486c, {0x9e, 0x56, 0xc3, 0x92, 0x2a, 0x33, 0x01, 0x82}};
BOOL STDMETHODCALLTYPE DllMain(HMODULE hModule, DWORD ul_reason_for_call, LPVOID lpReserved)
{
return TRUE;
}
STDAPI DllGetClassObject(REFCLSID rclsid, REFIID riid, LPVOID *ppv)
{
if (ppv == nullptr || rclsid != CLSID_CorProfiler)
{
return E_FAIL;
}
auto factory = new ClassFactory;
if (factory == nullptr)
{
return E_FAIL;
}
LOG("Tracer Started");
return factory->QueryInterface(riid, ppv);
}
STDAPI DllCanUnloadNow()
{
return S_OK;
}注意这里的 CLSID_CorProfiler 就是你刚才生成的自己的 GUID,后续还会用到。当 DllGetClassObject 请求到我们自己的 GUID 时,我们需要返回自己的 ClassFactory。
ClassFactory
ClassFactory 是一个 COM 标准对象,他需要提供我们的 ICorProfilerCallback 给 CoreCLR。直接实现如下代码即可:
ClassFactory.h
// Copyright (c) .NET Foundation and contributors. All rights reserved.
// Licensed under the MIT license. See LICENSE file in the project root for full license information.
#pragma once
#ifndef _WIN32
#include "specstrings_undef.h"
#endif
#include <atomic>
#include "corhlpr.h"
class ClassFactory : public IClassFactory
{
private:
std::atomic<int> refCount;
public:
ClassFactory();
virtual ~ClassFactory();
HRESULT STDMETHODCALLTYPE QueryInterface(REFIID riid, void **ppvObject) override;
ULONG STDMETHODCALLTYPE AddRef(void) override;
ULONG STDMETHODCALLTYPE Release(void) override;
HRESULT STDMETHODCALLTYPE CreateInstance(IUnknown *pUnkOuter, REFIID riid, void **ppvObject) override;
HRESULT STDMETHODCALLTYPE LockServer(BOOL fLock) override;
};ClassFactory.cpp
// Copyright (c) .NET Foundation and contributors. All rights reserved.
// Licensed under the MIT license. See LICENSE file in the project root for full license information.
#ifndef _WIN32
#include "specstrings_undef.h"
#endif
#include "CorProfiler.h"
#include "ClassFactory.h"
#include "Logger.h"
ClassFactory::ClassFactory() : refCount(0)
{
}
ClassFactory::~ClassFactory()
{
}
HRESULT STDMETHODCALLTYPE ClassFactory::QueryInterface(REFIID riid, void **ppvObject)
{
if (riid == IID_IUnknown || riid == IID_IClassFactory)
{
*ppvObject = this;
this->AddRef();
return S_OK;
}
*ppvObject = nullptr;
return E_NOINTERFACE;
}
ULONG STDMETHODCALLTYPE ClassFactory::AddRef()
{
return std::atomic_fetch_add(&this->refCount, 1) + 1;
}
ULONG STDMETHODCALLTYPE ClassFactory::Release()
{
int count = std::atomic_fetch_sub(&this->refCount, 1) - 1;
if (count <= 0)
{
delete this;
}
return count;
}
HRESULT STDMETHODCALLTYPE ClassFactory::CreateInstance(IUnknown *pUnkOuter, REFIID riid, void **ppvObject)
{
if (pUnkOuter != nullptr)
{
*ppvObject = nullptr;
return CLASS_E_NOAGGREGATION;
}
CorProfiler *profiler = new CorProfiler();
if (profiler == nullptr)
{
return E_FAIL;
}
LOG("CreateInstance success");
return profiler->QueryInterface(riid, ppvObject);
}
HRESULT STDMETHODCALLTYPE ClassFactory::LockServer(BOOL fLock)
{
return S_OK;
}这里的 CorProfiler 就是我们自己的 ICorProfilerCallback 对象。
ICorProfilerCallback
ICorProfilerCallback 是一个定义了许多 Callback 的接口。我们需要提供一个实现这个接口的对象给 CoreCLR,然后 CoreCLR 就会调用相应的接口来触发事件,从而提供信息给我们的 Profiler。
需要注意的时,跟随着版本迭代,ICorProfilerCallback 有很多不同版本,例如 ICorProfilerCallback2,ICorProfilerCallback3等。所有这些版本都一一继承,而每一个都会新增一些 Callback。所以如果你需要的事件只存在于某个特定版本的接口,那么你必须实现版本相等或更高的接口。
代码如下。
CorProfiler.h
#pragma once
#ifndef _WIN32
#include "specstrings_undef.h"
#endif
#include <atomic>
#include "Helper.h"
#include "Logger.h"
#include "cor.h"
#include "corprof.h"
class CorProfiler : ICorProfilerCallback9
{
private:
std::atomic<int> refCount;
ICorProfilerInfo15 *corProfilerInfo;
public:
CorProfiler();
~CorProfiler();
HRESULT STDMETHODCALLTYPE QueryInterface(REFIID riid, void **ppvObject) override
{
if (
riid == __uuidof(ICorProfilerCallback9) ||
riid == __uuidof(ICorProfilerCallback8) ||
riid == __uuidof(ICorProfilerCallback7) ||
riid == __uuidof(ICorProfilerCallback6) ||
riid == __uuidof(ICorProfilerCallback5) ||
riid == __uuidof(ICorProfilerCallback4) ||
riid == __uuidof(ICorProfilerCallback3) ||
riid == __uuidof(ICorProfilerCallback2) ||
riid == __uuidof(ICorProfilerCallback) ||
riid == IID_IUnknown)
{
*ppvObject = this;
this->AddRef();
return S_OK;
}
*ppvObject = nullptr;
return E_NOINTERFACE;
}
ULONG STDMETHODCALLTYPE AddRef(void) override
{
return std::atomic_fetch_add(&this->refCount, 1) + 1;
}
ULONG STDMETHODCALLTYPE Release(void) override
{
int count = std::atomic_fetch_sub(&this->refCount, 1) - 1;
if (count <= 0)
{
delete this;
}
return count;
}
HRESULT STDMETHODCALLTYPE Initialize(IUnknown *pICorProfilerInfoUnk) override;
HRESULT STDMETHODCALLTYPE Shutdown(void) override;
HRESULT STDMETHODCALLTYPE AppDomainCreationStarted(AppDomainID appDomainId) { return S_OK; };
HRESULT STDMETHODCALLTYPE AppDomainCreationFinished(AppDomainID appDomainId, HRESULT hrStatus) { return S_OK; };
HRESULT STDMETHODCALLTYPE AppDomainShutdownStarted(AppDomainID appDomainId) { return S_OK; };
HRESULT STDMETHODCALLTYPE AppDomainShutdownFinished(AppDomainID appDomainId, HRESULT hrStatus) { return S_OK; };
HRESULT STDMETHODCALLTYPE AssemblyLoadStarted(AssemblyID assemblyId) { return S_OK; };
HRESULT STDMETHODCALLTYPE AssemblyLoadFinished(AssemblyID assemblyId, HRESULT hrStatus) { return S_OK; };
HRESULT STDMETHODCALLTYPE AssemblyUnloadStarted(AssemblyID assemblyId) { return S_OK; };
HRESULT STDMETHODCALLTYPE AssemblyUnloadFinished(AssemblyID assemblyId, HRESULT hrStatus) { return S_OK; };
HRESULT STDMETHODCALLTYPE ModuleLoadStarted(ModuleID moduleId) { return S_OK; };
HRESULT STDMETHODCALLTYPE ModuleLoadFinished(ModuleID moduleId, HRESULT hrStatus) { return S_OK; };
HRESULT STDMETHODCALLTYPE ModuleUnloadStarted(ModuleID moduleId) { return S_OK; };
HRESULT STDMETHODCALLTYPE ModuleUnloadFinished(ModuleID moduleId, HRESULT hrStatus) { return S_OK; };
HRESULT STDMETHODCALLTYPE ModuleAttachedToAssembly(ModuleID moduleId, AssemblyID AssemblyId) { return S_OK; };
HRESULT STDMETHODCALLTYPE ClassLoadStarted(ClassID classId) { return S_OK; };
HRESULT STDMETHODCALLTYPE ClassLoadFinished(ClassID classId, HRESULT hrStatus) { return S_OK; };
HRESULT STDMETHODCALLTYPE ClassUnloadStarted(ClassID classId) { return S_OK; };
HRESULT STDMETHODCALLTYPE ClassUnloadFinished(ClassID classId, HRESULT hrStatus) { return S_OK; };
HRESULT STDMETHODCALLTYPE FunctionUnloadStarted(FunctionID functionId) { return S_OK; };
HRESULT STDMETHODCALLTYPE JITCompilationStarted(FunctionID functionId, BOOL fIsSafeToBlock) { return S_OK; };
HRESULT STDMETHODCALLTYPE JITCompilationFinished(FunctionID functionId, HRESULT hrStatus, BOOL fIsSafeToBlock) { return S_OK; };
HRESULT STDMETHODCALLTYPE JITCachedFunctionSearchStarted(FunctionID functionId, BOOL *pbUseCachedFunction) { return S_OK; };
HRESULT STDMETHODCALLTYPE JITCachedFunctionSearchFinished(FunctionID functionId, COR_PRF_JIT_CACHE result) { return S_OK; };
HRESULT STDMETHODCALLTYPE JITFunctionPitched(FunctionID functionId) { return S_OK; };
HRESULT STDMETHODCALLTYPE JITInlining(FunctionID callerId, FunctionID calleeId, BOOL *pfShouldInline) { return S_OK; };
HRESULT STDMETHODCALLTYPE ThreadCreated(ThreadID threadId) override;
HRESULT STDMETHODCALLTYPE ThreadDestroyed(ThreadID threadId) override;
HRESULT STDMETHODCALLTYPE ThreadAssignedToOSThread(ThreadID managedThreadId, DWORD osThreadId) override;
HRESULT STDMETHODCALLTYPE RemotingClientInvocationStarted(void) { return S_OK; };
HRESULT STDMETHODCALLTYPE RemotingClientSendingMessage(GUID *pCookie, BOOL fIsAsync) { return S_OK; };
HRESULT STDMETHODCALLTYPE RemotingClientReceivingReply(GUID *pCookie, BOOL fIsAsync) { return S_OK; };
HRESULT STDMETHODCALLTYPE RemotingClientInvocationFinished(void) { return S_OK; };
HRESULT STDMETHODCALLTYPE RemotingServerReceivingMessage(GUID *pCookie, BOOL fIsAsync) { return S_OK; };
HRESULT STDMETHODCALLTYPE RemotingServerInvocationStarted(void) { return S_OK; };
HRESULT STDMETHODCALLTYPE RemotingServerInvocationReturned(void) { return S_OK; };
HRESULT STDMETHODCALLTYPE RemotingServerSendingReply(GUID *pCookie, BOOL fIsAsync) { return S_OK; };
HRESULT STDMETHODCALLTYPE UnmanagedToManagedTransition(FunctionID functionId, COR_PRF_TRANSITION_REASON reason) override;
HRESULT STDMETHODCALLTYPE ManagedToUnmanagedTransition(FunctionID functionId, COR_PRF_TRANSITION_REASON reason) { return S_OK; };
HRESULT STDMETHODCALLTYPE RuntimeSuspendStarted(COR_PRF_SUSPEND_REASON suspendReason) { return S_OK; };
HRESULT STDMETHODCALLTYPE RuntimeSuspendFinished(void) { return S_OK; };
HRESULT STDMETHODCALLTYPE RuntimeSuspendAborted(void) { return S_OK; };
HRESULT STDMETHODCALLTYPE RuntimeResumeStarted(void) { return S_OK; };
HRESULT STDMETHODCALLTYPE RuntimeResumeFinished(void) { return S_OK; };
HRESULT STDMETHODCALLTYPE RuntimeThreadSuspended(ThreadID threadId) { return S_OK; };
HRESULT STDMETHODCALLTYPE RuntimeThreadResumed(ThreadID threadId) { return S_OK; };
HRESULT STDMETHODCALLTYPE MovedReferences(ULONG cMovedObjectIDRanges, ObjectID oldObjectIDRangeStart[], ObjectID newObjectIDRangeStart[], ULONG cObjectIDRangeLength[]) { return S_OK; };
HRESULT STDMETHODCALLTYPE ObjectAllocated(ObjectID objectId, ClassID classId) { return S_OK; };
HRESULT STDMETHODCALLTYPE ObjectsAllocatedByClass(ULONG cClassCount, ClassID classIds[], ULONG cObjects[]) { return S_OK; };
HRESULT STDMETHODCALLTYPE ObjectReferences(ObjectID objectId, ClassID classId, ULONG cObjectRefs, ObjectID objectRefIds[]) { return S_OK; };
HRESULT STDMETHODCALLTYPE RootReferences(ULONG cRootRefs, ObjectID rootRefIds[]) { return S_OK; };
HRESULT STDMETHODCALLTYPE ExceptionThrown(ObjectID thrownObjectId) { return S_OK; };
HRESULT STDMETHODCALLTYPE ExceptionSearchFunctionEnter(FunctionID functionId) { return S_OK; };
HRESULT STDMETHODCALLTYPE ExceptionSearchFunctionLeave(void) { return S_OK; };
HRESULT STDMETHODCALLTYPE ExceptionSearchFilterEnter(FunctionID functionId) { return S_OK; };
HRESULT STDMETHODCALLTYPE ExceptionSearchFilterLeave(void) { return S_OK; };
HRESULT STDMETHODCALLTYPE ExceptionSearchCatcherFound(FunctionID functionId) { return S_OK; };
HRESULT STDMETHODCALLTYPE ExceptionOSHandlerEnter(UINT_PTR __unused) { return S_OK; };
HRESULT STDMETHODCALLTYPE ExceptionOSHandlerLeave(UINT_PTR __unused) { return S_OK; };
HRESULT STDMETHODCALLTYPE ExceptionUnwindFunctionEnter(FunctionID functionId) { return S_OK; };
HRESULT STDMETHODCALLTYPE ExceptionUnwindFunctionLeave(void) { return S_OK; };
HRESULT STDMETHODCALLTYPE ExceptionUnwindFinallyEnter(FunctionID functionId) { return S_OK; };
HRESULT STDMETHODCALLTYPE ExceptionUnwindFinallyLeave(void) { return S_OK; };
HRESULT STDMETHODCALLTYPE ExceptionCatcherEnter(FunctionID functionId, ObjectID objectId) { return S_OK; };
HRESULT STDMETHODCALLTYPE ExceptionCatcherLeave(void) { return S_OK; };
HRESULT STDMETHODCALLTYPE COMClassicVTableCreated(ClassID wrappedClassId, REFGUID implementedIID, void *pVTable, ULONG cSlots) { return S_OK; };
HRESULT STDMETHODCALLTYPE COMClassicVTableDestroyed(ClassID wrappedClassId, REFGUID implementedIID, void *pVTable) { return S_OK; };
HRESULT STDMETHODCALLTYPE ExceptionCLRCatcherFound(void) { return S_OK; };
HRESULT STDMETHODCALLTYPE ExceptionCLRCatcherExecute(void) { return S_OK; };
HRESULT STDMETHODCALLTYPE ThreadNameChanged(ThreadID threadId, ULONG cchName, WCHAR name[]) override;
HRESULT STDMETHODCALLTYPE GarbageCollectionStarted(int cGenerations, BOOL generationCollected[], COR_PRF_GC_REASON reason) { return S_OK; };
HRESULT STDMETHODCALLTYPE SurvivingReferences(ULONG cSurvivingObjectIDRanges, ObjectID objectIDRangeStart[], ULONG cObjectIDRangeLength[]) { return S_OK; };
HRESULT STDMETHODCALLTYPE GarbageCollectionFinished(void) { return S_OK; };
HRESULT STDMETHODCALLTYPE FinalizeableObjectQueued(DWORD finalizerFlags, ObjectID objectID) { return S_OK; };
HRESULT STDMETHODCALLTYPE RootReferences2(ULONG cRootRefs, ObjectID rootRefIds[], COR_PRF_GC_ROOT_KIND rootKinds[], COR_PRF_GC_ROOT_FLAGS rootFlags[], UINT_PTR rootIds[]) { return S_OK; };
HRESULT STDMETHODCALLTYPE HandleCreated(GCHandleID handleId, ObjectID initialObjectId) { return S_OK; };
HRESULT STDMETHODCALLTYPE HandleDestroyed(GCHandleID handleId) { return S_OK; };
HRESULT STDMETHODCALLTYPE InitializeForAttach(IUnknown *pCorProfilerInfoUnk, void *pvClientData, UINT cbClientData) { return S_OK; };
HRESULT STDMETHODCALLTYPE ProfilerAttachComplete(void) { return S_OK; };
HRESULT STDMETHODCALLTYPE ProfilerDetachSucceeded(void) { return S_OK; };
HRESULT STDMETHODCALLTYPE ReJITCompilationStarted(FunctionID functionId, ReJITID rejitId, BOOL fIsSafeToBlock) { return S_OK; };
HRESULT STDMETHODCALLTYPE GetReJITParameters(ModuleID moduleId, mdMethodDef methodId, ICorProfilerFunctionControl *pFunctionControl) { return S_OK; };
HRESULT STDMETHODCALLTYPE ReJITCompilationFinished(FunctionID functionId, ReJITID rejitId, HRESULT hrStatus, BOOL fIsSafeToBlock) { return S_OK; };
HRESULT STDMETHODCALLTYPE ReJITError(ModuleID moduleId, mdMethodDef methodId, FunctionID functionId, HRESULT hrStatus) { return S_OK; };
HRESULT STDMETHODCALLTYPE MovedReferences2(ULONG cMovedObjectIDRanges, ObjectID oldObjectIDRangeStart[], ObjectID newObjectIDRangeStart[], SIZE_T cObjectIDRangeLength[]) { return S_OK; };
HRESULT STDMETHODCALLTYPE SurvivingReferences2(ULONG cSurvivingObjectIDRanges, ObjectID objectIDRangeStart[], SIZE_T cObjectIDRangeLength[]) { return S_OK; };
HRESULT STDMETHODCALLTYPE ConditionalWeakTableElementReferences(ULONG cRootRefs, ObjectID keyRefIds[], ObjectID valueRefIds[], GCHandleID rootIds[]) { return S_OK; };
HRESULT STDMETHODCALLTYPE GetAssemblyReferences(const WCHAR *wszAssemblyPath, ICorProfilerAssemblyReferenceProvider *pAsmRefProvider) { return S_OK; };
HRESULT STDMETHODCALLTYPE ModuleInMemorySymbolsUpdated(ModuleID moduleId) { return S_OK; };
HRESULT STDMETHODCALLTYPE DynamicMethodJITCompilationStarted(FunctionID functionId, BOOL fIsSafeToBlock, LPCBYTE pILHeader, ULONG cbILHeader) { return S_OK; };
HRESULT STDMETHODCALLTYPE DynamicMethodJITCompilationFinished(FunctionID functionId, HRESULT hrStatus, BOOL fIsSafeToBlock) { return S_OK; };
HRESULT STDMETHODCALLTYPE DynamicMethodUnloaded(FunctionID functionId) { return S_OK; };
};这是一个实现了 ICorProfilerCallback9 的示例。注意 QueryInterface 中,前面的匹配 GUID 语句必须只能匹配当前版本相等或更低的接口。否则 CoreCLR 会判断错版本从而导致调用不存在的 Callback 而崩溃。
CorProfiler.cpp
CorProfiler::CorProfiler() : refCount(0), corProfilerInfo(nullptr)
{
}
CorProfiler::~CorProfiler()
{
if (this->corProfilerInfo != nullptr)
{
this->corProfilerInfo->Release();
this->corProfilerInfo = nullptr;
}
}
HRESULT STDMETHODCALLTYPE CorProfiler::Initialize(IUnknown *pICorProfilerInfoUnk)
{
HRESULT queryInterfaceResult = pICorProfilerInfoUnk->QueryInterface(__uuidof(ICorProfilerInfo15), reinterpret_cast<void **>(&this->corProfilerInfo));
if (FAILED(queryInterfaceResult))
{
return E_FAIL;
}
return S_OK;
}
HRESULT STDMETHODCALLTYPE CorProfiler::Shutdown()
{
if (this->corProfilerInfo != nullptr)
{
this->corProfilerInfo->Release();
this->corProfilerInfo = nullptr;
}
return S_OK;
}
HRESULT STDMETHODCALLTYPE CorProfiler::ThreadCreated(ThreadID threadId)
{
return S_OK;
}
HRESULT STDMETHODCALLTYPE CorProfiler::ThreadDestroyed(ThreadID threadId)
{
return S_OK;
}
HRESULT STDMETHODCALLTYPE CorProfiler::ThreadAssignedToOSThread(ThreadID managedThreadId, DWORD osThreadId)
{
return S_OK;
}
HRESULT STDMETHODCALLTYPE CorProfiler::ThreadNameChanged(ThreadID threadId, ULONG cchName, WCHAR name[])
{
return S_OK;
}
HRESULT STDMETHODCALLTYPE CorProfiler::UnmanagedToManagedTransition(FunctionID functionId, COR_PRF_TRANSITION_REASON reason)
{
return S_OK;
}这里下面 return S_OK 的接口理论上和上面在头文件里实现的是同样类型的。只不过我们后续要用到这些 Callback,所以写在 cpp 文件里。
注意这里在 initialize 中使用了 pICorProfilerInfoUnk 的 QueryInterface 的方法。这个方法是用来获取 ICorProfilerInfo15 对象的。ICorProfilerInfo15 也是 CoreCLR Profiling API 提供的一个关键对象,它含有许多我们会用到的有用函数。
Hook 函数 Enter/Leave/Tailcall 事件
做完这些准备工作后,我们可以开始实现最基本的函数调用监听了。
汇编 Stub
由于性能原因,CoreCLR 要求你的函数 Enter/Leave/Tailcall(下文简称 ELT)事件回调必须是 naked 的。即必须由汇编直接编写。Medium的文章中给出了一个汇编 stub,用于调用进真正的 C++ 函数。我这里删除了一些无用的寄存器,贴出:
Windows
EXTERN EnterStub:PROC
EXTERN LeaveStub:PROC
EXTERN TailcallStub:PROC
_text SEGMENT PARA 'CODE'
ALIGN 16
PUBLIC EnterNaked
EnterNaked PROC FRAME
PUSH RAX
.PUSHREG RAX
PUSH RCX
.PUSHREG RCX
PUSH RDX
.PUSHREG RDX
SUB RSP, 20H
.ALLOCSTACK 20H
.ENDPROLOG
CALL EnterStub
ADD RSP, 20H
POP RDX
POP RCX
POP RAX
RET
EnterNaked ENDP
ALIGN 16
PUBLIC LeaveNaked
LeaveNaked PROC FRAME
PUSH RAX
.PUSHREG RAX
PUSH RCX
.PUSHREG RCX
PUSH RDX
.PUSHREG RDX
SUB RSP, 20H
.ALLOCSTACK 20H
.ENDPROLOG
CALL LeaveStub
ADD RSP, 20H
POP RDX
POP RCX
POP RAX
RET
LeaveNaked ENDP
ALIGN 16
PUBLIC TailcallNaked
TailcallNaked PROC FRAME
PUSH RAX
.PUSHREG RAX
PUSH RCX
.PUSHREG RCX
PUSH RDX
.PUSHREG RDX
SUB RSP, 20H
.ALLOCSTACK 20H
.ENDPROLOG
CALL TailcallStub
ADD RSP, 20H
POP RDX
POP RCX
POP RAX
RET
TailcallNaked ENDP
_text ENDS
ENDLinux
.section .text
.globl EnterNaked
.globl LeaveNaked
.globl TailcallNaked
EnterNaked:
push %rax
push %rcx
push %rdx
call EnterStub
pop %rdx
pop %rcx
pop %rax
ret
LeaveNaked:
push %rax
push %rcx
push %rdx
call LeaveStub
pop %rdx
pop %rcx
pop %rax
ret
TailcallNaked:
push %rax
push %rcx
push %rdx
call TailcallStub
pop %rdx
pop %rcx
pop %rax
ret
这三个 Naked 函数就是我们待会会传给 CoreCLR 的 Callback。同时,我们也需要在 C++ 端实现 EnterStub/LeaveStub/TailcallStub 三个方法。
编写 Stub 方法
首先定义一下 PROFILER_STUB 宏(你不需要和我写的一模一样,只要确保这部分函数签名一致即可):
if !defined(_WIN32)
#define PROFILER_STUB EXTERN_C __attribute__((visibility("hidden"))) void STDMETHODCALLTYPE
#else
#define PROFILER_STUB EXTERN_C void STDMETHODCALLTYPE
#endif然后是 Stub 方法:
PROFILER_STUB EnterStub(FunctionIDOrClientID functionId, COR_PRF_ELT_INFO eltInfo)
{
}
PROFILER_STUB LeaveStub(FunctionIDOrClientID functionId, COR_PRF_ELT_INFO eltInfo)
{
}
PROFILER_STUB TailcallStub(FunctionIDOrClientID functionId, COR_PRF_ELT_INFO eltInfo)
{
}
你可以在这里面实现更多的逻辑,我们暂且先不写东西。
注册回调
我们前面提到 ICorProfilerInfo 是一个重要的对象。他的一个功能就是注册函数监听器回调。
代码如下。
DWORD eventMask =
COR_PRF_MONITOR_ENTERLEAVE |
COR_PRF_ENABLE_FUNCTION_ARGS |
COR_PRF_ENABLE_FUNCTION_RETVAL |
COR_PRF_ENABLE_FRAME_INFO;
auto hr = this->corProfilerInfo->SetEventMask(eventMask);
if (hr != S_OK)
{
LOG("ERROR: Profiler SetEventMask failed (HRESULT: 0x%08X)", (unsigned)hr);
}
hr = this->corProfilerInfo->SetEnterLeaveFunctionHooks3WithInfo(EnterNaked, LeaveNaked, TailcallNaked);
if (hr != S_OK)
{
LOG("ERROR: Profiler SetEnterLeaveFunctionHooks3WithInfo failed (HRESULT: 0x%08X)", (unsigned)hr);
}我们先设置一些 EventMask,让 CoreCLR 为我们准备充足的信息并确保调用我们的Callback。具体定义:COR_PRF_MONITOR Enumeration - .NET Framework | Microsoft Learn
除了用于设置函数监听器以外,它还可以控制很多其他功能,例如监听线程变化等。
然后使用 SetEnterLeaveFunctionHooks3WithInfo 函数注册我们的三个 Naked 回调。
启用 Profiler
至此我们基础的代码已经完型了,接下来需要挂载 Profiler。通过设置环境变量,可以让 CoreCLR 识别我们的 Profiler。样例如下。
```
$env:CORECLR_ENABLE_PROFILING=1
$env:CORECLR_PROFILER="{a2648b53-a560-486c-9e56-c3922a330182}"
$env:CORECLR_PROFILER_PATH="./build/windows/x64/debug/sw2tracer.dll"这里的 CORECLR_PROFILER_PATH 和 CORECLR_PROFILER 换成你自己的路径和 GUID 即可。
未完待续...
Comments