前回は、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 のデータシートのメモリマップを貼っておきます。
リンカスクリプトは、必須なのは、SECTIONSコマンド(SECTIONS{ から始まって } まで)です。
ENTRY()
など、関数のように括弧で囲った機能をコマンドと呼びます。
MAIN_STACK_SIZE
など、変数のように使ってるのは、シンボルです。
では、順番に見ていきます。
ENTRY
ENTRY()
は、エントリポイントを設定します。今回の例では、引数の Reset_Handler関数が、エントリポイントになります。
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;
PROCESS_STACK_SIZE = 0x400;
HEAP_SIZE = 0x100;
PROVIDE
PROVIDE もシンボルを定義します。ただし、オブジェクトファイルで同じ名前のシンボルが定義されていた場合は、その値を使用します(オブジェクトファイルの定義を優先します)。
まず、__main_stack_start
を 0x20020000
と定義します。メインスタックとあるので通常使うスタックという意味だと思います。スタックは小さいアドレスに向かって使われていきます。
その後、__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 から配置されるという意味になります。
SECTIONS
{
.isr_vector :
{
. = ALIGN(4);
KEEP(*(.isr_vector))
. = ALIGN(4);
} >ROM
「.textセクション」の定義です。新しい文法だけ説明します。_etext = .;
は、このときのロケーションカウンタの値を _etext
というシンボルに設定するという意味になります(.textセクションの終了したアドレスを記憶している)。
.text :
{
. = ALIGN(4);
*(.text)
*(.text*)
*(.glue_7)
*(.glue_7t)
*(.eh_frame)
KEEP (*(.init))
KEEP (*(.fini))
. = ALIGN(4);
_etext = .;
} >ROM
「.rodataセクション」が定義されています。「.rodataセクション」は const で宣言された書き換えないデータのセクションです。ROMに割り当てられます。
以降は、同じような内容が続きます。
.rodata :
{
. = ALIGN(4);
*(.rodata)
*(.rodata*)
. = 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の)アドレスを取得します。
_sidata = LOADADDR(.data);
>RAM AT> ROM
は、まず、>RAM
で、「.dataセクション」としては RAM に割り当てられます。後ろの AT > ROM
は、初期値を格納している領域は ROM に配置するという意味になります。
.data :
{
. = ALIGN(4);
_sdata = .;
*(.data)
*(.data*)
. = ALIGN(4);
_edata = .;
} >RAM AT> ROM
. = ALIGN(4);
.bss :
{
_sbss = .;
__bss_start__ = _sbss;
*(.bss)
*(.bss*)
*(COMMON)
. = ALIGN(4);
_ebss = .;
__bss_end__ = _ebss;
} >RAM
ここは、他と異なります。._user_heap_stack
セクションは、_pvHeapStart
のアドレスが割り当てられます。_pvHeapStart
は上で計算しました。
NOLOAD
は、オブジェクトは出力されないという意味です。ヒープ領域とスタック領域なので、出力されないのは理解できますが、それなら .bss も NOLOAD が指定されてても良さそうです。と思って、Webで検索したら、bss に NOLOAD を指定してる例もありました。この辺がリンカスクリプトの難しいところですね。
._user_heap_stack _pvHeapStart(NOLOAD) :
{
. = ALIGN(8);
PROVIDE ( end = . );
PROVIDE ( _end = . );
. = . + HEAP_SIZE;
. = ALIGN(8);
} >RAM
/DISCARD/
は特殊なセクションで、出力ファイルに含まれず破棄されます。
/DISCARD/ :
{
libc.a ( * )
libm.a ( * )
libgcc.a ( * )
}
.ARM.attributes 0 : { *(.ARM.attributes) }
}
おわりに
今回は、リンカスクリプトの内容の確認を行って、理解を深めました。
次回は、今回の内容を踏まえて、QEMUで実行したELFファイルの内容を見ていきたいと思います。
最後になりましたが、エンジニアグループのランキングに参加中です。
気楽にポチッとよろしくお願いいたします🙇
今回は以上です!
最後までお読みいただき、ありがとうございました。