Reading parameters value with the .NET Profiling APIs

Christophe Nasarre
4 min readNov 16, 2021

Introduction

From the list of arguments with their type, it becomes possible to figure out their value when a method gets called. The rest of this post describes how to access method call parameters and get the value of numbers and strings.

Where are my parameters?

When you pass COR_PRF_ENABLE_FUNCTION_ARGS to ICorProfilerInfo::SetEventMask, the runtime prepares a COR_PRF_FUNCTION_ARGUMENT_INFO structure before your enter callback is called:

I have to admit that the Microsoft Docs did not really help me to figure out what is the meaning of each field of this structure because the word “range” is very confusing here…

Based on my experiments, numRanges gives you the number of parameters; including the implicit this parameter in case of a non-static method. It is different from the signature that we have already parsed from the metadata where this is not mentioned. The ranges fields is an array of COR_PRF_FUNCTION_ARGUMENT_RANGE ; one per parameter:

The startAddress points to where the parameter value is stored in memory.

However, in addition to the FunctionID, you only receive a COR_PRF_ELT_INFO in your enter callback. You need to call ICorProfilerInfo3:: GetFunctionEnter3Info to get the corresponding COR_PRF_FUNCTION_ARGUMENT_INFO you are interested in. As often with COM, you need to call a first time to get the size of the buffer to allocate and a second time to fill it up:

Before iterating on the parameters, you need to deal with non-static method and their implicit this parameter stored in pArgumentInfo->ranges[0]:

Next, write a loop to iterate on each parameter based on the parameter count obtained previously from the metadata:

Simple type parameters case

The GetObjectValue() helper function looks like the following:

The way to get the value of a parameter really depends on its type. I know that a length is provided by the COR_PRF_FUNCTION_ARGUMENT_INFO structure but I did not used it except for sanity check.

The value for simple types are easy to compute because they are mostly stored at the given address :

The other types require more knowledge about how the CLR stores their value.

The string case

This is the first reference type we meet and, as for all reference types, the given address points to the memory where the reference (i.e. address of the object in the managed heap) is stored. It allows you to check against null parameter before looking at the “real” managed reference:

At that point, you need to know how an instance of a reference type instance is stored by the CLR in the managed heap. Hopefully, Sergey Tepliakov, a software engineer at Microsoft, has provided a lot of details about that, especially where does the address stored by a managed reference point to:

It means that you have to skip the Method Table pointer pointed to by the address you have. This applies to any reference types instance!

But for our string current case, you still need to know how a string is stored (i.e. its length followed by the buffer of UTF16 characters). I recommend that you read the post from Matt Warren about the subject because it also covers a lot of interesting details related to string implementation. However, you should simply rely on the implementation details provided by the CLR via ICorProfilerInfo3:: GetStringLayout2:

These two variables give you the offsets to use to access both the string size and the beginning of the array of WCHAR storing each character.

As shown in this table, you need to skip 8/4 bytes to read the length. It is just the confirmation that you need to jump over the address of the Method Table stored as a 64/32 bit value (i.e. an address in x64/x86). The length itself is stored as a 32 bit number (4 bytes) both in x64 an x86. So the array containing the consecutive UTF16 characters just follows (i.e. its offset from the reference address is 12/8 bytes). For example, here is what you get in Visual Studio Memory panel with the reference to the 3 characters “CLR” string for a 64 bit application:

With this information in hand, it is easy to detect empty strings or copy the UNICODE string into a simple char* buffer:

The GetPointerAfterNBytes function simply helps me dealing with pointer arithmetic in C++

The next post will describe how to get the value of array parameters and the basics of extracting fields value from a reference type instance.

References

--

--

Christophe Nasarre

Loves to understand how things work (MVP Developer Technologies)