From d0ae774d5905e7bb325c04594c919b4f2b63b455 Mon Sep 17 00:00:00 2001 From: Stefan Stipanovic Date: Thu, 13 Aug 2026 21:23:36 +0200 Subject: [PATCH] fix(esp_system): enable linking with LLD on RISC-V targets --- .gitlab/ci/build.yml | 5 +++ Kconfig | 10 +++++ .../main/ld/bootloader.sections.common.ld | 4 ++ components/esp_system/ld/ld.debug.sections | 1 + components/esp_system/ld/ld.discard.sections | 2 + .../esp_system/ld/ld.elf.internal.sections | 8 ++++ .../arch/riscv/esp_tee_vector_table_clic.S | 7 +++- .../esp_tee/subproject/main/ld/elf_misc.ld.in | 14 +++++++ .../subproject/main/ld/esp_tee_ld.cmake | 4 +- components/riscv/vectors_clic.S | 7 +++- tools/cmake/project.cmake | 37 ++++++++++++++++++- tools/cmake/utilities.cmake | 5 ++- tools/cmakev2/project.cmake | 36 +++++++++++++++++- tools/cmakev2/utilities.cmake | 7 +++- tools/test_apps/system/.build-test-rules.yml | 4 +- .../system/clang_build_test/README.md | 4 +- .../sdkconfig.ci_clang.esp32p4.lld | 3 ++ 17 files changed, 147 insertions(+), 11 deletions(-) create mode 100644 components/esp_system/ld/ld.elf.internal.sections create mode 100644 tools/test_apps/system/clang_build_test/sdkconfig.ci_clang.esp32p4.lld diff --git a/.gitlab/ci/build.yml b/.gitlab/ci/build.yml index 04f0193cb57..b50c0aadc98 100644 --- a/.gitlab/ci/build.yml +++ b/.gitlab/ci/build.yml @@ -35,10 +35,15 @@ TEST_BUILD_OPTS_EXTRA: "" script: # CI specific options start from "--parallel-count xxx". could ignore when running locally + # + # In addition to the default sdkconfig.ci.* configs, build the + # sdkconfig.ci_clang.* configs, which are meant only for the clang build + # jobs (the generic build jobs would build them with the GCC toolchain). - run_cmd idf-build-apps build -p tools/test_apps/system/clang_build_test components/esp_security/test_apps/fault_assert_opt_check -t $IDF_TARGET + --config-rules "sdkconfig.ci=default" "sdkconfig.ci.*=" "sdkconfig.ci_clang.*=" --modified-components \"${MR_MODIFIED_COMPONENTS}\" --modified-files \"${MR_MODIFIED_FILES}\" $TEST_BUILD_OPTS_EXTRA diff --git a/Kconfig b/Kconfig index 6dd1ae87fae..308a3e5922a 100644 --- a/Kconfig +++ b/Kconfig @@ -828,6 +828,16 @@ mainmenu "Espressif IoT Development Framework Configuration" result, consider enabling LTO selectively for specific components by adding the -flto=auto compile option to them. + config COMPILER_USE_LLD + bool "Use LLD (ld.lld) as the linker" + depends on IDF_TARGET_ARCH_RISCV + default n + help + Link the application with the LLVM LLD linker instead of the GNU ld + provided by the toolchain. The "ld.lld" binary must be available in + PATH. Options not supported by LLD (e.g. --no-warn-rwx-segments) + are omitted from the link automatically. + choice COMPILER_ORPHAN_SECTIONS prompt "Orphan sections handling" default COMPILER_ORPHAN_SECTIONS_ERROR diff --git a/components/bootloader/subproject/main/ld/bootloader.sections.common.ld b/components/bootloader/subproject/main/ld/bootloader.sections.common.ld index 5c4345ba1c4..ed88147f696 100644 --- a/components/bootloader/subproject/main/ld/bootloader.sections.common.ld +++ b/components/bootloader/subproject/main/ld/bootloader.sections.common.ld @@ -100,6 +100,8 @@ SECTIONS KEEP (*(EXCLUDE_FILE (*crtend.*) .ctors)) KEEP (*(SORT(.ctors.*))) KEEP (*(.ctors)) + KEEP (*(SORT_BY_INIT_PRIORITY(.init_array.*))) + KEEP (*(.init_array)) __init_array_end = ABSOLUTE(.); KEEP (*crtbegin.*(.dtors)) KEEP (*(EXCLUDE_FILE (*crtend.*) .dtors)) @@ -153,6 +155,8 @@ SECTIONS */ /DISCARD/ : { *(.rela.*) } +#include "ld.elf.internal.sections" + /** * This section is not included in the binary image; it is only present in the ELF file. * It is used to keep certain symbols in the ELF file. diff --git a/components/esp_system/ld/ld.debug.sections b/components/esp_system/ld/ld.debug.sections index 2f94fddfa85..707927cc282 100644 --- a/components/esp_system/ld/ld.debug.sections +++ b/components/esp_system/ld/ld.debug.sections @@ -70,6 +70,7 @@ .note.GNU-stack 0 : { *(.note.GNU-stack) } .riscv.attributes 0 : { *(.riscv.attributes) } +#include "ld.elf.internal.sections" /** * .xt.prop and .xt.lit sections will be used by the debugger and disassembler diff --git a/components/esp_system/ld/ld.discard.sections b/components/esp_system/ld/ld.discard.sections index 8851195e686..e9b2ae5a6a4 100644 --- a/components/esp_system/ld/ld.discard.sections +++ b/components/esp_system/ld/ld.discard.sections @@ -15,6 +15,8 @@ #endif // CONFIG_LIBC_NEWLIB *(.fini) + *(.fini_array) + *(.fini_array.*) #if CONFIG_IDF_TARGET_ARCH_XTENSA *(.eh_frame_hdr) #endif // CONFIG_IDF_TARGET_ARCH_XTENSA diff --git a/components/esp_system/ld/ld.elf.internal.sections b/components/esp_system/ld/ld.elf.internal.sections new file mode 100644 index 00000000000..df787e05b7a --- /dev/null +++ b/components/esp_system/ld/ld.elf.internal.sections @@ -0,0 +1,8 @@ + /** + * Linker-generated ELF sections. Must be explicitly placed in linker scripts + * when using ld.lld with --orphan-handling=warn/error. GNU ld special-cases + * these sections; ld.lld does not. + */ + .symtab 0 : { *(.symtab) } + .strtab 0 : { *(.strtab) } + .shstrtab 0 : { *(.shstrtab) } diff --git a/components/esp_tee/subproject/main/arch/riscv/esp_tee_vector_table_clic.S b/components/esp_tee/subproject/main/arch/riscv/esp_tee_vector_table_clic.S index 30e4804fbb5..19b87dec983 100644 --- a/components/esp_tee/subproject/main/arch/riscv/esp_tee_vector_table_clic.S +++ b/components/esp_tee/subproject/main/arch/riscv/esp_tee_vector_table_clic.S @@ -17,11 +17,16 @@ #define ASTDBG_ISR _tee_ns_intr_handler #endif // CONFIG_ESP_SYSTEM_HW_STACK_GUARD - .section .exception_vectors_table.text + .section .exception_vectors_table.text, "ax" /* Prevent the compiler from generating 2-byte instruction in the vector tables */ .option push .option norvc + /* The vector tables have a fixed layout, so keep linker relaxation from + * touching them: with relaxation enabled the assembler emits the .balign + * padding as R_RISCV_ALIGN-managed bytes, of which GNU ld may retain more + * than the exact amount, changing the table size between builds. */ + .option norelax /** * Non-hardware vectored interrupt entry. MTVEC CSR points here. diff --git a/components/esp_tee/subproject/main/ld/elf_misc.ld.in b/components/esp_tee/subproject/main/ld/elf_misc.ld.in index f55edb267dc..020fc988fa8 100644 --- a/components/esp_tee/subproject/main/ld/elf_misc.ld.in +++ b/components/esp_tee/subproject/main/ld/elf_misc.ld.in @@ -56,6 +56,7 @@ .comment 0 : { *(.comment) } .note.GNU-stack 0: { *(.note.GNU-stack) } +#include "ld.elf.internal.sections" #if CONFIG_IDF_TARGET_ARCH_RISCV .riscv.attributes 0: { *(.riscv.attributes) } @@ -70,5 +71,18 @@ */ *(.rela.*) *(.got .got.plt) /* TODO: GCC-382 */ + + /** + * GNU ld removes them via --gc-sections; LLD treats them as GC roots, + * so they must be discarded explicitly to not trip --orphan-handling=error. + */ + *(.init) + *(.init_array) + *(.init_array.*) + *(.fini) + *(.fini_array) + *(.fini_array.*) + *(.eh_frame_hdr) + *(.eh_frame) } #endif diff --git a/components/esp_tee/subproject/main/ld/esp_tee_ld.cmake b/components/esp_tee/subproject/main/ld/esp_tee_ld.cmake index f780497f062..0d0aebe0051 100644 --- a/components/esp_tee/subproject/main/ld/esp_tee_ld.cmake +++ b/components/esp_tee/subproject/main/ld/esp_tee_ld.cmake @@ -18,11 +18,13 @@ target_linker_script(${COMPONENT_LIB} INTERFACE "${heap_dir}/port/${target}/ld/$ file(MAKE_DIRECTORY "${CMAKE_CURRENT_BINARY_DIR}/ld") +idf_component_get_property(esp_system_dir esp_system COMPONENT_DIR) + # Preprocess esp_tee.ld.in linker script to include configuration, becomes esp_tee.ld add_custom_command( OUTPUT ${ld_output} COMMAND "${CMAKE_C_COMPILER}" -C -P -x c -E -o ${ld_output} -I ${config_dir} - -I "${CMAKE_CURRENT_LIST_DIR}" ${ld_input} + -I "${CMAKE_CURRENT_LIST_DIR}" -I "${esp_system_dir}/ld" ${ld_input} MAIN_DEPENDENCY ${ld_input} DEPENDS ${sdkconfig_header} COMMENT "Generating esp_tee.ld linker script..." diff --git a/components/riscv/vectors_clic.S b/components/riscv/vectors_clic.S index 7776ce3cb82..5f10f8774ca 100644 --- a/components/riscv/vectors_clic.S +++ b/components/riscv/vectors_clic.S @@ -29,11 +29,16 @@ .global _interrupt_handler .global _panic_handler - .section .exception_vectors_table.text + .section .exception_vectors_table.text, "ax" /* Prevent the compiler from generating 2-byte instruction in the vector tables */ .option push .option norvc + /* The vector tables have a fixed layout, so keep linker relaxation from + * touching them: with relaxation enabled the assembler emits the .balign + * padding as R_RISCV_ALIGN-managed bytes, of which GNU ld may retain more + * than the exact amount, changing the table size between builds. */ + .option norelax /** * Non-hardware vectored interrupt entry. MTVEC CSR points here. diff --git a/tools/cmake/project.cmake b/tools/cmake/project.cmake index c8a031329f6..aba36dc78a6 100644 --- a/tools/cmake/project.cmake +++ b/tools/cmake/project.cmake @@ -1053,8 +1053,42 @@ macro(project project_name) target_link_options(${project_elf} PRIVATE "-Wl,--defsym=IDF_TARGET_${idf_target}=0") # Enable map file output target_link_options(${project_elf} PRIVATE "-Wl,--Map=${mapfile}") + if(CONFIG_COMPILER_USE_LLD) + if(CMAKE_C_COMPILER_ID MATCHES "Clang") + # The last --ld-path on the link line takes precedence over the + # one set in the toolchain file, selecting LLD for the link. + # The bare name is looked up in PATH. + target_link_options(${project_elf} PRIVATE "--ld-path=ld.lld") + else() + # GCC has no --ld-path; -fuse-ld=lld makes the driver search + # its search paths and PATH for a binary named "ld.lld". + target_link_options(${project_elf} PRIVATE "-fuse-ld=lld") + endif() + # Bare-metal images have no dynamic loader, so PT_GNU_RELRO is + # meaningless. LLD enables relro by default and errors out when + # relro sections (e.g. TLS .flash.tdata) are not contiguous in the + # IDF linker scripts, so disable it. + target_link_options(${project_elf} PRIVATE "-Wl,-z,norelro") + # Some targets (e.g. esp32s3) use NOLOAD dummy sections that + # deliberately overlap other sections' address ranges, because two + # memory regions alias the same bus address space. GNU ld skips + # NOBITS sections in its overlap check; LLD does not, so disable + # the check there. + target_link_options(${project_elf} PRIVATE "-Wl,--no-check-sections") + set(linker_binary "ld.lld") + else() + set(linker_binary "${CMAKE_LINKER}") + endif() + # Report which linker the link will use, with its version banner + execute_process(COMMAND ${linker_binary} "--version" + OUTPUT_VARIABLE linker_version + ERROR_QUIET + OUTPUT_STRIP_TRAILING_WHITESPACE) + string(REGEX REPLACE "\n.*" "" linker_version "${linker_version}") + message(STATUS "Linker: ${linker_binary} (${linker_version})") + unset(linker_version) # Check if linker supports --no-warn-rwx-segments - execute_process(COMMAND ${CMAKE_LINKER} "--no-warn-rwx-segments" "--version" + execute_process(COMMAND ${linker_binary} "--no-warn-rwx-segments" "--version" RESULT_VARIABLE result OUTPUT_QUIET ERROR_QUIET) @@ -1069,6 +1103,7 @@ macro(project project_name) # Throw error if orphan sections are found target_link_options(${project_elf} PRIVATE "-Wl,--orphan-handling=error") endif() + unset(linker_binary) unset(idf_target) endif() diff --git a/tools/cmake/utilities.cmake b/tools/cmake/utilities.cmake index a1b32fb9646..16fd3eac83a 100644 --- a/tools/cmake/utilities.cmake +++ b/tools/cmake/utilities.cmake @@ -225,7 +225,10 @@ function(preprocess_linker_file cmake_target script_in output_var preserve_suffi if(script_parent_name STREQUAL idf_target) # Add "../.." directory to include path (e.g. for esp_system component) get_filename_component(dir_to_include "${script_parent_dir}" DIRECTORY) - set(extra_cflags "-I\"${dir_to_include}\"") + # Add esp_system/ld directory to include path for the shared linker + # script fragments (e.g. ld.elf.internal.sections) + idf_component_get_property(esp_system_dir esp_system COMPONENT_DIR) + set(extra_cflags "-I\"${dir_to_include}\" -I\"${esp_system_dir}/ld\"") endif() # Keep comments (-C): historical behavior for cmakev1 linker scripts. It was diff --git a/tools/cmakev2/project.cmake b/tools/cmakev2/project.cmake index 8d9caeb14de..8e2f8413780 100644 --- a/tools/cmakev2/project.cmake +++ b/tools/cmakev2/project.cmake @@ -512,8 +512,42 @@ function(__init_project_configuration) string(TOUPPER ${target_upper} target_upper) # Add this symbol as a hint for esp_idf_size to guess the target name list(APPEND link_options "-Wl,--defsym=IDF_TARGET_${target_upper}=0") + if(CONFIG_COMPILER_USE_LLD) + if(CMAKE_C_COMPILER_ID MATCHES "Clang") + # The last --ld-path on the link line takes precedence over the + # one set in the toolchain file, selecting LLD for the link. + # The bare name is looked up in PATH. + list(APPEND link_options "--ld-path=ld.lld") + else() + # GCC has no --ld-path; -fuse-ld=lld makes the driver search + # its search paths and PATH for a binary named "ld.lld". + list(APPEND link_options "-fuse-ld=lld") + endif() + # Bare-metal images have no dynamic loader, so PT_GNU_RELRO is + # meaningless. LLD enables relro by default and errors out when + # relro sections (e.g. TLS .flash.tdata) are not contiguous in the + # IDF linker scripts, so disable it. + list(APPEND link_options "-Wl,-z,norelro") + # Some targets (e.g. esp32s3) use NOLOAD dummy sections that + # deliberately overlap other sections' address ranges, because two + # memory regions alias the same bus address space. GNU ld skips + # NOBITS sections in its overlap check; LLD does not, so disable + # the check there. + list(APPEND link_options "-Wl,--no-check-sections") + set(linker_binary "ld.lld") + else() + set(linker_binary "${CMAKE_LINKER}") + endif() + # Report which linker the link will use, with its version banner + execute_process(COMMAND ${linker_binary} "-v" + OUTPUT_VARIABLE linker_version + ERROR_QUIET + OUTPUT_STRIP_TRAILING_WHITESPACE) + string(REGEX REPLACE "\n.*" "" linker_version "${linker_version}") + message(STATUS "Linker: ${linker_binary} (${linker_version})") + unset(linker_version) # Check if linker supports --no-warn-rwx-segments - execute_process(COMMAND ${CMAKE_LINKER} "--no-warn-rwx-segments" "--version" + execute_process(COMMAND ${linker_binary} "--no-warn-rwx-segments" "--version" RESULT_VARIABLE result OUTPUT_QUIET ERROR_QUIET) diff --git a/tools/cmakev2/utilities.cmake b/tools/cmakev2/utilities.cmake index a914f2057a9..931a423a090 100644 --- a/tools/cmakev2/utilities.cmake +++ b/tools/cmakev2/utilities.cmake @@ -1268,12 +1268,17 @@ function(__preprocess_linker_script script_in script_out flags component_include # ld.common file, so add the parent directory to the C preprocessor # search path. Works for that layout; a component with a different # layout, or one that must drop -C, should pass FLAGS instead. + # esp_system/ld is added as well so that scripts of other components + # can include the shared fragments living there (e.g. + # ld.elf.internal.sections). set(base_flags "-C") get_filename_component(script_parent_dir "${script_in}" DIRECTORY) get_filename_component(script_parent_name "${script_parent_dir}" NAME) if(script_parent_name STREQUAL idf_target) get_filename_component(dir_to_include "${script_parent_dir}" DIRECTORY) - string(APPEND base_flags " -I\"${dir_to_include}\"") + idf_component_get_property(esp_system_dir esp_system COMPONENT_DIR) + string(APPEND base_flags + " -I\"${dir_to_include}\" -I\"${esp_system_dir}/ld\"") endif() else() # Explicit FLAGS replace the whole default set, including -C. config_dir diff --git a/tools/test_apps/system/.build-test-rules.yml b/tools/test_apps/system/.build-test-rules.yml index ddb9bdc316d..c841e477355 100644 --- a/tools/test_apps/system/.build-test-rules.yml +++ b/tools/test_apps/system/.build-test-rules.yml @@ -51,9 +51,9 @@ tools/test_apps/system/build_tests/trax_esp32s2: tools/test_apps/system/clang_build_test: enable: - - if: IDF_TARGET in ["esp32", "esp32s2", "esp32s3", "esp32c2", "esp32c3", "esp32c5", "esp32c6", "esp32h2"] + - if: IDF_TARGET in ["esp32", "esp32s2", "esp32s3", "esp32c2", "esp32c3", "esp32c5", "esp32c6", "esp32h2", "esp32p4"] temporary: true - reason: the other targets are not supported yet, esp32p4 # TODO: IDF-14355 + reason: the other targets are not supported yet tools/test_apps/system/cxx_no_except: enable: diff --git a/tools/test_apps/system/clang_build_test/README.md b/tools/test_apps/system/clang_build_test/README.md index 99461edd606..c410436a3a4 100644 --- a/tools/test_apps/system/clang_build_test/README.md +++ b/tools/test_apps/system/clang_build_test/README.md @@ -1,4 +1,4 @@ -| Supported Targets | ESP32 | ESP32-C2 | ESP32-C3 | ESP32-C5 | ESP32-C6 | ESP32-H2 | ESP32-S2 | ESP32-S3 | -| ----------------- | ----- | -------- | -------- | -------- | -------- | -------- | -------- | -------- | +| Supported Targets | ESP32 | ESP32-C2 | ESP32-C3 | ESP32-C5 | ESP32-C6 | ESP32-H2 | ESP32-P4 | ESP32-S2 | ESP32-S3 | +| ----------------- | ----- | -------- | -------- | -------- | -------- | -------- | -------- | -------- | -------- | This project is for testing if the application can be built with Clang toolchain. diff --git a/tools/test_apps/system/clang_build_test/sdkconfig.ci_clang.esp32p4.lld b/tools/test_apps/system/clang_build_test/sdkconfig.ci_clang.esp32p4.lld new file mode 100644 index 00000000000..8af138eaef8 --- /dev/null +++ b/tools/test_apps/system/clang_build_test/sdkconfig.ci_clang.esp32p4.lld @@ -0,0 +1,3 @@ +# Build test: link with LLD instead of GNU ld (esp32p4 only) +CONFIG_IDF_TARGET="esp32p4" +CONFIG_COMPILER_USE_LLD=y