Rust製組込みOS TockでC言語アプリケーションを動かす

Tockとは

このブログでも何度か紹介したRust製組込みOSです。
以前の記事
ターゲットはCortex-MのようなCPUリソースが限られたようなプロセッサです。
Rust Embeddedグループ発足前から公開されていて、Rust純粋でちゃんとしたOSを組む先駆けにもなっています。Rustで書かれているだけでなく、組込みOSの設計としてもおもしろいものになっています。
以前は専用ボードへの実装しか公開されていなかったのですが、最近になりSTM社製のNUCLEO-446REボードのサポートが追加されました。
手元にあったNUCLEO−429ZIボード用のサポートも追加してもらえるようPRを投げ、無事マージされたことにより、自分の手元でも動かせるようになりました。

このTockの性質として、ユーザーアプリケーションは独立にビルドする仕組みとなっているため、任意の言語で書くことができるというものがあります。
今回はC言語を用いて簡単なアプリケーションを書いてみて、さらにその仕組みを簡単に見ていきたいと思います。

サンプルアプリケーション

libtock-cというライブラリがTockのカーネルを叩くための各種関数を提供しています。
これを用いて簡単なアプリケーションを書いてみました

garasubo/tockapp

このアプリケーションはキーボードの入力を受けるとLチカが動き出し、もう一度入力を受けると止まる、というものになっています。
サンプルアプリケーションのレポジトリではlibtock-cをgitのサブモジュールとして取り込んで、ビルドはlibtock-c内のAppMakefile.mkに全面的に依存しています。

libtock-cのレポジトリ自体にもサンプルコードがあります。
今回のサンプルでは使っていないのですが、Newlibによって実装されたC標準ライブラリやluaのランタイムもあります。

ブートプロセス

サンプルアプリケーションでは普通にmain関数を書いているわけですが、これがTock側からどう呼ばれているかを見てみましょう。
リンカスクリプトがlibtock-cのuserland_generic.ldにあります。まずは、アプリケーションがどのようにビルドされるか見てみましょう。

userland_generic.ld

11行目、ENTRY(_start)となっていますが、これはlibtock/crt0.cに定義されています。26行目の.crt0_headerの構造についてもここで定義されています。

crt0.c

アプリケーションはメモリ上のどこにおかれるかはビルド時には決定できません。さらにCortex-Mには仮想アドレス機構はないため、すべて物理アドレスで扱う必要があります。
そのため、_start関数では4つのメモリレイアウトに関する情報を受けとり、それをもとにアプリケーション上の情報を書き換えるということをやってます。
スタックやヒープ領域、デバッグ用の情報などを設定する他に、グローバルオフセットテーブル(GOT)の書き換えもやってます。
GOTの書き換えは_startで呼び出される_c_startで行われています。
159行目のループ中で最上位ビットで場合分けを行っていますが、
これはリンカスクリプトでROM領域は0x80000000以上の領域、RAM領域は0x00000000に配置されていることを利用して(17、18行目)、
本来アプリケーション領域にある定数を指すものなのか、スタック上に配置されるグローバル変数なのかを判別してアドレスを調整しているためです。

システムコール

カーネルとのやりとりはSVC命令を用いたシステムコールにより実現しています。SVC命令を介しているのでそこのインターフェースさえ何らかの方法で実装できれば、Rust以外の言語でもカーネルの機能を呼び出せるという仕組みです。
これはlibtock/tock.cで実装されていて、サンプルアプリケーションでは直接は呼び出していませんが、libtock-c内の関数を呼び出すことにより間接的に使っています。
システムコールは5つのみです。簡単に説明すると

  • yield: そのアプリケーションの終了をする。アプリケーションはスケジュールされなくなる
  • subscribe: ドライバのコールバック関数を登録する(例:タイマー割り込みで関数を呼び出してもらう)
  • command: ドライバに対して指示を出す(例:LEDを点灯させる)
  • allow: カーネルとアプリケーション間で特定のメモリを共有させる(例:タイマードライバにコールバック制御用のデータ構造体を渡す)
  • memop: ヒープ領域を変更してもらったり、現在のメモリレイアウトの情報を手に入れるなど、メモリの操作を依頼する

おまけ

サンプルアプリケーションでtock_timer_tの実体をユーザーアプリケーション側に持っていて、そのアドレスをカーネルに渡しています。
普通の場合はむしろ構造体の実体はカーネル側に持っておき、そのアドレスなりIDなりをユーザーアプリケーションが持つのが一般的だと思いますが、これには理由があります(以前のブログで紹介した論文にも書いてあります)。
今回は扱いませんでしたが、Tockでは複数のアプリケーションを動かすこともできます。
メモリ領域はアプリケーションごとに独立に持っていて、カーネルや他のアプリケーションとは基本的に共有しません。
もしカーネル側が実体を持っているとすると、あるユーザーアプリケーションが大量のタイマーを要求してきた場合、他のアプリケーションがメモリ不足によりタイマーを手に入れられない場合が考えられます。
アプリケーション側がタイマーの構造体を抱えれば、大量に要求してきたアプリケーションのみがメモリ不足になり、カーネル及び他のアプリケーションが困るということにはなりません。

参考