Skip to content

Use include linker scripts#2841

Merged
will-v-pi merged 36 commits intoraspberrypi:developfrom
will-v-pi:include-linker-scripts
Mar 26, 2026
Merged

Use include linker scripts#2841
will-v-pi merged 36 commits intoraspberrypi:developfrom
will-v-pi:include-linker-scripts

Conversation

@will-v-pi
Copy link
Copy Markdown
Contributor

@will-v-pi will-v-pi commented Feb 26, 2026

This should make it easier to modify linker scripts, without needing to copy the entire script, as you can only modify the parts you need to and use the SDK defaults for the rest.

For example, if you want a linker script using only a specific region of RAM, the custom linker script would be:

RAM_ORIGIN = 0x20010000;
RAM_LENGTH = 128k

INCLUDE "memmap_default.incl"

This could also be achieved using CMake functions:

pico_set_linker_script_var(kitchen_sink_ram_custom RAM_ORIGIN 0x20020000)
pico_set_linker_script_var(kitchen_sink_ram_custom RAM_LENGTH 128k)

For more complexity, you can also override specific files in the linker script by creating an override directory:

pico_add_linker_script_override_path(kitchen_sink_ram_section ${CMAKE_CURRENT_LIST_DIR}/kitchen_sink_ram_section_scripts)

then any files placed in that directory will override the defaults - eg to add a custom section after data before heap, you would add a section_extra_post_data.incl file:

SECTIONS
{
    .extra_data : {
        __extra_data_start__ = .;
        *(.extra_data*)
        . = ALIGN(4);
        __extra_data_end__ = .;
    } > RAM AT> RAM_STORE
    __extra_data_source__ = LOADADDR(.extra_data);
}

or to add an overlay section (kitchen_sink_simple_overlay) you would use:

SECTIONS
{
    PROVIDE(__overlays_start__ = .);
    OVERLAY : {
        .overlay_first {
            KEEP (*(.first*))
            . = ALIGN(4);
        }
        .overlay_second {
            KEEP (*(.second*))
            . = ALIGN(4);
        }
    } > RAM AT> RAM_STORE
    PROVIDE(__overlays_end__ = .);
}

Both of these would override the default empty section_extra_post_data.incl file

@will-v-pi will-v-pi requested a review from kilograham February 26, 2026 12:07
@will-v-pi
Copy link
Copy Markdown
Contributor Author

will-v-pi commented Feb 26, 2026

The files have been split up as follows:

  • All common files are in src/rp2_common/pico_standard_link/script_include:
  • All platform-specific files are now in src/rpXXXX/pico_platform_link/script_include
  • The main linker scripts used are now in src/rpXXXX/pico_platform_link/memmap_XXX.ld, and link straight to the common ones in src/rp2_common/pico_standard_link/script_include
  • All linker scripts intended to be included by INCLUDE have the .incl file extension, and scripts intended to be linked directly with --script have the .ld file extension

