体験!マイコンボードで組込みLinux

第2回組込みLinuxが起動するまで

Linux起動の概要

Linux起動前

Linuxでは基本的にシステムの初期設定をするので、Linux以前の状態ではほとんどの初期設定処理は不要です。

Linux以前ではLinuxをロードする対象であるSDRAM領域を初期化する処理は必要となります。一般的な組込みLinuxボードでは大容量フラッシュROM上にLinuxカーネルが書き込まれていることが多いので、RAM領域が使えるようにさえすれば、あとはROM上からRAM上へデータコピーをするだけでLinuxが起動できます。

SH7706LSRボードでは大容量フラッシュROMではなくSDカード上にLinuxカーネルがファイルとして書き込まれていますので、Linux起動以前にSDカードアクセスやファイルシステムの操作ができていないといけません。SH7706LSRボードではSDカードアクセスやファイルシステムの操作が可能なMESというシステムが書き込まれていますので、MESベースでLinuxを起動すればいいわけです。Linux以前の状態ではLinuxカーネルをRAMメモリ上にコピーをして所定の番地にジャンプをして、Linuxを起動します。

初期RAMDISK

Linuxが起動すると、小規模なLinuxファイルシステムである初期RAMDISKベースでミニLinuxシステムが起動します。このミニLinuxシステムでは本稼働のファイルシステムが動作するのに必要な処理が可能になります。環境によっては何もしなくても本稼働のファイルシステムが動作するケースもあります。

たとえば、本稼働のファイルシステムのベースとなるメディアのデバイスドライバがモジュールの場合は、本稼働のファイルシステムが起動する前にミニLinuxシステムでモジュールのロードをします。ネットワークファイルシステムベースの場合は、本稼働のファイルシステムが起動する前にネットワーク通信の設定などを行います。

ミニLinuxシステムの処理は、初期RAMDISKのファイルシステム上のlinuxrcという名称のスクリプトで記述をします。linuxrcのスクリプト処理が終了すると、ファイルシステムがミニLinuxシステムから本稼働のファイルシステムに切り替わり、Linuxの起動となります。

initプロセス

本稼働のファイルシステム上でLinuxが起動すると、まずinitプロセスが起動します。initプロセスは本稼働のファイルシステム上にある/etc/inittabの記述内容に従ってLinuxシステムの初期化処理を実行します。/etc/inittabではランレベルの指定、ブート時の処理スクリプト、ランレベルごとの処理スクリプト、ランレベルごとのログインプロンプトを出すための端末制御などの内容を記述します。

Linuxシステムの初期化処理は、/etc/inittabで指定したブート時の処理スクリプトで具体的に記述をします。初期化が終了すると/etc/inittabで指定されたランレベルに対応した端末処理が起動し、ログインプロンプトが表示されて、Linuxの初期化が終了し、Linuxの通常操作ができるようになります。

組込みLinuxの起動について

PCの場合はLILOやgrubをそのままのかたちで利用すればLinuxが起動できますので、Linux起動についての知識は不要です。組込みボードの場合は、ハードウェアが多種多様なので、標準のLILOやgrubがそのまま使えません。このためLinux起動についての知識や技術開発などが必要になってきます。

組込みボードでのLinux起動へのアプローチには、主に2つの方法があります。1つはLILOやgrubの内部仕様を熟知し、全ソースコードを完全に解読して、対象となる組込みボードに対してLILOやgrubなどを移植する方法です。

もう1つは、白紙の状態からブートローダを自作する方法です。単純な組込みボードではWindowsとLinuxのマルチローダなどは不要であり、たいていの場合はLinuxが起動すれさえばいいケースが多いので、最低限の部分を満たしたブートローダを自作するほうが労力がかからないと思います。

Linuxのロード場所

Linuxの実体は、Linuxカーネルソースをコンパイルしたあとにトップフォルダに生成されるvmlinuxというイメージファイルです。基本的には組込みボードのRAM領域にvmlinuxのイメージをそのままコピーをしてから所定の番地へジャンプをすれば、Linuxが起動します。

まず、vmlinuxを組込みボードのどの番地からコピーしたらいいかということですが、Linuxカーネル再構築時のメニューで先頭番地を指定し、それでコンパイルしたLinuxカーネルを組込みボードのRAM上の指定した番地からコピーをしてから、所定の番地へジャンプする方法があります。また、Linuxカーネル再構築時のメニューで先頭番地を指定しないLinuxカーネルを組込みボードの任意のRAM領域にコピーをして、所定の番地へジャンプする方法もあります。

そうすると、今回の例で取り上げるSHプロセッサは他のプロセッサとは異なり、プログラムのオフセットを自由に設定でき、動的プログラム配置を可能にするセグメントレジスタやベースレジスタがないので、どうやってコンパイル済みのカーネルが任意のメモリアドレスに対応可能なのかという疑問が出てきます。

