概要
もしあなたが,ほんの少しでも以下のような疑問を感じたことがあれば,ベアメタル・プログラマとしての素質があると思います.
-
main関数はいったい誰が動かしているのか?
-
自分が作成した関数(プログラム)はなぜ動くのか?
-
なぜ自分のプログラムは周期的に動作するのか?
-
なぜ自分のプログラムは不定期イベント(マウスクリック等)で呼び出されるのか?
などなど,普段あたり前のように思っていることでも,突き詰めてみるとなぜ?どうして?に行き当たるのではないでしょうか.
ベアメタル・プログラミングの魅力
そもそもベアメタルとは何か?
ググってみると,いろいろな意味付けがありますが,この記事で意図しているものに一番近いものは,
『ベアメタルとは、OSなどが何も導入されていない、まっさらな状態のコンピュータのこと。』
のことです.
OSも何もない世界…,想像できるでしょうか?
ROM,RAM,ハードウェアデバイスは何も初期化されていない世界で,自分が作成したプログラムをなんの介在もなしに動作させる.
これこそが,ベアメタル・プログラミングの最大の魅力と思います.
これから,自分の気力が続く限り,
『ベアメタル・プログラミングの世界』から『OSがある世界』まで
athrillを使用して,順序立てて?適度に寄り道しながら?解説して行けたらなと思っています.
main関数が動き出すまで
第一回目は,冒頭でお話した
『main関数はいったい誰が動かしているのか?』
について,紐解いていきたいと思います.
CPUとROM/RAM
まず,最初に,CPUとROM/RAMについて前提知識を整理しておきます(前提知識がある方は読み飛ばしてください).
ベアメタル・プログラミングを行っていく上では,これらの知識は大前提となりますので,必要最小限度の内容のみまとめておきます.
※非常に簡略化して説明しておりますので,詳細は専門書を参照してください.
ROM
まず,普段作成しているプログラム(高級言語で作成されたもの)ですが,
これらはコンパイルして,CPUが解釈できる機械語と呼ばれるものに変換する必要があります.
一般的には,この機械語命令の集合は,ROM(Read Only Memory)と呼ばれる場所に配置されます.
ROMは読み込み専用のメモリ領域であり,値が書き換わることがない命令の集合やデータはここに配置されます.
RAM
一方,値が書き換わるデータは,RAM(Random Access Memory)に配置されます.
典型的な例としては,値を書き換えるグローバル変数はすべからくRAMに配置されます.
CPU
CPUはプログラム(機械語)の内容を読み取り,四則演算やメモリアクセス等を行うことで,プログラムがやりたいことを実現してくれます.
CPUがこれらの機能を実現する上で重要な構成要素として,以下があります.
- プログラムカウンタ
- 次に実行するプログラムが格納されているメモリのアドレスを記憶しているレジスタです.
- 汎用レジスタ
- 演算した結果を一時記憶するためのレジスタです.V850の場合は,32個(r0〜r31)あります.
- フラグレジスタ
- CPUが演算実施した結果(オーバーフロー有無,符号,ゼロ有無等)を記憶しているレジスタです.
CPUは,機械語命令毎に,これらのレジスタを使用して以下を繰り返し続けるだけです.
- 機械語命令の取り出し
- 機械語命令の解読
- 命令実行(四則演算やメモリアクセス等)
そして,CPUが最初に実行する機械語命令の場所ですが,一般的に,ROM上の特定アドレス番地になります.
V850の場合は0x0です.
スタートアップルーチン
ここまでくると,main関数を誰が呼び出しているのか,想像がついてきたのではないでしょうか.
その答えは,当たり前ですが,CPUです.
ただ,いきなりmain関数をCPUが実行するというわけには行きません.
main関数に行き着くまでにいろいろな初期セットアップが必要になります.
そのセットアップを行ってくれるプログラムが『スタートアップルーチン(ブートストラップ)』です.
セットアップ内容としては以下があります.
- CPU初期化
- RAM領域の初期化(ゼロクリア)
- RAM領域の初期化(初期値設定)
- ハードウェア初期化
- OS初期化と起動(OS未使用の場合は不要)
スタートアップルーチンプログラム(例)
以下,スタートアップルーチンの最小セット例をV850で記述してみました.
di
Lea _stack_data, r3
addi STACK_SIZE, sp r3
jarl _bss_clear, lp /* RAM領域の初期化(ゼロクリア) */
jarl _data_init, lp /* RAM領域の初期化(初期値設定) */
ei
br _main /* main 関数呼び出し */
ご覧の通り,main関数を呼び出すまでの間に,RAMの初期化等がありますよね.
今回は,RAMに着目して解説をします.
main関数(例)
このスタートアップルーチンに対して,以下のようなmain関数をがあるとします.
static int global_value;
static int *global_value_pointer = &global_value;
int main(void)
{
*global_value_pointer = 999;
printf("Hello World!!\n");
while (1) {
;
}
}
C言語を知っている方なら,global_valueには当然初期値0が入り,
global_value_pointerには,global_valueのアドレスが初期値として入るので,
main関数の処理が動作すると,global_valueには999が入ることはすぐにお分かりになるでしょう.
これは,当たり前のことなのですが,スタートアップルーチンから data_init() を
呼び出さないとどうなるでしょうか,athrillで試してみましょう.
athrillで実験(data_init()呼び出しなし)
実行結果は以下のとおり,1つの警告と1つのエラーが発生しました(これらの警告・エラーはathrill固有機能です).
WARNING: Unitialized data read : variable=>global_value_pointer : main()@stack_data
この警告は,未初期化変数の読み込みが発生した場合に出力されるものです.
本来であれば,global_value_pointerは,スタートアップルーチンで初期化されるべきなのですが,
main関数に入ってくるまでの間に初期化されずに来てしまい,未初期化のまま変数参照したため警告されました.
mpu_put_data32:error: can not write data on ROM :addr=0x0 data=999
Exec Error code[0]=0x5f6a code[1]=0x1 type_id=0x6
CPU(pc=0x88c) Exception!!
そして,上記が重大なエラーであり,アドレス0番地(ROM領域)に999を書き込みしていること検出しています.
これは,未初期化のglobal_value_pointer(=0x0)に対するポインタアクセスをしてしまったため,
不正アクセスが発生し,athrillはCPU例外を送出して,強制停止することになりました.
athrillで実験(data_init()呼び出しあり)
では,スタートアップルーチンで,data_init()を呼び出すようにして,再度 main() 関数を実行してみましょう.
$ ../build/run.sh
Elf loading was succeeded:0x0 - 0xa9f : 2.671 KB
Elf loading was succeeded:0xaa0 - 0xea4 : 1.4 KB
ELF SYMBOL SECTION LOADED:index=17
ELF SYMBOL SECTION LOADED:sym_num=47
ELF STRING TABLE SECTION LOADED:index=18
[DBG>
HIT break:0x0
[NEXT> pc=0x0 vector.S 6
c
[CPU>Hello World!!
無事,main()関数から「Hello World!!」が出力されました.
さらに,グローバル変数の値を確認してみましょう.
[DBG>p global_value_pointer
global_value_pointer = (int *: 4 ) 0x5ff7404 @ 0x5ff7000(0x0)
[DBG>p global_value
global_value = 999 (int:4) @ 0x5ff7404(0x0)
当たり前ですが,期待した値が入っていますね!
ここでご紹介したデモのプログラム一式は,athrillプロジェクトで公開されていますので,ぜひお試しください.