1

I'm trying to learn RISC-V and wrote a factorial function, but it's running into a simulator error, hinting at a possible infinite loop. I'm not really sure how to debug my code at the moment, and was wondering if people could drop hints on what I might be doing wrong.

Thank you!

.globl factorial

.data
n: .word 8

.text
main:
    la t0, n  #t0 corresponds to n
    lw a0, 0(t0)
    jal ra, factorial

addi a1, a0, 0
addi a0, x0, 1
ecall # Print Result

addi a1, x0, '\n'
addi a0, x0, 11
ecall # Print newline

addi a0, x0, 10
ecall # Exit

factorial:
    addi sp sp -16
    sw s0 0(sp)  #s0 corresponds to i, initialised to n
    sw s1 4(sp)  #s1 corresponds to factorial that will be constantly updated; also initialised to 1
    sw s2 8(sp)  #s2 corresponds to n, or t0
    sw s3 12(sp)
    add s2 x0 t0
    addi s1 x0 1
    add s0 x0 t0
    addi s3 x0 4  #this is what we use to decrement s0 (i) by 1 each time
    loop: 
    beq s0 x0 exit
    mul s1 s1 s0
    sub s0 s0 s3
    j loop
    exit:
    lw s0 0(sp)
    lw s1 4(sp)
    lw s2 8(sp)
    lw s3 12(sp)
    addi sp sp 16
    ret
Peter Cordes
  • 328,167
  • 45
  • 605
  • 847
  • Why do you subtract 4 from `s0` at the end of each iteration? In any case, call the function with an argument of 1 and single-step through it and see what happens. If that works as expected, try 2 and see if that works. Keep going until you find that it does something unexpected. – Michael Sep 24 '21 at 13:17

1 Answers1

2

How to debug a factorial function I'm writing in RISC-V assembly?

I'm not really sure how to debug my code at the moment,

So, you want to learn debugging.  Yes, this is a mandatory skill for any programming, especially assembly language.  Debugging is an interactive process, which is poorly suited to a Q & A format.

The normal approach is to run every line of code and verify that it does what you think it is doing.  If any line of code doesn't do what you expect, then that's what to work on.  Every single line has to work properly or else the program won't run properly.

In assembly we call this single stepping.  The behavior of an instruction includes both the effect it has on the registers, and the effect on memory — the state of the program, if you will.  We verify that the registers and memory are all updated as expected, and also that it goes on to the proper next instruction — flow of control is equally important, and can also meet or mismatch expectations.

We should write small amounts of code and run them to verify they are working, rather than write a whole program and then see if it compiles/assembles and runs.  Much better to build incrementally onto working code, as often debugging a small piece of new code will change your understanding (e.g. of the machine, or of the problem you're trying to solve), and hence make writing the rest easier.

When testing some code, debug verify it (single step) with the smallest possible input first: so for factorial, for example, run it first with f(1) get that working, then work on f(2).

When doing function calls, you'll need to switch roles, first considering the caller, then the callee, then the caller again.  At the point of the call, verify the arguments are in the right registers and the stack, if applicable.  At the first instruction of the called function, verify the same, and also make note of the return address value (in the ra register) and the stack pointer value (sp), before stepping through the function.  When you store values to memory, verify the values and where they go, so that when you later use memory you are getting what you expect.

Erik Eidt
  • 23,049
  • 2
  • 29
  • 53