Raspberry PiでRaspbianを入れて動かすと普通にLinuxとして使えるけどまあそれじゃ特に普通のPCと変わらないよね… ということでゆくゆくはオレオレOSを…などと考えてコンテキストスイッチをベアメタルでやってみることにした。
環境構築
Linux(Ubuntu)にARM用のクロスコンパイラをインストールする
$ sudo apt install gcc-arm-none-eabi |
元にするプロジェクト
Raspberry Pi Zero WでベアメタルLチカ – 楽しくやろう。で公開されている
led_blynkがシンプルでわかりやすかった。
ビルドするとkernel.imgが作成される。
(Makefileのクロスコンパイル設定がarm-eabi-
なのでarm-none-eabi-
に変更する)
このブログなどをいろいろ見たところによると、Raspberry Pi Zero(ARM)はファームウェアの起動後、kernel.imgが0x8000
から実行されるらしい。
SD Cardに書き込んでRaspberry Pi Zeroで動かす
ブログ記事によると
makeするとkernel.imgが作成されますので、これをbootcode.binおよびstart.elfと共にSDカードへコピーするとLチカが動作します。
最新のbootcode.binとstart.elfはRaspberry PiのGitHubからダウンロードして下さい。
と書かれているが、まっさらの状態からのSD Cardの書き込み方がよくわからなかった…。
フォーマットした状態のSD CardにいったんRaspbianのイメージを書き込んで起動させるとあれこれとファームとなるファイルたち?が作られるので、その状態でから始める。 MacでSD Cardの口に挿入すると /Volumes/boot/ に見えるので、ビルドした kernel.img をコピーしてやる:
$ cp kernel.img /Volumes/boot/ # kernel.imgをSD Cardにコピー |
書き込んだらSD CardをRaspberry Pi Zeroに差し替えて起動するとLチカが動いた。
QEMUで動かす
最終的に実機で動かすというのはいいんだけど、コードを修正して試すたびにSD Cardにコピーして差し替えて起動させるというのを毎回行うのは面倒だから、エミュレータ環境で動かせると便利。
led_blynkのリポジトリにある別ディレクトリのqemu-armがQEMUのARMエミュレータを使って動かせるものになっている。
rpi.ldを比べてみると、エミュレータでは開始アドレスが0x8000
じゃなくて0x10000
になるらしい。
ARMをエミュレートするMac用のQEMUのインストールは brew install qemu
だけでいいのかな?忘れた…。
qemu-system-arm
が動けばよし。
起動は:
$ qemu-system-arm \ |
UARTの出力をターミナルで受け取れる、便利。
ただターミナルからの入力はエコーバックされなかった…なぜだ。 エコーバックされていた、自然すぎてターミナルが行っているものかと思った。
また実機ではUARTのアドレスが違うかも?ということと、またどう接続すればやりとりできるのかわかってないので今のところUARTはエミュレータでしか試せてない。
ベアメタルコンテキストスイッチ
さて動かせる環境が整ったので、ベアメタルでコンテキストスイッチさせてみる。 語句の厳密の定義はともかく、ここではベアメタルというのはOS上じゃなくてCPU直接のまっさらな状態、 コンテキストスイッチは1つのCPUだけど複数のCPUがあるかのように実行を切り替えて並行に動かす、程度の意味。
コンテキストスイッチは本当に実行を切り替えるだけで、仮想アドレスとかカーネル/ユーザモードの切替、メモリの保護とかの処理はまったくしないことにする。 またコンテキストスイッチは自動には行われず、ある関数を明示的に呼び出して行うことにする(ノンプリエンプティブ、協調的)。
起動ルーチン
ブートして呼び出される関数:
void _start(void) { |
spawn
でプロセス(的なもの)を作成し、あとは無限ループ。
wait
でコンテキストスイッチする。
子プロセス
便宜上「子」と呼んでいるが、親子関係があるわけではない:
void childfunc(int no) { |
spawn
される子プロセスも大本のプロセスと内容は変わらず、無限ループで自分の処理を行える。
プロセス情報とテーブル
プロセスが管理する情報と、そのテーブル:
typedef struct { |
プロセス情報としては単にスタックポインタだけを保持するが、実際にはプロセスごとにスタック領域を割り当てる。 そしてコンテキスト情報の保存・復帰にスタックが利用される。
テーブルはプロセスの配列と個数、現在実行中の番号。
プロセスの生成
プロセスを生成するspawn
関数:
extern void swtch(Proc* curr, Proc *next); |
スタックを確保してけつに呼び出す関数を積む。 また初期のコンテキスト(退避レジスタ)と初期の引数を積んで、最終的なスタックポインタをプロセス情報として保持。
ベアメタルにmalloc
関数は存在しないので自作する必要があるが、本題とは外れるので適当に:
static char heap[65536] __attribute__ ((aligned (16))); |
プロセス(コンテキスト)切り替え
プロセスを切り替えるwait
関数:
void wait(void) { |
単純に配列の順に進めてプロセスを切り替える。
コンテキスト切り替えのswtch
関数に現在と次のプロセスを渡して切り替える。
swtch
はCレベルでは書けないので、ARM用にアセンブラで書く:
// swtch.s |
今回のコンテキストスイッチはノンプリエンプティブ、関数呼び出して切り替えるので、保存する必要があるレジスタはARMの呼び出し規約によればr4
~`r11。 ARMは関数を呼び出す際に戻りアドレスを保存するのにスタックではなく
lr`レジスタを使用するので、それもスタックに対比する。
その時点のスタックポインタを現在のプロセス情報として保存する。
そして次のプロセスに切り替える。
次のプロセスのスタックポインタを取り出してpop
でコンテキストを復帰して、またlr
をpc
にpop
することで実行を再開する。
r0
は保存する必要はないが、spawn
で生成したプロセスが最初に呼び出す関数への引数として渡すためだけに追加している。
まとめ
ベアメタルでもコンテキストスイッチだけなら結構簡単に書ける。 C言語に慣れているとスタックポインタをいじってしまうなんてことしていいの?と感じるんだけど、CPUにとってみれば別にタブーではないのであった。
余談
*nix標準のfork
形式にしようと思ったが、仮想アドレスを実装して分岐したプロセスが同じアドレスを指せるようじゃないとアドレスをずらす必要があって面倒なことになるため見送った。