Level 56: Call and Return

Open level →

Task

Goal: program a function call: jal (J-format) jumps to a label and writes the next-instruction address (PC + 4) into rd; jalr jumps through a register — the target (rs1 + imm) with bit 0 cleared, the low address bit is always 0. Build sum_to_n: a call with the argument in x5, the function sums 5+4+3+2+1 into x6 and returns via jalr x0, 0(ra). The .end halt sits at the return point — in the middle of the program; the parser assembles the code below it too.

The first program that can call parts of itself: a function with an argument, a body, and a return. The schematic still does not change — execution runs on the reference core, as on levels 54 and 55. The new instructions are jal (the J-format) and jalr:

  • jal rd, label jumps to the label and simultaneously writes the next-instruction address (PC + 4) into rd — the return address. jal ra, label is a call; ra is the standard name of x1, the return-address register.
  • jalr rd, imm(rs1) jumps through a register: the target = rs1 + imm, with bit 0 of the sum cleared in hardware, and only then is the multiple-of-4 check applied. jalr x0, 0(ra) is the return from a function.

The canonical sum_to_n program — a function that sums the numbers from 1 to n (the argument in x5, the result in x6):

        addi x5, x0, 5      # 0:  the argument
        jal  ra, sum_to_n   # 4:  the call (ra = 8)
        .end                # 8:  the halt at the return point — the function is below
sum_to_n:
        addi x6, x0, 0      # 12
again:  beq  x5, x0, back   # 16
        add  x6, x6, x5     # 20
        addi x5, x5, -1     # 24
        beq  x0, x0, again  # 28
back:   jalr x0, 0(ra)      # 32

Note the .end: here it sits in the middle of the program. The halt is the place where jalr returns; the function is written below it, and the parser calmly assembles the code after the directive. The assembler emits the jal x0, 0 word at address 8, and the labels and instructions after it get their addresses as usual.

Level tasks:

  1. Pack the jal word in "Word from fields" (the J-format appeared on level 56) and take apart the offset split: imm[20]=w31, imm[10:1]=w30:21, imm[11]=w20, imm[19:12]=w19:12 — the offset base, as with beq, is the address of the instruction itself.
  2. Assemble sum_to_n in the Assembler tab. The pseudo-instructions j label (→ jal x0, label) and ret (→ jalr x0, 0(ra)) are also available from level 56.
  3. Before the first "Check" answer the question about ra: jal sits at address 4 — count PC + 4 yourself.
  4. Experiment in the Execution block with jalr targets: jalr x0, 1(x1) with x1 = 0x10 — the jump lands at 0x10 (bit 0 cleared); jalr x0, 2(x1) — the target 0x12 is not a multiple of 4, the error ERR_INSTR_ADDRESS_MISALIGNED.

Solution for self-check

First count ra and the cycles on paper. The solution below is a set of checkpoints.

What you should get: sum_to_n returns correctly: ra = 8 (the .end address), x6 = 15, x5 = 0; the predicted 25 cycles (3 + 6 + 5 × 3 + 1) match the trace, and jalr at PC 32 brings the PC back to address 8.

Step 1 — the return address. jal at address 4 writes 4 + 4 = 8 into ra: after the return, execution continues at address 8, where .end sits. So at the end of the run ra = 8, PC = 8, and the status is halted. The same number is asked by the predict before the first check.

Step 2 — count cycles. The entry is 3 instructions (addi, jal; the .end halt adds no cycle). The function: the beq guard executes 6 times (5 times not taken, the sixth is taken and leads to back), the body is 5 iterations × (add + addi + the jump-back beq), and the final jalr. Total: 3 + 6 + 5 × 3 + 1 = 25 cycles. The result: x6 = 1+2+3+4+5 = 15, x5 = 0.

Step 3 — take jalr apart. The target = (rs1 + imm) & ~1: bit 0 is always cleared, so jalr x0, 1(x1) with x1 = 0x10 jumps to 0x10 (the sum 0x11 became 0x10). But an odd bit 1 is not fixed: jalr x0, 2(x1) gives the target 0x12 — the core stops with ERR_INSTR_ADDRESS_MISALIGNED. A write to x0 does not block the jump: rd = x0 only skips saving the return address.

Step 4 — compare with the trace. Find in the trace: jal at PC 4 writes x1 = 8 and sends the PC to 12; five returns of beq to again; jalr at PC 32 returns the PC to 8; the .end halt record without a cycle count. Later in the course comes the stack: the function will get its own saved registers, and ra will start living in memory.

Check yourself

Why does jalr clear bit 0 of the target?

So that an odd rs1 + imm sum cannot create a misaligned address: bit 0 is cleared in hardware before the multiple-of-4 check. Bit 1 is not fixed — jalr x0, 2(x1) with x1 = 0x10 targets 0x12 and gives ERR_INSTR_ADDRESS_MISALIGNED.