こんにちは!組込み開発の現場で、日々カツカツのメモリ容量と格闘していませんか?
「RAMがあと数バイト足りない……!」
「フラッシュメモリの特定領域に、絶対に書き換えられたくない初期化データを配置したい」
「ブートローダーとメインアプリで、メモリマップを綺麗に分離したい」
C言語で組込み開発をしていると、こうしたハードウェアの制約に直面する瞬間が必ずやってきます。ネットで検索すると「リンカスクリプトを書こう」とサラッと書いてありますが、マニュアルを見ても呪文のような構文ばかりで、挫折しそうになりますよね。
でも、安心してください。GCCが提供する`__attribute__`(属性指定)とリンカスクリプト(ld)の仕組みを本質から理解すれば、ハードウェアのメモリ空間を完全にあなたの手足のようにコントロールできるようになります。
今回は、初心者の方でも迷わずステップアップできるように、ツールの役割から具体的なメモリ配置の制御まで、実務でそのまま使える知見を優しく紐解いていきましょう。これをマスターすれば、メモリ管理の不安から解放され、ワンランク上のファームウェア設計ができるようになりますよ。
—
1. GCC / Clangとリンカスクリプトの役割を本質から理解する
私たちが普段何気なく書いているC言語のコードが、どのようにしてマイコンのメモリ上で動くのか、その裏側のプロセスを整理しておきましょう。
コンパイルからリンクまでの本当の流れ
1. コンパイル(GCC / Clang):
C言語のソースコード(`.c`)を、CPUが直接理解できるアセンブリ、そして機械語のオブジェクトファイル(`.o`)に変換します。この段階では、関数や変数が「どこに配置されるべきか」はまだ仮置きの状態です。
2. リンク(GNU Linker: `ld`):
複数のオブジェクトファイルを結合し、最終的な実行バイナリ(ELFファイルなど)を生成します。ここで登場するのが「リンカスクリプト(`.ld`)」です。リンカスクリプトは、リンクプロセスに対して「どの関数やデータを、マイコンのどのメモリアドレスに配置するべきか」を指示する設計図になります。
なぜデフォルトのままではダメなのか?
標準的なGCCの設定では、コードは `.text`(プログラム領域)、初期値あり変数は `.data`、初期値なし変数は `.bss` という一般的なセクションにざっくりとまとめられ、マイコンのデフォルトのRAMやFlashに配置されます。
しかし、組込みの世界ではそうはいきません。
- 外付けSRAMや高速なTCM(Tightly Coupled Memory)など、特殊なメモリを有効活用したい
- 不揮発性に優れるフラッシュメモリの特定番地に、デバイスの個体IDやキャリブレーション値(定数)を焼き込みたい
- ブートローダー領域とアプリケーション領域を厳密に分けたい
こうした要求を満たすために、リンカスクリプトとGCCの拡張機能を組み合わせて使う必要があります。
—
2. 開発環境のセットアップと最小構成の確認
まずは、手元のPC(Linux環境やWSL2を想定)で、ベアメタル(OSなし)のC言語コードをビルドできる環境を整えましょう。今回はARM Cortex-Mなどをターゲットにしたクロスコンパイラを想定します。
必要なツールのインストール
UbuntuやDebian系環境であれば、以下のコマンドでGNUコンパイラチェーンとビルドツールを導入します。
ARMマイコン向けクロスコンパイル環境とビルドツールのインストール
sudo apt-get update
sudo apt-get install -y gcc-arm-none-eabi binutils-arm-none-eabi make
動作確認:最小限のHelloWorld的アプローチ
組込みの世界では「標準出力(printf)」が使えないことが多いため、メモリ上に変数が意図通り配置されるかを確認することが、最初の「HelloWorld」になります。
以下の3つのファイルを用意してください。
1. `main.c`: メインのCソースコード
2. `linker.ld`: 自作のリンカスクリプト
3. `Makefile`: ビルドを自動化する手順書
① `main.c`(変数の定義)
include
/ 通常のグローバル変数(RAMの.bss領域に配置される) /
uint32_t normal_variable = 100;
int main(void) {
while(1) {
// 無限ループ(動作確認用)
normal_variable++;
}
return 0;
}
② `linker.ld`(基本のリンカスクリプト)
/ ターゲットのメモリマップを定義する /
MEMORY
{
FLASH (rx) : ORIGIN = 0x08000000, LENGTH = 256K
RAM (rwx) : ORIGIN = 0x20000000, LENGTH = 64K
}
/ セクションの配置ルールを定義する /
SECTIONS
{
/ プログラムコードはFLASHの先頭から配置 /
.text : {
(.text)
} > FLASH
/ 初期化済みデータもFLASHに配置し、RAMにロードされる /
.data : {
(.data)
} > RAM AT> FLASH
/ 未初期化データはRAMに配置 /
.bss : {
(.bss)
} > RAM
}
③ `Makefile`(ビルドスクリプト)
コンパイラにARM用GCCを指定
CC = arm-none-eabi-gcc
LD = arm-none-eabi-ld
ビルドターゲット
all:
# ソースコードをコンパイルしてオブジェクトファイルを生成
$(CC) -c main.c -o main.o -mcpu=cortex-m4 -mthumb
# リンカスクリプトを使用してリンクを実行
$(LD) -T linker.ld main.o -o firmware.elf
@echo “ビルドが成功しました!firmware.elf が生成されました。”
clean:
rm -f .o .elf
これを実行してみましょう。
make
無事に `firmware.elf` が生成されれば、環境構築は完璧です!
—
3. 【実践】属性(`__attribute__`)とカスタムセクションの活用
ここからが本題です。「特定の変数や関数を、自分で定義した特別なメモリ領域に配置する」テクニックを解説します。
例えば、マイコンの中に「通常のRAM」とは別に、「高速にアクセスできるバックアップRAM(BKPSRAM)」が `0x20004000` から 4KB 分用意されていると仮定しましょう。ここに重要な設定データを配置したいとします。
ステップ1:リンカスクリプトに「専用メモリ領域」と「セクション」を追加する
`linker.ld` を以下のように拡張します。
MEMORY
{
FLASH (rx) : ORIGIN = 0x08000000, LENGTH = 256K
RAM (rwx) : ORIGIN = 0x20000000, LENGTH = 64K
/ 高速バックアップRAM領域を定義 (サイズ 4KB) /
BKPSRAM (rwx) : ORIGIN = 0x20004000, LENGTH = 4K
}
SECTIONS
{
.text : { (.text) } > FLASH
.data : { (.data) } > RAM AT> FLASH
.bss : { (.bss) } > RAM
/ ————————————————– /
/ ここから追加:カスタムセクションの配置ルール /
/ ————————————————– /
.bkp_data : {
(.bkp_data) / .bkp_dataというセクション名を持つデータをすべてここに集約 /
} > BKPSRAM
}
ステップ2:C言語側で `__attribute__` を使い分ける
次に、C言語のソースコード側で、GCCの拡張機能である `__attribute__((section(“…”)))` を使用し、変数や関数を先ほど定義したセクションに割り当てます。
include
/
- 【応用テクニック1】カスタムセクションへの変数配置
- 通常のRAMではなく、BKPSRAM(0x20004000以降)に強制配置される
/
__attribute__((section(“.bkp_data”)))
uint32_t persistent_system_config = 0xDEADBEEF;
/
- 【応用テクニック2】特定の関数をRAM上で実行する(RAM execution)
- フラッシュよりも高速なRAM上でコードを実行したい割り込み処理などに多用されます
/
__attribute__((section(“.bkp_data”), long_call))
void critical_fast_function(void) {
// 非常に高速な処理が求められるコード
volatile int i;
for(i = 0; i < 10; i++) {
// 何らかの処理
}
}
int main(void) {
// 変数を更新
persistent_system_config++;
// 関数を呼び出し
critical_fast_function();
while(1);
return 0;
}
ステップ3:配置結果を目視で確認する(重要!)
「本当に意図したアドレスに配置されたのか?」を確認するために、GNUが提供するバイナリ解析ツール `arm-none-eabi-nm` や `objdump` を使います。
以下のコマンドを実行して、シンボルのアドレスを確認してみましょう。
arm-none-eabi-nm firmware.elf | grep persistent_system_config
実行結果の例:
20004000 T persistent_system_config
おぉ!アドレスが `0x20004000`(BKPSRAMの先頭アドレス)にピタリと配置されているのがわかります。これが、リンカスクリプトと属性指定を組み合わせる魔術の正体です。
—
4. 現場で役立つ!さらに一歩進んだ応用テクニックと注意点
このテクニックを実際の商用製品に組み込む際、知っておくべきプロの知見をいくつかシェアします。
1. 初期値付き変数の罠(`AT>` の重要性)
RAMや特殊領域(非揮発性ではない領域)に `__attribute__` で初期値を持つ変数を置く場合、電源を切ると値が消えてしまいます。リンカスクリプトで `> RAM AT> FLASH` と記述しているのは、「初期値データはフラッシュに持たせておき、マイコン起動時のスタートアップルーチン(crt0など)で、RAM上の指定アドレスに値をコピーする」という処理を指示するためです。この仕組みを理解していないと、電源再投入時に値が化ける原因になります。
2. 定数(`const`)の扱いに注意する
`const` を付けた読み取り専用データは、デフォルトでは `.rodata` セクションに配置され、FLASHに置かれます。もし特定のフラッシュバンク(例: 内蔵Flashの最終セクターなど)に校正データを確実に配置したい場合も、今回紹介したカスタムセクションの技法がそのまま使えます。
—
まとめ
今回は、組込み開発におけるGCCのリンカスクリプトとセクション配置の深層について解説しました。
- リンカスクリプト(`.ld`)は、メモリマップとセクションの最終的な配置先を決定する設計図である。
- `__attribute__((section(“…”)))`を使うことで、C言語側から自由に特定の変数や関数を狙ったメモリ領域へアサインできる。
- アドレスやシンボルの配置結果は、`nm` などのツールで必ず検証する癖をつける。
この仕組みを自分の手でコントロールできるようになると、ハードウェアのスペックを極限まで引き出した洗練されたファームウェアが書けるようになります。「メモリが足りない」「特定のハードウェア特性を活かしたい」という壁にぶぶつかったとき、きっと思い出すはずです。
毎日の組込みコーディングが、よりエキサイティングで楽しいものになりますように。それでは、次の開発現場でお会いしましょう!