Skip to content

arch: Compile an LTO module to machine code when gcc links it. - #20846

Merged
acassis merged 6 commits into
apache:masterfrom
casaroli:module-lto-link
Oct 22, 2026
Merged

acassis merged 6 commits into
apache:masterfrom
casaroli:module-lto-link

Conversation

@casaroli

@casaroli casaroli commented Oct 20, 2026 •

Copy link
Copy Markdown
Contributor

Summary

With CONFIG_LTO_FULL (GCC), the Toolchain.defs of arm, arm64 and risc-v set LD to the gcc driver, and a loadable module (and a program of a kernel build) is linked with it, because MODULELD defaults to LD. The partial link (-r) of LTO objects keeps only LTO data, which the loader cannot use: exec failed: 22, and nm says plugin needed to handle lto object.

Where these Toolchain.defs set LD to gcc for LTO, they now also set LDMODULELTOFLAGS and LDELFLTOFLAGS: -flinker-output=nolto-rel and the compile options of the module. LDMODULEFLAGS and LDELFFLAGS use them next to -r. The link then compiles the objects to machine code with the options of the module (-mthumb, -mlong-calls, ...). Without these options the arm-none-eabi assembler stopped with invalid constant (3e8) after fixup.

crt0.o is now compiled without -flto. It must be machine code in any case, and as LTO data it made LTO warn that crt0 and the program declare main() differently, which -Werror makes an error.

The other commits fix the other links that gcc makes with LTO:

  • risc-v: LDELFFLAGS has --oformat elf32-littleriscv (or elf64), an option of the linker, and the gcc driver (LTO) and clang (the clang toolchain sets LD to clang) stop with unrecognized command-line option '--oformat'. The branches that set LD to a driver now set LDXLINKER to -Xlinker, and LDELFFLAGS has $(LDXLINKER) --oformat=.... With ld the option is the same as before.
  • arm64: Toolchain.defs adds -nostdlib to LDFLAGS, and the Makefile gives LDFLAGS to ld through -Xlinker, so gcc still added its start files. qemu-armv8a:knsh then stopped on the .eh_frame of crtbegin.o. -nostdlib now goes to gcc too, as on arm.
  • A kernel build: the export (mkexport.sh -u) had no LDSTARTGROUP and LDENDGROUP, so apps linked each program with --start-group, which gcc rejects. The branches that set LD to the driver now set them to -Wl,--start-group and -Wl,--end-group, and mkexport.sh writes them for every export.
  • risc-v without CONFIG_BINFMT_ELF_RELOCATABLE (rv-virt:knsh64): a program is linked to an executable with no compile options, so gcc added the crt0.o of its C library (multiple definition of '_start') and crashed in LTRANS without the ABI options. LDELFEXEFLAGS now gives the ELF options to this link.
  • arm with CONFIG_PIC (not FDPIC) or CONFIG_BINFMT_ELF_EXECUTABLE: a program is linked to an executable too. The PIC link gave it options of ld (unrecognized command-line option '--unresolved-symbols=ignore-in-object-files'), and gcc added its start files. These links now give the options of ld with $(LDXLINKER) and add $(LDELFEXEFLAGS) (the ELF options and -nostdlib).

This replaces the Make.defs part of apache/nuttx-apps#3884, as asked there. That PR keeps only the -Wl,--defsym= form of the program attributes for the gcc driver; a module links only with both.

Impact

  • CONFIG_LTO_FULL with GCC on arm, arm64 and risc-v: the modules (flat build) and the programs (kernel build) link to machine code and load.
  • risc-v with clang: LDELFFLAGS no longer stops the driver.
  • Not covered here, with CONFIG_LTO_FULL: a protected build fails on master already in the link of the user image (the board kernel/Makefile gives --undefined= and --start-group to $(LD)). The qemu-armv7a:knsh image (armv7-a, also armv7-r and armv8-r) fails on master already: dispatch_syscall() reads g_stublookup only in assembly, which LTO does not see (undefined reference to 'g_stublookup').
  • Other architectures do not set LD to a compiler driver: no change.
  • Without LTO, with clang (ld.lld) or with FDPIC (MODULELD is ld): no change. crt0.o is machine code also with LTO.
  • No documentation change.

Testing

qemu-armv8a:nsh (flat, Arm GNU 14.2, QEMU HVF) with CONFIG_LTO_FULL, CONFIG_ELF, CONFIG_EXECFUNCS_SYSTEM_SYMTAB and CONFIG_EXAMPLES_HELLO=m, apps with apache/nuttx-apps#3884, hello loaded from a 9p share:

