- Bug
- Splicing an application into the kernel gives the
(NOLOAD).stacksegment file content for SRAM that has none, somake programfails on every RP2 board. - Fix
- One
objcopy -R .stackper splice rule — five rules, four Makefiles, +13 −0. Proposed as tock/tock#5156. - Status
- confirmed on hardware The reporter ran
the fix on an RP2040 and
program-openocdwas A/B'd on an RP2350 — Section 8. Two boards of four andprogram-probeare still file-level only.
1What happens
Building any application into the Pico kernel fails at the last step:
$ APP=.../spi_controller_write_read.tbf make program arm-none-eabi-objcopy --set-section-flags .apps=LOAD,ALLOC ... raspberry_pi_pico-app.elf arm-none-eabi-objcopy: ...: section .relocate lma 0x20000000 adjusted to 0x20001500 arm-none-eabi-objcopy --update-section .apps=... raspberry_pi_pico-app.elf elf2uf2-rs ... raspberry_pi_pico-app.uf2 Error: "ELF contains memory contents for uninitialized memory at 20000000" make: *** [Makefile:48: program] Error 1
I reproduced that string exactly, with the reporter's own tool, on a kernel
built from 2a7a40450.
picotool refuses the same file for the same reason, exits 253 and
leaves a zero-byte UF2 behind, so this is not one converter being fussy:
$ elf2uf2-rs raspberry_pi_pico-app.elf out.uf2 Error: "ELF contains memory contents for uninitialized memory at 20000000" $ picotool uf2 convert raspberry_pi_pico-app.elf out.uf2 ERROR: ELF contains memory contents for uninitialized memory at 0x20000000
The suggested cause in the issue is that elf2uf2-rs
does not deal with RAM
. That reading is worth retiring early, because it
sends you looking at the converter. The same converter takes the same kernel
without complaint one step earlier — the control at the bottom of
figure 4 — and what it is objecting to is a real
defect in the file it was handed.
2What the two objcopy steps do to the program headers
The
rule runs objcopy twice: once to give .apps
contents, once to fill it with the .tbf. Both are needed —
--update-section on its own fails with section has no
contents, because .apps is declared (NOLOAD) in
the linker script. Giving it contents is the intended change. Everything else
in the table below is collateral.
The seven LOAD segments, before and after the objcopy
The kernel as linked
| Sections | Address | Region | MemSiz | FileSiz | File bytes |
|---|---|---|---|---|---|
| .text | 0x10000000 | flash | 0x00100 | 0x00100 | |
| .text | 0x10000100 | flash | 0x174a0 | 0x174a0 | |
| .ARM.exidx .storage | 0x100175a0 | flash | 0x00a60 | 0x00a60 | |
| .stack | 0x20000000 | SRAM | 0x01500 | 0x00000 | |
| .apps | 0x10040000 | flash | 0x00004 | 0x00000 | |
| .relocate .sram | 0x20001500 | SRAM | 0x02b8c | 0x00000 | |
| .attributes | 0x10018000 | flash | 0x0002c | 0x0002c |
After both objcopy steps
| Sections | Address | Region | MemSiz | FileSiz | File bytes |
|---|---|---|---|---|---|
| .text | 0x10000000 | flash | 0x00100 | 0x00100 | |
| .text | 0x10000100 | flash | 0x174a0 | 0x174a0 | |
| .ARM.exidx .storage | 0x100175a0 | flash | 0x00a60 | 0x00a60 | |
| .stack | 0x20000000 | SRAM | 0x01500 | 0x000000x01500 | |
| .apps | 0x10040000 | flash | 0x01000 | 0x000000x01000 | |
| .relocate .sram | 0x20001500 | SRAM | 0x02b8c | 0x00000 | |
| .attributes | 0x10018000 | flash | 0x0002c | 0x0002c |
Both SRAM segments carry no file bytes, which is what a flash image requires: nothing in the file claims to be the contents of memory that has none.
One SRAM segment now carries
0x1500 file bytes — 5,376 of them, for .stack at
0x20000000. The .apps row changing was the point of the
exercise. The .stack row is the bug, and it is what both
converters refuse.
raspberry_pi_pico.elf and of the
raspberry_pi_pico-app.elf built from it, read with
arm-none-eabi-readelf -lW. The bar draws FileSiz against that
segment's own MemSiz, so a hatched bar is a segment that occupies memory and
takes up no room in the file. Kernel built from 2a7a40450; the two
.text sizes move with the build, the .stack row does
not.3The section never changed, and the flags are not the cause
This is the part that rules out the converter, and it is worth stating
plainly because it is counter-intuitive: .stack is still
NOBITS afterwards. Its section header is untouched. Only
the program header changed, and UF2 converters read segments, not
sections — correctly, since segments are what a loader uses.
Section headers, the same two files
| Section | Type, as linked | Type, after objcopy | Address | Size |
|---|---|---|---|---|
| .stack | NOBITS | NOBITS | 20000000 | 001500 |
| .relocate | PROGBITS | REL | 20001500 | 000000 |
| .apps | NOBITS | PROGBITS | 10040000 | 001000 |
| .sram | NOBITS | NOBITS | 20001500 | 002b8c |
readelf -SW on both files.
.apps going NOBITS→PROGBITS is the
requested change. .stack does not move, which is why nothing that
reads the section table can see this bug. .relocate is reported
as REL afterwards, which is odd and still unexplained. The row
matters for another reason: that empty section is what moves the
.stack program header, which is the next thing below.The obvious reading is that giving .apps contents is what forces
BFD to lay the segments out again, and that is what this page said until
2026-09-10. It is wrong. An objcopy with no arguments at all, on
the kernel with nothing spliced into it, produces the same program header
— and deleting one empty section stops it:
Where the FileSiz comes from. Three copies, with no app spliced into any.
VARIANT STACK-FILE/MEM
------- --------------
as linked, no objcopy at all 0x00000/0x01500
objcopy, no arguments at all 0x01500/0x01500
objcopy -R .relocate 0x00000/0x01500
llvm-objcopy, no arguments 0x00000/0x01500
.relocate's reconstructed load address, and where the section says it is:
.stack size 00001500 vma 20000000 lma 20000000
.relocate size 00000000 vma 20001500 lma 20000000
The trigger is .relocate — the empty one. ELF section
headers carry no load address, so BFD reconstructs one as it reads the file, and
it does so from file offsets rather than addresses. The linker gives
.stack, .relocate, .apps,
.sram and .attributes a single shared offset, so
zero-length .relocate matches the .stack segment first
and comes back holding .stack's load address: the
lma 20000000 above, on a section that lives at
0x20001500 and belongs in flash. .attributes sits at
that same offset and escapes, because a segment whose p_filesz is 0
cannot cover its 0x2c bytes. Being zero-length is what lets
.relocate fit anywhere.
objcopy then maps sections into segments by load address, so
.relocate lands inside the .stack segment; its address
is pushed out to that segment's end, which is the warning
Section 1 already showed happening; and because it is
PROGBITS the segment has to be given file bytes reaching it. That
distance is .stack's size. In every file produced this way,
.relocate's file offset minus .stack's is
0x1500 — and so is the p_filesz.
Two things follow that the flags reading had backwards. The section flags are
innocent: program is simply the only rule that runs
objcopy at all, which is why make flash, handing the
linker's own ELF straight to the converter, has never failed. And so is the
linker — it wrote p_filesz 0, and
llvm-objcopy, which copies program headers instead of rebuilding
them, hands that number back unchanged. The defect enters in the round trip
through BFD, and nowhere before it.
The reach is wider than the RP2 boards, and the emptiness predicts which
ones. Of the 25 Arm board ELFs I had built, 24 pick up the same bogus
p_filesz from a bare objcopy; the only one that does
not, nrf52840dk-test-kernel, is the only one whose
.relocate is not empty. Six boards outside the RP2 family run this
same splice rule. Two of them I have builds for and both carry it —
teensy40 and weact_f401ccu6, at
0x2000 each, because the number is always .stack's own
size. opentitan/earlgrey-cw310 should be clean, its
.relocate being 0x1c rather than empty, but that is a prediction and
not a measurement: it is RISC-V, and the Arm objcopy here will not
read it. imxrt1050-evkb, psc3m5_evk and
cy8cproto_62_4343_w I have not built at all. None of the six hands
its result to a UF2 converter, which is why none of them has noticed.
Where that comes from, in binutils' own source
Everything above is behaviour, read off files. This is the code that produces
it, from binutils-gdb at HEAD on sourceware — bfd/elf.c and
include/elf/internal.h. It is not diffed against the 2.47 that
produced the numbers, so: the behaviour is measured, the source is current
master, and the two agree.
Reading the file gives the section a load address it never had.
_bfd_elf_make_section_from_shdr walks the program headers for every
allocated section. One that carries contents takes the second branch, and that
branch works in file offsets:
if ((newsect->flags & SEC_LOAD) == 0)
newsect->lma = (phdr->p_paddr
+ hdr->sh_addr - phdr->p_vaddr) / opb;
else
/* We used to use the same adjustment for SEC_LOAD
sections, but that doesn't work if the segment
is packed with code from multiple VMAs.
Instead we calculate the section LMA based on
the segment LMA. It is assumed that the
segment will contain sections with contiguous
LMAs, even if the VMAs are not. */
newsect->lma = (phdr->p_paddr
+ hdr->sh_offset - phdr->p_offset) / opb;
.relocate is PROGBITS and allocated, so
SEC_LOAD is set and the second form runs. Its
sh_offset and the .stack segment's
p_offset are the same number, the subtraction is zero, and it comes
back holding that segment's p_paddr unchanged —
0x20000000, the number objdump prints above. The
comment there is worth reading: the file-offset form is deliberate, and it is
the right answer for a segment packed with code from several VMAs. It is only
wrong for a section with no size.
Why .relocate and not .attributes. A section
is only offered a segment if ELF_SECTION_IN_SEGMENT accepts it, and
for anything that is not SHT_NOBITS that means fitting inside the
segment's file span:
/* Any section besides one of type SHT_NOBITS must have file
offsets within the segment. */
&& ((sec_hdr)->sh_type == SHT_NOBITS
|| ((bfd_vma) (sec_hdr)->sh_offset >= (segment)->p_offset
&& (!(strict)
|| ((sec_hdr)->sh_offset - (segment)->p_offset
<= (segment)->p_filesz - 1))
&& (((sec_hdr)->sh_offset - (segment)->p_offset
+ ELF_SECTION_SIZE(sec_hdr, segment))
<= (segment)->p_filesz)))
The bold clause decides it. The .stack segment has
p_filesz 0, so the only section that can fit inside its file
span is one of size zero. .relocate is size zero and passes;
.attributes, sharing the same sh_offset, is 0x2c bytes
and fails. That is the entire difference between the section this breaks and the
one sitting next to it in the file.
The tie-break, which names the problem out loud. The loop assigns a load address for every segment a section matches, and stops at the first one that passes this:
/* With contiguous segments, we can't tell from file
offsets whether a section with zero size should
be placed at the end of one segment or the
beginning of the next. Decide based on vaddr. */
if (hdr->sh_addr >= phdr->p_vaddr
&& (hdr->sh_addr + hdr->sh_size
<= phdr->p_vaddr + phdr->p_memsz))
break;
For .relocate: 0x20001500 ≥ 0x20000000, and
0x20001500 + 0 ≤ 0x20000000 + 0x1500. Both hold — the
second is an inclusive bound, and the section has no size to push it over. So
the loop breaks on the .stack segment and never reaches the segment
that begins at 0x20001500, which is the one the linker put
it in.
The same header defines a stricter form of that test, and its documentation
describes this file exactly: ELF_SECTION_IN_SEGMENT_STRICT, under
which “a zero size section won't match at the end of a segment, unless
the segment is also zero size.” The reader calls the non-strict
one.
The write side is then mechanical, and Section 1
already showed its output: rewrite_elf_program_header groups
sections into segments by load address, so .relocate is placed in
the .stack segment;
assign_file_positions_for_load_sections finds its load address
below the segment's running end, pushes it up to that end, and prints
section .relocate lma 0x20000000 adjusted to 0x20001500; and a
segment holding a section with contents 0x1500 bytes in is given a file span
that reaches it.
4Every candidate fix, measured
Six variants, all run against the same kernel by
reproduce.sh, which is the script that
produced these numbers rather than a description of one. Pick a row.
What each candidate actually does
The rule as it is today
- .stack FileSiz
- 0x01500
- elf2uf2-rs
- refused
- app in the UF2
- —
- _estack symbol
- kept
No UF2 is produced at all, so make program stops with a
non-zero status. It does not silently flash a kernel with no app.
Drop the section from the flashing artifact
- .stack FileSiz
- segment gone
- elf2uf2-rs
- converts
- app in the UF2
- 16 blocks at 0x10040000
- _estack symbol
- lost
.stack is (NOLOAD) and reserves space no
image ever needs to carry, so removing it costs the flashed bytes
nothing. It does drop _sstack and _estack from
the symbol table — but only from -app.elf, the
artifact that exists to be converted. The kernel ELF you would debug
with is a different file and is not touched, and the linker resolved
those symbols into the code long before this step.
The workaround posted in the issue
- .stack FileSiz
- segment gone
- elf2uf2-rs
- converts
- app in the UF2
- 16 blocks at 0x10040000
- _estack symbol
- lost
Identical output to the row above, because the two extra removals do nothing here — and one of them is a hazard rather than a no-op. Section 5 is about why.
Ask for the section to stay unloaded
- .stack FileSiz
- 0x01500
- elf2uf2-rs
- refused
- app in the UF2
- —
- _estack symbol
- kept
No effect. The section was already NOBITS; saying so
again does not reach the program header, which is where the wrong number
is.
The same, without alloc
- .stack FileSiz
- 0x01500, remapped
- elf2uf2-rs
- refused
- app in the UF2
- —
- _estack symbol
- kept
Worse, and instructively so. The segment survives with its virtual
address moved to 0x20001500 while its physical address stays
at 0x20000000, still carrying 0x1500 file bytes. The
converter refuses it for the same reason as before.
A different spelling of the section flags
- .stack FileSiz
- 0x01500
- elf2uf2-rs
- refused
- app in the UF2
- —
- _estack symbol
- —
alloc,contents and alloc,load,contents both
behave exactly as LOAD,ALLOC does. No spelling of the flags
avoids the problem, and Section 3 says why: the flags are not what
causes it. Running objcopy at all is.
readelf -lW, the app by decoding the UF2's
block headers, the symbol from readelf -sW.5The workaround in the issue has a trap in it
The issue's fix removes three sections:
-R .stack -R .relocate -R .bss. It works,
and I would not recommend copying it.
-R .relocateis a live hazard..relocateholds kernel data that is initialised to a value: it is placed in flash, and Tock copies it into SRAM at boot. Stripping it from the image would leave that data uninitialised. It happens to be safe today only because the section is empty — I measuredsize 0x0on all four RP2040 boards in the tree:raspberry_pi_pico,raspberry_pi_pico_w,pico_explorer_baseandnano_rp2040_connect. The day any kernel change introduces one initialised static, this workaround starts producing a kernel with garbage in.data, and nothing in the build will say so. That same emptiness is what causes the bug in the first place — Section 3 — so this removal does work, and works by deleting the trigger rather than the symptom. It is still the one I would not ship. (Those are not the same four as the boards fixed below, which swapraspberry_pi_pico_w— it has noprogramrule at all — for the RP2350raspberry_pi_pico_2.)-R .bssdoes nothing. Tock's layout has no.bsssection; the kernel's zeroed memory is.sram. There are zero matches for.bssin the ELF.
-R .stack on its own is sufficient, and it is the only one of
the three that addresses the segment the converter named.
6Converting is not the same as landing
One thing worth checking before trusting any fix to this: whether the application bytes are in the resulting UF2 at all. The failure mode above is loud, but a nearby one is silent.
Two UF2s from the same kernel
| Input | Bytes | Blocks | Blocks carrying the app |
|---|---|---|---|
| the kernel alone, no objcopy | 204800 | 400 | none — correctly, there is no app in it |
| after the fix, with a 4096-byte app | 212992 | 416 | 400–415, at 0x10040000–0x10040f00 |
The application is sixteen blocks appended to four
hundred: 4,096 bytes, the whole of it, at exactly the address
_sapps names. Decoded out of the UF2 block headers, not
inferred from an exit status.
The reason to check this rather than trust an exit code:
llvm-objcopy is not a drop-in here. It updates the section
but leaves the program header alone. The UF2 then converts with no error at
all, and contains no application — a silent version of this bug, which
costs more to find than the loud one. I hit that while working around this on a
Pico 2 and it is the reason figure 3 has an "app in the UF2" column
instead of a pass mark.
7The change
One line per rule, and there are five of them — every place an RP2 board splices an application into its kernel:
$(KERNEL_WITH_APP): $(KERNEL)
ifeq ($(APP),)
$(error Please define the APP variable with the TBF file to flash an application)
endif
arm-none-eabi-objcopy --set-section-flags .apps=LOAD,ALLOC $(KERNEL) $@
arm-none-eabi-objcopy --update-section .apps=$(APP) $@
+ arm-none-eabi-objcopy -R .stack $@
The section is removed only from <platform>-app.elf, the
artifact that exists to be converted. The kernel ELF is a different file and
keeps its symbols.
The same make target on all four RP2 boards that can splice an app, with the line and without it
| Board | Target | Converter | Without the line | With it |
|---|---|---|---|---|
| raspberry_pi_pico | program | elf2uf2-rs | rc=2, no UF2 | rc=0, app at 0x10040000 |
| raspberry_pi_pico_2 | program | picotool | rc=2, empty UF2 | rc=0, app at 0x10040000 |
| pico_explorer_base | program | elf2uf2-rs | rc=2, no UF2 | rc=0, app at 0x10040000 |
| nano_rp2040_connect | flash-app | elf2uf2-rs | rc=2, no UF2 | rc=0, app at 0x10040000 |
All four fail today with the string from the issue, and
all four take the application afterwards — 16 blocks, 4,096 bytes,
at the address _sapps names, decoded from the UF2 block
headers in every case.
make target with a 4,096-byte stand-in
.tbf, with the change applied and with it stashed. The fifth
rule, raspberry_pi_pico_2's program-openocd, is
changed the same way and is not in the table — it hands the ELF to
openocd rather than a converter, so there is no UF2 to decode. It has since
been run on a board, and it fails differently:
Section 8.The four boards carry five rules between them, because
raspberry_pi_pico_2 splices twice: once for program
and once for program-openocd. raspberry_pi_pico's
program-probe shares the one rule with program, so it
is fixed by the same line.
8Confirmed on hardware, on two chips
Everything above is measured on files, and a UF2 that converts and holds the application at the right address is not yet a board that boots it. Two runs on real silicon have since closed that gap. Different chips, different people, different flashing routes — and the same output.
confirmed The reporter, on an RP2040
that change worked at least for the RP2040
potto216, who filed the issue, put the line into
raspberry_pi_pico's program rule, converted with
elf2uf2-rs on his own machine and his own binutils, and flashed
the result over BOOTSEL. Not one leg of that path is the one measured
above.
RP2040 Revision 2 ASIC tock$ Initialization complete. Enter main loop Hello world! tock$ list PID ShortID Name Quanta Syscalls Restarts Grants State 0 Unique console 0 10 0 0/ 9 Terminated
The second run is the fifth rule, program-openocd, on an RP2350
over SWD — the one this page could not reach when it was written. It is
broken today too, and it breaks in a third way that neither converter shows.
The board's own program-openocd rule, with the line and without it
| Rule as it is | make | openocd | The board afterwards |
|---|---|---|---|
| Without the line | rc=2 | exits 1 at verify_image |
halted, console silent |
| With it | rc=0 | programs and verifies clean | resets itself, application runs |
openocd does not refuse the ELF the way a UF2
converter does. program writes the flash segments and
skips the SRAM one —
Warn : no flash bank found for address 0x20000000 — so
.stack is never written. verify_image then checks
every segment, that one included, against SRAM nothing wrote, and gives up:
Error: checksum mismatch, then 128 diffs beginning
diff 0 address 0x20000000. Was 0x88 instead of 0x00. It exits
before reaching the reset, which is why the board is left
halted instead of running.
make program-openocd
target, APP=console.tbf, on a Pico 2 W through a Debug
Probe. The Makefile is unmodified; only OPENOCD= is pointed at a
shim that ships the ELF to the machine holding the probe and runs the real
openocd there. The application region was erased between the two runs, so the
second row's result is its own.The flash was correct the whole time. Read back over SWD after the
failing run, 0x10040000 is byte-identical to the .tbf
that went in — a power cycle would have booted it. This rule fails at the
verify, not at the write, and the board looking dead afterwards is the part a
user would report.
Initialization complete. Enter main loop Hello world! tock$ list PID ShortID Name Quanta Syscalls Restarts Grants State 0 Unique console 0 10 0 0/ 4 Terminated
Same application, same ten syscalls, same clean exit, on two chips reached
two different ways. The Grants column differs because the two
boards expose different numbers of drivers.
9Reproducing it
No board and no application are needed. Nothing about the failure depends
on what is spliced in, so any 4,096 bytes stand in for a .tbf.
Save
reproduce.sh at the root of a Tock
checkout, build a kernel, and hand it the ELF:
$ (cd boards/raspberry_pi_pico && make)
$ sh reproduce.sh target/thumbv6m-none-eabi/release/raspberry_pi_pico.elf
VARIANT STACK-FILE/MEM UF2 _ESTACK APP
------- -------------- --- ------- ---
Makefile as it stands 0x01500/0x01500 REFUSED kept -
-R .stack removed ok lost 416 blocks, app in 16 at 0x10040000-0x10040f00
-R .stack -R .relocate -R .bss removed ok lost 416 blocks, app in 16 at 0x10040000-0x10040f00
set .stack=alloc,noload 0x01500/0x01500 REFUSED kept -
set .stack=noload removed REFUSED kept -
set .apps=alloc,load,contents 0x01500/0x01500 REFUSED - -
The control: the same kernel with no objcopy at all.
converts, 400 blocks, app in 0 at nowhere
Where the FileSiz comes from. Three copies, with no app spliced into any.
VARIANT STACK-FILE/MEM
------- --------------
as linked, no objcopy at all 0x00000/0x01500
objcopy, no arguments at all 0x01500/0x01500
objcopy -R .relocate 0x00000/0x01500
llvm-objcopy, no arguments 0x00000/0x01500
.relocate's reconstructed load address, and where the section says it is:
.stack size 00001500 vma 20000000 lma 20000000
.relocate size 00000000 vma 20001500 lma 20000000
the warning the bare copy prints, and -R .relocate does not:
arm-none-eabi-objcopy: m-plain.elf: section .relocate lma 0x20000000 adjusted to 0x20001500
reproduce.sh is the script that
printed that. It needs arm-none-eabi-objcopy,
arm-none-eabi-readelf, arm-none-eabi-objdump,
elf2uf2-rs and python3. The last row of the second
table needs llvm-objcopy as well, and says so when it is missing;
the run above set LLVM_OBJCOPY to the copy inside a rustup
toolchain.
10What this does not establish
- Two boards, not four. Section 8 covers
raspberry_pi_picoon an RP2040 andraspberry_pi_pico_2on an RP2350.pico_explorer_baseandnano_rp2040_connectare measured on the files only. They are RP2040 boards carrying the same rule, so the remaining risk is small — but small is not none, and nobody has put an application on one. program-probeis still untested.raspberry_pi_pico'sprogram-probehands the ELF toprobe-rs. It builds the same broken file today and the same line fixes it, but whether probe-rs refuses the image, writes the stack segment into SRAM and carries on, or fails at a verify the way openocd does, I have not checked: no RP2040 board here and no probe-rs. The number is not the same on each board either —.stackis 0x1500 on the Pico and 0x3000 on the Pico 2, so 5,376 bytes on one and 12,288 on the other.- One toolchain for the variant matrix. GNU Binutils 2.47.20260726. The reporter hit the same failure, and confirmed the same fix, on a different machine and a different binutils — so neither the bug nor the fix is version-specific. The six-candidate matrix in Section 4 is the part only ever run on mine.
- Still not a binutils bug report. Section 3 establishes the chain — which section, which address, which step — but not whose defect it is. BFD reconstructs a load address the linker never wrote down, and gets it wrong for a zero-length section, using a test binutils also ships a stricter form of — why the reader does not use that form, I did not establish. Whether any of it is a bug to file, or an ambiguity the linker should not have emitted by giving five sections one file offset, I have not established either, and I have not searched the binutils tracker for it. Nothing here has been reported upstream. The Makefile fix does not depend on which it is.