30代エンジニアの備忘録のカバー画像

30代エンジニアの備忘録

組み込み開発・信号処理・プログラミングの技術メモと短編小説

メモリマップ入門|組み込みエンジニアが知るべきメモリ配置とリンカスクリプト

シリーズ この記事は「コンパイル・リンク・ビルド(組み込み基礎)」の一部です。 シリーズ目次を見る

(2026年5月時点)メモリマップとは、コード・初期化データ・BSS・スタック/ヒープ等が、実行時に どのアドレス帯に載るか を示した設計図です。MCU でも Linux でも考え方は似ていますが、BSP や実行環境の前提で細部が変わります。本記事は Cortex-M 系の典型的なフラッシュ実行を想像しやすい例に絞って整理します。

ブレなく聞ける質問メモ(3つ) 参考:GNU ld リンカスクリプト

  1. Flash に置くコードと SRAM に載る書き換え可能領域の境界は、この .ld だとどこか?(_estack やヒープとの関係は?)
  2. _sidata / _sdata / _edata とコピーループ:起動時に どの ROM 側イメージを RAM のどこへ何バイト複写する契約か?
  3. COMMON と .bss の取り込み順:テンポラリ(tentative)定義が 複数 .o にまたがったとき、リンカがサイズを統合する狙いを理解できているか?
💡
見落ちがち: .bss はゼロ初期化とはいえ NOLOAD と初期化順は環境依存。教材の並び順と ST の Cube/BSP 既定の Reset_Handler/シンボル名は一致しないことが多いので、教材の写経より 自分のプロジェクトが生成した .ld と startup を正にしてください。

メモリマップの全体像とは?

組み込み MCU では、実行コードや const 相当を Flash に置き、書き換え対象は SRAM、という二分がよく議論されます(XIP と RAM 構成、キャッシュ、ITCM/.data の初期コピー等は SoC/BSP 依存)。

メモリレイアウトの全体像
メモリレイアウトの全体像
各セクションの役割
  • .text(テキスト領域):機械語命令が格納される。ROM/Flashに配置。読み取り専用(実機で「関数に入っているはずなのにブレークが効かない」などはフラッシュ開始アドレス・オフセットとデバッガのロード情報のずれ確認から)
  • .rodata(読み取り専用データ):const変数や文字列リテラル。ROM/Flashに配置
  • .data(初期化済みデータ):初期値を持つグローバル/static変数。初期値はROMに保存され、起動時にRAMへコピーされる(コピー元・終端シンボルの取り違えだと「起動直後だけ値がおかしい」が出やすい)
  • .bss(未初期化データ):初期値なしのグローバル/static変数。起動時にRAM上で0クリアされる。ROMにサイズ情報のみ記録(使用量が読みだけ膨らむときは COMMON 統合や重複定義も棚卸し)
  • ヒープ:malloc などで伸びる領域(使わない構成も多い。成長方向は実装依存)
  • スタック:関数呼び出し用。Cortex-M の教材ではスタックポインタを RAM 末尾付近から降りて使うことが多いが、ツールチェーンで決まる

リンカスクリプトでメモリを制御する方法は?

組み込みでは、MEMORY と SECTIONS で ROM/RAM にセクションを割り当てます。以下は理解用の簡略例で、量産ファームウェアではメーカまたはCube等が生成した .ld を正にしてください。

リンカスクリプトとメモリ配置の関係
リンカスクリプトとメモリ配置の関係
/* STM32F401用リンカスクリプトの例 */
MEMORY {
  FLASH (rx)  : ORIGIN = 0x08000000, LENGTH = 512K
  SRAM  (rwx) : ORIGIN = 0x20000000, LENGTH = 96K
}

SECTIONS {
  .text : {
    *(.text*)        /* すべてのコードセクション */
    *(.rodata*)      /* 読み取り専用データ */
  } > FLASH

  .data : {
    _sdata = .;      /* .dataセクションの開始アドレス */
    *(.data*)
    _edata = .;      /* .dataセクションの終了アドレス */
  } > SRAM AT > FLASH  /* RAMに配置、初期値はFlashに保存 */

  .bss : {
    _sbss = .;
    *(.bss*)
    *(COMMON)
    _ebss = .;
  } > SRAM

  /* スタック領域 */
  ._stack : {
    . = ALIGN(8);
    _estack = . + 0x4000;  /* 16KBのスタック */
  } > SRAM
}

スタートアップコードとリンカの連携とは?

リンカスクリプトで決めた _sdata/_edata/_sbss/_ebss や ロード側アドレス(_sidata など、プロジェクトにより名前差)スタートアップから参照されます。Reset_Handler が .data の初期値をロード領域から RAM に展開し、.bss をゼロ埋めしたあと C の初期化へ進み、main() に入ります。

/* Reset_Handler の流れイメージ(シンボル名はBSP依存) */
void Reset_Handler(void) {
    uint32_t *src, *dst;
    /* .data: 読み込み元(Flash側)から RAM へコピー */
    src = &_sidata;   /* ← リンクが吐くロードアドレス側の開始 */
    dst = &_sdata;
    while (dst < &_edata) *dst++ = *src++;
    /* .bss をゼロクリア */
    dst = &_sbss;
    while (dst < &_ebss) *dst++ = 0;

    SystemInit(); /* クロックなど(プロジェクトにより) */
    main();
}

.map ファイルは何のために使う?

# (例).map を出す — ツールチェーンでオプション名が異なるのでドキュメントを確認
gcc main.o -Wl,-Map=output.map -o myapp

まとめ:メモリマップを読むときの確認手順は?

  • メモリマップは どのアドレス帯に何を載せるかの契約。セクション・スタートアップ・リンカ出力はセットで読む
  • .data の初期値イメージ(Flash)と RAM 実行領域のギャップは、スタートアップでのコピーで説明がつく
  • readelf、objdump、リンカが出す .map で、想定より肥大化していないかを確認できる
💡
スタックやヒープのはみ出しは、実行時だけ不安定になる典型です。BSP のスタックサイズや .bss/スタック境界を確認し、-fstack-usage のような静的な手掛かりも活用してください。