Skip to content

tools/mpy_ld: Support picolibc on RISC-V (.srodata, absolute LO12).#19378

Open
o-murphy wants to merge 1 commit into
micropython:masterfrom
o-murphy:fix/tools-mpy-ld-risc-v-srodata-lo12
Open

tools/mpy_ld: Support picolibc on RISC-V (.srodata, absolute LO12).#19378
o-murphy wants to merge 1 commit into
micropython:masterfrom
o-murphy:fix/tools-mpy-ld-risc-v-srodata-lo12

Conversation

@o-murphy

@o-murphy o-murphy commented Jun 24, 2026

Copy link
Copy Markdown

Native modules built with LINK_RUNTIME=1 for rv32imc/rv64imc could not link against picolibc's libc.a/libm.a as soon as a floating-point library function (sinf, expf, powf, ...) was called with a runtime (non-constant) argument. Two issues in mpy_ld.py were responsible:

  1. load_object_file() only recognised section names starting with ".rodata", ".text", ".bss" etc. picolibc stores RISC-V float/double constants in the small-data read-only sections ".srodata.cst4" / ".srodata.cst8", which were silently skipped, leaving symbols that point into them without a .section attribute.

  2. process_riscv32_relocation() handled R_RISCV_LO12_I/S with the same parent-lookup logic as the PCREL variants. For PCREL the relocation symbol is a local label pointing at the matching HI20 instruction in .text, so the parent lookup is required. For the absolute variant used by picolibc, the symbol points directly at the data itself, so there is no parent to find and the lookup fell through to assert 0.

Add ".srodata" to the recognised section prefixes, and give the absolute LO12 case its own code path that computes the address directly instead of going through a HI20 parent.

Tested on rv32imc with gcc-riscv64-unknown-elf 13.2.0 and picolibc-riscv64-unknown-elf 1.8.6 (Ubuntu 24.04): a natmod calling sinf/cosf/atan2f/sqrtf/expf/powf with runtime arguments now builds and links against picolibc successfully, where it previously failed with two separate exceptions (see PR description). Module size also drops by ~1.5 KB compared to vendoring fdlibm, since picolibc isn't duplicated into the natmod.

Closes: #19364
Signed-off-by: o-murphy thehelixpg@gmail.com

Summary

tools/mpy_ld.py cannot link native modules against picolibc's
prebuilt libc.a/libm.a on RISC-V (rv32imc/rv64imc,
LINK_RUNTIME=1) once a libm function is called with a runtime value.
Two independent bugs are responsible, both in RISC-V-specific code
paths, so this never showed up for ARM/Xtensa.

Reproducer — a natmod with a runtime (non-constant) float argument
into libm:

// test_libm.c
#include "py/dynruntime.h"
#include <math.h>

static mp_obj_t test(mp_obj_t x_obj) {
    float x = mp_obj_get_float(x_obj);   // runtime value -- important
    float r = sinf(x) + cosf(x) + atan2f(x, 0.5f) + expf(x) + powf(x, 2.0f);
    return mp_obj_new_float(r);
}
static MP_DEFINE_CONST_FUN_OBJ_1(test_obj, test);

mp_obj_t mpy_init(mp_obj_fun_bc_t *self, size_t n_args, size_t n_kw, mp_obj_t *args) {
    MP_DYNRUNTIME_INIT_ENTRY
    mp_store_global(MP_QSTR_test, MP_OBJ_FROM_PTR(&test_obj));
    MP_DYNRUNTIME_INIT_EXIT
}
# Makefile
MPY_DIR = ../../..
MOD = test_libm
SRC = test_libm.c
ARCH = rv32imc
LINK_RUNTIME = 1
include $(MPY_DIR)/py/dynruntime.mk

A compile-time-constant argument (sinf(1.0f)) is folded by GCC at
-Os and never reaches the linker as an external call, so a naive
"hello world" natmod can look fine — the bug only appears once a real
runtime value flows into the library call, which is what any practical
use of <math.h> on a natmod does.

Bug 1. load_object_file():

elif s.name.startswith((".literal", ".text", ".rodata", ".data.rel.ro", ".bss")):

picolibc keeps RISC-V float/double constants in .srodata.cst4 /
.srodata.cst8 (the RISC-V "small data" area — doesn't exist on ARM,
which is why LINK_RUNTIME=1 is unaffected there). Not being on the
list, those sections are skipped, and symbols pointing into them are
left without a .section attribute:

