Arguments descriptor arrays in Dart

@cryptax
3 min readSep 19, 2024

--

While reading Dart assembly, you may have noticed lines such as the following:

ldr             x4, [PP, #0x68]  ; [pp+0x68] List(5) [0, 0x1, 0x1, 0x1, Null]
bl #0x1cc040 ; [dart:core] Uri::parse

The first line retrieves a list object from the Object Pool, and stores its address in register x4. Then, the second line calls the method parse of class Uri.

Why do we have this dummy list object? Why is it always stored in x4?

Short Explanation: this is an arguments descriptor array.

Positional and named arguments

In Dart, functions may have positional or named arguments, and those arguments can be optional or mandatory. Optional arguments are marked with square brackets []. Named arguments are marked by curly braces {}.

The prototype of parse() is: Uri parse(String uri, [int start=0, int? end]). Precisely, we have a first positional and mandatory argument, uri, then 2 optional positional arguments, start and end. The default value of start is 0. Not important in this article, but end is nullable, which explains the ? in the prototype.

Arguments descriptor array

The arguments descriptor array is explained in a source code comment:

// An arguments descriptor array consists of the type argument vector length (0
// if none); total argument count (not counting type argument vector); total
// arguments size (not counting type argument vector); the positional argument
// count; a sequence of (name, position) pairs, sorted by name, for each named
// optional argument; and a terminating null to simplify iterating in generated
// code.
class ArgumentsDescriptor : public ValueObject {
public:
explicit ArgumentsDescriptor(const Array& array);

// Accessors.
intptr_t TypeArgsLen() const; // 0 if no type argument vector is passed.
intptr_t FirstArgIndex() const { return TypeArgsLen() > 0 ? 1 : 0; }
intptr_t CountWithTypeArgs() const { return FirstArgIndex() + Count(); }
intptr_t Count() const; // Excluding type arguments vector.
intptr_t Size() const; // Excluding type arguments vector.
intptr_t SizeWithTypeArgs() const { return FirstArgIndex() + Size(); }
intptr_t PositionalCount() const; // Excluding type arguments vector.
intptr_t NamedCount() const { return Count() - PositionalCount(); }

What’s important?

  • The type argument vector describe the types of arguments. If none is supplied, we get the value 0.
  • We can deduce the number of named arguments from the total count of arguments and substract the number of positional arguments (see the NamedCount() method).

Registers

Dart assigns specific registers for some tasks / objects. For example, on ARM64 platforms, we already know it uses x26 to point to the current thread (THR), x27 for the Object Pool (PP) and x28 to point to the heap.
There is yet another reserved registers, x4, for the arguments descriptor.

See Dart SDK source code:

const Register ARGS_DESC_REG = R4;  // Arguments descriptor register.

It’s r4 for ARM32, x4 for ARM64 and r10 for x86–64.

Putting pieces together

Let’s put together what we learned to understand the following lines:

ldr             x4, [PP, #0x68]  ; [pp+0x68] List(5) [0, 0x1, 0x1, 0x1, Null]
bl #0x1cc040 ; [dart:core] Uri::parse

At offset PP+0x68, we have an arguments descriptor array. As we are on ARM64, we store it in x4. This array is useful to Uri::parse to understand which arguments is supplied to it.

The arguments descriptor array contains 0 type argument vector (not supplied) and a single argument. As Uri::parse has 1 single mandatory argument and 2 optional arguments, we know that in this case it will receive only the mandatory argument (the URI).

Thanks to Vyacheslav Egorov for his explanations!

— Cryptax

--

--

@cryptax

Mobile and IoT malware researcher. The postings on this account are solely my own opinion and do not represent my employer.