Assembly (MIPS, SPIM)/Assembly (GCC, MIPS Linux O32),
319 313 309 305 270 256 255 241 233 bytes
.ascii"!This program wasn't written in "
.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.".
This program wasn't written in SPIM, it was built for MIPS! on SPIM (on a little endian machine).
This program wasn't written in MIPS, it was built for SPIM! on a real MIPS64 machine from the GCC Compiler Farm (specifically
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_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
.words 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
| = 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
# 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.
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 "
# 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)
li $a2, 54
# do_syscall(sys_write, string_1, strlen(string_1))
# 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
li $a2, 5
# do_syscall(sys_write, string_2, strlen(string_2))
# 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
# 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)