Implement RISC-V dynamic linking - #323
Conversation
|
As the The updated GitHub Actions also downloads the 32-bit variant to validate RISC-V dynamic linking. |
Evaluate Run 32-bit applications on 64-bit Linux kernel, which is exactly RV32-on-RV64 userspace compatibility, not emulation. |
I'm not sure whether I understand correctly. Do you mean that the proposed changes should be verified on a RISC-V machine? |
See sysprog21/kbox#18 |
446b538 to
44f2f47
Compare
|
RISE RISC-V Runners' documentation explicitly states that binaries must be compiled for riscv64. According to the FAQ - What architectures are supported?.
I created another branch ( Based on both the documentation and my test, it appears that RISE RISC-V runners lack support for 32-bit executables. |
|
RISC-V calling convention:
Item 1: is done by the register allocation phase.The register allocator uses virtual registers (vreg0-vreg7) to allocate reigsters for arguments when encountering a function call. vreg0-vreg7 will be mapped to a0-a7, so first eight arguments are naturally passed to these registers. Since the current shecc only supports up to 8 arguments, no extra arguments need to be passed to the stack. Thus, we can skip this handling. Item 2: is ensured by the RISC-V code generator.When handling the stack pointer, the code generator will guarantee that Item 3:
Therefore, this item can also be considered complete. Further details and explanations can be found in |
2599d74 to
ee9db4a
Compare
de67942 to
1af9140
Compare
|
Since there may be additional requirements to address, I am still keeping this pull request as a draft currently, and will continue to make improvements if necessary. |
5f8b272 to
9be9f62
Compare
Introduce ELF_MACHINE_ARM32 (0x28) and ELF_MACHINE_RV32 (0xf3) to support architecture-specific logic in future developments.
This commit primarily improves the ELF handling and code generator to
enable the compiler to produce a dynamically linked executable targeting
the RISC-V architecture.
- Allow the ELF handling to generate RELA relocation table.
- Use REL relocation when the target architecture is Arm. Othereise,
use RELA relocation for RISC-V.
- Improve GOT generation process.
- Arm: reserve three entries.
- RISC-V: reserve two entries.
- Implement PLT generation for RISC-V.
- The generation process follows the RISC-V ABI. The first PLT entry
uses 8 instructions to call '_dl_runtime_resolve'. The subsequent
entry uses 4 instructions to perform an indirect function call via
GOT.
- Refine the function call handling for the RISC-V code generator.
- Perform a direct call for internal functions
- Otherwise, use PLT table to peform an indirect call for external
functions.
- Enhance the build system:
- Allow the build system to generate dynamically linked compilers when
targeting the RISC-V architecture.
- Detect the sysroot path of the RISC-V GNU toolchain automatically.
Modify the 'update-snapshots' and 'check-snapshots' make targets to include generation and validation of new snapshots for the RISC-V architecture using dynamic linking.
9be9f62 to
14220c1
Compare
About
|
The update workflow now downloads a RISC-V GNU toolchain to provide necessary dependencies and validate the dynamically linked compiler targeting the RISC-V architecture.
Because two architecture-specific makefile fragments contain similar snippets for locating the cross-compilation toolchain path, this commit consolidates them into a shared build logic, thereby reducing code duplication.
A new shell script is introduced to validate whether generated executables targeting RISC-V correct comply with the RISC-V ABI. The tests include: - Parameter Passing: tests function calls with different numbers of arguments. - Stack Alignment: validates whether the stack is always 16-byte aligned when calling a function. - Return Values: confirms if the return value is correct after a function returns. - External Calls: verifies whether dynamically linked programs can call external functions. - Register Preservation: verify whether the contents of function argument registers are properly preserved when calling a function. - Structure Passing: validates if a small structure object can be passed correctly.
- Expand instructions on utilizing dynamic linking for both the Arm and RISC-V architectures. - Describe the stack frame layout for the RISC-V implementation. - Explain caller and callee behaviors when targeting the RISC-V architecture. - Illustrate the RISC-V PLT stub implementation, including assembly code snippets and design intentions. - Add reference links about RISC-V. - glibc implementation of '__dl_runtime_resolve' for RISC-V. - RISC-V ABIs specifications. - Improve the explanation of the runtime execution flow of a dynamically linked program. - Correct the description of callee behavior for the Arm architecture. - Clarify that registers r4-r11 are callee-saved, not caller-saved. - Explain that the saved lr is loaded into pc to return to the caller.
Since the dynamic linking is now supported for the RISC-V architecture, this updates the relevant introductions and usage guides.
14220c1 to
4a556ea
Compare
jserv
left a comment
There was a problem hiding this comment.
Review notes on the RV32 dynamic linking implementation. Findings are inlined at their locations.
| * After the main function completes its execution, it must use | ||
| * the content of ra to transfer control back to __libc_start_main(). | ||
| */ | ||
| emit(__addi(__t1, __a0, 0)); |
There was a problem hiding this comment.
The dynamic-startup wrapper stashes argc/argv in t1/t2, then calls GLOBAL_FUNC (global initializers) before restoring them to a0/a1 for main (lines 707-708). t1/t2 are caller-saved temporaries the backend uses freely (div/mod, address arithmetic), so any nontrivial global initializer clobbers argc/argv and main receives garbage. Save them in the wrapper's stack frame, or use callee-saved s registers with proper save/restore.
| /* Call __libc_start_main() via PLT[1] */ | ||
| ofs = (dynamic_sections.elf_plt_start + PLT_FIXUP_SIZE) - | ||
| (elf_code_start + elf_code->size); | ||
| emit(__jal(__ra, ofs)); |
There was a problem hiding this comment.
This jal to PLT[1] (and the extern calls at OP_call / OP_address_of_func) spans .text + .rodata + .rel(a).plt to reach .plt, so the offset is roughly code_size + rodata_size. rv_encode_J in src/riscv.c has no range check, unlike rv_encode_I/S/B which all fatal() on overflow. Past the J-type reach of ±1 MB the high bits are silently dropped and the jump lands at the wrong address, a silent miscompile the small hello/fib tests never approach. Add the same guard to rv_encode_J: if (imm > 1048575 || imm < -1048576) fatal("Offset too large"); (src/riscv.c is not part of this PR, so this comment is anchored on the call site.)
| \#define ELF_MACHINE 0xf3\n$\ | ||
| \#define ELF_FLAGS 0\n$\ | ||
| \#define DYN_LINKER \"/lib/ld-linux.so.3\"\n$\ | ||
| \#define DYN_LINKER \"/lib/ld-linux-riscv32-ilp32d.so.1\"\n$\ |
There was a problem hiding this comment.
The interpreter is ld-linux-riscv32-ilp32d.so.1 (double-float ABI) while ELF_FLAGS remains 0, which declares soft-float. The object's e_flags should carry EF_RISCV_FLOAT_ABI_DOUBLE (0x4) to match the ilp32d loader, or the target should select an ILP32 soft-float linker/toolchain. As written the ABI tag contradicts the chosen interpreter and may be rejected or misinterpreted by strict tools.
| */ | ||
| emit(__addi(__t1, __a0, 0)); | ||
| emit(__addi(__t2, __a1, 0)); | ||
| emit(__sw(__ra, __sp, -4)); |
There was a problem hiding this comment.
sw ra, sp, -4 writes below sp before sp is decremented (same idiom as OP_define at line 298). RISC-V defines no stack red zone, so this stores outside the current frame; a signal or interrupt in that window corrupts the saved ra. Decrement sp first, then store ra at a positive in-frame offset.
| */ | ||
|
|
||
| func_plt_ofs = PLT_FIXUP_SIZE + PLT_ENT_SIZE; | ||
| func_got_ofs = PTR_SIZE << 2; |
There was a problem hiding this comment.
func_got_ofs = PTR_SIZE << 2 hardcodes the ARM GOT layout (first extern at GOT[4]). For RV32 (RESERVED_GOT_NUM=2) the first extern lives at GOT[3], so this value is wrong for RISC-V. It is latent today only because func->got_offset (assigned below) is write-only, the RV32 PLT computes its slot positionally. Use PTR_SIZE * (RESERVED_GOT_NUM + 1), or drop the dead got_offset field.
| elf_write_int(dynamic_sections.elf_got, 0); | ||
| elf_write_int(dynamic_sections.elf_got, 0); | ||
| for (int i = PTR_SIZE * 3; i < dynamic_sections.got_size; i += PTR_SIZE) | ||
| switch (ELF_MACHINE) { |
There was a problem hiding this comment.
GOT init relies on implicit ELF_MACHINE_ARM32 -> ELF_MACHINE_RV32 case fallthrough. It is correct for both arches (ARM writes GOT[0]=&.dynamic then two zeros; RV32 writes two zeros), but it emits -Wimplicit-fallthrough, and a stray break or a case reorder would silently corrupt ARM GOT init with no failure on the RV32 CI leg. Add a /* fall through */ annotation, or write the shared zero entries explicitly per case.
| exit_code=$? | ||
|
|
||
| # Check result | ||
| if [[ $exit_code -eq 0 ]]; then |
There was a problem hiding this comment.
A test is counted as passed on exit_code == 0 alone; an expected_output mismatch is only a warning, and only under VERBOSE. A broken external-call path that still exits 0 passes silently, which is exactly the failure mode this suite is meant to catch. Treat a non-matching expected_output as a failure.
| if [ "${{ matrix.architecture }}" = "arm" ]; then | ||
| sudo apt-get install -q -y gcc-arm-linux-gnueabihf | ||
| else | ||
| sudo wget -q https://github.com/riscv-collab/riscv-gnu-toolchain/releases/download/2026.07.15/riscv32-glibc-ubuntu-24.04-gcc.tar.xz |
There was a problem hiding this comment.
The riscv32 toolchain tarball is fetched with sudo wget and extracted with no checksum or signature verification, so a compromised or swapped release would run unchecked in CI. Pin and verify a SHA256 before extraction rather than trusting the download; also sudo wget into the workspace is unnecessary.
The proposed changes primarily improve the ELF generation and the RISC-V backend, enabling the build system to generate a dynamically linked shecc targeting the RISC-V architecture.
Although the current changes allow both bootstrapping and test suite to complete successfully, this is still a work in progress. The TODO items are listed as follows:
riscv-abi.sh) to validate the RISC-V ABI.arm.mkandriscv.mkinto a common build logic (e.g.: configureRUNNER_LD_PREFIX).Summary by cubic
Adds RV32 dynamic linking with RELA using an ABI-compliant PLT/GOT and a
__libc_start_mainentry. IntroducesELF_MACHINE_ARM32/ELF_MACHINE_RV32to select REL (Arm) vs RELA (RISC‑V). CI now builds and tests static and dynamic targets for botharmandriscv, and a new ABI suite validates RV32 calling, stack, and external-call behavior.New Features
.rela.plt; PLT0=32B, stubs=16B; per‑archRESERVED_GOT_NUM(RV32=2, ARM=3);DYN_LINKER/lib/ld-linux-riscv32-ilp32d.so.1; correctDT_RELA*/DT_REL*,DT_PLTREL,DT_PLTRELSZ,DT_JMPREL.__libc_start_mainand returns via savedra; 16‑byte stack alignment; syscall trampoline kept for static..rela.pltsupport withuse_relaplt; arch‑driven section/program headers; arch macros (ELF_MACHINE_*) used throughout.hello,fib); CI runs static+dynamic forarm/riscv.Dependencies
mk/common.mkwith per‑archTOOLCHAIN_CANDIDATESand autoRUNNER_LD_PREFIX; GitHub Actions downloads a riscv32 glibc toolchain and updatesPATH; README/docs updated for Arm/RISC‑V dynamic linking.Written for commit 4a556ea. Summary will update on new commits.