前回の第10回でDMAを学びました。「DMAバッファはグローバル変数にしなければならない」「スタック変数はDMAに渡せない」――そう言われたとき、「なぜスタック変数とグローバル変数でメモリの扱いが違うのか」 が気になりませんでしたか?

その答えは リンカスクリプト にあります。


📖 前回の記事

第10回:DMAという発想 ― CPUを暇にする転送アーキテクチャを徹底理解 ―

📍 連載トップページ

【全13回連載】ポインタの先にある組み込みの世界


✅ この記事でできるようになること

  • .text / .data / .bss が「何で、どこに置かれるか」を説明できる
  • リンカスクリプト(.ld)の MEMORY / SECTIONS ブロックを読んで理解できる
  • arm-none-eabi-size の出力からFlash・RAM使用量を正確に読める
  • mapファイルで「どのモジュールが何バイト食っているか」を特定できる
  • スタックオーバーフローと const 付け忘れの落とし穴を回避できる

目次

  1. リンカスクリプトとmapファイルとは
  2. メモリ配置の全体像
  3. .text / .data / .bss の正体
  4. 起動シーケンス:誰が .data をコピーするか
  5. リンカスクリプトを読む
  6. arm-none-eabi-size と mapファイル
  7. 実践:メモリ使用量を調べて削減する
  8. よくある落とし穴
  9. まとめ

リンカスクリプトとmapファイルとは

リンカが解決する問題

C ファイルをコンパイルすると .o(オブジェクトファイル)が生成されます。しかしこの段階では「関数 A は Flash のどのアドレスに置くか」がまだ決まっていません。

main.o                         stm32f4xx_hal_uart.o
├── main()         ← 未確定    ├── HAL_UART_Transmit()  ← 未確定
├── led_toggle()   ← 未確定    └── ...
└── g_counter      ← 未確定

リンカ(arm-none-eabi-ld)の仕事は、これらすべての .o ファイルを受け取り、

  1. シンボル参照を解決する(main.c が呼んでいる HAL_UART_Transmit はどこにあるか?)
  2. 最終的なアドレスを確定してひとつの .elf バイナリに結合する

そのとき「どのメモリ領域にどのセクションを置くか」をリンカに教える指示書が リンカスクリプト.ld ファイル)です。

リンカスクリプトの何が嬉しいか

リンカスクリプトがなければ、「Flash は 0x08000000 から始まる」という情報をリンカが知る方法がありません。コードを Flash に置くのか SRAM に置くのか、割り込みベクタテーブルはどこか——すべてリンカが勝手に決めてしまいます。

リンカスクリプトを明示することで実現できること:

できること 具体例
ブートローダーとアプリを分離 Flash 先頭 32KB をブートローダー専用、残りをアプリに割り当て
TCM へのコード配置 割り込みハンドラを Tightly Coupled Memory(超高速)に置いてレイテンシゼロに
マルチコアの共有メモリ定義 Core0 と Core1 が通信するバッファを固定アドレスに配置
外部メモリの追加 外部 SRAM や QSPI Flash を MEMORY ブロックに追加して透過的に使う
セクション配置の最適化 よく使う定数テーブルを Flash の高速アクセス領域に集める
✅ ブートローダー開発での典型的な使い方

STM32 でブートローダーを作る場合、Flash を「ブートローダー(0x08000000〜0x08007FFF)」と「アプリ(0x08008000〜)」に分割します。リンカスクリプトでアプリ側の ORIGINLENGTH を変えるだけで、アプリが自動的に正しいアドレスに配置されます。ジャンプアドレスをハードコードする必要がありません。

マイコン以外でも使われているリンカスクリプト

リンカスクリプトは組み込み専用の技術ではありません。GCC や LLVM を使うあらゆる環境でリンカスクリプトは動いています。

環境 リンカ リンカスクリプトの扱い
組み込み(ARM) arm-none-eabi-ld 明示的に .ld を書く(本記事)
Linux カーネル GNU ld arch/arm/kernel/vmlinux.lds.S など各アーキで必須
Linux アプリ GNU ld / lld GCC のデフォルトスクリプトを自動適用
macOS Apple ld64 __TEXT__DATA セグメントを定義
Windows MSVC link.exe /ENTRY オプションや .def ファイルで制御
WebAssembly wasm-ld .wasm のメモリセクション配置を制御

Linux カーネルのリンカスクリプト(vmlinux.lds)は、ブートコードを特定のアドレスに置いたり、割り込みベクタを集めたりと、STM32 のリンカスクリプトと構造がほぼ同じです。マイコンのリンカスクリプトを読み書きできるなら、Linux カーネルビルドの仕組みも半分は理解できている状態です。

💡 PC アプリにリンカスクリプトが「見えない」理由

Linux で gcc main.c -o main とすると、GCC が自動でデフォルトのリンカスクリプトを適用します。そのデフォルトスクリプトが「.text はここ、.data はここ」という標準的な ELF 配置を知っているため、通常のアプリ開発では意識する必要がありません。

マイコンには OS もデフォルトのメモリマップもないため、必ず明示的に書く必要があります。「存在するが見えていない(PC)」か「自分で書かなければならない(マイコン)」かの違いです。

mapファイルとは:ビルドの「成績表」

mapファイルはリンカが生成する、配置結果の全記録です。「最終的にどのシンボルがどのアドレスに置かれたか、何バイト使ったか」がすべて書かれています。

用途 具体的に何をするか
Flash/RAM 使用量の確認 セクションごとのサイズを集計して上限監視
肥大化の犯人を探す どのモジュール・どの関数が最もサイズが大きいかを特定
クラッシュアドレスの逆引き ハードフォルト時のアドレスから関数名を特定
リンカシンボルの確認 _estack_sbss が想定アドレスになっているか検証

🗺️ メモリ配置の全体像

STM32F401RE のメモリマップ

STM32F401RE(NUCLEO-F401RE)が持つメモリは大きく2種類です。

