XlogicX Blog

Assembly_is_Too_High_Level_-_Propeller_NOPs,_like_if_never

This post is not about x86/64, as the title goes, this is about the Propeller architecture.

But first, here's some x86 (hahaha):
xchg
Note that if you encode the XCHG instruction with EAX, EAX, it would just be 0x90; the exact same machine code as:

Sorry to have to put everyone though so much x86 for a Propeller post, but I think there's a very strong likeness here; the propeller chip also does not have dedicated machine-code for the NOP. I find a propeller NOP to be much more interesting.

 

About Propeller ASM and Machine-Code structure:

Unlike x86, machine instructions for Propeller are not variable in size; each instruction occupies 32-bits. There are 6 bits that specify which instruction (up to 64 possible different instructions), 2 flag setting bits (Zero and Carry), a bit that specifies if the destination operand gets written to, a bit specifying immediate or memory location mode, 4 bits for flag tests, 9 bits for the destination, and another 9 bits for the source operand. This totals up to 32-bits. EVERY instruction has this structure. It may be complicated at first to divide up these bits, but once you get past that, this architecture is incredibly simple and elegant.

Below is a table that describes the WRBYTE instruction. I picked this one because it is the first binary instruction (000000):

Although, for fun, We can look at the last binary instruction (WaitVid):

 

 

Conditional Checks (-CON-):

4 bits isn't that much, let's spell out each of them. Where:
Z is Zero Flag
C is Carry Flag
* is AND
! is NOT
= is Equal

0001: !C*!Z
0010: !C*Z
0011: !C
0100: C*!Z
0101: !Z
0110: C!=Z
0111: !Cv!Z
1000: C*Z
1001: C=Z
1010: Z
1011: !CvZ
1100: C
1101: Cv!Z
1110: CvZ

I skipped 2 special values. 1111 means IF_ALWAYS, most instructions default to this condition if not specified (you will see this above in the tables for WRBYTE and WaitVid. That leaves 0000: IF_NEVER. In Assembly, you can literally put IF_NEVER in front of any instruction you feel like, and it effectively makes it a NOP! In machine code you would just 0 out those 4 bits.

Although, if you actually write 'NOP' in assembly, it has to pick one of these variations. If you were to assemble a NOP and the disassemble it, it would look like a carefully crafted WRBYTE instruction.

PoC:

Let's look at some propeller Assembly code with a NOP, a couple WRBYTEs and a corresponding machine-code dump. I drew arrows from the assembly to the corresponding machine-code for clarity.

Note that the memory model is Little-Endian (data goes backward per byte). Let's break apart the 2nd wrbyte instruction into its pecies, as it's the main non-zero instruction in the dump. I will take the hex and separate it out into the INSTR, ZCRI, CON, DEST, and SRC bit fields.

Hex 0x03063C00 = Convert from Little Endian -> 0x003C0603 = binary 0000 0000 0011 1100 0000 0110 0000 0011

Clearer grouping = 000000 0000 1111 000000011 000000011

000000 = WRBYTE

0000 = Don't set Zero or Carry flags, R = 0 means Write in the context of this instruction, R = 1 would be Read, I = 0 means non-immediate value (pointer)

1111 = Same as saying if_always

000000011/000000011 = This refers to the 3rd (counting from zero) 32 bit value in memory (including instructions!). Looking at the assembly, nop is the 0th, 'wrbyte variable, variable' is 1st, 'wrbyte start, start' is 2nd, and our variable declaration is the 3rd. So that's how this happens. Propeller interestingly does not have a distinct code/data isolation.

 

Create a valid NOP without a NOP:

We see that assembling a NOP produces 0x00000000 in machine code. Let's see what it would take to write some assembly that would produce this same value without using an actual NOP in assembly (spoiler in the above image). I will first break down our zeros into their fields:

000000 = WRByte

0000 = Don't write flags, already Write mode with 0 bit, and we have to use pointer mode; not immediate

0000 = we can only get this set of 0's by specifying the if_never prefix

000000000/000000000 = This is the tricky part (kind of). This has to refer to the 0th 32-bit memory value (our first instruction). Turns out that our Start label contains just that (just as it likewise would in x86 assembly). This means that we can use our 'Start' label for the Dest and the Source operands. We end up with 'if_never wrbyte Start, Start.' As seen in the machine dump, this did infact produce our all 0's machine code identical the real NOP.

So many flavors of NOP:

If you set the CON field to 0000 on any instruction (either directly in machine code or by using the if_never), you effectively make it a NOP. A NOP takes 4 clock ticks. Instructions that take otherwise significantly more clock ticks should only take the 4 clock ticks if you shove an if_never into the CON field.

Post Disclaimer: None of this is really useful knowledge, it's just fun to hack for hacking's sake.