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