Expand description
What a function does to read the arguments its own signature does not name.
Design: spec/12-abi-and-runtime.md, which is where the layout below comes from.
A variadic callee has a problem an ordinary one does not. Six of its arguments arrived in
general purpose registers and eight more in vector ones, and it cannot know which of those hold
anything, because what it was passed is a thing only the caller knew. Registers are also not
addressable, and va_arg walks arguments one after another at run time, which is walking
addresses. So the convention says the callee spills all fourteen of them into a block of its own
frame on the way in, and from then on every argument it was passed is somewhere in memory: the
ones that came in registers are in that block, and the ones that did not are in the caller’s
argument area where they were left.
That block is the register save area, and a va_list is four fields saying how far into the
arguments the walk has got:
offset 0 gp_offset bytes into the save area of the next argument from a gpr
offset 4 fp_offset bytes into the save area of the next argument from an xmm
offset 8 overflow_arg_area the next argument that came in the caller's memory
offset 16 reg_save_area the bottom of the save areava_start fills all four in. The two offsets do not start at zero: the arguments the signature
does name took registers too, and they took the first ones, so each offset starts past them.
va_arg is then one question asked at run time, which is whether the offset for its file has run
off the end of the save area. If it has not, the argument is in the save area and the offset
steps on by a slot. If it has, the argument is in the caller’s memory and the overflow pointer
steps on by a word instead.
§Why the layout is exactly the psABI’s and not a convenient one
Nothing outside the function can see the save area, so its shape looks like a private decision.
It is not one, because a va_list is a thing a program hands to another function, and the
function it usually hands it to is vfprintf in the C library, which somebody else compiled and
which walks the list by the rules in the psABI document. So the offsets are the document’s
offsets, the area is the document’s one hundred and seventy six bytes, and the eight bytes
between two general purpose slots and the sixteen between two vector ones are the document’s too.
The upper half of a vector slot is the document’s too, and what is in it is the top of a
_Float128. A slot is sixteen bytes wide because the register is, and a quad is the one type
here that fills one, so the spill writes all sixteen bytes of every vector register and a
va_arg of a quad reads all sixteen back. gcc writes the same sixteen with the same instruction,
which is what makes a list built here readable by a walk somebody else compiled. Anything wider
than a register would be a vector type, which is issue #200 and is not a thing yet.
§What is here and what is next door
va_arg becomes a compare and a branch, and this is where, because a rewrite that needs new
blocks has to happen before selection for the reason crate::expand gives. Everything it needs
is in the list it was handed, so it needs nothing from the frame and can run here.
An aggregate read off a list is the same instruction under another name, because an aggregate is not a value and there is nothing for one result to be, so that one answers where the object is instead. Over two eightbytes it is class MEMORY whatever its members are, which means it is in the caller’s argument area and there is no question to ask about which half of the walk it is in: the overflow pointer says where it is and steps on past it. Sixteen bytes and under arrived in registers, and then the question is the one a scalar asks, with two differences. The object takes a register of each file for each of its eightbytes, so the room in the save area has to be there for all of them at once and the offsets step on by all of them at once. And the halves of it in the save area are not next to each other, so the answer cannot be an address in the area: the eightbytes are copied out into a buffer of the function’s own and the answer is that.
Which file each eightbyte came from is the classification, which is an answer about a C type and not one the size and the alignment give. It arrives on the instruction, worked out by the front end, which is the last thing to hold a type. An object with no slots on it is one the classification sent to the argument area, and that is what tells the two halves below apart.
va_start is the other way round. Three of the four fields it writes are distances into a frame
that does not exist yet, so it stays an instruction as far as crate::lower, which builds it
out of the frame the way it builds an alloca. The spill that fills the save area is written
there for the same reason.
§The other kind of list
Windows has none of that. Its convention counts the two register files as one run of positions, so an argument’s position says which register of either file it is in and the two walks above are one walk. It also gives every argument exactly one eight byte slot whatever it is: anything that is not one, two, four or eight bytes travels as the address of a copy the caller owns, and a float beyond the ones the signature names travels in the general purpose register at its position as well as in the vector one, because a callee with no prototype has no way to know which file to look in.
What that comes to is that every argument a variadic callee was passed is already one contiguous
run of words in the caller’s argument area, since the first four of them are homed in the thirty
two bytes of shadow space the caller reserved above the return address and the rest follow.
There is nothing to gather and nowhere to gather it to. So a va_list is a char * pointing at
the next of those words, va_start is one lea and one store, and va_arg is a load and an
eight byte step with no compare, no branch and no second file. The register save area of the
four field list is, on this convention, the caller’s shadow space, and the callee’s prologue
writes its leftover argument registers into it rather than into a block of its own.
An argument that travelled by reference costs one more load and that is the whole of the
difference: the slot holds the address of the copy rather than the copy. Which arguments those
are is a question about the size and nothing else, so the classification the front end put on a
va_object is not read here at all.
Structs§
- Area
- How big a callee’s register save area is and where its two halves are.
Constants§
- FP_
OFFSET - Where the count of vector register bytes already walked is.
- GP_
OFFSET - Where the count of general purpose register bytes already walked is.
- OVERFLOW
- Where the pointer to the next argument in the caller’s memory is.
- SAVE_
AREA - Where the pointer to the bottom of the register save area is.
- SIZE
- How many bytes the four field
va_listis, which is what ava_copyof one moves. - VECTOR_
SLOT - How wide the slot one vector register is saved in is, which is how wide the register is whatever this actually writes into it.
Functions§
- lists
- Rewrites every
va_arg,va_copyandva_endin the function, and leavesva_startalone.