master this PR
hello link -Wl,--defsym=... only -flinker-output=nolto-rel and the module options (with -Werror)
nm hello plugin needed to handle lto object 18 defined symbols, no LTO data
run nxposix_spawn_exec: ERROR: exec failed: 22 Hello, World!!

The same test with QEMU TCG and hello on a semihosting hostfs, apps with apache/nuttx-apps#3884:

configuration build hello
rv-virt:nsh64, CONFIG_LTO_FULL (master: unrecognized command-line option '--oformat') flat Hello, World!!
rv-virt:nsh64, no LTO (ld) flat Hello, World!!
mps2-an500:nsh, CONFIG_LTO_FULL, Arm GNU 15.3 flat Hello, World!!
rv-virt:knsh64, no LTO kernel ostest Exiting with status 0
rv-virt:knsh64, CONFIG_LTO_FULL (master: unrecognized command-line option '--start-group') kernel ostest Exiting with status 0
qemu-armv8a:knsh, CONFIG_LTO_FULL, HVF (master: .eh_frame of crtbegin.o) kernel ostest Exiting with status 0
qemu-armv8a:nsh, CONFIG_LTO_FULL, HVF flat ostest Exiting with status 0
qemu-armv8a:knsh and :nsh, no LTO kernel, flat ostest Exiting with status 0
mps3-an547:picostest (CONFIG_PIC), CONFIG_LTO_FULL, romfs on /pic (master: unrecognized command-line option '--unresolved-symbols=ignore-in-object-files') flat /pic/hello Hello, World!!, /pic/ostest Exiting with status 0
mps3-an547:picostest, no LTO flat the same
qemu-armv7a:knsh (CONFIG_BINFMT_ELF_EXECUTABLE), no LTO kernel ostest Exiting with status 0

LDELFFLAGS and LDMODULEFLAGS as make expands them, before and after this change: the same for qemu-armv8a:nsh, qemu-armv8a:knsh, mps2-an500:nsh, rv-virt:nsh and rv-virt:knsh (risc-v: --oformat=elf32-littleriscv in place of --oformat elf32-littleriscv), and for mps3-an547:picostest and qemu-armv7a:knsh; with CONFIG_LTO_FULL (qemu-armv8a:nsh, mps2-an500:nsh, rv-virt:nsh) they have -flinker-output=nolto-rel and the module options.

tools/checkpatch.sh -c -u -m -g passes.

@github-actions github-actions Bot added Arch: arm Issues related to ARM (32-bit) architecture Arch: arm64 Issues related to ARM64 (64-bit) architecture Arch: risc-v Issues related to the RISC-V (32-bit or 64-bit) architecture Size: S The size of the change in this PR is small labels Oct 21, 2026
@github-actions

github-actions Bot commented Oct 21, 2026 •

Copy link
Copy Markdown

MemBrowse Memory Report · Explore what changed →

No memory changes detected for:


Catch memory regressions in CI with MemBrowse. Free for open source.

Comment thread arch/arm/src/common/Toolchain.defs Outdated
Comment thread arch/arm/src/common/Toolchain.defs Outdated
With CONFIG_LTO_FULL, the Toolchain.defs of arm, arm64 and risc-v set
LD to the gcc driver, and a loadable module (and a program of a kernel
build) is linked with it, because MODULELD defaults to LD.  The partial
link (-r) of LTO objects keeps only LTO data, which the loader cannot
use: "Failed to execute program 'hello': -22", and nm says "plugin
needed to handle lto object".

Where these Toolchain.defs set LD to gcc for LTO, they now also set
LDMODULELTOFLAGS and LDELFLTOFLAGS: -flinker-output=nolto-rel and the
compile options of the module.  LDMODULEFLAGS and LDELFFLAGS use them
next to -r.  The link then compiles the objects to machine code with
the options of the module (-mthumb, -mlong-calls, ...).  Without these
options the arm-none-eabi assembler stopped with "invalid constant
(3e8) after fixup".

crt0.o is now compiled without -flto.  It must be machine code in any
case, and as LTO data it made LTO warn that crt0 and the program
declare main() differently, which -Werror makes an error.

Assisted-by: Claude Code:claude-opus-5-5
Signed-off-by: Marco Casaroli <marco.casaroli@gmail.com>
jerpelea
jerpelea previously approved these changes Oct 21, 2026
LDELFFLAGS has "--oformat elf32-littleriscv" (or elf64), an option of
the linker.  The link of an ELF program uses MODULELD, which is LD by
default, and with clang, or with gcc and CONFIG_LTO_FULL, LD is the
compiler driver.  The driver stops: "riscv-none-elf-gcc: error:
unrecognized command-line option '--oformat'", "clang: error: unknown
argument: '--oformat'".

