土日の勉強ノート

AI、機械学習、最適化、Pythonなどについて、技術調査、技術書の理解した内容、ソフトウェア/ツール作成について書いていきます

STM32(ARM Cortex-M)をQEMUで動かす(リンカスクリプト編)

前回は、QEMU (ターゲットは STM32F4-Discovery)で動かしたサンプルソースのスタートアップルーチンの内容を確認しました。

今回は、サンプルソースのリンカスクリプトの内容を確認していきます。

それでは、やっていきます。

参考文献

STM32F4 のマニュアル

下記リンクのドキュメント→リファレンスマニュアル、ドキュメント→プログラミングマニュアルなどを参照してください。最近は、マニュアルが日本語化されていて、とても便利です。

デザイン/サポート | STM32, STM8ファミリはSTの32bit/8bit汎用マイクロコントローラ製品

GNU リンカのマニュアル

https://sourceware.org/binutils/docs/ld/

はじめに

「QEMUを動かす」の記事一覧です。良かったら参考にしてください。

QEMUを動かすの記事一覧

今回も、Interface のサンプルソースを使わせていただきます。

サンプルソースは、Interfaceのホームページからダウンロードします。

下記の「7月号 仮想から実機まで マイコン開発入門」の「特集 第3部第1章 エミュレータQEMUを活用した開発の手引き」の「関連ファイル一式」をダウンロードします。

www.cqpub.co.jp

それでは、やっていきます!

stm32f407vg.ld

STM32F407xx のデータシートのメモリマップを貼っておきます。

STM32F405xx STM32F407xx のデータシートのメモリマップ
STM32F405xx STM32F407xx のデータシートのメモリマップ

リンカスクリプトは、必須なのは、SECTIONSコマンド(SECTIONS{ から始まって } まで)です。

ENTRY() など、関数のように括弧で囲った機能をコマンドと呼びます。

MAIN_STACK_SIZE など、変数のように使ってるのは、シンボルです。

では、順番に見ていきます。

ENTRY

ENTRY() は、エントリポイントを設定します。今回の例では、引数の Reset_Handler関数が、エントリポイントになります。

/* Entry Point */
ENTRY(Reset_Handler)

MEMORY

MEMORY は、メモリ領域を定義します。ここで定義した RAM と ROM は、後ろのセクションを定義するときに出てきます。

ORIGIN は、開始アドレスで、LENGTH は長さです。括弧内は属性で、r:読み撮り可能、w:書き込み可能、x:実行可能です。

RAM は、0x20000000 から 0x20000(128KB)が定義されています。

ROM は、0x08000000 から 0x100000(1MB)が定義されています。

データシートを見ると、CCM data RAM というものがあるようですが、リンカスクリプトでは定義していないようです。

MEMORY
{
  RAM (rwx)    : ORIGIN = 0x20000000, LENGTH = 0x20000
  ROM (rx)     : ORIGIN = 0x08000000, LENGTH = 0x100000
}

シンボルの定義

単純に、それぞれシンボルを定義してます。

MAIN_STACK_SIZE    = 0x400; /* size: 0x400 (1024 bytes) */
PROCESS_STACK_SIZE = 0x400; /* size: 0x400 (1024 bytes) */
HEAP_SIZE          = 0x100; /* size: 0x100 ( 256 bytes) */

PROVIDE

PROVIDE もシンボルを定義します。ただし、オブジェクトファイルで同じ名前のシンボルが定義されていた場合は、その値を使用します(オブジェクトファイルの定義を優先します)。

まず、__main_stack_start0x20020000 と定義します。メインスタックとあるので通常使うスタックという意味だと思います。スタックは小さいアドレスに向かって使われていきます。

その後、__process_stack_start は、プロセススタックということで、メインスタックの 1KB の後に確保されます。プロセススタックも 1KB 確保されます。

最後にヒープ領域の開始アドレスは、プロセススタックの 1KB と、ヒープサイズ(256byte)の後に定義されます。ヒープはアドレスの大きい方に向かって使っていくので、このような定義になっています。

PROVIDE (__main_stack_start    = 0x20020000);
PROVIDE (__process_stack_start = __main_stack_start - MAIN_STACK_SIZE);
PROVIDE (_pvHeapStart = __process_stack_start - PROCESS_STACK_SIZE - HEAP_SIZE);

SECTIONS

SECTIONS は、セクションを定義します。セクションとは、メモリ領域を塊で定義して、それぞれの塊に、属性を設定することが出来ます。

書式としては、最初にセクション名があって、括弧内で属性を設定します。上から順番に割り当てられていきます。

