Skip to content

libco: riscv64: Add RISC-V64 Linux support - #13

Open
cosmo0920 wants to merge 1 commit into
edsiper:masterfrom
cosmo0920:cosmo0920-riscv64-linux-support
Open

libco: riscv64: Add RISC-V64 Linux support#13
cosmo0920 wants to merge 1 commit into
edsiper:masterfrom
cosmo0920:cosmo0920-riscv64-linux-support

Conversation

@cosmo0920

@cosmo0920 cosmo0920 commented Aug 7, 2026

Copy link
Copy Markdown
Contributor

The implementation details as follows.

The backend works because it saves exactly the state that the RISC-V ELF ABI says must survive an ordinary C function call. From each coroutine’s perspective, co_switch() behaves like a function that pauses and later returns normally.

State Why it is preserved
ra Becomes the resume address for the suspended coroutine

The register classification comes from the RISC-V ELF psABI calling convention.

The C wrapper passes two context pointers to the assembly routine:

previous_handle = co_active();
co_active_handle = handle;
co_switch_riscv64(handle, previous_handle);

By the normal RISC-V calling convention:

a0 = target context
a1 = current context

The assembly first saves the current coroutine:

sd ra, 0(a1)
sd sp, 8(a1)
sd s0, 16(a1)
...
sd s11, 104(a1)

It then loads the target coroutine:

ld ra, 0(a0)
ld sp, 8(a0)
ld s0, 16(a0)
...
ld s11, 104(a0)
ret

The final ret jumps to the ra loaded from the target context.

For a suspended coroutine, that ra points immediately after its earlier call to co_switch_riscv64(). Execution therefore continues as though co_switch() had just returned.

For a new coroutine, co_create() initializes:

context->ra = (uintptr_t) co_entry_trampoline;
context->sp = (uintptr_t) context + total_size;
context->entrypoint = entrypoint;

Its first ret enters the trampoline on the newly allocated stack. The trampoline invokes the actual coroutine entry function and aborts if that function unexpectedly returns.

The backend does not need to save a0a7 or t0t6. They are caller-saved registers. When the compiler generates a call to co_switch(), it already spills any live caller-saved values that will be needed afterward.

Likewise:

  • gp is fixed by the ABI and must not be modified.
  • tp identifies the current OS thread. Fluent Bit coroutines remain on the same pthread, so it must remain unchanged.
  • Standard vector registers are caller-saved. RVV code must spill live vector values before calling co_switch().

This backend therefore assumes that a coroutine is never migrated to another pthread, matching Fluent Bit’s current coroutine model.

The compiler exposes the selected ABI through predefined macros:

  • LP64: neither floating-point macro is defined.
  • LP64F: __riscv_float_abi_single
  • LP64D: __riscv_float_abi_double

LP64D uses 64-bit operations:

fsd fs0, 112(a1)
...
fld fs0, 112(a0)

LP64F uses 32-bit operations:

fsw fs0, 112(a1)
...
flw fs0, 112(a0)

Soft-float LP64 does not preserve floating-point registers because that ABI does not classify them as callee-saved program state.

On a typical DC ROMA II LP64D environment, GCC should report:

gcc -dM -E - </dev/null |
grep -E '__riscv($|_)|__riscv_float_abi'

Expected relevant definitions include:

#define __riscv 1
#define __riscv_xlen 64
#define __riscv_float_abi_double 1

These macros select riscv64.c and enable its double-precision register-save path.

RISC-V requires the stack pointer to remain 16-byte aligned. Both the requested stack size and context header are rounded to multiples of 16. Linux RV64 malloc() supplies suitably aligned memory, so the calculated top of the allocation remains 16-byte aligned.

The stack grows downward from that address, while the saved context resides at the bottom of the allocation:

low address
┌──────────────────────────┐
│ Saved context            │
│ ra, sp, s0-s11, FP state │
├──────────────────────────┤
│ Coroutine stack          │
│                          │
│                 ↓ growth │
└──────────────────────────┘ ← initial sp, 16-byte aligned
high address

The test commit does more than verify that switching does not crash:

  1. The primary coroutine loads known values into s0s11, fs0fs11, and fcsr.
  2. It switches to a worker coroutine.
  3. The worker deliberately overwrites those registers with zero.
  4. The worker switches back.
  5. The primary coroutine compares every restored register with its original pattern.

Correct return flow also implicitly verifies ra and sp: an incorrect value for either normally returns to the wrong instruction or accesses the wrong stack and crashes.

RV64E and LP64Q are excluded because their register sets or floating-point widths require different layouts. They continue to the generic backend instead of silently using an incompatible context representation.


Additional Contexts

The corresponding canary PR of Fluent Bit is here:

fluent/fluent-bit#12243.

That PR succeeded to work with the actual RISC-V64 Linux laptop which is called as DC-ROMA II that has 8-cores of RISC-V 64bit processors.

The implementation details as follows.