The linker scripts have been split up into these separate files:

  • default_locations.ld - platform specific file defining default memory locations (eg RAM_ORIGIN_DEFAULT = 0x20000000;)
  • set_memory_locations.incl - common file to set memory origins and lengths (eg RAM_ORIGIN = DEFINED(RAM_ORIGIN) ? RAM_ORIGIN : RAM_ORIGIN_DEFAULT;)
  • Separate files for each memory region:
    • memory_flash.incl - flash region
    • memory_ram.incl - ram region
    • memory_scratch.incl - scratch X & Y regions
  • Separate memory alias files to define what regions are used for ram and scratch storage:
    • memory_aliases_default.incl - store everything in flash (eg REGION_ALIAS("RAM_STORE", FLASH);)
    • memory_aliases_no_flash.incl - store everything in the same location it is loaded (eg REGION_ALIAS("RAM_STORE", RAM);)
  • Separate sections_xxx.incl files to include the individual section files:
    • sections_XXX.incl - default, copy_to_ram, and no_flash variants
      • sections_XXX_text.incl - contains stuff that is generally read only (text, rodata, binary_info), one for each type (default, no_flash, copy_to_ram) - default files are for RP2350, with RP2040 overriding them as necessary in it's pico_platform
      • sections_XXX_data.incl - contains data and bss, one for each type but common for all platforms (can still be overridden by individual platforms)
  • Separate files for individual sections or groups of tightly linked sections:
    • section_XXX_text.incl - contains the text section - RP2040 overrides this for no_flash as it requires a different layout
      • section_default_text.incl also includes default_text_excludes.incl and default_rodata_excludes.incl files, to allow overriding what standard code is placed in Flash vs SRAM
    • section_XXX_rodata.incl - contains the rodata section
    • section_XXX_data.incl - contains the data section, along with tdata
    • section_boot2.incl - contains the boot2 section - RP2040 overrides this to ensure it's boot2 is compatible with the bootrom
    • sections_arm_ex.incl - contains Arm extab and exidx
    • section_binary_info.incl - contains binary_info
    • section_ram_vector_table.incl - contains the ram_vector_table
    • section_uninitialized_data.incl - contains uninitialized_data
    • section_bss.incl - contains bss, along with tbss
    • section_heap.ld - contains the heap, with custom location setting available with HEAP_LOC and HEAP_LIMIT
    • section_scratch.ld - contains scratch and stack, making use of the earlier memory aliases for storage locations to give a common file for all types
    • section_flash_begin.ld - contains flash_begin
    • section_flash_end.ld - contains flash_end
    • section_end.ld - contains common end section, mostly defining symbols for user code
    • section_platform_end.ld - contains platform specific assertions (eg that embedded block is in first 4096 on rp2350)

@lurch
Copy link
Copy Markdown
Contributor

lurch commented Feb 27, 2026

Would this change also simplify the linker-script modifications in #2840 ?

@will-v-pi
Copy link
Copy Markdown
Contributor Author

Would this change also simplify the linker-script modifications in #2840 ?

Yes, it would mean that PR modifies fewer files

@kilograham kilograham added this to the 2.2.1 milestone Mar 2, 2026
will-v-pi added 15 commits March 6, 2026 15:29
Allows for much simpler custom linker scripts
…ript_var variables

Means that CMake doesn't need to know the default memory addresses for different platforms
…l files easier

Restructured so that it includes the platform-specific files before common ones, so common ones can be overridden
Use new include_linker_script_dir and use_linker_script_file functions to add the linker arguments
Breaking change for Bazel builds using different binary types, instead of setting PICO_DEFAULT_LINKER_SCRIPT to eg `//src/rp2_common/pico_crt0:no_flash_linker_script` it is now `//src/rp2_common/pico_standard_link:no_flash_linker_script`
Treat rp2040 layout (boot2 instead of embedded blocks) as the outlier
@will-v-pi will-v-pi force-pushed the include-linker-scripts branch from afd45f4 to 9c0ed0c Compare March 6, 2026 15:41
Allows overriding what to exclude from .text/.rodata and put in .data
@will-v-pi will-v-pi force-pushed the include-linker-scripts branch from 8c4476c to 51b433d Compare March 16, 2026 14:26
@will-v-pi
Copy link
Copy Markdown
Contributor Author

will-v-pi commented Mar 16, 2026

An example of using this PR to replace the MicroPython linker scripts: micropython/micropython@master...will-v-pi:micropython:include-linker-scripts

This replaces the whole copied linker script (which for RP2040 was still mostly pre-2.0.0), by only overriding:

  • memory_flash.incl as it has custom flash sizing
  • default_text_excludes.incl and default_rodata_excludes.incl, as it puts more stuff in SRAM
  • section_heap.incl as it sets a different __HeapLimit to reduce reported firmware size
  • section_extra_post_platform_end.incl for each platform, to set some more symbols and add an assert
  • Uses custom memmap to override scratch sizes - the alternative would be to use pico_set_linker_script_var

@kilograham kilograham self-assigned this Mar 16, 2026
@kilograham
Copy link
Copy Markdown
Contributor

  1. i'm in two minds as to whether pico_platform_link should be a separate library - yes it is somewhat orthogonal, but equally all the library does is provide linker scripts if they aren't otherwise defined, so that could be done in pico_platform (note that I'm not super keen on the name pico_platform_link though I guess (i've just realized) that it is a complement to pico_standard_link
  2. I think maybe we should go with script_include by default for the script include path directorie
  3. I'm super keen on requiring particular directory layouts in pico_standard_link - perhaps we should allow the cmake to build an ordered list to a PICO_LINKER_SCRIPT_INCLUDE_PATH
  4. For consistency maybe the memmap should be in the toplevel of pico_standard_link - ah i guess this is because it is used as an include, which is fine (a vote for script_include or linker_include) however it makes the pico_platform_link assume that its memmap will NOT be included. this is fine, but maybe we should worry less about the two different directories, and just go with renaming; what i mean by this is that it is a bit confusing today where platform/memmap_default for example just includes "memmap_default.ld" - maybe we should prefix or suffix all files that are intended to be included. Note this would prevent you from not defining a top-level memmap_default in the platform, but we don't seem to do that today. note we could still allow that by explicitly provided a default memmap_default.lt that just includes "memmap_default_part.ld" or whatever
  5. more on naming, a good step has been made towards the names being canonical, however it is a bit confusing because some section_ files contain multiple sections; i.d. exepct section_foo to contain just a foo section, otherwise sections_bar where bar is a name for the collection of sections

yup, this is all hard!

@will-v-pi
Copy link
Copy Markdown
Contributor Author

  1. Yep, happy to pop it all inside pico_platform, as pico_platform_link isn't actually a library at all
  2. I'll rename both standard_scripts and platform_scripts subdirectories to script_include
  3. I assume you mean you're not super keen - I will change it to add to PICO_LINKER_SCRIPT_INCLUDE_PATH instead, but leave pico_add_linker_script_override_path doing what it does now (as otherwise you'd have to add to the start of that list, because the list will already exist before you can add an override path)
  4. I could leave the root linker scripts (currently in pico_platform_link) as they are, and rename the standard ones they include to memmap_xxx_standard.ld or something? Unfortunately memmap_xxx_default.ld doesn't work because then you get memmap_default_default.ld
  5. I can do that and split them up further, so that each section_xxx_yyy.ld file only contains only one section, then there are sections_xxx_yyy.ld files which contain multiple sections (or multiple section_xxx_yyy.ld files), and sections_xxx.ld files which contain only sections_xxx_yyy.ld/section_xxx_yyy.ld files.

Intended for platform specific overrides, whereas post_end is for cross-platform overrides, similar to section_end vs section_platform_end
@will-v-pi
Copy link
Copy Markdown
Contributor Author

@kilograham I think I have made all of those changes now, and also added empty files for easy overriding, so I think this is ready for re-review

Existing method of just setting the linker script didn't work, because the PICO_NO_FLASH/PICO_COPY_TO_RAM define was only available in pico_platform, but needed to be propogated to things that only use pico_base_headers
@will-v-pi
Copy link
Copy Markdown
Contributor Author

In doing this PR, I've noticed that setting PICO_DEFAULT_LINKER_SCRIPT to use the no_flash and copy_to_ram linker scripts in Bazel was not working, due to PICO_NO_FLASH/PICO_COPY_TO_RAM only being set on pico_platform but being required by some things that only depend on pico_platform_internal or pico_base_headers (eg pico_crt0 and pico_standard_binary_info).

This was causing compilation failure on no_flash because standard_binary_info.c references __flash_binary_end when PICO_NO_FLASH is not set, and for copy_to_ram it wouldn't run because crt0 doesn't copy ram_text unless PICO_COPY_TO_RAM is set.

I've fixed that in this PR by implementing a PICO_DEFAULT_BINARY_TYPE setting, which sets the flag in pico_base_headers and also picks the correct linker script.

CC @armandomontanez in case my understanding of this is incorrect

endfunction()

# PICO_CMAKE_CONFIG: PICO_DEFAULT_BINARY_TYPE, The default binary type to use, type=string, default=default, group=pico_standard_link
# PICO_CMAKE_CONFIG: PICO_DEFAULT_BINARY_TYPE, The default binary type to use, type=string, default=default, group=build
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is it worth clarifying what "default" means, or will it resolve to different things in different scenarios?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It's annoying naming, but should be maintained for consistency - the "default" binary type is a flash binary, as opposed to "no_flash" which is an SRAM-only binary, and "copy_to_ram" which is a flash binary that copies all it's code to SRAM before executing it

Copy link
Copy Markdown
Contributor

@armandomontanez armandomontanez left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In doing this PR, I've noticed that setting PICO_DEFAULT_LINKER_SCRIPT to use the no_flash and copy_to_ram linker scripts in Bazel was not working, due to PICO_NO_FLASH/PICO_COPY_TO_RAM only being set on pico_platform but being required by some things that only depend on pico_platform_internal or pico_base_headers (eg pico_crt0 and pico_standard_binary_info).

That makes sense, I remember there's a pretty gnarly circular dependency that's difficult to unwind (hence the _internal bits). The workaround you put together looks fine.

]),
)

include_linker_script_dir(
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'd pitch an interface like this:

linker_script(
    name = "default_locations",
    library_paths = ["."],
    additional_linker_inputs = glob(["*.incl"]),
    srcs = [
        "default_locations.ld",
    ],
)

If that's too much of a pain to implement, feel free to punt and leave me an action item to clean it up.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for all your comments - I've changed it to this interface:

linker_scripts(
    name = "rp2040_linker_script_includes",
    link_scripts = ["default_locations.ld"],
    include_scripts = glob(["*.incl"]),
)

I think this makes it clearer - the link_scripts are .ld files explicitly linked with -T, whereas include_scripts are .incl files which have their package directory included with -L and are added as additional inputs

I couldn't see how to implement library_paths = ["."], so instead opted for getting the paths from the .label.package of the include_scripts, which I think is good solution

…e for individual binaries

Can remove full no_flash etc test builds, as there can now be kitchen_sink_no_flash etc builds like in CMake
Now uses single linker_scripts function to allow both including and linking scripts

Returns DefaultInfo with all necessary files

Prepends ctx.label.workspace_root to link_include_dir

Gets link_include_dir directly from the include_scripts passed in
… bazel

Also adds pico_set_linker_script transition to set linker script for individual binaries
@kilograham
Copy link
Copy Markdown
Contributor

note, it would be nice (and i toyed briefly locally) with putting some directory structure in here, but i guess you would have done that already if the linker did the right thing to allow overrides for say INCLUDE binary_types/default/memory_aliases.incl

@kilograham
Copy link
Copy Markdown
Contributor

kilograham commented Mar 25, 2026

perhaps the nicest thing would be to just draw a tree in a README.md for all the default linker script types ([erhaps at the platform level)

e.g.

|-set_memory_locations.incl
|-memory_flash.incl
  |-pico_flash_region.ld
|-memory_ram.incl
|-memory_scratch.incl
|-memory_generated.incl
|-memory_extra.incl
|-memory_aliases_default.incl
|-sections_default.incl

but with full tree and better ASCII corners!

Note i don'y know if it is worth highlighting any directives but maybe ENTRY_POINT is the only one

@kilograham
Copy link
Copy Markdown
Contributor

^ I guess that could go in a comment in the top-level makefile in platforms/

*we could certainly write a quick tool to generate

@kilograham
Copy link
Copy Markdown
Contributor

that could be a separate PR though to unblock other stuff

@will-v-pi
Copy link
Copy Markdown
Contributor Author

note, it would be nice (and i toyed briefly locally) with putting some directory structure in here, but i guess you would have done that already if the linker did the right thing to allow overrides for say INCLUDE binary_types/default/memory_aliases.incl

I wasn't doing this because then users have to create the correct directory structure in the override directories rather than just needing the correct filename.

There are also lots of common files shared between 2 or 3 binary types (eg memory_aliases_default.incl is used by default and copy_to_ram binaries, I will rename it to memory_aliases_flash.incl to be clearer).

But it would probably work to put them in folders if you think that would be better

Remove whitespace, and rename memory_aliases_default to memory_aliases_flash, as it applies to copy_to_ram too
@kilograham
Copy link
Copy Markdown
Contributor

Note I would actually write a script that does this as a comment in all the scripts and incl files (and lists direct parents)

@kilograham kilograham marked this pull request as ready for review March 25, 2026 17:06
kilograham
kilograham previously approved these changes Mar 25, 2026
@will-v-pi
Copy link
Copy Markdown
Contributor Author

perhaps the nicest thing would be to just draw a tree in a README.md for all the default linker script types ([erhaps at the platform level)

e.g.

|-set_memory_locations.incl
|-memory_flash.incl
  |-pico_flash_region.ld
|-memory_ram.incl
|-memory_scratch.incl
|-memory_generated.incl
|-memory_extra.incl
|-memory_aliases_default.incl
|-sections_default.incl

but with full tree and better ASCII corners!

Note i don'y know if it is worth highlighting any directives but maybe ENTRY_POINT is the only one

Something like this output?

memmap_default.ld  [pico_platform (rp2040)]
└── memmap_default.incl  [pico_standard_link (rp2_common)]
    ├── set_memory_locations.incl  [pico_standard_link (rp2_common)]
    ├── memory_flash.incl  [pico_standard_link (rp2_common)]
    ├── memory_ram.incl  [pico_standard_link (rp2_common)]
    ├── memory_scratch.incl  [pico_standard_link (rp2_common)]
    ├── memory_generated.incl  [pico_standard_link (rp2_common)]
    ├── memory_extra.incl  [pico_standard_link (rp2_common)]
    ├── memory_aliases_flash.incl  [pico_standard_link (rp2_common)]
    └── sections_default.incl  [pico_standard_link (rp2_common)]
        ├── sections_default_text.incl  [pico_standard_link (rp2_common)]
        │   ├── section_flash_begin.incl  [pico_standard_link (rp2_common)]
        │   ├── section_default_text.incl  [pico_standard_link (rp2_common)]
        │   │   └── default_text_excludes.incl  [pico_standard_link (rp2_common)]
        │   ├── section_boot2.incl  [pico_standard_link (rp2_common)]
        │   ├── section_default_rodata.incl  [pico_standard_link (rp2_common)]
        │   │   └── default_rodata_excludes.incl  [pico_standard_link (rp2_common)]
        │   ├── sections_arm_ex.incl  [pico_standard_link (rp2_common)]
        │   └── section_binary_info.incl  [pico_standard_link (rp2_common)]
        ├── section_generated_post_text.incl  [pico_standard_link (rp2_common)]
        ├── section_extra_post_text.incl  [pico_standard_link (rp2_common)]
        ├── sections_default_data.incl  [pico_standard_link (rp2_common)]
        │   ├── section_ram_vector_table.incl  [pico_standard_link (rp2_common)]
        │   ├── section_uninitialized_data.incl  [pico_standard_link (rp2_common)]
        │   ├── section_default_data.incl  [pico_standard_link (rp2_common)]
        │   └── section_bss.incl  [pico_standard_link (rp2_common)]
        ├── section_generated_post_data.incl  [pico_standard_link (rp2_common)]
        ├── section_extra_post_data.incl  [pico_standard_link (rp2_common)]
        ├── section_heap.incl  [pico_standard_link (rp2_common)]
        ├── sections_scratch.incl  [pico_standard_link (rp2_common)]
        ├── section_generated_post_scratch.incl  [pico_standard_link (rp2_common)]
        ├── section_extra_post_scratch.incl  [pico_standard_link (rp2_common)]
        ├── sections_stack.incl  [pico_standard_link (rp2_common)]
        ├── section_flash_end.incl  [pico_standard_link (rp2_common)]
        ├── section_end.incl  [pico_standard_link (rp2_common)]
        ├── section_generated_post_end.incl  [pico_standard_link (rp2_common)]
        ├── section_extra_post_end.incl  [pico_standard_link (rp2_common)]
        ├── section_platform_end.incl  [pico_platform (rp2040)]
        ├── section_generated_post_platform_end.incl  [pico_standard_link (rp2_common)]
        └── section_extra_post_platform_end.incl  [pico_standard_link (rp2_common)]

Co-authored-by: Andrew Scheller <lurch@durge.org>
@kilograham
Copy link
Copy Markdown
Contributor

perhaps the nicest thing would be to just draw a tree in a README.md for all the default linker script types ([erhaps at the platform level)
e.g.

|-set_memory_locations.incl
|-memory_flash.incl
  |-pico_flash_region.ld
|-memory_ram.incl
|-memory_scratch.incl
|-memory_generated.incl
|-memory_extra.incl
|-memory_aliases_default.incl
|-sections_default.incl

but with full tree and better ASCII corners!
Note i don'y know if it is worth highlighting any directives but maybe ENTRY_POINT is the only one

Something like this output?

memmap_default.ld  [pico_platform (rp2040)]
└── memmap_default.incl  [pico_standard_link (rp2_common)]
    ├── set_memory_locations.incl  [pico_standard_link (rp2_common)]
    ├── memory_flash.incl  [pico_standard_link (rp2_common)]
    ├── memory_ram.incl  [pico_standard_link (rp2_common)]
    ├── memory_scratch.incl  [pico_standard_link (rp2_common)]
    ├── memory_generated.incl  [pico_standard_link (rp2_common)]
    ├── memory_extra.incl  [pico_standard_link (rp2_common)]
    ├── memory_aliases_flash.incl  [pico_standard_link (rp2_common)]
    └── sections_default.incl  [pico_standard_link (rp2_common)]
        ├── sections_default_text.incl  [pico_standard_link (rp2_common)]
        │   ├── section_flash_begin.incl  [pico_standard_link (rp2_common)]
        │   ├── section_default_text.incl  [pico_standard_link (rp2_common)]
        │   │   └── default_text_excludes.incl  [pico_standard_link (rp2_common)]
        │   ├── section_boot2.incl  [pico_standard_link (rp2_common)]
        │   ├── section_default_rodata.incl  [pico_standard_link (rp2_common)]
        │   │   └── default_rodata_excludes.incl  [pico_standard_link (rp2_common)]
        │   ├── sections_arm_ex.incl  [pico_standard_link (rp2_common)]
        │   └── section_binary_info.incl  [pico_standard_link (rp2_common)]
        ├── section_generated_post_text.incl  [pico_standard_link (rp2_common)]
        ├── section_extra_post_text.incl  [pico_standard_link (rp2_common)]
        ├── sections_default_data.incl  [pico_standard_link (rp2_common)]
        │   ├── section_ram_vector_table.incl  [pico_standard_link (rp2_common)]
        │   ├── section_uninitialized_data.incl  [pico_standard_link (rp2_common)]
        │   ├── section_default_data.incl  [pico_standard_link (rp2_common)]
        │   └── section_bss.incl  [pico_standard_link (rp2_common)]
        ├── section_generated_post_data.incl  [pico_standard_link (rp2_common)]
        ├── section_extra_post_data.incl  [pico_standard_link (rp2_common)]
        ├── section_heap.incl  [pico_standard_link (rp2_common)]
        ├── sections_scratch.incl  [pico_standard_link (rp2_common)]
        ├── section_generated_post_scratch.incl  [pico_standard_link (rp2_common)]
        ├── section_extra_post_scratch.incl  [pico_standard_link (rp2_common)]
        ├── sections_stack.incl  [pico_standard_link (rp2_common)]
        ├── section_flash_end.incl  [pico_standard_link (rp2_common)]
        ├── section_end.incl  [pico_standard_link (rp2_common)]
        ├── section_generated_post_end.incl  [pico_standard_link (rp2_common)]
        ├── section_extra_post_end.incl  [pico_standard_link (rp2_common)]
        ├── section_platform_end.incl  [pico_platform (rp2040)]
        ├── section_generated_post_platform_end.incl  [pico_standard_link (rp2_common)]
        └── section_extra_post_platform_end.incl  [pico_standard_link (rp2_common)]

Though since it is script generated let’s align the columns… put something like rp2_common/pico_standard_link as the second column

@will-v-pi will-v-pi merged commit bc9806e into raspberrypi:develop Mar 26, 2026
6 checks passed
@kilograham
Copy link
Copy Markdown
Contributor

Also wrt the trees - let’s
Make the tool able to be run again (and check the output) so we can add it to the commit checks

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.

4 participants