ブートローダとスタートアップコードの実装
組み込み機器の電源投入からmainまでには、見落とされがちな処理がいくつも走ります。この空白を理解していないと、起動不良の切り分けで糸口を見失いがちです。
この記事では、ブートローダとスタートアップの責務、Reset_Handlerからmainまでの典型フロー、.dataと.bssの初期化、ブートとアプリの遷移設計、ハマりやすいポイントを順に解説していきましょう。
- ブートローダとスタートアップの違い
- Reset_Handler以降の典型フロー
- .dataと.bssの初期化が意味するもの
- ブートローダ+アプリ構成でのメモリマップと初期化
- 実装でハマりやすいポイントと切り分け
ブートローダとスタートアップの違い
両者は混同されがちですが、責務も実行タイミングも異なります。区別が曖昧なまま設計を進めると、初期化の分担の過程で漏れが発生し、起動不良の原因究明が長引きます。
ブートローダはアプリを起動するソフトウェアで、ファームウェア更新やアプリ選択、署名検証などを主な役割とします。マイコンの種類によっては、ブートローダのメモリ上の配置に推奨があったり、その領域の書き換えを行う仕組みがあらかじめ用意されている物もあります。例えば、USARTやUSB DFU経由でフラッシュメモリを書き換える仕組みなどがあります。
スタートアップコードは、C言語のmainを呼ぶまでに実行環境を整えるコードです。ベクタテーブルの設定、メモリ初期化、ランタイムライブラリ準備が主な仕事になります。電源投入直後はブートローダ、main直前はスタートアップです。ブートローダがない構成はありえますが、スタートアップは省略できません。
Reset_Handler以降の典型フロー
このセクションでは処理の全体像を解説します。なお、.dataや.bssの意味については次のセクションで個別に取り上げることとします。
ARM Cortex-Mシリーズのリセット動作は、Cortex-M3 Devices Generic User Guideで規定されています。リセット時、CPUはベクタテーブル先頭ワードを初期スタックポインタとしてMSP(Main Stack Pointer)にロードし、続く2番目のワードをReset_HandlerのアドレスとしてPCにロードします。ハンドラアドレスの最下位ビットを1にしてThumb状態を示す決まりです。
Reset_Handlerの中身はCMSIS-Coreのスタートアップ仕様に従うのが一般的です。まず SystemInitでクロック設定など最小限の初期化を済ませ、続いて.dataコピーと.bssゼロクリアを実施し、最後にmainへ制御が渡ります。
.dataと.bssの初期化が意味するもの
初期値を持つ変数と持たない変数では、起動時の処理内容が違います。両者の違いを押さえれば、リンカスクリプトの記述意図も読み取れるでしょう。
C言語では、初期値を持つグローバル変数や静的変数は.dataに、初期値を持たない変数は.bssに配置されます。mainが呼ばれる前に、.dataはフラッシュからRAMへコピー、.bssはゼロクリアが完了している必要があります。
ISO/IEC 9899:2024の「6.7.11 Initialization」では、静的記憶域期間を持つ未初期化オブジェクトは暗黙的に初期化されると規定されており、スタートアップ処理はこの規格要件を満たすために動くものです。
.data=ROMなどからRAMへのコピー
初期値を持つ変数は、フラッシュROMに焼かれた初期値をRAMへコピーして使います。RenesasのCC-RLコンパイラドキュメントでも、スタートアップ処理としてdata属性領域の初期値コピーとbss属性領域のゼロクリアをおこなうと明記されています。
ROMのまま使えない理由は、主に次の二つです。
第一にROMは基本的に書き換え不可であるため、変数更新を伴う処理が成立しません。
第二にRAMの方が高速にアクセスでき、実行性能の面でも有利になります。GNU ld manualのOutput Section LMA節では、リンカスクリプトとLMA(ロードアドレス)を用いて、「.data」をコピーするスクリプトの例が記されています。
.bss=ゼロクリアが必要な理由
.bssセクションについて、ELF gABI Version 4.2ではSHT_NOBITSタイプを持ち、ファイル上に実体を持たずサイズ情報だけを保持すると規定されています。
ファイル上に0が焼かれていないため、起動時にゼロ書き込みをおこなわないと、グローバル変数が起動前のRAMの任意値を持ったまま使われ、再現性のない不可解な動作の原因になりかねません。ROM上に0を焼く方式では、バイナリサイズが.bss領域分だけ無駄に膨らみます。NOBITSを使う設計には、フラッシュ容量を節約するという合理性があるのです。
ブートローダ+アプリ構成でのメモリマップと初期化
ブートローダがある構成と、単一バイナリで直接mainに入る構成では、初期化の分担と制御の渡し方が変わります。ここではブートローダがある前提で説明します。単一バイナリ構成の方も、ベクタテーブル再配置の話は将来必要となる知識として読み進めてください。
例えば、あるマイコンは典型構成として、フラッシュ先頭にブートローダ、後続領域にアプリケーションを置く例が示されています。両者は独立したスタートアップを持ち、それぞれが自前のベクタテーブルを抱えています。マイコンによって、典型構成や推奨される構成が異なるので、それぞれのマニュアルを確認しながら構成を決めましょう。
メモリマップとベクタ
ブートとアプリのコードは、リンカスクリプトで物理アドレスが決まる仕組みです。ARM Cortex-M3 Generic User Guideによれば、VTORレジスタのTBLOFFフィールドにベクタテーブルの基準アドレスを書き込めば、参照先ベクタテーブルを切り替えられます。再配置可能範囲は0x00000080から0x3FFFFF80まで、アライメント要件は最小32ワードと規定されており、割り込み数が多い場合は2のべき乗で切り上げたサイズが必要です。
遷移と初期化の分担
ブートからアプリへ遷移する瞬間には、CPU状態を整える処理を要します。アプリ側ベクタテーブル先頭からMSP初期値を読み取って設定し、続く2番目のワードから Reset_Handlerのアドレスを取得して分岐する手順です。
.dataと.bssの初期化は、アプリ側のスタートアップで改めて走らせるのが基本です。ブート時のRAM状態はアプリ起動時に保証されないため、アプリは独立して起動できる前提で設計しましょう。
実装でハマりやすいポイントと切り分け
コンパイルが通っただけでは動かない、というのが起動まわりの鉄則です。ここでは main到達の判定とHardFault解析の2段構えで切り分けを進めましょう。
最初に確認するのは、main関数に到達できているかどうかです。デバッガが使えるならブレークポイント、使えない環境ならLED点灯やUART出力で判定します。main未到達なら、疑う層はベクタテーブル、スタートアップ、リンカスクリプトの順です。mapファイルとリンカスクリプトの整合性を確認すれば、配置ミスやLMA設定漏れがすぐ見つかります。
起動直後にHardFaultが発生する場合は、Arm/KeilのAN209が示すように、HFSR、CFSR、MMFAR、BFARを読み取って原因を絞り込みます。スタックポインタ初期値の設定ミスや、.dataコピー先RAM範囲のオーバーフローが典型的な発生源です。フォールトレジスタを見ても原因がつかめない場合は、ベクタテーブルのアライメントミスや、Reset_Handlerを含む重要シンボルがリンカの最適化で消えるケースを疑いましょう。GNU ldのKEEP指定やPROVIDEによる保護を活用すれば、こうした消失を防げます。


