Search code examples
cassemblyreverse-engineeringriscv

Learning RISC-V assembly and need help converting a C loop


I'm learning how to convert RISC-V assembly code to C, and I don't understand this conversion. A few questions I have:

  1. why is t1 being initialized to 6 instead of 0?
  2. We're using bne to compare t1 and t2, but we were taught to use the opposite, meaning the C code would be while (t1 == t0). It appears this opposite technique was used for if (t1 ==0). Why?
  3. Lastly, why is addi t0, t0, -1 used instead of sub?

Any insight is so greatly appreciated. My classroom environment is fast paced and not very question friendly, so I'm nervous to ask in lecture.

main:
    # Tests simple looping behavior
    li t0, 60
    li t1, 0
loop:
    addi t1, t1, 5
    addi t0, t0, -1
    bne t1, t0, loop
    bne t1, zero, success
failure:
    li a0, 0
    li a7, 93 
    ecall
    
success:
    li a0, 42 
    li a7, 93
    ecall

This is the answer I was given:

int main(){

    int t0 = 60;
    int t1 = 6;
    
    while(t1 != t0){
        t1 = t1 + 5;
        t0 = t0 - 1;
    }

    if(t1 == 0){
        int a0 = 0;
        return 0;
    }else{
        int a0 = 42;    
        return 0;    
    }
}

We were taught to use the opposite of bne/beq when converting C to RISC-V, so it's confusing why the 'correct' C conversion for this RISC-V assembly would include while (t1 != t0).

Initializing t1 to 6 also doesn't make any sense to me. It looks to be clearly loaded to 0 with 'li t1, 0'.


Solution

  • The C doesn't match the RISC-V assembly very well.
    You're right, the asm clearly does t1 = 0 not t1 = 6. Perhaps a typo or OCR error if this material was printed out and scanned back in or something. Or maybe someone just changed their mind about the starting point for the loops but forgot to update one of the versions. 0 and 6 do both lead to the loop terminating without wrapping around (or actually C signed-overflow undefined behaviour since this isn't unsigned.)

    Also, those ecalls are _exit(0) and _exit(42), not return statements. The asm doesn't use its return address.


    The rest looks right, though. Try it yourself, single-stepping through the asm vs. through the C program, watching registers or variables change, respectively. It should be clear that execution stays in the loop until they're equal, i.e. while they're not equal.

    The most direct C representation for an idiomatic asm loop like that is do{}while(t1 != t0); with the condition at the bottom like the asm has. (Showing it as a for or while loop depends on the initializers making the condition true so the loop body runs at least one iteration.) See Why are loops always compiled into "do...while" style (tail jump)?

    (3) Try changing the asm to use subi with an immediate 1. Most assemblers will reject that because RISC-V doesn't have subi, except maybe as a pseudo-instruction. It doesn't have a FLAGS / condition-code register, so subtracting is exactly equivalent to adding a negative, and RISC-V always sign-extends immediate operands. The only thing it would gain from a subi opcode is being able to change the value by +4096 .. -4095 instead of addi's range of -4096 .. +4095 with its 12-bit immediate. That's obviously not worth having another opcode for.

    MIPS is very similar to RISC-V in this way (no FLAGS and not having a hardware subi); see What is the "relationship" between addi and subi? about that and the ISA vs. software assembler pseudo-instruction design choices.