The branches that set LD to a driver now set LDXLINKER to -Xlinker, and
LDELFFLAGS has "$(LDXLINKER) --oformat=elf32-littleriscv" (or elf64).
With ld the option is the same as before.

Assisted-by: Claude Code:claude-opus-5-5
Signed-off-by: Marco Casaroli <marco.casaroli@gmail.com>
@casaroli
casaroli dismissed stale reviews from jerpelea and xiaoxiang781216 via b7abc05 October 21, 2026 08:20
jerpelea
jerpelea previously approved these changes Oct 21, 2026
With CONFIG_LTO_FULL and GCC, gcc links nuttx.  Toolchain.defs adds
-nostdlib to LDFLAGS, and the Makefile gives LDFLAGS to ld through
-Xlinker, so gcc still added its start files (crtbegin.o, ...) and its
default libraries.  The linker script of qemu-armv8a:knsh discards
.eh_frame, and the link stopped:

  `.eh_frame' referenced in section `.text.frame_dummy' of
  .../crtbegin.o: defined in discarded section `.eh_frame' of
  .../crtbegin.o

Give -nostdlib to gcc too, as arch/arm does.

Assisted-by: Claude Code:claude-opus-5-5
Signed-off-by: Marco Casaroli <marco.casaroli@gmail.com>
With CONFIG_LTO_FULL and GCC on arm, arm64 and risc-v, or with clang on
risc-v, LD is the compiler driver.  The arch Makefile then links nuttx
with -Wl,--start-group and -Wl,--end-group.  The export of a kernel
build (mkexport.sh -u) did not have LDSTARTGROUP and LDENDGROUP, so
apps linked each program with its default --start-group, and the
driver stopped:

  riscv-none-elf-gcc: error: unrecognized command-line option
  '--start-group'

The Toolchain.defs branches that set LD to the driver now set
LDSTARTGROUP and LDENDGROUP, and mkexport.sh writes them for every
export.  Without a value the apps default stays --start-group.

Assisted-by: Claude Code:claude-opus-5-5
Signed-off-by: Marco Casaroli <marco.casaroli@gmail.com>
…ions.

Without CONFIG_BINFMT_ELF_RELOCATABLE (rv-virt:knsh64), a program is
linked to an executable, and LDELFFLAGS has no compile options.  With
CONFIG_LTO_FULL, gcc links it, adds the start files of its C library,
and compiles the LTO objects without the ABI of the program:

  multiple definition of `_start'; .../riscv-none-elf/lib/crt0.o:
  first defined here
  streams.h:783:5: internal compiler error: Segmentation fault: 11

The gcc branch now sets LDELFEXEFLAGS to the ELF compile options
(-nostdlib, -march, -mabi, ...), and LDELFFLAGS has them when the
program is not relocatable, as it has them with -r.

Assisted-by: Claude Code:claude-opus-5-5
Signed-off-by: Marco Casaroli <marco.casaroli@gmail.com>
With CONFIG_PIC (not FDPIC), or with CONFIG_BINFMT_ELF_EXECUTABLE, a
program is linked to an executable.  With CONFIG_LTO_FULL, gcc links
it: the PIC link gave it options of ld, and both links gave it no
compile options, so gcc added its start files:

  arm-none-eabi-gcc: error: unrecognized command-line option
  '--unresolved-symbols=ignore-in-object-files'
  crt0.c:169: multiple definition of `_start'

The gcc branch now sets LDXLINKER to -Xlinker and LDELFEXEFLAGS to the
ELF compile options and -nostdlib.  The PIC link gives its options of
ld with $(LDXLINKER), and both executable links add $(LDELFEXEFLAGS).
Without LTO the options are the same as before.

Assisted-by: Claude Code:claude-opus-5-5
Signed-off-by: Marco Casaroli <marco.casaroli@gmail.com>
@github-actions github-actions Bot added Area: Build system Size: M The size of the change in this PR is medium and removed Size: S The size of the change in this PR is small labels Oct 21, 2026
@acassis
acassis merged commit 2ba1898 into apache:master Oct 22, 2026
76 of 91 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Arch: arm Issues related to ARM (32-bit) architecture Arch: arm64 Issues related to ARM64 (64-bit) architecture Arch: risc-v Issues related to the RISC-V (32-bit or 64-bit) architecture Area: Build system Size: M The size of the change in this PR is medium

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants