ARM
組み込み
STM32
リンカスクリプト

STM32 Nucleo Boardリンカスクリプト

More than 1 year has passed since last update.

STM32 Nucleo Boardリンカスクリプト

RaspberryPiでSTM32の開発環境構築の環境で開発を行う。
私の知識ではリンカスクリプトをスクラッチで書くことはできないため、
githubからソースコードを入手した。学習のため中身を詳しく読んでみることにした。

開発ターゲット

STM32 Nucleo Board STM32F303K8

参照したソースコード

STM32-Communication-Test/STM32F303K8Tx_FLASH.ld

リンカスクリプト

リンカスクリプトは、コンパイルして作成される実行形式ファイルのメモリ配置を定義している。
リンカスクリプトは、そのままメモリマップになる。

メモリマップ

STM32F303K8のメモリマップは次のようになっている。
mmap.PNG

メモリとセクション

メモリ上に展開されたプログラムは単に機械語コードが置かれているわけではなく、いくつかのセクションから構成されている。
実行ファイル内部もはいくつかのセクションに分かれており、コピーはセクション単位で行われる。
一般には次のセクションを持つ。

セクション名 意味
.text CPUが実行するプログラムコードが置かれる
.rodata 読み出し専用のデータが置かれる
.data 初期値を持つ静的変数などが置かれる
.bss 初期値を持たない静的変数などが置かれる

リンカスクリプトの書式

リンカスクリプトには大きく分けてMEMORYとSECTIONSという項目がある。
MEMORYコマンドはメモリ空間と空間の属性定義を行う。
SECTIONSコマンドはセクションがメモリのどこに置かれるのかを記述する。

MEMORYコマンドの書式

MEMORYコマンドの書式は以下のようになる。

MEMORY
{
  name [(attr)] : ORIGIN = origin, LENGTH = len
  ...
}

属性は以下の通り。

属性 意味
r 読み出し専用セクション
w 読み書き両用セクション
x 実行可能セクション
a 割り当て可能セクション
i 初期化済みセクション

SECTIONSコマンドの書式

SECTIONSコマンドの書式は以下のようになる。

SECTIONS{
  section [address] [(type)] :
  [AT(lma)]
  [ALIGN(section_align)]
  [SUBALIGN(subsection_align)]
  [constraint]
  {
  output-section-command
  output-section-command
  ...
  } [>region] [AT>lma_region] [:phdr :phdr ...] [=fillexp]

  ...
}

解説

実際のソースを読んで、どのようにリンカスクリプトを記述するのか確認していく。

/* Entry Point */
ENTRY(Reset_Handler)
/* Highest address of the user mode stack */
_estack = 0x20003000;    /* end of RAM */
/* Generate a link error if heap and stack don't fit into RAM */
_Min_Heap_Size = 0x200;      /* required amount of heap  */
_Min_Stack_Size = 0x400; /* required amount of stack */

最初に、エントリポイントと定数の設定を行っている。
ENTRY(symbol)でエントリポイントを設定する。
シンボル名のReset_Handlerはスタートアップルーチンで定義している。
リンカスクリプトに定義されたシンボルは、変数名として参照することができる。

続いてMEMORYコマンドでメモリの領域を定義する。
これはメモリマップと対応している。

/* Specify the memory areas */
MEMORY
{
  RAM (xrw)      : ORIGIN = 0x20000000, LENGTH = 12K
  CCMRAM (rw)    : ORIGIN = 0x10000000, LENGTH = 4K
  FLASH (rx)     : ORIGIN = 0x8000000, LENGTH = 64K
}

RAM、CCMRAM、FLASHの領域を定義している。
例えば、RAM領域は
・開始アドレス=0x20000000
・領域の長さ=12KByte
・読み出し/書き込み/実行が可能
となる。

ここから先はSECTIONコマンドでセクションを定義している。
セクションの配置が細かく記述されている。

/* Define output sections */
SECTIONS
{
  .isr_vector :
    :
    中略
    :
  .ARM.attributes 0 : { *(.ARM.attributes) }
}

続いてSECTIONSの中を見てみる。

  /* The startup code goes first into FLASH */
  .isr_vector :
  {
    . = ALIGN(4);
    KEEP(*(.isr_vector)) /* Startup code */
    . = ALIGN(4);
  } >FLASH

ここでは.isr_vectorを定義している。
割り込みベクタを配置しているセクションでFLASHに配置している。
.はロケーションカウンタと呼ばれ、現在のアドレスを示す(初期値は0)。
オブジェクトが1バイト出力されれば、ロケーションカウンタも1バイト加算される。
. = ALIGN(4);により現在のアドレスを4Byteアラインにしている。
KEEPは入力が無くても出力を指定するコマンド。
従って、この.isr_vectorセクションは必ず出力される。
>FLASHでMEMORYによって定義したFLASH領域に配置する。

  /* The program code and other data goes into FLASH */
  .text :
  {
    . = ALIGN(4);
    *(.text)           /* .text sections (code) */
    *(.text*)          /* .text* sections (code) */
    *(.glue_7)         /* glue arm to thumb code */
    *(.glue_7t)        /* glue thumb to arm code */
    *(.eh_frame)

    KEEP (*(.init))
    KEEP (*(.fini))

    . = ALIGN(4);
    _etext = .;        /* define a global symbols at end of code */
  } >FLASH

ここでは.textセクションを定義している。
プログラムコードが配置されるセクションでFLASHに配置している。
*はワイルドカードで全てのファイル名を示す。
.glue_7、.glue_7tはARM-THUMBインターワーキングオプションを指定すると、コンパイラによって合成されるコード。
.eh_frameはランタイムC++ 例外の解決に使用されるコード。
.initはmain前の初期化処理コード。
.finiはプログラムの終了処理コード。
_etextにはセクションの終了アドレスを格納している。

  /* Constant data goes into FLASH */
  .rodata :
  {
    . = ALIGN(4);
    *(.rodata)         /* .rodata sections (constants, strings, etc.) */
    *(.rodata*)        /* .rodata* sections (constants, strings, etc.) */
    . = ALIGN(4);
  } >FLASH

ここでは.rodataセクションを定義している。
定数データが配置されるセクションでFLASHに配置している。

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

ここでは.ARM.extab、.ARMを定義している。
ARM.extab、ARM.exidxはnewlibを使うときに必要となるセクションでFLASHに配置している。
例外処理のために使われているらしい。

  .preinit_array     :
  {
    PROVIDE_HIDDEN (__preinit_array_start = .);
    KEEP (*(.preinit_array*))
    PROVIDE_HIDDEN (__preinit_array_end = .);
  } >FLASH
  .init_array :
  {
    PROVIDE_HIDDEN (__init_array_start = .);
    KEEP (*(SORT(.init_array.*)))
    KEEP (*(.init_array*))
    PROVIDE_HIDDEN (__init_array_end = .);
  } >FLASH
  .fini_array :
  {
    PROVIDE_HIDDEN (__fini_array_start = .);
    KEEP (*(SORT(.fini_array.*)))
    KEEP (*(.fini_array*))
    PROVIDE_HIDDEN (__fini_array_end = .);
  } >FLASH

ここでは.preinit_array、.init_array、.fini_arrayを定義している。
それぞれFLASHに配置している。
グローバル変数でクラスのインスタンスを定義した場合、
main関数の呼び出しよりも先にそれらのコンストラクタが呼び出される必要がある。
g++はそのような変数に対する初期化コードへの関数ポインタを
.preinit_array, .init_arrayというセクションに、終了処理を.fini_arrayに出力する。

  /* used by the startup to initialize data */
  _sidata = LOADADDR(.data);

ここでは_sidataシンボルを定義している。
LOADADDR(.data)は.dataのFLASH上でのアドレスを返す。
_sidataはスタートアップルーチンでFLASH上の初期値をRAMにコピーする際に使用する。
そのため、ここでFLASH上のアドレスを取得している。

  /* Initialized data sections goes into RAM, load LMA copy after code */
  .data : 
  {
    . = ALIGN(4);
    _sdata = .;        /* create a global symbol at data start */
    *(.data)           /* .data sections */
    *(.data*)          /* .data* sections */

    . = ALIGN(4);
    _edata = .;        /* define a global symbol at data end */
  } >RAM AT> FLASH

ここでは.dataセクションを定義している。
.dataは初期値を持つ静的変数のセクション。
最後の行は、>RAM AT > FLASHとなっている。
これは、プログラムが動作するときのアドレス(RAM)と
データを保持するためのアドレス(FLASH)を指定している。
初期値は電源を切っても消えない場所に保持しておく必要がある。
このシステムではFLASHに保持しており、そのときの指定がAT > FLASHとなる。
_sdata、edataにはセクションの開始アドレス、終了アドレスを格納する。

  _siccmram = LOADADDR(.ccmram);

  /* CCM-RAM section */
  .ccmram :
  {
    . = ALIGN(4);
    _sccmram = .;       /* create a global symbol at ccmram start */
    *(.ccmram)
    *(.ccmram*)

    . = ALIGN(4);
    _eccmram = .;       /* create a global symbol at ccmram end */
  } >CCMRAM AT> FLASH

ここでは.ccmramセクションを定義している。
CCM RAMに出力するデータのセクション。
CCMRAM AT> FLASHを指定しており、
初期値はFLASHに保持されるが、プログラム動作時にはCCMRAMに配置される。

  /* Uninitialized data section */
  . = ALIGN(4);
  .bss :
  {
    /* This is used by the startup in order to initialize the .bss secion */
    _sbss = .;         /* define a global symbol at bss start */
    __bss_start__ = _sbss;
    *(.bss)
    *(.bss*)
    *(COMMON)

    . = ALIGN(4);
    _ebss = .;         /* define a global symbol at bss end */
    __bss_end__ = _ebss;
  } >RAM

ここでは.bssセクションを定義している。
初期値なし静的変数のセクションでRAMに配置している。
COMMONはコモンシンボル用の入力セクションである。
_sbss、ebssにはセクションの開始アドレス、終了アドレスを格納する。

  /* User_heap_stack section, used to check that there is enough RAM left */
  ._user_heap_stack :
  {
    . = ALIGN(8);
    PROVIDE ( end = . );
    PROVIDE ( _end = . );
    . = . + _Min_Heap_Size;
    . = . + _Min_Stack_Size;
    . = ALIGN(8);
  } >RAM

ここでは._user_heap_stackセクションを定義している。
RAMにスタック/ヒープ用の空き領域があるか確認するために配置している。
_Min_Heap_Size、_Min_Stack_Sizeが確保できなくなるとリンカエラーが出る。

  /* Remove information from the standard libraries */
  /DISCARD/ :
  {
    libc.a ( * )
    libm.a ( * )
    libgcc.a ( * )
  }

  .ARM.attributes 0 : { *(.ARM.attributes) }

/DISCARD/は出力セクション削除で、入力セクションは捨てられる。
.ARM.attributesはBuild Attributesを含んだセクションとのことだが、メモリに配置されない。

以上でこのコードは終了だがldは奥が深くARMの知識も必要となるため、使いこなすのはかなり大変そうだ。

参考サイト

The GNU Linker
初めてのC言語
GNU Cを使いこなそう
mikan++
メモ書き
ありがとうございます。