はじめに (対象読者・この記事でわかること)

この記事は、組み込みLinuxの一種であるuClinuxを使って開発を始めたいが、「MMUがないとメモリ保護がどうなるの?」「malloc()しても平気?」と疑問に思っているエンジニア向けです。
uClinuxでは通常のLinuxと違い仮想記憶管理が使えないため、メモリ割り当ての挙動が大きく異なります。本記事を読むと、flatバイナリの配置方法、スタック/ヒープの確保、複数プロセス間でのメモリリソースの競合を避ける設計手法が身につきます。実機で起きた「確保したアドレスが書き換わる」「リブートが頻発」という現象の切り分けにも応用できるでしょう。

前提知識

この記事を読み進める上で、以下の知識があるとスムーズです。 - C言語でのポインタと動的メモリ確保(malloc/free)の基礎 - ARMやNios IIなど組み込みCPUのメモリマップを読めること - Linuxでプロセスがfork/execされる流れの概略

uClinuxとメモリ管理の特殊性

uClinux(microcontroller Linux)は、MMU(メモリ管理装置)のないCPU向けにLinuxカーネルを移植したものです。デスクトップLinuxが仮想アドレス空間を使ってプロセスごとに独立した0x400000から始まる空間を作るのに対し、uClinuxは物理アドレスをそのまま扱います。そのため通常のELFバイナリは使えず、フラット(flat)形式のバイナリを直接ROM/RAM上に配置します。
メモリ保護がないため、あるプロセスが誤って0x8000番地に書き込めば、カーネル空間や他タスクのデータを破壊しかねません。動的確保もbrk/sbrkがなく、mallocは内部でmmap相当の領域を探してくるため、リンカスクリプトで十分な空きを確保してやらないと「out of memory」で即リジェクトされます。
この制約を理解した上で、リンカスクリプト、バーンタイムマネージャ(RTLD)、メモリプールを設計することがuClinuxアプリ開発の第一歩になります。

メモリ割り当ての実装とトラブルシューティング

ステップ1:リンカスクリプトで空き領域を確保する

まず、メモリマップを決めます。例としてNios IIの1 MiB RAM(0x0000_0000〜0x0010_0000)を想定します。

Ld
MEMORY { ram : ORIGIN = 0x00000000, LENGTH = 1M } SECTIONS { .text : { *(.text*) } > ram .rodata : { *(.rodata*) } > ram .data : { *(.data*) } > ram .bss : { *(.bss*) } > ram /* ヒープ/スタック用に空きを作る */ . = ALIGN(8); __heap_start = .; .heap : { *(.heap) } > ram __heap_end = ORIGIN(ram) + LENGTH(ram) - 0x1000; /* スタック1 KiB分確保 */ __stack_top = ORIGIN(ram) + LENGTH(ram); }

__heap_start__heap_endの間がmalloc領域として使われます。__stack_topはスタートアップコードでspにロードされます。
uClinuxのflatローダは、.bss以降をゼロクリアしてくれないため、ゼロ初期化が必要な変数は.bssに配置するようにします。

ステップ2:mallocラッパでアライメントを守る

uClibcのmallocは8バイトアラインメントですが、DMAバッファを扱う場合は32バイトやキャッシュライン単位で確保したいことがあります。以下のラッパを用意しておきます。

C
void *uc_malloc(size_t sz) { const size_t align = 32; void *p = malloc(sz + align); if (!p) return NULL; /* 余った領域をフックしてアラインメント調整 */ void *aligned = (void *)(((uintptr_t)p + align - 1) & ~(align - 1)); /* 元ポインタを1ワード手前に保存しておく */ ((void **)aligned)[-1] = p; return aligned; } void uc_free(void *p) { if (p) free(((void **)p)[-1]); }

これでDMAコントローラに渡すバッファも安全に確保できます。

ステップ3:複数プロセスでメモリを衝突させない

uClinuxではforkが使えないので、デーモンを増やすときはvfork + execveか、あらかじめ複数のバイナリを用意しておきます。
プロセスごとに固定アドレスを割り当てる場合は、リンカスクリプトで--defsym=_start=0x00020000のように分離します。
動的にロードしたい場合は、RTLD(flat runtime loader)のrelocでGOTを書き換えるため、バイナリをROMに焼く際に.gotセクションをRAM上に配置しておく必要があります。

ハマった点:メモリ破壊でランダムリブート

開発中、UARTログを出していると突然「DMA timeout」でリブートすることがありました。
原因は、DMAバッファがキャッシュラインと重なっており、非キャッシュアクセスとキャッシュライトバックが混在していたため、カーネルが不正データを見てパニックを起こしていました。

解決策

リンカスクリプトでDMAバッファ専用セクションを作り、キャッシュ属性を「デバイス」に変更します。

Ld
.dma_buf (NOLOAD) : { . = ALIGN(32); __dma_start = .; *(.dma_buf) . = ALIGN(32); __dma_end = .; } > ram :uncached

BSP側でMMU_TABLEuncached属性を追加して、キャッシュ無効領域にマップします。
これで同時アクセスが起きなくなり、リブートは完全に収まりました。

まとめ

本記事では、MMUなしのuClinux環境でメモリをどう割り当て・保護するかを解説しました。

  • flatバイナリでは仮想アドレスが使えないため、リンカスクリプトで物理メモリを直接管理する
  • mallocはヒープ領域を正しく確保しないと即失敗する
  • DMAバッファなど特殊用途はアライメントとキャッシュ属性に注意

この記事を通して、uClinuxでも「メモリ保護がない」ことを前提とした設計・実装ができるようになりました。
次回は、プロセス間通信(FIFOやshared memory)を使って、より大規模なアプリを安全に組む方法を紹介します。

参考資料

  • uClinux公式wiki Memory Management Without MMU
  • 《Embedded Linux Primer》第10章「Memory Management in uClinux」
  • Nios IIソフトウェア開発ガイド「Linker Script Reference」