Accessing arrays and class fields with .NET profiling APIs

Christophe Nasarre
5 min readDec 18, 2021

--

Introduction

After getting basic and strings parameters, it is time to look at arrays and reference types.

Accessing managed arrays

You check against null array parameter the same way as for string:

The ELEMENT_TYPE_SZARRAY applies to single dimension arrays including jagged arrays. ELEMENT_TYPE_ARRAY is used for matrice :

Since arrays are reference types, we know that the managed reference points to the address of the Method Table but we need more insights to get the elements. Again, Sergey Tepliakov explains in great details how single dimension arrays are laid out in memory:

The length is stored in front of the elements as you can see in Visual Studio for the following 10 elements integer array:

var ints = new int[] { 0, 1, 2, 3, 4, 5, 6, 7, 8, 9 };

Note that “jagged” arrays (i.e. array of array such as int[][]) are stored the same way: each element of the first array contains a reference to another array:

The layout is a little bit different for matrices (i.e. multi-dimensional arrays) such as this 2 x 4 integers array:

var matrix = new int[,] { { 1, 1, 1, 1 }, { 2, 2, 2, 2 } };

In that case, the total element count appears before each dimension length. The elements are stored row after row.

The profiling API ICorProfilerInfo2::GetArrayObjectInfo gives us all the implementation details we need:

Here is the description of each parameter:

  • an ObjectID (i.e. a reference to an object in the managed heap) corresponding to an array.
  • the number of dimensions (a.k.a. rank) so 1 for ELEMENT_TYPE_SZARRAY array. I will show in a moment how to get it for matrices.
  • an allocated array to receive the size of each dimension
  • an allocated array to receive the lower bound of each dimension; should be 0 for C#
  • the address of the beginning of the elements

So it is easy to detect an empty array: it means that its length is 0:

The next step is to get the value of each array element. It is easy to get the ClassID of a given object by calling ICorProfilerInfo::GetClassFromObject and then ICorProfilerInfo::IsArrayClass will provide the array rank and its elements CorElementType and ClassID.

With these details, iterating over each element to get its value is not that complicated:

The GetElementValue is where you need to use the element type to compute the value but also to know how many byte you need to move forward to look at the next element:

For matrices, it is needed to know the rank ahead of time to allocate the GetArrayObjectInfo out parameters:

The following code shows how to compute each dimension length:

Getting fields of a reference type instance

Since most “basic” types have been covered, it is now time to discuss the case of reference type parameters. Let’s take the following simple class as an example:

On purpose, one property and two fields are defined. The C# compiler translates the automatic property syntax into a backing field to store the value

And the corresponding get/set accessors pair:

So when an instance of this class is passed as a parameter to the ClassParamReturnClass(ClassType obj) method, you should be able to list these three fields and access their value to build the following output:

--> ClassType ClassParamReturnClass (ClassType obj)
| int32 <IntProperty>k__BackingField = 84
| int32 intField = 42
| String stringField = 43
ClassType obj = 0x00000276D0A98E78

When you read Sergey Tepliakov in Managed object internals, Part 4. Fields layout, it sounds quite hard to achieve due to the complicated padding rules dictating where each field is stored in memory. Hopefully, the profiling API will help you a lot with a three steps process:

  1. Get the offset of each field value
  2. Get the name of each field
  3. Get the type of each field and then compute the value using the offset

First, you need the ClassID corresponding to the type of the reference you want to dump and we have seen that ICorProfilerInfo::GetClassFromObject is perfect for that. Then, pass it to ICorProfilerInfo2::GetClassLayout to get the number of fields and their offset within an instance. This API expects you to call it once to get the number of fields and a second time to get the offset that are stored in a buffer you allocate after the first call.

The COR_FIELD_OFFSET structure has a confusing name because it contains more than just the offset:

The ridOfField part gives you the metadata token corresponding to a field as shown in ILSpy:

It will allow you to get its name and the usual binary signature for its type via IMetaDataImport::GetFieldProps.

So you first need to get the IMetaDataImport implementation for the class module:

It is now time to iterate on each field to get its name, type and value for the given object:

The only thing that differs from the blob signature parsing you have seen earlier is that it starts with a “calling convention” (yes I know that we are talking about field and not method!) equal to IMAGE_CEE_CS_CALLCONV_FIELD.

The field value is stored in memory at ulOffset bytes after the address pointed to by the given managed reference.

The next episode will describe how to dump value type instances, the return values and exceptions handling: a good way to start 2022!

References

--

--

Christophe Nasarre

Loves to understand how things work (MVP Developer Technologies)