Assembly (MIPS, SPIM)/Assembly (GCC, MIPS Linux O32), 319 313 309 305 270 256 255 241 233 bytes
.globl main
main:la $5,p+9
lh $3,-7($5)
li $6,54
jal w
sub $5,5
li $6,5
jal w
xor $3,5
w:move $2,$3
syscall
jr $ra
.data
p:.word 0xf0fa4,0x5350494d
.ascii"!This program wasn't written in "
.word 0x4d495053
.ascii", it was built for "
Try it online!
Since it reuses argc
, it must be called with no arguments. This is implied given that "neither program should take any input from the user.".
Outputs This program wasn't written in SPIM, it was built for MIPS!
on SPIM (on a little endian machine).
Outputs This program wasn't written in MIPS, it was built for SPIM!
on a real MIPS64 machine from the GCC Compiler Farm (specifically gcc22
).
Explanation
Two words: Endian memes.
MIPS is big endian, while SPIM is little endian. This is perfect for us.
The first magic number is a "hybrid" encoding of the sys_write
syscall. We store the SPIM syscall number in the most significant 16 bits, and the Linux syscall number in the least significant 16 bits.
We then do a halfword load on the address + 2.
On big endian, this would load the least significant bytes, while on little endian, it would load the most significant bytes. That is why you never cast pointers in C if you want your code to be portable. 😏
This is actually what we want: we will load the Linux syscall on big endian, and the SPIM syscall on little endian.
This is also used to obtain the sys_exit
syscall number.
On SPIM, the syscall for sys_write
is 15, while the syscall for sys_exit
is 10.
On Linux, the syscall for sys_write
is 4004, while the syscall for sys_exit
is 4001.
15 XOR 10 is 4004 XOR 4001 is 5, so sys_write
XOR 5 is sys_exit
, quick maffs! 🤓
Therefore, to magically convert both the SPIM and MIPS syscalls from sys_write
to sys_exit
, we can simply XOR by 5!
The second two magic numbers are "SPIM" and "MIPS" in big endian packed ASCII, respectively. These are inline with the strings.
"SPIM" being simply "MIPS" backwards means that it will be reversed on little endian. On MIPS, this will be "SPIM" and "MIPS", respectively, while on SPIM, it will be "MIPS" and "SPIM", respectively.
The reason we store the strings backwards is because it is shorter, and more importantly, both GAS and SPIM will insert alignment bytes for .word
s if they are not 4 byte aligned which mangle our output.
By coincidence, the second half of the string ends one byte after a word boundary, and the first half of the string is one byte off from being aligned, so we just merge them together to remove all padding (and allowing us to merge the last !
into the next .ascii
block).
| = word boundary, @ = padding, $ = EOS, W = .word
| | | | | | | | | W | | | | | | W |
@This program wasn't written in MIPS, it was built for @SPIM!$
| W | | | | | | | | | W | | | | | |
SPIM!This program wasn't written in MIPS, it was built for $
Note that while it looks like I could save space by writing 0xf0fa4
in base 10, I can't, because for some reason, SPIM doesn't like comma separated numbers unless they are in hex.
Ungolfed version (not compatible with SPIM because SPIM doesn't allow arithmetic in .word
):
.data
hybrid_syscall:
# sys_write syscall. SPIM is the high 16 bits, MIPS Linux
# is the low 16 bits.
# By doing a halfword read, we can automatically read the
# right syscall number thanks to endianness.
.word (15 << 16) | 4004 # -9(string_1)
string_2: # "SPIM!"
# "SPIM" in big endian packed ASCII.
# SPIM will store this backwards as "MIPS".
.word ('S' << 24) | ('P' << 16) | ('I' << 8) | 'M'
# This "!" also aligns string_1 so the "MIPS" is aligned.
.ascii "!"
string_1: # "This program wasn't written in MIPS, it was built for "
.ascii "This program wasn't written in "
# "MIPS" in big endian packed ASCII.
# SPIM will store this backwards as "SPIM".
.word ('M' << 24) | ('I' << 16) | ('P' << 8) | 'S'
.ascii ", it was built for "
.text
.globl main
main:
# Pointer to string_1
la $a1, string_1
# Load the Linux sys_write syscall on big endian and the
# SPIM sys_write syscall on little endian.
# *(uint16_t *)(hybrid_syscall + 2) == sys_write
# Uses a relative offset from string_1.
lh $v1, -9+2($a1)
# strlen(string_1)
li $a2, 54
# do_syscall(sys_write, string_1, strlen(string_1))
jal do_syscall
# Pointer to string_2
# In the golfed version, we subtract five from the last
# pointer, because a1 is always returned unmodified.
la $a1, string_2
# strlen(string_2)
li $a2, 5
# do_syscall(sys_write, string_2, strlen(string_2))
jal do_syscall
# Do an XOR to magically convert sys_write to sys_exit.
xor $v1, 5
# FALLTHROUGH: do_syscall(sys_exit)
# Runs a syscall.
# C equivalent: syscall(v1, ...)
# v1: syscall number
# varargs: passed to syscall as-is
do_syscall:
# Move the syscall number into v0.
move $v0, $v1
# As long as our program is called with no arguments
# which is implied by the rules, we can reuse argc to pass
# 1 as the first argument, which corresponds to stdout.
# li $a0, 1
# write(STDOUT_FILENO, string, length)
# exit(1)
syscall
# Return
jr $ra
argv[0]
? \$\endgroup\$