答えはMMUにあります。前回MMUについて解説したと思いますが、MMUというのはプロセッサの論理的なメモリアドレスとハードウェアの物理的なアドレスの対応を自由自在に変更できるものです。MMUを使えば物理メモリの場所に関係なく論理アドレスを設定できるので、どこのメモリアドレスにあってもいいわけです。

カーネルの物理構造

組込みボード上でLinuxを起動させるローダーを開発する場合は、何よりも対象となるLinuxカーネルの物理構造を把握しなければなりません。Linuxカーネルは単純にコードがベタになっていて先頭にジャンプすればいいというわけではありません。

図1 Linuxカーネルの概要
図1 Linuxカーネルの概要

Linuxカーネルは図1のような構造になっています。Linuxカーネルは市販デバッガでもソースコードデバッグなどができるようにそれ自身ELF形式になっていますので、先頭にはELFヘッダなどが存在します。Linuxカーネルは起動時にパラメータ指定が必須で、その他に自由にオプションが設定できるようにカーネルオプションを指定することができます。

Linuxカーネルは起動時に必須なパラメータ指定はLinuxカーネルの先頭から1000H番地から始まる整数配列に数値を格納し、カーネルオプションはLinuxカーネルの先頭から1100H番地にテキスト形式そのものの内容を格納します。Linuxカーネルの実行コードはLinuxカーネルの先頭の2000H番地から存在し、Linuxカーネルをメモリ上にコピーし、カーネルパラメータやカーネルオプションを設定してから2000H番地にジャンプをします。

これでLinuxカーネルが起動します。

ブートローダ

カーネルパラメータ仕様

Linuxは基本的にPCをターゲットとしていますので、カーネルパラメータもPCを前提にした仕様となっています。組込みボードの場合はその一部だけ使えればいいので、必要な部分のみカーネルパラメータを設定します。

今回のブートローダで使う部分は初期RAMDISKに関する部分のみとなります。カーネルパラメータはLinuxカーネルのオフセット1000Hから始まる4バイト整数の配列で指定します。それぞれのパラメータは以下のような内容になっています。

0番目 : MOUNT_ROOT_RDONLY
読み込み専用でルートファイルシステムをマウントするかどうかを設定し、MS_RDONLYビット(ビット0)で読み込み専用に設定します。
1番目 : RAMDISK_FLAGS
初期RAMDISKの設定で、RAMDISK_PROMPT_FLAG(ビット15)で初期RAMDISKのプロンプトを設定、RAMDISK_LOAD_FLAG(ビット14)で初期RAMDISKを使うことを設定します。
2番目 : ORIG_ROOT_DEV
ルートデバイスのデバイスをメジャー番号とマイナー番号で設定します。
3番目 : LOADER_TYPE
ブートローダのタイプを設定しますが、組込みボードの場合は、通常、設定不要です。
4番目 : INITRD_START
初期RAMDISKの開始番地を指定します。
5番目 : INITRD_SIZE
初期RAMDISKの開始番地のサイズを指定します。

ブートローダの実際

SH7706ボードにおけるLinuxブートローダの例はリスト1のとおりになっています。Linuxブートローダのソースコードにはコメントが記入されていますので、詳細はソースコード中のコメントを参照してください。