種類 開始アドレス サイズ 役割
Flash 0x08000000 512 KB プログラムコード・定数を永続保存
SRAM 0x20000000 96 KB 実行時のデータ・スタック・ヒープ
graph TD subgraph Flash["Flash 512KB (0x08000000〜)"] F1[".text
コード・割り込みベクタ"] F2[".rodata
文字列リテラル・const"] F3[".data 初期値
グローバルのコピー元"] end subgraph SRAM["SRAM 96KB (0x20000000〜)"] S1[".data
初期化済みグローバル"] S2[".bss
ゼロ初期化グローバル"] S3["heap ↑
malloc領域"] S4["stack ↓
ローカル変数"] end F3 -->|起動時コピー| S1

Flash は「電源を切っても消えない不揮発性メモリ」、SRAM は「電源を切ると消える揮発性メモリ」です。プログラムは Flash から実行され、実行時に変化するデータだけが SRAM に置かれます。

💡 なぜ Flash の速度が問題にならないのか

STM32F401 の Flash アクセスにはART Accelerator(プリフェッチ+命令キャッシュ) が搭載されています。CPU クロック 84MHz でも命令フェッチがほぼ 0 ウェイトで動作するため、通常の使い方では Flash の読み出し速度は問題になりません。

プリフェッチ(Prefetch)とは: CPU が「今から実行するだろう」と予測して、次の命令を Flash から先読みしておく仕組みです。CPU が命令を要求する前にデータを用意しておくことで、Flash の読み出し遅延(レイテンシ)をCPU側から隠蔽できます。

命令キャッシュ(Instruction Cache)とは: 最近読み出した Flash の命令コードを SRAM より高速な小容量メモリ(キャッシュ)に保持しておく仕組みです。同じコード(ループ処理など)を繰り返し実行する場合、2回目以降は Flash へのアクセスなしにキャッシュから命令を供給できます。


📦 .text / .data / .bss の正体

C コードを書いたとき、変数の宣言方法によってどのセクションに配置されるかが決まります。

.text セクション

コードと定数を格納するセクションです。Flash 上に置かれます。

/* 以下はすべて .text(または .rodata)に配置される */

void led_toggle(void) {          /* 関数コード → .text */
    HAL_GPIO_TogglePin(GPIOA, GPIO_PIN_5);
}

const uint8_t lut[256] = { ... }; /* const配列 → .rodata(.textのサブセクション)*/
const char msg[] = "Hello\r\n";   /* 文字列リテラル → .rodata */

.rodata(read-only data)は .text の一部として Flash に格納されます。const を付けた変数は Flash に置かれ、RAMを消費しません

.data セクション

初期値を持つグローバル変数・staticローカル変数を格納するセクションです。

/* 以下は .data に配置される */

uint32_t g_counter = 100;          /* 初期値あり → .data */
static uint8_t s_mode = 2;         /* static ローカルも同様 */

/* 関数の中でも static なら .data */
void foo(void) {
    static int s_count = 0;        /* .data */
    s_count++;
}

.data は少し特殊で、Flash と SRAM の両方に存在します。

  • Flash 上(LMA:Load Memory Address):電源オフ中も保持するための初期値のコピー
  • SRAM 上(VMA:Virtual Memory Address):実行時に CPU が実際にアクセスする場所

電源投入時、起動コード(startup.s)が Flash の初期値を SRAM にコピーします。

graph LR subgraph Flash["Flash(LMA)"] FD[".data初期値\n例: g_counter=100"] end subgraph SRAM["SRAM(VMA)"] SD[".data実行時領域\ng_counter"] end FD -->|"startup.s が起動時にコピー"| SD SD -->|"実行中に読み書き"| CPU["CPU"]
💡 LMA と VMA
  • LMA(Load Memory Address):バイナリファイル(.bin/.hex)に書き込まれているアドレス。Flash 上の物理的な位置。
  • VMA(Virtual Memory Address):プログラムが実行時に期待するアドレス。CPU がアクセスするときのアドレス。

.text は Flash 上で実行されるので LMA = VMA。.data は Flash に保存されて SRAM にコピーされるので LMA(Flash)≠ VMA(SRAM)になります。

.bss セクション

初期値がゼロ(またはゼロ初期化)のグローバル変数・staticローカル変数を格納するセクションです。

/* 以下は .bss に配置される */

uint32_t g_error_count;            /* 初期値なし(=0初期化) → .bss */
static uint8_t rx_buf[256];        /* ゼロ初期化 → .bss */
uint8_t g_flag = 0;                /* 明示的に0 → .bss(最適化でbssへ) */

.bss の初期値は Flash に保存されません。 代わりに起動コードが「ゼロで埋める」操作だけを行います。この仕組みにより、大きなゼロ初期化バッファを持っていても Flash 消費量は増えません

セクション Flash SRAM 特徴
.text ✅ 占有 コード・const。変更不可
.rodata ✅ 占有 文字列リテラル・const配列
.data ✅ 占有(初期値) ✅ 占有 Flash と SRAM の両方を使う
.bss ✗(サイズ情報のみ) ✅ 占有 Flash 節約。ゼロ初期化保証
stack ✅ 占有(動的) ローカル変数・関数呼び出し情報
heap ✅ 占有(動的) malloc で確保する領域
💡 この章に出てくる用語の早引き
用語 意味
セクション .text.data.bss など、役割ごとにまとめられたメモリの「箱」。リンカが各箱を適切なメモリ領域に配置する
シンボル 関数名・変数名のこと。リンカにとっては「アドレスに付けられた名前」。mapファイルでシンボル一覧を確認できる
不揮発性 電源を切っても中身が消えない性質(Flash)。対義語は揮発性(SRAM)
アライメント データをCPUが効率よくアクセスできる境界(4バイト・8バイト単位など)に配置すること。リンカスクリプトの . = ALIGN(8) がこれを指示している
LMA / VMA LMA(ロード時のアドレス)とVMA(実行時のアドレス)。.data は Flash に LMA として保存され、SRAM の VMA にコピーされる

⚙️ 起動シーケンス:誰が .data をコピーするか

main() が呼ばれる前に、起動コード(startup_stm32f401retx.s) が以下の順序で初期化を行います。

graph TD A["リセットハンドラ Reset_Handler"] --> B["スタックポインタ SP を _estack に設定"] B --> C[".data を Flash→SRAM にコピー\n(_sidata→_sdata〜_edata)"] C --> D[".bss をゼロクリア\n(_sbss〜_ebss を 0 で埋める)"] D --> E["SystemInit() 呼び出し\n(クロック・FPU設定など)"] E --> G["__libc_init_array() 呼び出し\n(C++ グローバルコンストラクタ)"] G --> F["main() 呼び出し"]

実際の startup_stm32f401retx.s の該当箇所(ARM アセンブリ)をC擬似コードで示します。

/* startup_stm32f401retx.s の Reset_Handler を C 擬似コードで表現 */

void Reset_Handler(void)
{
    /* 1. .data セクションを Flash(LMA)から SRAM(VMA)にコピー */
    uint32_t *src = &_sidata;   /* Flash 上の初期値の先頭 */
    uint32_t *dst = &_sdata;    /* SRAM 上の .data 先頭 */
    while (dst < &_edata) {
        *dst++ = *src++;
    }

    /* 2. .bss セクションをゼロクリア */
    uint32_t *bss = &_sbss;
    while (bss < &_ebss) {
        *bss++ = 0;
    }

    /* 3. システム初期化(クロック・FPU設定など) */
    SystemInit();

    /* 4. C++ グローバルコンストラクタを呼び出す(純粋Cプロジェクトでは空) */
    __libc_init_array();

    /* 5. main に移行 */
    main();
}
✅ グローバル変数がゼロで始まる理由

C の仕様では「初期値なし」のグローバル変数はゼロ初期化が保証されています。これが実現できるのは、マイコンの startup.s が .bss をゼロクリアしているからです。

ローカル変数(スタック変数)は startup.s が面倒を見てくれないため、初期化なしで読むと不定値になります。

__libc_init_array() について: C++ で記述した場合、グローバルオブジェクトのコンストラクタがこのタイミングで呼ばれます。純粋な C プロジェクトでは中身が空のため実質的に何もしませんが、関数自体は必ず呼ばれています。

起動順序の注意: STM32F401 の CubeIDE 生成スタートアップでは .data/.bss 初期化が SystemInit よりに行われます。STM32H7 や外部 SDRAM を使うボードでは SRAM 初期化の前に MPU/FMC の設定が必要なため順序が異なる場合があります。


📄 リンカスクリプトを読む

STM32CubeIDE が生成するプロジェクトには STM32F401RETx_FLASH.ld というリンカスクリプトが含まれています。

MEMORY ブロック

まず物理メモリの配置を宣言します。

MEMORY
{
  RAM    (xrw) : ORIGIN = 0x20000000, LENGTH = 96K
  FLASH  (rx)  : ORIGIN = 0x08000000, LENGTH = 512K
}
  • ORIGIN:開始アドレス
  • LENGTH:サイズ
  • (rx) / (xrw):アクセス権限(r=read, x=execute, w=write)

SECTIONS ブロック

次に各セクションをどのメモリに配置するかを指定します。

SECTIONS
{
  /* コードと定数 → Flash */
  .text :
  {
    *(.text)         /* 全オブジェクトファイルの .text セクション */
    *(.text*)
    *(.rodata)
    *(.rodata*)
  } >FLASH

  /* .data セクション:SRAM に置くが初期値は Flash に保存 */
  _sidata = LOADADDR(.data);   /* Flash 上の LMA(初期値の位置) */

  .data :
  {
    _sdata = .;      /* SRAM 上の VMA 先頭(startup.s が参照) */
    *(.data)
    *(.data*)
    _edata = .;      /* SRAM 上の VMA 末尾 */
  } >RAM AT> FLASH   /* VMA=RAM, LMA=FLASH */

  /* .bss セクション:SRAM のみ(Flash には何も書かない) */
  .bss :
  {
    _sbss = .;
    *(.bss)
    *(.bss*)
    *(COMMON)
    _ebss = .;
  } >RAM

  /* スタックとヒープの予約領域 */
  ._user_heap_stack :
  {
    . = ALIGN(8);
    . = . + _Min_Heap_Size;
    . = . + _Min_Stack_Size;
    . = ALIGN(8);
  } >RAM
}

>RAM AT> FLASH の部分が .data の二重配置を指示しています。「SRAM に VMA を置くが、LMA(バイナリでの実体)は Flash に置く」という意味です。

重要なシンボル一覧

startup.s や HAL が参照するリンカシンボルです。

シンボル 意味
_estack スタックの初期値(SRAM 末尾)。SP レジスタの初期値
_sidata Flash 上の .data 初期値の先頭アドレス(LMA)
_sdata SRAM 上の .data 先頭アドレス(VMA)
_edata SRAM 上の .data 末尾アドレス
_sbss .bss 先頭アドレス
_ebss .bss 末尾アドレス
_Min_Stack_Size スタック最小予約量(デフォルト 0x400 = 1KB)
_Min_Heap_Size ヒープ最小予約量(デフォルト 0x200 = 512B)
⚠️ スタックは _Min_Stack_Size だけ保証されるわけではない

_Min_Stack_Size は「少なくともこのサイズを SRAM の末尾に確保する」という宣言です。実行時のスタック消費量がこれを超えても、リンカは警告を出しません。スタックは .bss の直下に向かって伸びるため、オーバーフローすると .bss を静かに上書きします。


🔍 arm-none-eabi-size と mapファイル

arm-none-eabi-size の見方

ビルド後に以下のコマンドを実行すると、セクションごとのサイズが分かります(STM32CubeIDE ではビルドログに自動表示されます)。

$ arm-none-eabi-size firmware.elf
   text    data     bss     dec     hex filename
  12480     116    2072   14668    394c firmware.elf
意味 実際に消費するメモリ
text .text + .rodata のバイト数 Flash
data .data のバイト数 Flash(初期値)+ SRAM(実行時)
bss .bss のバイト数 SRAM のみ
dec 合計(10進)

Flash 使用量text + dataSRAM 使用量data + bss(+スタック・ヒープの動的部分)で求められます。

\text{Flash使用量} = \text{text} + \text{data} \text{SRAM使用量(静的)} = \text{data} + \text{bss}

上の例では:

  • Flash:12480 + 116 = 12,596 バイト(512KB 中の 2.4%)
  • SRAM(静的):116 + 2072 = 2,188 バイト(96KB 中の 2.3%)
💡 SRAM の「静的」と「動的」

arm-none-eabi-size が示す SRAM はあくまでも静的確保分です。実行時のスタック消費量(関数呼び出しの深さ・ローカル変数のサイズ)と malloc によるヒープ消費量はここには含まれません。実際の SRAM 上限との差分が「スタック+ヒープに使える残量」です。

mapファイルの読み方

STM32CubeIDE のビルドで生成される firmware.map を開くと、セクションとシンボルの詳細なレイアウトが分かります。

まず全体のメモリ使用サマリーを確認します。

Memory region         Used Size  Region Size  %age Used
             RAM:       2188 B        96 KB      2.22%
           FLASH:      12596 B       512 KB      2.40%

次に .text セクションの内訳を見ます(抜粋)。

.text           0x0800000c     0x2c04
 *(.text)
 .text          0x0800000c      0x1d4 ./Core/Src/main.o
 .text          0x080001e0      0x2a0 ./Drivers/STM32F4xx_HAL_Driver/Src/stm32f4xx_hal_uart.o
 .text          0x08000480       0xb4 ./Drivers/STM32F4xx_HAL_Driver/Src/stm32f4xx_hal_dma.o
 ...

各行の意味:

  • 0x0800000c:配置されたアドレス
  • 0x1d4:サイズ(16進)= 468 バイト
  • ./Core/Src/main.o:どのオブジェクトファイルか

.bss セクションも同様に確認できます。

.bss            0x20000090      0x818
 .bss           0x20000090        0x4 ./Core/Src/main.o
 .bss           0x20000094      0x800 ./Core/Src/usart.o   ← rx_buf[2048] など

mapファイルが持つ「情報漏洩」のリスク

mapファイルにはすべての関数名・変数名・そのアドレス・サイズが記録されています。デバッグには便利ですが、逆に言えばファームウェアの内部構造が丸読みできる設計図でもあります。

セキュリティが求められる製品では以下に注意してください:

  • mapファイルを製品とともに出荷しない(量産 .bin とは別に厳密に管理)
  • デバッグシンボル付き .elf を本番環境に持ち出さない
  • 出荷バイナリはシンボルストリップ済みの .bin のみにする(arm-none-eabi-strip
⚠️ 「.mapファイルの設定ミス1行」が51万行の流出を招いた事件

mapファイルが示す「内部構造を丸ごと露出する」リスクは組み込みだけの問題ではありません。

Web の世界には「ソースマップ(source map)」という仕組みがあります。難読化・圧縮された JavaScript と元のソースコードを対応づけるファイルで、拡張子は .map。名前も役割も「どこに何があるかを示す地図」という点で、リンカのmapファイルと本質的に同じです。

2026年3月31日、.npmignore*.map の1行を書き忘れたことで、Claude Code のソースコード 51万行が全世界に公開されました。 内部には未公開プロジェクトの設計図まで含まれており、「設定1行の欠落」がどれほど深刻な結果をもたらすかを示す事例となりました。

👉 詳細を読む:Claude Codeソースコード流出事件 ― .mapファイルと情報漏洩のメカニズム


🛠️ 実践:メモリ使用量を調べて削減する

大きなバッファを追加したときの変化

/* 追加前 */
/* 特になし */

/* 追加後 */
uint8_t log_buf[4096];   /* グローバル宣言 */

arm-none-eabi-size の変化:

/* 追加前 */
   text    data     bss     dec
  12480     116    2072   14668

/* 追加後 */
   text    data     bss     dec
  12480     116    6168   18764   ← bss が 4096 増加(Flash は変わらず)

log_buf は初期値なし(ゼロ初期化)のため .bss に入ります。Flash 使用量は変わらず、SRAM だけ 4KB 増えます。

初期値ありにすると Flash も増える

/* 初期値をつけると .data に入る */
uint8_t log_buf[4096] = { 0xFF };   /* 先頭だけ 0xFF、残りは 0 */
   text    data     bss     dec
  12480    4212    2072   18764   ← data が 4096 増加(Flash も増える)

Flash と SRAM の両方が 4KB 増えます。初期値が必要なければ ゼロ初期化(初期値なし)のほうが Flash を節約できます。

mapファイルで「最も大きいシンボル」を探す

# .bss の大きい順に並べる(Linux/Mac の場合)
grep -A1 "\.bss" firmware.map | sort -k2 -rn | head -20

# Windows の場合は firmware.map をテキストエディタで開き、
# "\.bss" で検索してサイズ列を確認する
✅ RAM 削減の優先順位

mapファイルで RAM 消費の大きいシンボルを見つけたら、以下の順で対策を検討します。

  1. バッファサイズを適切に絞る(2048 バイト必要なのに 4096 確保していないか)
  2. const を付けられないか確認する(定数テーブルは Flash へ移行できる)
  3. static から動的確保(malloc)に変更できるか検討する(使わないときは返せる)
  4. アルゴリズムを変更する(バッファリングを減らせないか)

⚠️ よくある落とし穴

落とし穴1:スタックオーバーフロー(静かな破壊)

スタックは SRAM の 上位アドレスから下位アドレス方向に伸び、ヒープは下位から上位方向に伸びます。スタックがヒープ領域まで侵食すると malloc の管理構造を破壊し、さらにオーバーフローが進むと .bss のグローバル変数も上書きします。malloc を使わないプロジェクトではヒープがほぼゼロのため、.bss が直接の被害を受けます。

高アドレス
stack ↓ 下に伸びる
(未使用領域)
heap ↑ 上に伸びる
.bss  ← オーバーフローで上書き!
.data
_estack (0x20018000) / SP 初期値
← 0x20000000
低アドレス

症状: グローバル変数が突然おかしな値になる。特定の関数を呼ぶと別の場所が壊れる。再現性がない。

/* ❌ スタックを大量に消費する例 */
void process_data(void)
{
    char work_buf[4096];   /* ローカル変数で 4KB → スタックオーバーフロー候補 */
    uint8_t temp[2048];
    /* ... */
}

/* ✅ static に変更してスタックを節約 */
void process_data(void)
{
    static char work_buf[4096];   /* .bss に移動 → スタックを消費しない */
    static uint8_t temp[2048];
    /* ... */
}
⚠️ スタックオーバーフローの検出

STM32 にはハードウェアのスタック保護機能はありません(Cortex-M33 系の一部では MPU で検出可能)。スタックオーバーフローはハードフォルトや不可解なデータ破壊として現れます。

デバッグ中は .bss の末尾付近に特定のパターン(例:0xDEADBEEF)を書いておき、ループで確認する「スタックカナリア」手法が有効です。

/* startup 後に呼ぶ */
extern uint32_t _ebss;
volatile uint32_t *canary = &_ebss - 4;
*canary = 0xDEADBEEF;

/* メインループで監視 */
if (*canary != 0xDEADBEEF) {
    /* スタックオーバーフロー検出 */
    Error_Handler();
}

落とし穴2:const の付け忘れ

/* ❌ const がない → .data に入り、RAM を消費する */
uint8_t sin_table[256] = { 0, 1, 3, 5, ... };   /* 256バイト のRAM を消費 */

/* ✅ const をつける → .rodata(Flash)に移動、RAM を消費しない */
const uint8_t sin_table[256] = { 0, 1, 3, 5, ... };

大きなルックアップテーブルや文字列定数に const を忘れると、初期化データが Flash と SRAM の両方を食います。arm-none-eabi-sizedata が予想より大きい場合、const 忘れを疑ってください。

落とし穴3:ヒープとスタックの衝突

malloc でヒープを使う場合、ヒープはスタックの下から上方向に伸びます。ヒープとスタックが衝突すると mallocNULL を返す場合もありますが、検出できずに破壊が起きることもあります。

/* ❌ ヒープを大量に使う場合は残りスタックに注意 */
uint8_t *big_buf = malloc(32768);   /* 32KB 確保 → ヒープとスタックが圧迫される */
if (big_buf == NULL) {
    /* malloc 失敗の対処 */
}

組み込みの場合、多くの場面で malloc よりも静的確保(グローバル変数・staticローカル) を使うほうが安全です。ヒープの断片化も起きません。

落とし穴4:リンカスクリプトの _Min_Stack_Size を小さくしすぎる

/* STM32CubeIDE のデフォルト */
_Min_Heap_Size = 0x200;    /* 512 B */
_Min_Stack_Size = 0x400;   /* 1 KB */

_Min_Stack_Size はリンカが「SRAM の末尾にこのサイズを予約する」という宣言で、実際の使用量を制限するものではありません。HAL ライブラリは内部で ISR ハンドラをコールするため、ISR スタックフレーム(数百バイト)+ 関数呼び出し深度を考慮して余裕を持った値に設定してください。

経験則として:小規模な STM32F4 プロジェクトでは _Min_Stack_Size = 0x800(2KB)程度に増やすと安全です。


まとめ

今回学んだこと:

概念 内容
.text / .rodata コード・const定数。Flash のみ消費。実行時は変更不可
.data 初期値ありグローバル変数。Flash(初期値コピー)+ SRAM(実行時)を両方消費
.bss ゼロ初期化グローバル変数。SRAM のみ消費。Flash 節約に有効
LMA / VMA .data は Flash(LMA)と SRAM(VMA)に二重に存在する
startup.s .data コピー→ .bss ゼロクリア→ SystemInit→ main の順で初期化
arm-none-eabi-size text+data=Flash使用量、data+bss=SRAM静的使用量
mapファイル モジュール・シンボルごとの配置とサイズを把握できる
スタックオーバーフロー スタックは下方向に伸び、.bss を無音で上書きする
const付け忘れ テーブル・定数に const がないと .data に入り SRAM を浪費する
📌 リンカスクリプトは「設計図」だと思うこと

リンカスクリプトを読めるようになると、「なぜこのコードは動くのか」の根拠がフラッシュとRAMのレベルまで見えてきます。

DMAバッファをグローバルにしなければならない理由、起動直後にグローバル変数が初期化されている保証の根拠、const を付けるべき場所――これらはすべてリンカスクリプトとセクション配置の知識でつながっています。

「mapファイルを見る」習慣を持つだけで、メモリ不足時のデバッグ時間を大幅に短縮できます。

次回は 「最適化とアセンブラ(コンパイラに任せて、でも理解する)」 です。-O2 最適化で何が変わるか、インライン展開・ループ展開・volatile の最適化への影響、そして逆アセンブルでコンパイラの仕事を覗いてみます。


次回予告

🚀 第12回:最適化とアセンブラ(コンパイラに任せて、でも理解する)

-O2 最適化が有効なとき何が起きるか。インライン展開・ループ展開・volatile の挙動への影響。arm-none-eabi-objdump で逆アセンブルしてコンパイラの判断を観察します。


よくある質問(FAQ)

Q. .bss に入った変数は本当にゼロが保証されますか?

はい。C の仕様(C99 §6.7.9)により、初期値なしのグローバル変数・staticローカル変数はゼロ初期化が保証されています。これは startup.s の .bss ゼロクリア処理によって実現されています。ただし malloc で確保したメモリは初期化されないので注意してください(ゼロ初期化が必要なら calloc を使います)。

Q. Flash が満杯になるとどうなりますか?

リンカがエラーを出してビルドが失敗します。「region `FLASH’ overflowed by X bytes」というメッセージが表示されます。実行時に突然起きるわけではないため、容量オーバーはビルド時に確実に検出できます。

Q. SRAM が足りなくなると何が起きますか?

静的確保のオーバーフローはリンカがエラーを出します(「region `RAM’ overflowed」)。しかしスタックやヒープの動的オーバーフローはリンカでは検出できません。実行時に不可解な動作やハードフォルトとして現れます。これが落とし穴1で説明したスタックオーバーフローの危険性です。

Q. .text に大きな配列を置けますか(Flash に直接置く)?

はい。const キーワードをつけたグローバル変数は .rodata(Flashの一部)に配置されます。ただし Flash の書き込み回数には上限(一般に10万〜100万回)があるため、頻繁に書き換えるデータには使えません。また Flash の読み出しはランダムアクセスが遅い(プリフェッチキャッシュがミスすると数サイクルのストール)ので、アクセス頻度が高い大テーブルは SRAM にコピーして使う場合もあります。

Q. リンカスクリプトを自分で編集する必要はありますか?

通常は不要です。ただし以下の場面では編集が必要になります:(1)外部 SRAM や QSPI Flash を追加する、(2)特定のコードを TCM(Tightly Coupled Memory)などの高速メモリに配置する、(3)ブートローダーと共存させるためにアドレスを変更する。


関連記事