You're not crazy, the code really is that weird!
Adding two pointers basically never makes sense, so this is kind of a trick question.
The equivalent C does look wrong / insane:
intptr_t *A = ...; // in $s6
A[1] = (intptr_t)&A[0];
f = A[1] + (intptr_t)&A[0];
Note that signed overflow is undefined behaviour in C, so it's legal to compile it to a MIPS add
which will trap on signed overflow. If we'd used uintptr_t
, the required overflow semantics would be wrapping / truncation, which add
doesn't implement.
(Real-world C compilers for MIPS always use addu
/ addiu
, not add
, even for signed int, because undefined behaviour means anything is allowed, including wrapping. It's even required if you compile with gcc -fwrapv
. Since MIPS is a 2's complement machine, addu
is the same binary operation as add
, it differs only in not trapping on signed overflow: when the inputs are the same sign but the output has a different sign from that.)
In terms of C that will compile back to something closer to the given asm, or at least represent every asm operations with a C temporary var:
I used GNU C register-global variables instead of function args so the function body would be using the actual correct register (and without cluttering the asm with extra instructions to save/restore and init those registers). So this lets me get GCC to make a block of asm that has s
registers as inputs and outputs, instead of the normal calling convention.
#include <stdint.h>
register intptr_t *A asm("s6");
// register char *B asm("s7"); // unused, no idea what type makes sense
register intptr_t f asm("s0");
void foo()
{
volatile intptr_t *t0_ptr = A+1; // volatile forces store and reload
intptr_t t1 = (intptr_t)A;
*t0_ptr = t1; //sw $t1, 0($t0) //A[1] = &A[0]
intptr_t t0_int = *t0_ptr; //lw $t0, 0($t0) //$t0 = &A[0]
f = t0_int + t1; //add $s0, $t1, $t0 //f = &A[0] + &A[0]
//return f;
}
Note that $t0
gets used for 2 different things here, with different types: one being a pointer into the array, and the other a value from the array. I expressed this with two different C variables, because that's how things normally go. (Compilers will reuse the same register for a different variable when one is "dead" before / as the other one is needed.)
The resulting asm from GCC5.4 for MIPS, with options to make MARS-compatible asm: -O2 -march=mips3 -fno-delayed-branch
. MIPS3 means no load delay slots, like the code in the question which uses the lw
result in the instruction after the load. (Godbolt compiler explorer)
foo:
move $2,$22 # $v0, $s6 pointless copy into $v0
sw $22,4($2) # A[1] = A
lw $3,4($22) # v1 = A[1]
addu $16,$22,$3 # $s6 = (intptr_t)A + A[1]
j $31
nop # branch-delay slot
(GCC uses numeric register names, not the ABI names like $s?
for call-preserved, $t?
for call-clobbered scratch regs, etc. http://www.cs.uwm.edu/classes/cs315/Bacon/Lecture/HTML/ch05s03.html has a table.)
Another way to write it, with less rigour: the important difference is the lack of volatile
to force the compiler to reload.
void bar() {
A[1] = &A[0];
f = A[1] + (intptr_t)&A[0];
}
bar:
move $2,$22 # still a useless copy
sw $22,4($2)
sll $16,$22,1 # 2 * (intptr_t)A; no reload, just CSE the store value.
j $31
nop
Of course there would be other ways to express this, e.g. using A
as an array of pointers instead of an array of intptr_t
, int
, or int32_t
.
I chose integers because C pointer types magically scale by the type width when you do pointer addition.