Traceback (most recent call last):
  File "tools/mpy_ld.py", line 1679, in <module>
    main()
  File "tools/mpy_ld.py", line 1675, in main
    do_link(args)
  File "tools/mpy_ld.py", line 1560, in do_link
    link_objects(env, len(native_qstr_vals))
  File "tools/mpy_ld.py", line 1321, in link_objects
    do_relocation_text(env, sec.addr, r)
  File "tools/mpy_ld.py", line 755, in do_relocation_text
    (addr, value) = process_riscv32_relocation(env, text_addr, r)
  File "tools/mpy_ld.py", line 932, in process_riscv32_relocation
    addr = s.section.addr + s["st_value"]
           ^^^^^^^^^
AttributeError: 'Symbol' object has no attribute 'section'

Bug 2. Fixing bug 1 alone (just adding .srodata to the prefix
list) uncovers a second bug in process_riscv32_relocation():
R_RISCV_LO12_I/R_RISCV_LO12_S go through the same parent-lookup
loop as the PCREL variants, which is wrong for the absolute case
picolibc emits (the symbol already points at the target data, e.g.
.LC1 in .srodata.cst4 — there's no HI20 parent to find):

Traceback (most recent call last):
  File "tools/mpy_ld.py", line 1679, in <module>
    main()
  File "tools/mpy_ld.py", line 1675, in main
    do_link(args)
  File "tools/mpy_ld.py", line 1560, in do_link
    link_objects(env, len(native_qstr_vals))
  File "tools/mpy_ld.py", line 1321, in link_objects
    do_relocation_text(env, sec.addr, r)
  File "tools/mpy_ld.py", line 755, in do_relocation_text
    (addr, value) = process_riscv32_relocation(env, text_addr, r)
  File "tools/mpy_ld.py", line 956, in process_riscv32_relocation
    assert 0, r
           ^
AssertionError: <Relocation (RELA): Container({'r_offset': 252, 'r_info': 153115,
'r_info_sym': 598, 'r_info_type': 27, 'r_addend': 0})>

(r_info_type: 27 = R_RISCV_LO12_I.)

The fix adds .srodata to the recognised section prefixes, and
gives the absolute-LO12 case (no HI20 parent found) its own branch
that computes the address directly instead of asserting:

--- a/tools/mpy_ld.py
+++ b/tools/mpy_ld.py
@@ -941,21 +941,27 @@ def process_riscv32_relocation(env, text_addr, r):
         R_RISCV_LO12_S,
     ):
         parent = None
-        for potential_parent in s.section.reloc:
-            if potential_parent["r_offset"] != s["st_value"]:
-                continue
-            if potential_parent["r_info_type"] not in (
-                R_RISCV_GOT_HI20,
-                R_RISCV_PCREL_HI20,
-                R_RISCV_HI20,
-            ):
-                continue
-            parent = potential_parent
-            break
-        if parent is None:
-            assert 0, r
+        if hasattr(s.section, "reloc"):
+            for potential_parent in s.section.reloc:
+                if potential_parent["r_offset"] != s["st_value"]:
+                    continue
+                if potential_parent["r_info_type"] not in (
+                    R_RISCV_GOT_HI20,
+                    R_RISCV_PCREL_HI20,
+                    R_RISCV_HI20,
+                ):
+                    continue
+                parent = potential_parent
+                break
         addr = s.section.addr + s["st_value"]
-        reloc = parent.computed_reloc
+        if parent is not None:
+            reloc = parent.computed_reloc
+        elif r_info_type in (R_RISCV_LO12_I, R_RISCV_LO12_S):
+            # Absolute LO12: sym points directly to the data (e.g. .LC1 in
+            # .srodata.cst4 from picolibc). The full address is already known.
+            reloc = addr + r_addend
+        else:
+            assert 0, r
         reloc_type = RISCV_RELOCATIONS_TYPE_MAP[r_info_type]
@@ -1147,7 +1153,7 @@ def load_object_file(env, f, felf):
             if s.data_size == 0:
                 # Ignore empty sections
                 pass
-            elif s.name.startswith((".literal", ".text", ".rodata", ".data.rel.ro", ".bss")):
+            elif s.name.startswith((".literal", ".text", ".rodata", ".srodata", ".data.rel.ro", ".bss")):
                 sec = Section.from_elfsec(s, felf)
                 sections_shndx[idx] = sec
                 if s.name.startswith(".literal"):

Testing

Verified all three states on the reproducer above, ARCH=rv32imc,
MICROPY_FLOAT_IMPL=float, gcc-riscv64-unknown-elf 13.2.0 +
picolibc-riscv64-unknown-elf 1.8.6 (Ubuntu 24.04), against a clean,
unmodified v1.28.0 checkout for each step:

State Result
unpatched, LINK_RUNTIME=1 AttributeError (bug 1, above)
.srodata fix only AssertionError (bug 2, above)
both hunks applied links and runs correctly