The backend works because it saves exactly the state that the RISC-V ELF ABI says must survive an ordinary C function call. From each coroutine’s perspective, `co_switch()` behaves like a function that pauses and later returns normally.

| State | Why it is preserved |
|---|---|
| `ra` | Becomes the resume address for the suspended coroutine |
| `sp` | Restores that coroutine’s independent stack |
| `s0`–`s11` | Integer registers classified as callee-saved |
| `fs0`–`fs11` | Floating-point callee-saved registers under LP64F/LP64D |
| `fcsr` | Preserves floating-point rounding mode and exception flags |

The register classification comes from the [RISC-V ELF psABI calling convention](https://riscv-non-isa.github.io/riscv-elf-psabi-doc/).

The C wrapper passes two context pointers to the assembly routine:

```c
previous_handle = co_active();
co_active_handle = handle;
co_switch_riscv64(handle, previous_handle);
```

By the normal RISC-V calling convention:

```text
a0 = target context
a1 = current context
```

The assembly first saves the current coroutine:

```asm
sd ra, 0(a1)
sd sp, 8(a1)
sd s0, 16(a1)
...
sd s11, 104(a1)
```

It then loads the target coroutine:

```asm
ld ra, 0(a0)
ld sp, 8(a0)
ld s0, 16(a0)
...
ld s11, 104(a0)
ret
```

The final `ret` jumps to the `ra` loaded from the target context.

For a suspended coroutine, that `ra` points immediately after its earlier call to `co_switch_riscv64()`. Execution therefore continues as though `co_switch()` had just returned.

For a new coroutine, [co_create()](C:/Users/cosmo/Documents/GitHub/fluent-bit/lib/flb_libco/riscv64.c:173) initializes:

```c
context->ra = (uintptr_t) co_entry_trampoline;
context->sp = (uintptr_t) context + total_size;
context->entrypoint = entrypoint;
```

Its first `ret` enters the trampoline on the newly allocated stack. The trampoline invokes the actual coroutine entry function and aborts if that function unexpectedly returns.

The backend does not need to save `a0`–`a7` or `t0`–`t6`. They are caller-saved registers. When the compiler generates a call to `co_switch()`, it already spills any live caller-saved values that will be needed afterward.

Likewise:

- `gp` is fixed by the ABI and must not be modified.
- `tp` identifies the current OS thread. Fluent Bit coroutines remain on the same pthread, so it must remain unchanged.
- Standard vector registers are caller-saved. RVV code must spill live vector values before calling `co_switch()`.

This backend therefore assumes that a coroutine is never migrated to another pthread, matching Fluent Bit’s current coroutine model.

The compiler exposes the selected ABI through predefined macros:

- LP64: neither floating-point macro is defined.
- LP64F: `__riscv_float_abi_single`
- LP64D: `__riscv_float_abi_double`

LP64D uses 64-bit operations:

```asm
fsd fs0, 112(a1)
...
fld fs0, 112(a0)
```

LP64F uses 32-bit operations:

```asm
fsw fs0, 112(a1)
...
flw fs0, 112(a0)
```

Soft-float LP64 does not preserve floating-point registers because that ABI does not classify them as callee-saved program state.

On a typical DC ROMA II LP64D environment, GCC should report:

```bash
gcc -dM -E - </dev/null |
grep -E '__riscv($|_)|__riscv_float_abi'
```

Expected relevant definitions include:

```text
```

These macros select [riscv64.c](C:/Users/cosmo/Documents/GitHub/fluent-bit/lib/flb_libco/libco.c:18) and enable its double-precision register-save path.

RISC-V requires the stack pointer to remain 16-byte aligned. Both the requested stack size and context header are rounded to multiples of 16. Linux RV64 `malloc()` supplies suitably aligned memory, so the calculated top of the allocation remains 16-byte aligned.

The stack grows downward from that address, while the saved context resides at the bottom of the allocation:

```text
low address
┌──────────────────────────┐
│ Saved context            │
│ ra, sp, s0-s11, FP state │
├──────────────────────────┤
│ Coroutine stack          │
│                          │
│                 ↓ growth │
└──────────────────────────┘ ← initial sp, 16-byte aligned
high address
```

The test commit does more than verify that switching does not crash:

1. The primary coroutine loads known values into `s0`–`s11`, `fs0`–`fs11`, and `fcsr`.
2. It switches to a worker coroutine.
3. The worker deliberately overwrites those registers with zero.
4. The worker switches back.
5. The primary coroutine compares every restored register with its original pattern.

Correct return flow also implicitly verifies `ra` and `sp`: an incorrect value for either normally returns to the wrong instruction or accesses the wrong stack and crashes.

RV64E and LP64Q are excluded because their register sets or floating-point widths require different layouts. They continue to the generic backend instead of silently using an incompatible context representation.

Signed-off-by: Hiroshi Hatake <hiroshi@chronosphere.io>
@cosmo0920
cosmo0920 force-pushed the cosmo0920-riscv64-linux-support branch from 56b952f to 58640a5 Compare August 7, 2026 06:35
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant