みつきんのメモ

組み込みエンジニアです。Interface誌で「My オリジナルLinuxの作り方」連載中

Pico SDKのリンカスクリプトを眺める

はじめに

リンカスクリプト初心者がPico SDKリンカスクリプトを眺める。今回眺めるのはmemmap_default.ld

大きく分けて下記のようなブロックに別れている

  • MEMORY
  • ENTRY
  • SECTIONS

elfにもリンカスクリプトの詳しくないので間違っている情報があるはず。

MEMORY

メモリ領域のアドレスとサイズ定義している。

MEMORY
{
    FLASH(rx) : ORIGIN = 0x10000000, LENGTH = 2048k
    RAM(rwx) : ORIGIN =  0x20000000, LENGTH = 256k
    SCRATCH_X(rwx) : ORIGIN = 0x20040000, LENGTH = 4k
    SCRATCH_Y(rwx) : ORIGIN = 0x20041000, LENGTH = 4k
}

このあたりはハードウェアに強く依存する。

FLASH-ROMの領域を0x10000000から2048KiB、RAMの領域を0x20000000から256KiBに割り当てている。

SCRATCH_XとSCRATCH_Yがよくわからない。

RP2040 Datasheetによると、RAMは全部で264KiB搭載されていて、6個の独立したバンク(SRAM0-5)で構成されている。

そのうちSRAM0-3の4つ分を1つの領域として使って、残りの2つの領域(SRAM4,5)をそれぞれSCRATCH_X、SCATCH_Yとして割り当てている。ということらしい。

f:id:mickey_happygolucky:20210221170643p:plain
メモリのアドレスマップ

ENTRY

elfのエントリポイントとして_entry_pointを登録している。

SECTIONS

セクション情報を定義している。

.flash_begin

FLASHの先頭の位置を保存している。

    .flash_begin : {
        __flash_binary_start = .;
    } > FLASH

.boot2

.boot2セクションがFLASHの先頭に配置されるようになっている。

    .boot2 : {
        __boot2_start__ = .;
        KEEP (*(.boot2))
        __boot2_end__ = .;
    } > FLASH

    ASSERT(__boot2_end__ - __boot2_start__ == 256,
        "ERROR: Pico second stage bootloader must be 256 bytes in size")

boot2のサイズがチェックサムを含めて256バイトである必要があるので、__boot2_end____boot2_start__の差分でサイズをチェックしている。

.text

プログラム本体が配置される部分。

    .text : {
        __logical_binary_start = .;
        KEEP (*(.vectors))
        KEEP (*(.binary_info_header))
        __binary_info_header_end = .;
        KEEP (*(.reset))
        /* TODO revisit this now memset/memcpy/float in ROM */
        /* bit of a hack right now to exclude all floating point and time critical (e.g. memset, memcpy) code from
         * FLASH ... we will include any thing excluded here in .data below by default */
        *(.init)
        *(EXCLUDE_FILE(*libgcc.a: *libc.a:*lib_a-mem*.o *libm.a:) .text*)
        *(.fini)
        /* Pull all c'tors into .text */
        *crtbegin.o(.ctors)
        *crtbegin?.o(.ctors)
        *(EXCLUDE_FILE(*crtend?.o *crtend.o) .ctors)
        *(SORT(.ctors.*))
        *(.ctors)
        /* Followed by destructors */
        *crtbegin.o(.dtors)
        *crtbegin?.o(.dtors)
        *(EXCLUDE_FILE(*crtend?.o *crtend.o) .dtors)
        *(SORT(.dtors.*))
        *(.dtors)

        *(.eh_frame*)
        . = ALIGN(4);
    } > FLASH

.vectorsベクターテーブルをFLASH上のboot2の直後(0x10000100)に配置する。

.binary_info_headerは後述の.binary_infoを探すための情報のようだ。

残りの部分は一般的なelfのセクションで、各セクションの詳細はここが詳しい。

セクション 概要
.init ELFのコード初期化
.text プログラムの本体
.fini ELFのコード後処理
.ctor コンストラク
.dtor デストラク
.eh_frame バックトレース取得用

.init/.finiと.ctor/.dtorについてはこの記事が詳しい。

.rodata

読み取り専用データが配置される。

    .rodata : {
        *(EXCLUDE_FILE(*libgcc.a: *libc.a:*lib_a-mem*.o *libm.a:) .rodata*)
        . = ALIGN(4);
        *(SORT_BY_ALIGNMENT(SORT_BY_NAME(.flashdata*)))
        . = ALIGN(4);
    } > FLASH

特定のライブラリに含まれるセクションは除外しているようだ。また、アライメントや順序に配慮して配置しているようだ。

.ARM.extab、.ARM.exidx

newlibを使用する時に必要になるセクションらしい。ここが詳しい。

    .ARM.extab :
    {
        *(.ARM.extab* .gnu.linkonce.armextab.*)
    } > FLASH

    __exidx_start = .;
    .ARM.exidx :
    {
        *(.ARM.exidx* .gnu.linkonce.armexidx.*)
    } > FLASH
    __exidx_end = .;

例外テーブルと例外インデックスらしい。

.binary_info

.binary_infoはpicoが独自で持っている情報のようだ。

/**
 * Binary info is intended for embedding machine readable information with the binary in FLASH.
 *
 * Example uses include:
 *
 * - Program identification / information
 * - Pin layouts
 * - Included features
 * - Identifying flash regions used as block devices/storage
 */

おそらくpicotool infoで読み取るための情報が入ると思われる。

    /* Machine inspectable binary information */
    . = ALIGN(4);
    __binary_info_start = .;
    .binary_info :
    {
        KEEP(*(.binary_info.keep.*))
        *(.binary_info.*)
    } > FLASH
    __binary_info_end = .;
    . = ALIGN(4);

    /* End of .text-like segments */
    __etext = .;

ここまででtext(実行可能コード)領域が終了。

.ram_vector_table

vectorテーブルをRAM上に構築するするための領域。

   .ram_vector_table (COPY): {
        *(.ram_vector_table)
    } > RAM

.data

初期化済みのデータが配置される。

    .data : {
        __data_start__ = .;
        *(vtable)

        *(.time_critical*)

        /* remaining .text and .rodata; i.e. stuff we exclude above because we want it in RAM */
        *(.text*)
        . = ALIGN(4);
        *(.rodata*)
        . = ALIGN(4);

        *(.data*)

        . = ALIGN(4);
        *(.after_data.*)
        . = ALIGN(4);
        /* preinit data */
        PROVIDE_HIDDEN (__mutex_array_start = .);
        KEEP(*(SORT(.mutex_array.*)))
        KEEP(*(.mutex_array))
        PROVIDE_HIDDEN (__mutex_array_end = .);

        . = ALIGN(4);
        /* preinit data */
        PROVIDE_HIDDEN (__preinit_array_start = .);
        KEEP(*(SORT(.preinit_array.*)))
        KEEP(*(.preinit_array))
        PROVIDE_HIDDEN (__preinit_array_end = .);

        . = ALIGN(4);
        /* init data */
        PROVIDE_HIDDEN (__init_array_start = .);
        KEEP(*(SORT(.init_array.*)))
        KEEP(*(.init_array))
        PROVIDE_HIDDEN (__init_array_end = .);

        . = ALIGN(4);
        /* finit data */
        PROVIDE_HIDDEN (__fini_array_start = .);
        *(SORT(.fini_array.*))
        *(.fini_array)
        PROVIDE_HIDDEN (__fini_array_end = .);

        *(.jcr)
        . = ALIGN(4);
        /* All data end */
        __data_end__ = .;
    } > RAM AT> FLASH

.dataは初期化済みデータなので、初期値を含んだデータの内容をプログラムのバイナリ、ここではFLASHに格納する必要があるが、書き換え可能である必要があるため実行時にはRAMに配置される必要がある。そのような場合にはRAM AT> FLASHのように指定する。

このあたりの内容はここが詳しい。

.uninitialized_data

    .uninitialized_data (COPY): {
        . = ALIGN(4);
        *(.uninitialized_data*)
    } > RAM

.scratch_x、.scratch_y

おそらく実行時SCRATCH_X、SCRATCH_Yに配置されるデータの初期値を格納する領域。

    /* Start and end symbols must be word-aligned */
    .scratch_x : {
        __scratch_x_start__ = .;
        *(.scratch_x.*)
        . = ALIGN(4);
        __scratch_x_end__ = .;
    } > SCRATCH_X AT > FLASH
    __scratch_x_source__ = LOADADDR(.scratch_x);

    .scratch_y : {
        __scratch_y_start__ = .;
        *(.scratch_y.*)
        . = ALIGN(4);
        __scratch_y_end__ = .;
    } > SCRATCH_Y AT > FLASH
    __scratch_y_source__ = LOADADDR(.scratch_y);

scratch_x、scratch_yマクロを使用してデータを定義するとこれらのセクションに配置されるようになっている。

#define __scratch_x(group) __attribute__((section(".scratch_x." group)))
#define __scratch_y(group) __attribute__((section(".scratch_y." group)))

.bss、.heap

初期値を指定しない、スタック以外のデータを配置する。

    .bss  : {
        . = ALIGN(4);
        __bss_start__ = .;
        *(SORT_BY_ALIGNMENT(SORT_BY_NAME(.bss*)))
        *(COMMON)
        . = ALIGN(4);
        __bss_end__ = .;
    } > RAM

    .heap (COPY):
    {
        __end__ = .;
        end = __end__;
        *(.heap*)
        __HeapLimit = .;
    } > RAM

Stack領域の計算

リンカスクリプトののこりの部分はスタックを配置する領域を計算する。

    /* .stack*_dummy section doesn't contains any symbols. It is only
     * used for linker to calculate size of stack sections, and assign
     * values to stack symbols later
     *
     * stack1 section may be empty/missing if platform_launch_core1 is not used */

    /* by default we put core 0 stack at the end of scratch Y, so that if core 1
     * stack is not used then all of SCRATCH_X is free.
     */
    .stack1_dummy (COPY):
    {
        *(.stack1*)
    } > SCRATCH_X
    .stack_dummy (COPY):
    {
        *(.stack*)
    } > SCRATCH_Y

    .flash_end : {
        __flash_binary_end = .;
    } > FLASH

    /* stack limit is poorly named, but historically is maximum heap ptr */
    __StackLimit = ORIGIN(RAM) + LENGTH(RAM);
    __StackOneTop = ORIGIN(SCRATCH_X) + LENGTH(SCRATCH_X);
    __StackTop = ORIGIN(SCRATCH_Y) + LENGTH(SCRATCH_Y);
    __StackOneBottom = __StackOneTop - SIZEOF(.stack1_dummy);
    __StackBottom = __StackTop - SIZEOF(.stack_dummy);
    PROVIDE(__stack = __StackTop);

    /* Check if data + heap + stack exceeds RAM limit */
    ASSERT(__StackLimit >= __HeapLimit, "region RAM overflowed")

    ASSERT( __binary_info_header_end - __logical_binary_start <= 256, "Binary info must be in first 256 bytes of the binary")
    /* todo assert on extra code */

基本的には、コア0のスタックをSCRATCH_Yに、コア1のスタックをSCRATCH_Xに配置するなっているようだ。

コア0のスタックを後ろに配置することで、コア1が動かない場合にその分の領域を使用できるようにするという意図があるらしい。

最後の2つのASSERTは下記のチェックを行っている。

  1. data + heap + stackがRAMのサイズより大きいとエラーになる
  2. Binary info(header)がboot2を除いたバイナリの先頭の256バイトに配置される必要がある

まとめ

boot2やbinary_infoなどのRP2040/Pico特有の制限に関してリンカスクリプトのASSERTでチェックを行っている。

.textや.dataなどに関しては至ってまともなリンカスクリプトになっているようだ。

scratch_x、scratch_yに関しては、コアごとに分ける必要があるデータ領域はこちらに割り振るようになっているようだ。

リンカスクリプトやelfに関しては初心者なので理解が間違っているところがある可能性は高い。

その場合はコメントやツイッターで私のメンタルがやられない程度にツッコミを入れてくれるとありがたい。

参考

*1:constructor