Also rebuilt a larger, real-world natmod (~20 libm call sites covering
sin/cos/atan2/sqrt/pow/exp) with and without the patch to
check for regressions and measure size impact:

vendored fdlibm (current workaround) patched, LINK_RUNTIME=1
Build succeeds succeeds
.mpy text size 36072 B 34536 B
powf/expf available no — not present in lib/libm yes, from picolibc

Not tested on real RISC-V hardware, only QEMU/build-level — same
verification depth as the existing rv32imc/rv64imc natmod CI jobs,
which currently only build (no runtime test exists for these archs).

Trade-offs and Alternatives

None — this only relaxes section-prefix matching and removes an overly
strict assert; behaviour for ARM/Xtensa/x86/x64 (which don't generate
.srodata or absolute LO12 relocations) is unchanged. An alternative
would be to keep vendoring fdlibm instead of fixing the linker, but
that duplicates code already in the target's libc, and MicroPython's
bundled single-precision lib/libm doesn't provide pow/exp at all,
so it isn't a full substitute.

Generative AI

I used generative AI tools when creating this PR, but a human has
checked the code and is responsible for the code and the description
above.

@codecov

codecov Bot commented Jun 24, 2026

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 98.51%. Comparing base (552fd21) to head (dba01bd).

Additional details and impacted files
@@           Coverage Diff           @@
##           master   #19378   +/-   ##
=======================================
  Coverage   98.51%   98.51%           
=======================================
  Files         177      177           
  Lines       22927    22927           
=======================================
  Hits        22586    22586           
  Misses        341      341           

☔ View full report in Codecov by Harness.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

@o-murphy o-murphy force-pushed the fix/tools-mpy-ld-risc-v-srodata-lo12 branch from 47fb00c to bfe512b Compare June 24, 2026 22:56
@github-actions

github-actions Bot commented Jun 24, 2026

Copy link
Copy Markdown

Code size report:

Reference:  py/mpstate: Move mp_verbose_flag to MP_STATE_VM struct. [552fd21]
Comparison: tools/mpy_ld: Support picolibc on RISC-V (.srodata, absolute LO12). [merge of dba01bd]
  mpy-cross:    +0 +0.000% 
   bare-arm:    +0 +0.000% 
minimal x86:    +0 +0.000% 
   unix x64:    +0 +0.000% standard
      stm32:    +0 +0.000% PYBV10
      esp32:    +0 +0.000% ESP32_GENERIC
     mimxrt:    +0 +0.000% TEENSY40
        rp2:    +0 +0.000% RPI_PICO_W
       samd:    +0 +0.000% ADAFRUIT_ITSYBITSY_M4_EXPRESS
  qemu rv32:    +0 +0.000% VIRT_RV32

Native modules built with LINK_RUNTIME=1 for rv32imc/rv64imc could not
link against picolibc's libc.a/libm.a as soon as a floating-point
library function (sinf, expf, powf, ...) was called with a runtime
(non-constant) argument. Two issues in mpy_ld.py were responsible:

1. load_object_file() only recognised section names starting with
   ".rodata", ".text", ".bss" etc. picolibc stores RISC-V float/double
   constants in the small-data read-only sections ".srodata.cst4" /
   ".srodata.cst8", which were silently skipped, leaving symbols that
   point into them without a .section attribute.

2. process_riscv32_relocation() handled R_RISCV_LO12_I/S with the same
   parent-lookup logic as the PCREL variants. For PCREL the relocation
   symbol is a local label pointing at the matching HI20 instruction in
   .text, so the parent lookup is required. For the absolute variant
   used by picolibc, the symbol points directly at the data itself, so
   there is no parent to find and the lookup fell through to assert 0.

Add ".srodata" to the recognised section prefixes, and give the
absolute LO12 case its own code path that computes the address
directly instead of going through a HI20 parent.

Tested on rv32imc with gcc-riscv64-unknown-elf 13.2.0 and
picolibc-riscv64-unknown-elf 1.8.6 (Ubuntu 24.04): a natmod calling
sinf/cosf/atan2f/sqrtf/expf/powf with runtime arguments now builds and
links against picolibc successfully, where it previously failed with
two separate exceptions (see PR description). Module size also drops
by ~1.5 KB compared to vendoring fdlibm, since picolibc isn't
duplicated into the natmod.

Closes: micropython#19364
Signed-off-by: o-murphy thehelixpg@gmail.com
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.

mpy_ld.py: AttributeError: 'Symbol' object has no attribute 'section' when linking RISC-V natmod with picolibc

1 participant