はじめに
初心者さんmain()って電源入れたらすぐ動くんですよね?



いや、それが違うんだよ。その前に“超重要な処理”があるんだ
マイコンの電源を入れても、
いきなり C言語の main() が実行されるわけではありません。
ARMマイコンでは、まず Reset_Handler と呼ばれるリセット処理に入り、
そこから Startup Code(スタートアップコード)が実行されます。
そして .data / .bss の初期化が終わったあと、
最後に main() が呼ばれます。


この記事では、
- スタートアップコードとは何か
- .data / .bss がどう初期化されるのか
- なぜその処理が必要なのか
- 実務でどこを見るべきか
を、メモリの仕組みとセットで解説します。
Startup Code(スタートアップコード)とは?
スタートアップコードとは、
👉 main() が呼ばれる前に実行される初期化処理(Startup Routine)
です。
マイコンでは、
電源ON後にすぐ main() が実行されるわけではありません。
多くのARMマイコンでは、
まず Reset_Handler(リセットハンドラ)と呼ばれる起動処理に入ります。
ARMマイコンとは、
STM32などで広く使われている代表的な組み込み向けマイコンです。
これは、
👉 「電源投入直後に最初に実行される処理」
です。
そしてその中で、Startup Code(スタートアップコード)が実行されます。
主に以下を行います。
- スタックポインタ初期化
- データ領域(.data)のコピー
- BSS領域(.bss)のゼロクリア
- main() 呼び出し


メモリ領域解説シリーズ
本記事は「メモリ領域解説」シリーズの1つです。
メモリ領域解説シリーズの全体像はこちら


データ領域、BSS領域、スタック領域を含むメモリ領域の全体像についてはこちらの記事で解説しています。




ARMマイコンの起動イメージ
多くのARMマイコンでは、
電源投入後に以下のような流れで起動します。


つまり、
👉 main() の前に大量の準備処理が存在する
ということです。
なぜスタートアップコードが必要なのか?
ここが本質です。
電源投入直後のRAMは、
まだ何も初期化されていません。
つまり:
👉 .data も .bss も「ただのメモリ」
です。
電源投入直後の状態
| 領域 | 状態 |
|---|---|
| .data | 不定値 |
| .bss | 不定値 |
| stack | 未初期化 |



え、グローバル変数って最初から0とかじゃないんですか?



それはスタートアップコードがやってるだけなんだ
👉 スタートアップコードが無いと
- 初期値は反映されない
- BSSはゼロにならない
- グローバル変数は全部ゴミ
= ほぼ確実に暴走
スタートアップコードの処理の流れ


この順番には意味があります。
スタートアップコードが行う処理
一般的に次の処理を行います。
① スタックポインタ初期化
最初にやる理由:
👉 関数が呼べないから
スタックが無い状態では、
- 関数呼び出し
- ローカル変数
- 戻りアドレス保存
ができません。
👉 リンカスクリプトで定義されたアドレスを設定
SP = __stack_top__;

② データ領域(.data)のコピー
ここが重要ポイント
👉 .data は ROMに初期値が置かれている


なぜコピーが必要?
- RAMは高速(実行用)
- ROMは不揮発(保存用)
👉 起動時にROM → RAMへコピーする設計
実装イメージ
for (dst = __data_start__, src = __data_load__;
dst < __data_end__;
dst++, src++) {
*dst = *src;
}③ BSS領域(.bss)のゼロクリア
👉 BSSは「初期値なし変数」
例:
int global_var;
特徴
- ROMにデータを持たない
- サイズだけ確保される
👉 だから起動時にゼロで埋める
memset(__bss_start__, 0, __bss_end__ - __bss_start__);
なぜゼロクリアするのか?
これはC言語仕様です。
👉 未初期化グローバル変数は0になる
このルールを実現しているのが、
スタートアップコードです。
データ領域とBSS領域についてはこちらの記事で解説しています。


