openlang

Reading RISC-V calling conventions at -O0 or -O2

Calling conventions stick better when they're read off real compiler output than when someone memorizes a register table. A good exercise is a C function with three int parameters that returns their sum, compiled for RV32 once at -O0 and once at -O2.

At -O0 the listing is noisy. The function sets up a frame, stores a0, a1 and a2 to the stack, loads them back, adds, and leaves the result in a0 before ret. At -O2 it collapses to two add instructions and ret. One convention, two very different listings, and the comparison is the lesson. The arguments arrive in the argument registers either way, and the result leaves in a0 either way. Everything else was the compiler's own bookkeeping.

Then make the function call another function and ask why ra now has to be saved. ret is just jalr x0, 0(ra), and the inner call overwrites ra.

Skip the unoptimized step and people tend to think the stack traffic is part of the convention. It isn't. What I'm unsure about is the order. Starting at -O2 is cleaner, but then the frame setup turns up later as a surprise.

后续讨论 5

顺着彼此的依据读下去
  1. 固定链接

    If you want a convention people can poke at without a disassembler, the Lua C API is a nice warm-up. The function and then its arguments go on the virtual stack in order, you call lua_call(L, nargs, nresults), and the results take their place on the stack. It's a calling convention spelled out as library calls, and a wrong count shows up right away when you print lua_gettop(L). Oddly enough, the register version seems less mysterious after that.

  2. 固定链接

    One limit is worth stating up front. The C standard specifies no registers and no stack frames at all. The convention comes from the platform ABI, so the listing teaches the RISC-V psABI rather than C. That matters the first time someone compiles the same function for another target and concludes the language changed.

  3. 固定链接

    The nested-call version makes the better second listing, because the frame comes back for a reason. int outer(int x) { return inner(x) + 1; } at -O2 for rv32 comes out roughly like this, give or take instruction order.

    outer:
            addi    sp, sp, -16
            sw      ra, 12(sp)
            call    inner
            addi    a0, a0, 1
            lw      ra, 12(sp)
            addi    sp, sp, 16
            ret

    Sixteen bytes of stack to keep four bytes of ra, because the psABI wants sp aligned to sixteen. That alignment is what students ask about, and the -O0 listing shows it too without explaining it.

同一个问题的其他角度

  1. Notation first, machine later

    APL was a notation before it was a program. Iverson used it on paper to describe algorithms, and the 1962 book A Programming…

    回复 1最近活动 apl
  2. Where Pharo code actually lives

    Poking at Pharo after a long time with Ruby, and I'm stuck on something embarrassingly basic for someone used to files. I mad…

    回复 1最近活动 smalltalk
  3. A useful review comment on a retain cycle names the owner

    “Just use weak everywhere” is a review comment the author can't really answer. The usual case is a child object holding a del…

    回复 1最近活动 swift