The MIPS R4000, part 12: Calling convention

The Windows NT calling convention for the MIPS R4000 is similar to the other major MIPS calling conventions, but calling conventions for the MIPS are like snowflakes: Despite being made of the same underlying materials, no two are completely alike.

The short version of the parameter passing is that the first four parameters are passed in registers a0 through a3, and the remaining parameters go on the stack after a 16-byte gap. The 16-byte gap represents the home space for the register-based parameters. We've seen this convention before, in the x64 calling convention. Even if a function accepts fewer than four parameters, you must still provide a full 16 bytes of home space.

Things get weird when you mix in 64-bit values or floating point. The way to think about it is as if you were creating a C structure whose members are all the parameters, in order, except that any types smaller than a 32-bit value are promoted to a 32-bit value. If you have a 64-bit value (either integer or floating point), you may need to insert padding to get the parameter to be properly aligned.

Once you've laid out your parameters in the structure, you load the first sixteen bytes into a0 through a3, and the rest go on the stack. However, if a parameter that would normally be passed in a0 through a3 turns out to be a non-variadic floating point value, then it is stored in $f12/$f13 (for the first floating point value) or $f14/$f15 (for the second), and the corresponding integer register is left unused.

Here are some examples:

void f(int a, char b, short c, int d, int e);

Offset Parameter Passed as
00 int a a0
04 int b a1
08 int c a2
0C int d a3
10 int e 0x10(sp)

void f(float a, int b, double c, int d);

Offset Parameter Passed as
00 float a f12
04 int b a1
08 double c f14
0C f15
10 int e 0x10(sp)

void f(int a, double b, float c);

Offset Parameter Passed as
00 int a a0
04 padding
08 double b f12
0C f13
10 float c 0x10(sp)

void f(int a, ...);
f(1, 2, 0.0, 3);

Offset Parameter Passed as
00 1 a0
04 2 a1
08 0.0 a2
0C a3
10 3 0x10(sp)

In this last example, the floating point double-precision value 0.0 is a variadic parameter (matches the ... part of a function prototype), so it gets passed in the integer registers even though it's a floating point value. That's because one of the first things that variadic functions do is spill all their variadic register parameters onto the stack so they form a contiguous array of bytes. Passing all variadic parameters in integer registers means that this spilling can be done without knowing the types of the parameters. (Which is a good thing because the types of the parameters are frequently not known at compile time.)

The last wrinkle is if you're calling a function with no prototype. In that case, you don't know whether a parameter is variadic or not. If the parameter is a floating point value, then you have to pass it in both an integer register and a floating point register, because you don't know where the callee is going to look for it.

f(1, 2, 0.0, 3); // no prototype

Offset Parameter Passed as
00 1 a0
04 2 a1
08 0.0 a2 and f12
0C a3 and f13
10 3 stack

This explains the importance of the rule that if a parameter is passed in a floating point register, then the corresponding integer register is left unused. Without that rule, calling functions with no prototype would be a disaster because the register assignment would be different depending on whether the function takes variadic parameters or not.

With the exception of lightweight leaf functions, every function must include exception unwind codes in the module metadata so that the kernel can figure out what to do if an exception occurs.

A lightweight leaf function is one that can do its work using only the 16 bytes of home space, plus any scratch registers. It cannot move the stack pointer or modify any callee-preserved registers. Furthermore, the return address must remain in the ra register for the duration of the function.

You are allowed to promote your lightweight leaf function to a full function by a technique known as shrink-wrapping, which I described earlier.

(Some of the details of the calling convention are documented on MSDN. The documentation was originally written for Windows CE, but I figure they are still true for Windows NT, because why not reuse the compiler you already have?)

Comments (8)
  1. Daniel says:

    I’m surprised the Microsoft compiler takes so much effort just to allow calling a varargs function without a prototype.
    The C standard does not require this:, paragraph 15 says that functions without prototype are not compatible with varargs functions.
    With gcc, you will get a compiler error if you have both types of declaration (no prototype + with varargs) in the same compilation unit. And it’s undefined behavior if they’re in different compilation units.

    1. It’s so nice of you to cite a standard that wouldn’t exist until 26 years after the compiler was written. Do you have a time machine?

      1. Daniel says:

        OK, I admit I took the C11 standard for the section number.
        But the paragraph in question is basically unchanged from C89; it was in “Function declarators (including prototypes)”.

        Which was a bit earlier than the MIPS R4000. Of course the compiler probably just re-used the same behavior as on other platforms. But at least you can do the job with a smaller time machine ;)

        1. Darran Rowe says:

          Remember that the x64 calling convention defines the same thing.
          “For functions not fully prototyped, the caller will pass integer values as integers and floating-point values as double precision. For floating-point values only, both the integer register and the floating-point register will contain the float value in case the callee expects the value in the integer registers. ”
          So while it is true that C doesn’t allow this, it isn’t like all languages are C, so any programming language that sticks to this ABI, even ones that accept varag unprototyped functions, will work.

      2. Fabian Giesen says:

        And even when newer language standards forbid something, consider that platform ABIs need to work for all programs (and all language standards) supported on a platform. Including the odd K&R C program that (dynamically) links against a libc written against a newer language standard.

        The particular case of unprototyped varargs functions is a source of trouble in every C platform ABI that I’ve ever seen. The SysV x64 ABI definitely included. :)

        1. Jonathan Wilson says:

          In theory since Microsoft controlled the ABI for NT on MIPS and the (at the time they were producing NT on MIPS) only toolchain that produced code for NT on MIPS, they could have just said no to vararg functions with no prototype. But I suspect they supported such things on other platforms like x86 and therefore couldn’t drop support on a new platform.

          1. Remember, if you want to get people to port to your new platform, you want to make it easier for them, not harder. And saying “You must rewrite all your K&R code to be C89-compliant” is not exactly in the “easier” category.

          2. smf says:

            It was probably easier to code the no-prototype case than it was to hold meetings to argue that it shouldn’t be supported, knowing that at some point you’d probably need to implement it anyway.

Comments are closed.

Skip to main content