.data / .bss 初期化で重要な「リンカシンボル」
スタートアップコードは、
👉 「どこからどこまで初期化するか」
をリンカシンボルで管理しています。
代表例:
__data_start__
__data_end__
__data_load__
__bss_start__
__bss_end__これらのアドレスを使って、
.dataコピー.bssゼロクリア
を行っています。
例えば BSS 初期化は次のようになります。
memset(__bss_start__, 0, __bss_end__ - __bss_start__);ここが壊れると危険
もし範囲定義が間違うと、
- 初期化漏れ
- メモリ破壊
- 起動クラッシュ
などが発生します。
特に:
- ブートローダ導入
- メモリ配置変更
- リンカスクリプト変更
時には要注意です。
④ main() 呼び出し
ここで初めてCコードが動きます。
スタートアップコードが行う処理まとめ
まとめるとこのように処理が進みます。


この順番には意味があります。
スタックを先に初期化しないと関数呼び出しができず、
.dataや.bssの初期化処理も正しく実行できないためです。
ベクタテーブルとは?
組み込みソフトでは、
CPU起動直後に「ベクタテーブル(Vector Table)」が参照されます。
ベクタテーブルには、
- 初期スタックポインタ(SP)
- Reset_Handler
- 各種割り込みハンドラ
などのアドレスが登録されています。
ベクタテーブルのイメージ
これがベクタテーブルのイメージです。


CPUはリセット直後、
- 初期SP設定
- Reset_Handler実行
を行います。
さらに割り込み発生時には、
対応するISR(割り込み処理)へジャンプします。
つまりベクタテーブルは、
👉 CPU起動と割り込み制御の入口
になっています。
割り込みとの関係
割り込み処理では、
- CPU状態退避
- スタック使用
- ISR実行
などが行われます。
Startup Codeと割り込みは、
実はかなり密接につながっています。
割り込み処理については、
こちらの記事で詳しく解説しています。
割り込み処理の基本については、
こちらの記事で詳しく解説しています。


なぜスタートアップコードが重要なのか?
もしスタートアップコードが正しく動かなければ、
- 初期値が反映されない
- グローバル変数がゴミ値
- 予期しない暴走
が起きます。
組み込みで「たまに起きる不具合」は、
ここが原因のこともあります。
リンカスクリプトとの関係(実務ポイント)
スタートアップコードは、
👉 リンカシンボルを使って範囲を決定
代表例
__data_start__
__data_end__
__bss_start__
__bss_end__
重要性
ここがズレると…
- 初期化漏れ
- メモリ破壊
- 起動クラッシュ
👉 現場でめちゃくちゃ多いバグ原因
実務で見るべきポイント
ここが一番大事👇
✔ マップファイル
特に見るのは:
.dataサイズ.bssサイズ- RAM使用量
です。
メモリ不足の原因調査では、
かなり重要になります。
メモリマップについてはこちらの記事で解説しています。


✔ リンカスクリプト
重要ポイント:
- セクション配置
- RAM配置
- Flash配置
ここがズレると、
起動できなくなることもあります。
リンカスクリプトについてはこちらの記事で解説しています。


✔ スタートアップコード本体
特に確認するのは:
- コピー範囲
- クリア範囲
- SP初期値
です。
ブートローダ構成では特に注意
ブートローダあり構成では、
- 開始アドレス変更
- ベクタテーブル位置変更
などが発生します。
すると、
- 初期化範囲ズレ
- ISR飛び先異常
- 起動失敗
などの問題につながることがあります。
よくある不具合
- たまに変数がおかしい
- 再起動すると直る
- ビルド変えたら壊れた
👉 ほぼこれ疑っていい
まとめ
データ領域とBSS領域は、
スタートアップコードによって初期化されます。
この流れを理解すると、
- メモリ不足の原因追跡
- 初期化バグの切り分け
- 起動時間最適化
ができるようになります。
この記事が参考になった方へ
メモリ領域については他の記事でも解説しています。


またスタートアップコードにかかわるC言語の文法についても当サイトでは解説しています。


エンジニアとして技術を学ぶことは重要ですが、
キャリアや副業についても同時に考える必要があります。
副業の現実や市場価値、今後のキャリア戦略については、
こちらの記事でまとめています。


技術に関するご相談・開発・自動化ツール作成・記事執筆などのご依頼も承っています。
小さなご相談からでもお気軽にご連絡ください。






コメント