ARMv4T/ARMv5TE+ with libc, pop {pc}
bug, for old scoring system, 42 40 bytes, old score -4294967252 -4294967255
1009 e28f 4002 e92d e001 e04f f004 e49d
e002 e201 ff11 e12f 4674 2021 f7ff fffe
3401 d1fa bc01 4700
ARMv4T, the ISA used in the ARM7TDMI, famous for powering the Game Boy Advance, introduced the Thumb instruction set.
It is a 16-bit (later mixed with 32-bit) instruction set encoding that is more compact, but more limited, than the standard 32-bit ARM instruction set. I write a lot of my golfs in it, because it is torture fun.
Basically, the idea is, since ARM instructions are always 4 byte aligned, if you jump to an odd address, the CPU will switch to Thumb execution mode at the address rounded down, and vice versa for an even address.
One cool thing that ARM lets you do is pop
the return address directly into the program counter from the stack, resulting in incredibly easy stack cleanup (similar to leave; ret
in x86, but you can pop ALL registers at once)
push {..., lr} @ put the return address in the link register on the stack
pop {..., pc} @ pop the return address into the program counter to return
On ARMv5TE and later, this will magically switch between ARM and Thumb state.
However, ARMv4T had an infamous bug here.
They forgot to implement Thumb switching: pop {pc}
will ignore the lowest two bits and not switch to Thumb mode. Source: ARM docs
So all we have to do is write a polyglot that will do something different if it is in ARM or Thumb state, which is easy because the encodings are entirely different.
.text
.arm
.globl main
main:
add r1, pc, #9
push {r1, lr}
sub lr, pc, r1
@ define to simulate ARMv4T behavior
#ifdef SIMULATE_ARMV4T
b simulate_armv4t_bug
#else
pop {pc}
#endif
.Larmv4t_polyglot:
and lr, r1, #2 @ b .Lthumb_entry; (skipped)
bx r1
.thumb
.Lthumb_entry:
mov r4, lr
.Lloop:
movs r0, #'!'
bl putchar
adds r4, #1
bne .Lloop
pop {r0}
bx r0
#ifdef SIMULATE_ARMV4T
.arm
.align 2
simulate_armv4t_bug:
pop {r12}
@ clear thumb bit
bic r12, r12, #3
bx r12
#endif
This program starts in ARM state.
We need r1
to contain the address of .Larmv4t_polyglot
, with the Thumb bit set. (So .Larmv4t_polyglot | 1
, or .Larmv4t_polyglot + 1
)
Therefore, we add 9 to the program counter (which points 8 bytes after the currently executing instruction because of questionable design decisions) to get 16 bytes, or 4 instructions, ahead, with the Thumb bit set.
add r1, pc, #9
Push r1
and lr
to the stack. The former is so we can perform the pop {pc}
bug, and the latter is to save the return address so we can use lr
as a temp register and make function calls.
push {r1, lr}
Subtract r1
from pc
. This results in lr
being -1
.
sub lr, pc, r1
Then, we pop the value from r1
into the program counter.
An ARMv5+ processor will execute the polyglot in Thumb mode, but an ARMv4T processor will ignore the low 2 bits (rounding down) and execute .Larmv4t_polyglot
as ARM code.
To simulate the buggy behavior, define SIMULATE_ARMV4T
which will replace pop {pc}
with a veneer that jumps to that address in ARM mode.
pop {pc}
Now, it is time for our polyglot.
ARM mode: Set lr
to 0
by setting it to r1 & 2
. Since that contains the address of .Larmv4t_polyglot
plus one, we know the second bit is clear.
Then, jump to the polyglot again on ARMv4T (the address is still in r1
), this time using bx
, which DOES switch to Thumb mode.
.Larmv4t_polyglot:
and lr, r1, #2
bx r1
Thumb mode: Jump to .Lthumb_entry
.
.Larmv4t_polyglot.thumb:
b .Lthumb_entry
@ skipped
Now, we are in Thumb mode on both machines, we can do our output loop.
We first need to make a copy, because lr
will be overwritten.
mov r4, lr
Then, we loop calling putchar('!')
, until r4 += 1
== 0.
.Lloop:
movs r0, #'!'
bl putchar
adds r4, #1
bne .Lloop
On ARMv5TE+, r4
will be -1
, so the loop will only run for one iteration, but on ARMv4T, r4
will be 0
, so it will run 4294967296
times (0x100000000
) until it wraps back around to zero.
I could technically make this infinite, but the rules say that it must eventually finish.
Equivalent C:
uint32_t r4 = lr;
do {
putchar('!');
} while (++r4);
Finally, do an ARMv4T compatible return using the "Thumb interworking" hack: We pop into a low register and bx
to it to return from main
.
pop {r0}
bx r0
Program size: 40 bytes
Output length in ARMv5TE+: 1 byte
Output length in ARMv4T: 4294967296 bytes
Score: -4294967255
ARMv4/ARMv5TE+ with libc, same bug, for new scoring system, 42 bytes, score 0
Mostly the same, but instead of dumping near-infinite '!'
s, both will dump 42 bytes to stdout
. However, for a maximum negative score, all you have to do is change movs r5, #42
to movs r5, #128
for a score of -86.
Machine code:
1009 e28f 4002 e92d e080 e3b0 f004 e49d
e002 e201 ff11 e12f 252a 4676 19a8 f7ff
fffe 3d01 d1fa bc01 4700
(ARMv4T simulator left out, it is the same)
Assembly:
.text
.arm
.globl main
main:
add r1, pc, #9
push {r1, lr}
movs lr, #128
pop {pc}
.Larmv4t_polyglot:
and lr, r1, #2 @ b .Lthumb_entry; (skipped)
bx r1
.thumb
.Lthumb_entry:
movs r5, #42
mov r6, lr
.Lloop:
adds r0, r5, r6
bl putchar
subs r5, #1
bne .Lloop
pop {r0}
bx r0
The main differences are that instead of initially setting lr
to -1
, we set lr
to 128
, and that our print loop will always run 42 times, subtracting 1 from the char it prints each time.
The equivalent C loop looks like this:
uint32_t r5 = 42, r6 = ARMv4T ? 0 : 128;
do {
putchar(r5 + r6);
} while (--r5);
ARMv5TE+ output
termux ~/golf $ clang -m32 valid-through-the-ages.S
termux ~/golf $ ./a.out | hexdump -C | cut -d' ' -f 3-19
aa a9 a8 a7 a6 a5 a4 a3 a2 a1 a0 9f 9e 9d 9c 9b
9a 99 98 97 96 95 94 93 92 91 90 8f 8e 8d 8c 8b
8a 89 88 87 86 85 84 83 82 81
ARMv4T (simulated) output:
As stated before, the compiler option is just to emulate the bug on my device. It is not needed on actual hardware.
termux ~/golf $ clang -m32 valid-through-the-ages.S -DSIMULATE_ARMV4T
termux ~/golf $ ./a.out | hexdump -C | cut -d' ' -f 3-19
2a 29 28 27 26 25 24 23 22 21 20 1f 1e 1d 1c 1b
1a 19 18 17 16 15 14 13 12 11 10 0f 0e 0d 0c 0b
0a 09 08 07 06 05 04 03 02 01
Program size: 42 bytes
Difference in output length: None
Differing bytes: 42.
Score: 0
zzzz
andaaaa
, does that give me-4
? That's what it seems like to me. \$\endgroup\$