最初に定義されているのは「.isr_vectorセクション」です。「.(ドット)」はロケーションカウンタと言って、現在のアドレスを表します。

. = ALIGN(4); は、4byteのアライメントにロケーションカウンタを合わせるという意味です。既に4byteアライメントに合っていたら何もしません。

まず、括弧の中の *(.isr_vector) は、「.isr_vectorセクション」で、アスタリスク * は、全てのオブジェクトファイルという意味です。つまり、「.isr_vectorセクション」で定義されている全てのオブジェクトファイルという意味になります。

KEEP() は最適化対象から外すという意味で、必ず領域が確保されます。

最後の >ROM は、上で定義した ROM に出力されるという意味になります。「.isr_vectorセクション」は、0x08000000 から配置されるという意味になります。

/* Define output sections */
SECTIONS
{
  /* The startup code into ROM memory */
  .isr_vector :
  {
    . = ALIGN(4);
    KEEP(*(.isr_vector)) /* Startup code */
    . = ALIGN(4);
  } >ROM

「.textセクション」の定義です。新しい文法だけ説明します。_etext = .; は、このときのロケーションカウンタの値を _etext というシンボルに設定するという意味になります(.textセクションの終了したアドレスを記憶している)。

  /* The program code and other data into ROM memory */
  .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 */
  } >ROM

「.rodataセクション」が定義されています。「.rodataセクション」は const で宣言された書き換えないデータのセクションです。ROMに割り当てられます。

以降は、同じような内容が続きます。

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

  .ARM.extab   : {
      . = ALIGN(4);
      *(.ARM.extab* .gnu.linkonce.armextab.*)
      . = ALIGN(4);
  } >ROM

  .ARM : {
    . = ALIGN(4);
    __exidx_start = .;
    *(.ARM.exidx*)
    __exidx_end = .;
    . = ALIGN(4);
  } >ROM

PROVIDE_HIDDEN は、PROVIDE と同じく、オブジェクトファイルに定義がないときに定義します。HIDDEN は、外部から参照できないことを意味します。つまり、このファイルでだけ有効なシンボルということになります。

  .preinit_array     :
  {
    . = ALIGN(4);
    PROVIDE_HIDDEN (__preinit_array_start = .);
    KEEP (*(.preinit_array*))
    PROVIDE_HIDDEN (__preinit_array_end = .);
    . = ALIGN(4);
  } >ROM

SORT() は、名前を辞書順にソートしてから割り当てます。

  .init_array :
  {
    . = ALIGN(4);
    PROVIDE_HIDDEN (__init_array_start = .);
    KEEP (*(SORT(.init_array.*)))
    KEEP (*(.init_array*))
    PROVIDE_HIDDEN (__init_array_end = .);
    . = ALIGN(4);
  } >ROM

  .fini_array :
  {
    . = ALIGN(4);
    PROVIDE_HIDDEN (__fini_array_start = .);
    KEEP (*(SORT(.fini_array.*)))
    KEEP (*(.fini_array*))
    PROVIDE_HIDDEN (__fini_array_end = .);
    . = ALIGN(4);
  } >ROM

LOADADDR() は、ロードアドレスを取得します。LOADADDR(.data); は、初期値付きグローバル変数の初期値が格納されてる方の(ROMの)アドレスを取得します。

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

>RAM AT> ROM は、まず、>RAM で、「.dataセクション」としては RAM に割り当てられます。後ろの AT > ROM は、初期値を格納している領域は ROM に配置するという意味になります。

  /* Initialized data sections into RAM memory */
  .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> ROM

  /* Uninitialized data section into RAM memory */
  . = 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

ここは、他と異なります。._user_heap_stack セクションは、_pvHeapStart のアドレスが割り当てられます。_pvHeapStart は上で計算しました。

NOLOAD は、オブジェクトは出力されないという意味です。ヒープ領域とスタック領域なので、出力されないのは理解できますが、それなら .bss も NOLOAD が指定されてても良さそうです。と思って、Webで検索したら、bss に NOLOAD を指定してる例もありました。この辺がリンカスクリプトの難しいところですね。

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

/DISCARD/ は特殊なセクションで、出力ファイルに含まれず破棄されます。

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

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

おわりに

今回は、リンカスクリプトの内容の確認を行って、理解を深めました。

次回は、今回の内容を踏まえて、QEMUで実行したELFファイルの内容を見ていきたいと思います。

最後になりましたが、エンジニアグループのランキングに参加中です。

気楽にポチッとよろしくお願いいたします🙇

今回は以上です!

最後までお読みいただき、ありがとうございました。