fix(esp_system): enable linking with LLD on RISC-V targets

This commit is contained in:
Stefan Stipanovic
2026-08-13 21:23:36 +02:00
parent b2672c7937
commit d0ae774d59
17 changed files with 147 additions and 11 deletions

View File

@@ -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

10
Kconfig
View File

@@ -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

View File

@@ -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.

View File

@@ -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

View File

@@ -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

View File

@@ -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) }

View File

@@ -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.

View File

@@ -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

View File

@@ -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..."

View File

@@ -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.

View File

@@ -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()

View File

@@ -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

View File

@@ -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)

View File

@@ -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

View File

@@ -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:

View File

@@ -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.

View File

@@ -0,0 +1,3 @@
# Build test: link with LLD instead of GNU ld (esp32p4 only)
CONFIG_IDF_TARGET="esp32p4"
CONFIG_COMPILER_USE_LLD=y