リスト1 SH7706ボード用Linuxブートローダ
  1: #include <macro.h>
  2: #include <mes2.h>
  3: #include <h8/reg770x.h>
  4: #include <string.h>
  5: 
  6: #define RAMDISK_PROMPT_FLAG        0x8000
  7: #define RAMDISK_LOAD_FLAG        0x4000
  8: 
  9: int main(int argc, char **argv) {
 10:     int    fd, size, *param, rdsize, c;
 11:     char    *ptr, *mem, *rdptr, ram;
 12:     void    (*func)();
 13: 
 14:     // 初期RAMDISKの先頭アドレスの初期化
 15:     rdptr = 0;
 16: 
 17:     // 初期RAMDISKのサイズの初期化
 18:     rdsize = 0;
 19: 
 20:     // 初期RAMディスクのイメージファイルを開く
 21:     fd = open("initrd.img", OptRead);
 22: 
 23:     // 初期RAMディスクのイメージファイルの有無を判定
 24:     if(fd != -1) {
 25:         // 初期RAMディスクが存在する場合
 26: 
 27:         // 初期RAMDISKのファイルサイズを格納
 28:         rdsize = GetFileSize(fd);
 29: 
 30:         // 初期RAMDISKのためのメモリ領域を確保
 31:         rdptr = malloc(rdsize);
 32: 
 33:         if(rdptr == 0) {
 34: 
 35:             // 初期RAMDISKのためのメモリ領域を確保できない場合は異常終了させる
 36:             printf("No memory\r");
 37:             close(fd);
 38:             return -1;
 39:         }
 40: 
 41:         // 初期RAMDISKロードのメッセージ出力
 42:         printf("initrd loading.");
 43: 
 44:         // 初期RAMDISKイメージファイルを読み込みメモリ領域に4000Hずつコピー
 45:         mem = rdptr;
 46:         c = 0;
 47:         do {
 48:             size = read(fd, mem, 0x4000);
 49:             mem = &mem[0x4000];
 50: 
 51:             // 進捗状況を逐次表示
 52:             if((++c & 0x7) == 0) putchar('.');
 53:         } while(size == 0x4000);
 54:         putchar('¥r'), putchar('¥n');
 55: 
 56:         // 初期RAMDISKイメージファイルを閉じる
 57:         close(fd);
 58:     }
 59: 
 60:     // カーネルヘッダ領域データを保管するためのメモリを確保
 61:     ptr = malloc(0x2000);
 62:     if(ptr == 0) {
 63: 
 64:         // カーネルヘッダ領域データのためのメモリ領域を確保できない場合は異常終了させる
 65:         printf("No memory¥r");
 66:         return -1;
 67:     }
 68: 
 69:     // カーネルパラメータ領域のポインタをセット
 70:     param = (int*)(ptr + 0x1000);
 71: 
 72:     // Linuxカーネルのイメージファイルを開く
 73:     fd = open("vmlinux", OptRead);
 74: 
 75:     // Linuxカーネルのイメージファイルの有無を判定
 76:     if(fd == -1) {
 77: 
 78:         // Linuxカーネルが存在しない場合は異常終了させる
 79:         free(ptr);
 80:         printf("No vmlinux¥r¥n");
 81:         return -1;
 82:     }
 83: 
 84:     // カーネルヘッダ領域データをメモリに読み込む
 85:     read(fd, ptr, 0x2000);
 86: 
 87:     // カーネルオプションの内容をオフセット1100Hへセットする
 88:     strcpy(ptr + 0x1100, "mem=32M console=ttySC1,115200 root=/dev/shmmc2");
 89: 
 90:     // カーネルコード領域をロードするためのメモリアドレスの先頭をセット
 91:     mem = (char*)0x8c002000;
 92: 
 93:     // カーネルを実行するためのアドレスをセット
 94:     func = (void*)0x8c002000;
 95: 
 96: 
 97:     // カーネルロードのメッセージ出力
 98:     printf("vmlinux loading.");
 99: 
100:     // カーネルイメージファイルを読み込みメモリ領域に4000Hずつコピー
101:     c = 0;
102:     do {
103:         size = read(fd, mem, 0x4000);
104:         mem = &mem[0x4000];
105: 
106:         // 進捗状況を逐次表示
107:         if((++c & 0x7) == 0) putchar('.');
108:     } while(size == 0x4000);
109:     putchar('¥r'), putchar('¥n');
110: 
111:     // カーネルイメージファイルを閉じる
112:     close(fd);
113: 
114:     // 初期RAMDISKの有無を判定
115:     if(rdsize > 0) {
116: 
117:         // 初期RAMDISKに関するカーネルパラメータの設定
118:         param[1] = RAMDISK_PROMPT_FLAG | RAMDISK_LOAD_FLAG;
119:         param[4] = (int)rdptr & 0x3ffffff;
120:         param[5] = rdsize;
121:     }
122: 
123:     // 時間待ち
124:     sleep(500);
125: 
126:     // CPUでの割り込みを禁止しMESの動作を止める
127:     INT_DISABLE();
128: 
129:     // ウオッチドッグタイマーを止める
130:     WTCSR_WR = 0x8500;
131: 
132:     // 全ての割り込みを禁止する
133:     IPRA = 0;
134:     IPRB = 0;
135:     IPRC = 0;
136:     IPRD = 0;
137:     IPRE = 0;
138: 
139:     // カーネルヘッダ領域データを保管した領域からカーネルヘッダ領域へコピー
140:     memcpy((char*)0x8c000000, ptr, 0x2000);
141: 
142:     // カーネルコード領域の先頭へジャンプ
143:     (*func)();
144: }

Linuxブートローダでは最初に初期RAMDISKのファイルイメージを任意のメモリ領域に読み込み、カーネルパラメータに必要な初期RAMDISKの先頭アドレスとサイズを設定しています。もし、初期RAMDISKのファイルイメージが無い場合でもエラーにはせず、初期RAMDISKなしでLinuxを起動するようにします。

次にLinuxカーネルのファイルイメージからヘッダ部分である最初の2000Hのサイズを任意のメモリ領域へ読み込み、カーネルパラメータや必要に応じてカーネルオプションを指定します。そして、残りのLinuxカーネルのファイルイメージを全てメモリ領域上に読み込みます。

このブートローダはMESで動作していますので、ウォッチドッグタイマーを停止させ、全割り込みを禁止させることによりMESを停止させ、任意のメモリ領域に読み込んだLinuxカーネルのヘッダをコピーし、オフセット2000HにジャンプすることによりLinuxが起動します。

次回は

次回はルートファイルシステムの構築について解説します。

おすすめ記事

記事・ニュース一覧