Let's continue REing the branch instructions.
0xe0 2897 2330 branch if predicate 0xe1 1036 913 branch ? 0xe2 1707 1188 branch if not predicate 0xe3 120 89 ??? [XXX] 0xe4 2020 695 call if predicate 0xe6 92 84 call if not predicate 0xe8 694 552 ret 0xea 248 1 abra 0xef 23932 13521 bnop 0xf0 161 329 ??? [XXX] 0xff 918 642 exit [intr] imm16
The next thing to look at is clearly opcode e1. Let's look for some usage examples.
00000360: 6cce400f C add $a25 $a25 0x1 00000360: 6a06405f mov $sr96 $a25 00000360: 65d00003 mov $a26 0x3 00000360: e1ffffc0 bra1 0x35f [unknown: 000001c0] 00000361: e1000fa0 bra1 0x368 [unknown: 000001a0] 00000361: 6bcc00af mov $a25 $x48 00000361: 6bcc80af mov $a25 $x50 00000361: e1000fa0 bra1 0x368 [unknown: 000001a0] 00000362: ef0001ff bnop 00000362: e1000da0 bra1 0x368 [unknown: 000001a0] 00000362: 6bcd00af mov $a25 $x52 00000362: 6bcd80af mov $a25 $x54 00000363: e1000ba0 bra1 0x368 [unknown: 000001a0] 00000363: ef0001ff bnop 00000363: e1000ba0 bra1 0x368 [unknown: 000001a0] 00000363: 6bce00af mov $a25 $x56 00000364: 6bce80af mov $a25 $x58 00000364: e10009a0 bra1 0x368 [unknown: 000001a0] 00000364: ef0001ff bnop 00000364: e10009a0 bra1 0x368 [unknown: 000001a0] 00000365: 6bcf00af mov $a25 $x60 00000365: 6bcf80af mov $a25 $x62 00000365: e10007a0 bra1 0x368 [unknown: 000001a0] 00000365: ef0001ff bnop 00000366: fff90000 B exit intr 0 00000366: ef0001ff bnop 00000366: ef0001ff bnop 00000366: ef0001ff bnop 00000367: ef0001ff bnop 00000367: 4fffffff anop 00000367: bf000007 vnop 00000367: ef0001ff bnop 00000368: 7e967f17 B shr $a18 $a25 -0x1e 00000368: 63d6bfff xor $a26 $a26 -0x1 00000368: e8ffffff ret 00000368: 42ce7447 and $a25 $a25 $a26
Hm. Lots of branches, few other instructions. Looks like a switch() statement that reads from the right $x register. Note how the delay slots are used. Since the branch instructions are mostly identical, they must use some sort of counter that's incremented/decremented as a side effect of every branch. It's very likely that this counter is $sr96.
Digging around in other pieces confirms this - however, it seems that the counter can be any of $sr96-$sr99, and it's selected by bits 0-1 and bits 3-4 (both set to the same value) of the opcode. In addition, the first branch in series has bits 5-8 set to 1110, while subsequent ones have 1101. Note that, if 5-8 selects a $c bitfield as usual, 1110 corresponds to the always-0 bit, while 1101 corresponds to the unknown flag that's always been set by branches so far. Seems we'll soon figure out its purpose.
So, let's run an 0xe10009c0 branch with $sr96 set to 0xdead.
Branch not taken, $sr96 is now set to 0xdeac. Bit 13 of $c0 is not set.
How about the same instruction with $sr96 set to 0? Branch not taken, $sr96 set to 0, bit 13 of $c0 set.
0xe10009e0 with $sr96 set to 0xdead: branch taken, $sr96 is 0xdeac, $c0 bit 13 is not set.
0xe10009e0 with $sr96 set to 1: branch taken, $sr96 is 0, $c0 bit 13 is set.
I rechecked the e0 opcode just in case - it has no effect on $sr96, and $c0 bit 13 is always set, regardless of $sr96 value.
So it seems that e1 does two independent operations: it executes the normal e0 conditional branch, then it decrements $sr96-$sr99 if non-0, then sets bit 13 $c0 to 1 if it is now 0, and to 0 otherwise. I suppose that explains the sequence, more or less...
Let's just look which bitfield determines the $sr96-$sr99 register used.
Apparently, like for $c, bits 0-1 determine the output $sr to be used, bits 3-4 determine the input. Bit 2 doesn't disable the $sr write, however.
We also see that e3 is used a bit - there's a good chance it could be just e1 with a negated predicate. Again, a test confirms that.
If the instruction set is really that orthogonal, e5 and e7 could be calls with $sr96+ decrease. Another test, another confirmation.
How about return? There's a tiny chance e9 could be a ret with $sr96 manipulation...
Nope, not this time.
Looking around the code for e3 usage also shows it's used as a loop, with the $sr being a "remaining iterations" counter. We'll call $sr96-$sr99 the loop registers for that reason: $l0-$l3.
Time for f0 opcode. We've seen it used in the "clear all registers" sequence already, and it seems to be used 4 times to zero out 4 registers. Seems obvious. Let's try 0xf007dead.
0000f580: 0000dead 00000000 00000000 00000000
Yeah. And let's try 0xf018dead:
0000f580: 00000000 00000000 00000000 0000dead
0000f680: 00008000 00008000 00008000 0000a000
Interesting one. Seems the $cX/$lX registers are sort of tied to each other, with bit 13 of $cX being 1 iff $lX is 0. However, it's a loose coupling - we've already seen that a direct mov from $a can mess that up. This is probably what the "pointless" always-false first branch is for, and why the loop/switch counter has 1 added to it before the sequence.
0xe0 2897 2330 branch if predicate 0xe1 1036 913 branch if predicate + loop 0xe2 1707 1188 branch if not predicate 0xe3 120 89 branch if not predicate + loop 0xe4 2020 695 call if predicate 0xe5 - - call if predicate + loop 0xe6 92 84 call if not predicate 0xe7 - - call if not predicate + loop 0xe8 694 552 ret 0xea 248 1 abra 0xef 23932 13521 bnop 0xf0 161 329 mov $l imm16 0xff 918 642 exit [intr] imm16
Elapsed time: 2hShare on Twitter Share on Facebook