QEMU に LPC1768 を追加してみた
NXP の Cortex-M3 搭載マイコンである、LPC1768 のエミュレーションを QEMU に追加してみました。
ゲストバイナリとして、TOPPERS ASP for LPC1768 のサンプルプログラム(sample1)が動作することを目標にしており、LPC1768 の周辺デバイスとしては、現状、シリアルコントローラのみサポートしています。
ベースとした QEMU のバージョンは v1.1.0 です。
尚、ホスト環境は Ubuntu 11.10 で確認を行っています。
QEMU LPC1768 をビルド
まず、QEMU をビルドするためのパッケージの導入します。必要に応じてインストールしてください。
# apt-get install glib2.0-dev
次いで、QEMU LPC1768 をビルドします。
% git clone https://github.com/myokota/qemu-cortex-m3.git % cd qemu-cortex-m3 % ./configure --target-list=arm-softmmu % make
TOPPERS ASP for LPC1768 をビルド
まず、コンフィグレータをビルドするためのパッケージを導入します。必要に応じてインストールしてください。
% sudo apt-get install libboost-dev libboost-filesystem-dev libboost-program-options-dev libboost-regex-dev
次に、TOPPERS/ASP をビルドするためのツールチェインの導入します。ここではホームディレクトリの opt ディレクトリにインストールしています(どこでもOK)。
% wget http://www.codesourcery.com/sgpp/lite/arm/portal/package6493/public/arm-none-eabi/arm-2010q1-188-arm-none-eabi-i686-pc-linux-gnu.tar.bz2 % tar xf arm-2010q1-188-arm-none-eabi-i686-pc-linux-gnu.tar.bz2 -C ~/opt
そして、TOPPERS/ASP for LPC1768 をビルドします。
% wget "http://sourceforge.jp/frs/redir.php?m=iij&f=%2Ftoppersasp4lpc%2F53185%2Fasp_lpc1768_generic_gcc-20110910.tar.gz" -O asp_lpc1768_generic_gcc-20110910.tar.gz % tar xf asp_lpc1768_generic_gcc-20110910.tar.gz % cd asp/cfg % ./configure % make % cd .. % mkdir obj % cd obj % ../configure -T lpc1768_generic_gcc % make GCC_TARGET=~/opt/arm-2010q1/bin/arm-none-eabi depend (環境によっては、Use of "do" to call というメッセージが大量に表示されますが、無視してください) % make GCC_TARGET=~/opt/arm-2010q1/bin/arm-none-eabi
TOPPERS/ASP for LPC1768 をビルドしたディレクトリ内の obj/asp がバイナリイメージ(ELF) になります。
QEMU LPC1768 の実行
QEMU LPC1768 をビルドしたディレクトリに移動し、
% ./arm-softmmu/qemu-system-arm \ -M lpc1768_generic \ -serial stdio \ -kernel 'TOPPERS/ASPをビルドしたディレクトリへのパス'/obj/asp
として QEMU を起動します。
すると、以下のようにシリアルポートへの出力が標準出力上に表示されます。
TOPPERS/ASP Kernel Release 1.7.0 for LPC1768 Generic (Aug 19 2012, 12:10:59) Copyright (C) 2000-2003 by Embedded and Real-Time Systems Laboratory Toyohashi Univ. of Technology, JAPAN Copyright (C) 2004-2011 by Embedded and Real-Time Systems Laboratory Graduate School of Information Science, Nagoya Univ., JAPAN Copyright (C) 2010 by TOPPERS/ASP for LPC project http://sourceforge.jp/projects/toppersasp4lpc/ System logging task is started on port 1. Sample program starts (exinf = 0). task1 is running (001). | task1 is running (002). | task1 is running (003). |
シリアルポートの入力も標準入力から行います。
制約事項など
LPC1768 周辺デバイス
冒頭でも少し触れましたが、LPC1768 のデバイスでサポートしているものは、
- UART0
のみです。
また、LPC1768 のクロックは、クロックソースが内部CR発振器に設定(CLKSRCSEL.CLKSRC が 0)されている場合のみ、
の設定内容から、 SysTick エミュレーションの動作周波数を決定するようにしています。
クロックソースが内部CR発振器以外で設定された場合は、 SysTick エミュレーションの動作周波数は100MHz 固定としています。
QEMU に Armadillo-800 EVA を追加してみた
アットマークテクノさんの新しいArmadilloである Armadillo-800 EVAのエミュレーションを QEMU に追加してみました。
但し、扱えるデバイスはシリアルとタイマのみで Linux カーネルが起動する程度となっています。
また、動作確認した環境は、Debian 6.0、Ubuntu 11.10 です。
ビルド
QEMU の他、ゲストである Linux カーネルと Buildroot を使用したユーザランドの全てをビルドします。
準備
以降のビルド手順に必要なパッケージをホスト環境に導入します。
(Ubuntu 11.10 システムインストール直後の場合) # apt-get install g++ bison flex gettext glib2.0-dev u-boot-tools
また、必要に応じて作業ディレクトリを作成します
% mkdir ~/work % export WORKDIR=~/work
buildroot
% cd $WORKDIR % wget http://buildroot.uclibc.org/downloads/buildroot-2012.05.tar.bz2 % tar xf buildroot-2012.05.tar.bz2 % cd buildroot-2012.05 % cat > configs/armadillo800eva_defconfig << EOF BR2_arm=y BR2_cortex_a9=y BR2_TOOLCHAIN_EXTERNAL=y BR2_TOOLCHAIN_EXTERNAL_CODESOURCERY_ARM2009Q3=y BR2_TARGET_GENERIC_GETTY_PORT="ttySC1" BR2_TARGET_ROOTFS_CPIO=y BR2_TARGET_ROOTFS_CPIO_GZIP=y EOF % make armadillo800eva_defconfig % make
Linux カーネル
% cd $WORKDIR % git clone https://github.com/myokota/linux-a800eva-defconfig-for-qemu.git % wget http://armadillo.atmark-techno.com/files/downloads/kernel-source/linux-2.6.35-at/linux-2.6.35-a800eva-at2.tar.gz % tar xf linux-2.6.35-a800eva-at2.tar.gz % cd linux-2.6.35-a800eva-at2 % cp $WORKDIR/linux-a800eva-defconfig-for-qemu/armadillo800eva_qemu_defconfig arch/arm/configs
ビルドの前に、以下の修正を加えます。
diff --git a/arch/arm/mach-shmobile/board-armadillo800eva.c b/arch/arm/mach-shmo index a570b40..a2c0d95 100644 --- a/arch/arm/mach-shmobile/board-armadillo800eva.c +++ b/arch/arm/mach-shmobile/board-armadillo800eva.c @@ -128,6 +128,7 @@ static struct platform_device __maybe_unused usb_func_device .resource = usb_resources, }; +#ifdef CONFIG_MMC static void sdhi0_set_pwr(struct platform_device *pdev, int state) { gpio_set_value(GPIO_PORT107, state); @@ -304,6 +305,7 @@ static struct platform_device sh_mmcif_device = { .num_resources = ARRAY_SIZE(sh_mmcif_resources), .resource = sh_mmcif_resources, }; +#endif /* CONFIG_MMC */ static struct sh_fsi_dma sh_fsi2_dma = { .dma_porta_tx = { @@ -1044,9 +1046,11 @@ static struct platform_device gpio_led = { static struct platform_device *rma1evb_devices[] __initdata = { ð_device, +#ifdef CONFIG_MMC &sh_mmcif_device, &sdhi0_device, &sdhi1_device, +#endif //&usb_func_device, &ohci_device, &ehci_device,
ビルドを行います。
% export ARCH=arm % export CROSS_COMPILE=$WORKDIR/buildroot-2012.05/output/host/usr/bin/arm-none-linux-gnueabi- % make armadillo800eva_qemu_defconfig % make uImage
QEMU
% cd $WORKDIR % git clone https://github.com/myokota/qemu-a800eva.git % cd qemu-a800eva % ./configure --target-list=arm-softmmu % make
実行
以下で QEMU を起動します。
% $WORKDIR/qemu-a800eva/arm-softmmu/qemu-system-arm \ -M a800eva \ -kernel $WORKDIR/linux-2.6.35-a800eva-at2/arch/arm/boot/uImage \ -initrd $WORKDIR/buildroot-2012.05/output/images/rootfs.cpio.gz \ -append "console=ttySC1" \ -nographic \ -serial telnet:0.0.0.0:1200,server
すると、
QEMU waiting for connection on: telnet:0.0.0.0:1200,server
というメッセージが表示されるので、別の端末から、
% telnet localhost 1200
とします。すると、Linux カーネルが起動を開始するので、ログインプロンプトが表示されれば、root にてログイン(パスワードなし)が可能です。
Cortex-M3 まとめ
少し前にCortex-M3のエミュレータを作った際にメモしておいたことを以下にまとめてみる。
コードアドレスのLSBについて
- 例外ベクタのLSBは例外が Thumb で実行されるかどうかを示すため、例外ベクタのLSBは1とする必要がある
- PCを変更するときは、上記の理由でLSBを1にする必要がある
- LDR でコードアドレスをロードした時も LSB に 1 がセットされる
LDR R0, =label ; R0 set to 0xXXX1
Exception について
- 割り込みがアサートされると割り込み保留状態となる
- SETPEND(0xE000E200) で保留状態を表示
- 書き込んで保留させることも可能。
- CPU が割り込みハンドラを開始すると、
- 保留状態がクリアされる
- アクティブステータスレジスタ(0xE000E300、読み出し専用)の対応ビットが1になる
- ハンドラを退出するまで同じ割り込みをネストしない(優先度が同じため)
- 保留はクリアレジスタ(0xE000E280)で任意にクリアも可能
- 割り込みハンドラを退出すると、
- アクティブステータスレジスタの対応ビットはクリアされる
- FAULTMASK が有効の場合、クリアされる
- 各割り込みはSETPEND(0xE000E200)とSTIR(0xE000EF00)で保留させることが出来る
- 内部の割り込みもPENDXX(0xE000ED04)で可能
- 保留可能な割り込みはNMI、PendSV、SysTick
PendSV について
- 一言で言えば、OS(特権) から ソフトウェア割り込みを保留するためのもの
- 割り込みハンドラ中に遅延させたい処理(コンテキストスイッチ)等がある場合に使用する
- SVC 例外は保留されないが、PendSVは保留される
- PENDSVSET を 1 に設定すると PendSV 要因が保留される
Priority について
- 8bit で定義
- 値が小さい方が高い
- -3(reset)、-2(NMI)、-1(ハードフォールト) は固定
- 8bit は NVIC の Application Interrupt/Reset Control (0xE000ED0C) の PRIGROUP により2分割される
- 横取り優先度レベル
- サブ優先度レベル
- 横取り優先度レベルは、割り込み発生時の優先度の判断に使われる
- 高い優先度が発生したら横取りされる
- 同じ優先度が発生したら無視される
- サブ優先度レベルは、同じ横取り優先度レベルが発生したとき、横取りするための値
- 同じ横取り優先度レベルで、サブ優先度が高ければ横取り
バイナリのロード
前回のエントリでプロセスを起動できるようになりました。
が、その動作確認には、Kernel内部のローダに対して、Kernel内部に定義したテスト関数のエントリを指定しているだけでした。
次のステップとしてはサーバプロセスの起動になりますが、そのためにはKernel外部にあるバイナリイメージの読み込みが必要になります。
従って今回は細かい移植不具合の修正を含め、独立して生成したテストプログラムのロードを確認してみました(リビジョン8)。
バイナリフォーマット
MonaOSでは、実行イメージのファイルフォーマットとして、
- ELF
- PE
をサポートしていますが、そのためにはフォーマット解析を担う各サーバプロセスの起動が必要になります。
このため、テストプログラムはrawバイナリとして直接ロードしてみます。
バイナリイメージの作成
MonaOSにはテストプログラム用のディレクトリが用意されていたため、そこを利用しました。
テスト用プロセスの出力形式はPEフォーマットとなっているため、rawバイナリで出力するようにします。
また、テスト用のコードは以下のようにしました。
int main(int argc, char* argv[]) { int i = 0; int pid = syscall_get_pid(); int tid = syscall_get_tid(); while(1) { _printf("pid:%d/tid:%d[%d]\n", pid, tid, i++); sleep(pid*100); } return 0; }
最初なので、簡単なところからです。
_printf関数は、デバッグ用のシリアルに対してprintを行うMona標準関数です。
バイナリイメージの置き場所
Kernelから見えるデバイス上に作成したバイナリを格納します。
MonaOSでは、FD(FAT12)やCD-ROMの外部記憶装置から順次、サーバプロセスを読み込み、起動していくようです。
Bishopエミュレータ上における今回の確認では、代替としてお手軽にNOR Flash(以下、NOR)にしてみました。
バイナリを格納する
エミュレートしているNORの内容を変更するには、直接qemuのソースコードの変更が必要ですが、qemuに手を入れるのは避けました。
思いついた一番簡単な方法としては、NOR上のinitrd用の領域を利用する事です。
Bishopエミュレータは起動時、(起動したディレクトリにある)特定のファイルを、NOR上の対応したアドレスにマップしているようです。
モジュール | ファイル名 | アドレス |
---|---|---|
ブートローダ | U-Boot.bin | 0 |
Kernel | uImage | 0x00040000 |
initrd | initrd.uimg | 0x00330000 |
現状、MonarmのブートローダとKernelはそれぞれ上記のファイル名にしているため、NOR上に配置され、アクセスできるためうまく起動しています。
テスト用のバイナリイメージもこの方法でいくため、initrd.uimgというファイル名にしました。
バイナリを参照する
次に、実際にKernelからNOR上のテストプログラムのバイナリコードを参照する方法に関してです。
MMUが有効であるため、仮想アドレス上にNORのアドレスをマップする必要があります。
NORは物理アドレス0番地からですが、0〜8MiBの領域はKernelの領域のため、ストレートにマッピングする事はできません。
従って、以下のようにストレートにマップしているI/O領域直前の4MiBにマップしました。
マップすべき本当に必要な領域はinitrd用の領域の部分のみですが、NORのサイズは4MBなので全てマップしてしまいました。
またKernelからは、0x47c00000+0x00330000のアドレスをエントリポイントとしてプロセスをロードするようにします。
そうすれば作成したテストプログラムがプロセスとして起動するはずです。
実行
make後qemuを実行すると以下のログが出力され、プロセスがロードされた事を確認できました。
% qemu-system-arm -M pe201b -serial stdio -mtdblock null.fs -nographic Mona version.0.3.0Alpha9 on ARM920T(S3C2440A/Bishop) Aug 25 2008 00:28:52 Setting PIC [OK] System Total Memory 64[MB]. Paging on mainProcess:Load#1:Success(0) mainProcess:Load#2:Success(0) mainProcess:Load#3:Success(0) pid:5pid:4pid:3/tid:58/tid:60[0] [0] /tid:59[0] pid:3/tid:58[1] pid:4/tid:59[1] pid:5/tid:60[1] pid:3/tid:58[2] pid:4/tid:59[2] pid:3/tid:58[3] pid:5/tid:60[2] pid:3/tid:58[4] pid:4/tid:59[3] pid:5/tid:60[3] pid:3/tid:58[5] pid:4/tid:59[4] ・・・(数分後)・・・ pid:5/tid:60[2415] pid:3/tid:58[4012] pid:4/tid:59[3015] pid:3/tid:58[4013] pid:5/tid:60[2416] pid:4/tid:59[3016] pid:3/tid:58[4014] pid:3/tid:58[4015] pid:4/tid:59[3017] pid:5/tid:60[2417] pid:3/tid:58[4016] pid:4/tid:59[3018]
プロセス起動開始直後、表示が乱れているのはログ出力がシリアライズされていないためです。
おわりに
Kernel外部のバイナリイメージを起動する事が確認できました。
これによって、以降目標としている従来のサーバプロセスの起動に失敗した場合、
- Kernel側のロードシーケンスに問題があるのか
- またはサーバプロセス側のスタートアップ以降の処理に問題があるのか
の切り分けがしやすくなると思います。
追記
確認では、同じコードを3回ロードするようにしています。
(明記が漏れていました)
Kernel完了
Kernelディレクトリ以下の移植が一通り完了しました。
変更を加えた箇所を清書してないのでソースが汚いですが、区切りがいいのでまとめたいと思います(リビジョン7)。
前回から今回の更新にかけて行った事は以下になります:
- これまで一部無効にしていた箇所の有効化
- Process.cpp
- Sheduler.cpp
- ihandlers.cpp
- PageManager.cpp
- syscalls.cppの移植
- 関連して include/sys/types.h のシステムコールマクロも
- アボート例外の移植
- astart.S(旧ihandler.asm)
- Loader.cppの追加
以上から、前回から以下の事柄が可能になりました:
- Loaderからプロセスを起動できるようになった
- プロセス空間が切り替わるようになった
- スレッドが規定のポリシーでスケジューリングされるようになった
- デマンドページングが可能になった
これで積み残しの部分はほぼ解消しましたが、まだDMAページの設定が無効のままになっています。これに関してはデバイスドライバ移植までとっておきたいと思います。*1
また、細かい点では以下の修正・変更があります:
- Kernelメモリを6MBに戻した(0x20000〜0x80000)
- システムメモリを64MBに(プラットフォーム上は128MB)
- ページテーブルディスクリプタをファインページからコアースページに変更
- abortモード用のスタックを割り当てた(0x7000から)
カットアンドトライで進めているため無駄に修正をしていた部分が目立ちます。
移植概要
今回の移植でアーキテクチャに依存する箇所は以下の2点です。
■アボート例外
ARMではアボートとして以下があります*3:
ソース | 原因 |
---|---|
アライメント | 境界整列していない |
変換時の外部アボート | 外部メモリシステムで発生したエラー |
変換フォルト | セクションまたはページの記述子が無効として設定されている |
ドメイン | セクションまたはページのドメインがアクセス不可ドメインに設定されている |
許可フォルト | セクションまたはページのドメインがクライアントであった場合、ページ単位のアクセス許可されていない |
また、上記各アボートに対し、
- プリフェッチアボート
- データアボート
と、例外の種類すなわちベクタが分かれており、それぞれ、
- 命令にアクセスした時
- データにアクセスした時
に物理ページへ変換を行う過程で上述の原因に適合した場合、各々の例外が発生します。
オリジナルではページフォルトが発生した際の例外をハンドリングしています。x86のベクタNo14に該当します。
従って変換フォルトが発生した際に、フォルトアドレスとエラー内容を引数に既存のフォルトハンドラを呼び出すよう、そのまま移植しました。
変換フォルト以外のアボートは対応していません。
MonaOSのページフォルトの流れを簡単に示すと下図のようになると思います。
無効なページへのアクセスでページフォルトが発生しアセンブリで記述されたフォルトハンドラに制御が移り、以下の処理が行われます。
- 現スレッド構造体にコンテキストを保存
- フォルトアドレスとエラー内容をCPPで記述されたメインのフォルトハンドラの引数に設定
- フォルトハンドラ(メイン)を呼び出す
- フォルトしたアドレスに復帰する
MonaOSにおけるプロセスのメモリ空間は、セグメントとしてShared、Heap、Stackに区切られます。(それぞれに関しての詳細は追いきれてませんが、名称から推測してそのままの意味だと思います)
上述のフォルトハンドラから処理が移ったメインのフォルトハンドラでは、各セグメントに対するフォルト処理が行われます。
各々のセグメント処理を行う条件は以下の通りです。
- Shared
- エラー内容が無効なページへのアクセスによるものであり、かつ複数あるSharedセグメントにアクセスしたフォルトアドレスが含まれていた場合
- Heap
- Heapセグメントにアクセスしたフォルトアドレスが含まれていた場合
- Stack
- Stackセグメントにアクセスしたフォルトアドレスが含まれていた場合
上記に該当しなかった場合はそのプロセスを強制終了します。
■システムコール
x86においては、ユーザ定義のソフトウェア割り込みはINT命令を使用します。
命令のオペランドであるベクタNoは32〜255を使用することができ、MonaOSでは、お決まりの0x80番をシステムコールにアサインしています。
ARMでも同様の命令としてSWI命令がありそれを使用しますが、x86の場合とは若干勝手が異なります。
命令 | オペランドの扱い |
---|---|
INT | システムコールのベクタ番号であることを示す |
SWI | システムコールの種類(ID)であることを示す |
つまり、SWI命令によって直接システムコールのベクタに飛ぶので、ベクタ番号は命令にエンコードする必要がないのです。
SWI命令の32bit中、下位24bitが指定されたシステムコールIDになります。
SWI命令によって処理が移ったハンドラからシステムコールIDを取得するには、SWI命令が発生した(SWI命令がある)アドレス、つまり戻りアドレス-4のアドレスを取り出して下位24bitをマスクすれば取得できます。
従って、システムコールを発行する前段階のレジスタへの設定方法が異なります。
その対応は以下のようにしました。
用途 | MonaOS | Monarm |
---|---|---|
戻り値 | eax | r0 |
Call ID | ebx | なし |
引数1 | esi | r2 |
引数2 | ecx | r3 |
引数3 | edi | r4 |
引数4 | edx | r5 |
またシステムコールの仕組みは割り込みやフォルトの流れとほとんど一緒です。
システムコールに関しても処理の流れ自体に変更はありません。
プロセスロードの仕組み
Loaderの追加によってプロセスの生成が可能になりました。
Loaderからプロセスを生成する流れを簡単に追ってみます。
INITプロセスの処理(青)
新規にプロセスを生成する側のプロセス(INIT)の処理:
- 0x80000000からサイズ0x32000を共有メモリ(Shared segment)として設定する
- 新しいプロセスを生成する(コンテキストの生成)
- 生成したプロセスのメモリ空間の0xA0000000からサイズ0x32000を共有メモリとして設定する
- 共有メモリの識別IDを1.と同じ値にする
- 0x80000000にロードするプロセスのイメージをコピーする
- 0x80000000はマップされていないページのため、ページフォルトが発生する
- 0x80000000は共有メモリとして設定されているため、Shared segmentのフォルトハンドラで対処
- マッピングされていないため、新規に物理ページが割り当てられる
- ページングが行われて目的のイメージがメモリ上にコピーされる
Userプロセスの処理(紫)
起動される側のプロセス(User)の処理:
- 0xA0000000のコードをフェッチ(Userプログラムのエントリポイントは0xA0000000)
- 0xA0000000はマップされていないページのため、ページフォルトが発生する
- 0xA0000000は共有メモリとして設定されているため、Shared segmentのフォルトハンドラで対処
- ページングが行われてコードがフェッチ可能となる
といった感じだと思います。
INITプロセスの0x80000000とUserの0xA0000000が同一の物理メモリ領域に対応するのは、共有メモリの識別IDが同一であるためです。
お次は
自分で書いたソース(特にアセンブリ)をきれいにしつつ、各サーバプロセスの起動に移ろうと思います。
C/C++コメント抜き出し
C/C++のソースファイルからコメントアウトを除去するとか、xxコマンド(or スクリプト)ならone lineでできるよ、yyエディタならzz操作でできるよ、とかありそうですが、昔お世話になったlexで。
せっかくなので、メモしておきます。
%{ #include <stdio.h> static int cmt; %} %start CMT CMT2 %% <CMT>"*/" { fputs( cmt? yytext:" ", yyout ); BEGIN INITIAL; } <CMT>"\n" { fputs( yytext, yyout ); } <CMT>. { fputs( cmt? yytext:" ", yyout ); } <CMT2>"\n" { fputs( yytext, yyout ); BEGIN INITIAL; } <CMT2>. { fputs( cmt? yytext:" ", yyout ); } <INITIAL>"/*" { fputs( cmt? yytext:" ", yyout ); BEGIN CMT; } <INITIAL>"//" { fputs( cmt? yytext:" ", yyout ); BEGIN CMT2;} <INITIAL>"\t" { fputs( yytext, yyout ); } <INITIAL>"\n" { fputs( yytext, yyout ); } <INITIAL>. { fputs( cmt? " ":yytext, yyout ); } %% int yywrap(void) { return 1; } int main( int ac, char *av[]) { register int idx = 1; if ( ac < 2 ) { fprintf(stderr,"usage: %s [-c] FILE ", av[0]); return 1; } switch (getopt(ac, av, "c")) { case 'c': cmt = 1; idx++; break; default: break; } yyin = fopen(av[idx], "r"); if ( NULL == yyin ) { fprintf(stderr,"Can't open \"%s\" "., av[idx]); return 1; } yyout = stdout; yylex(); return fclose(yyin); }
-c オプション指定の場合はコメントアウト文のみ抽出するようにしてみました。
ルール部はもう少し考えればもっとカッコよくできるのかな。
割り込み処理〜コンテキストスイッチ
かなり簡単な方法ですが、コンテキストスイッチに成功しました。(リビジョン6)
深い部分に手を入れたため、知識不足による大いなる勘違いや、確認方法の間違いなどで根本的におかしいかもしれません…。
今回移植したモジュールは、
- KObject
- Process
- Scheduler
です。これらのモジュールに依存するコードは無効にしていましたが、今回の移植によって可能な限り有効にしました。
但し、必要最小限のコンテキスト切替の確認に重点を置いたため、今回も一部不完全で以下のようになっています。
- ThreadOperation::switchThread関数(Process.cpp)
- プロセスコンテナの変更の判断、及びKernel/Userプロセスの判断は無効。
- 常にKernelスレッド同士でコンテキストスイッチ(arch_switch_thread1)。
- プロセスコンテナの変更の判断、及びKernel/Userプロセスの判断は無効。
- Scheduler::SetNextThread関数(Scheduler.cpp)
- runqからの次スレッドの線形探索を無効。
- 2つのスレッドを1秒毎に交互に切り替わるように。
- runqからの次スレッドの線形探索を無効。
- irqHandler_0関数(タイマ割り込みハンドラ:ihandlres.cpp)
- Scheduleを無効。
- 無条件にSetNextThreadを実行するように。
- Scheduleを無効。
- startKernel関数(kernel.cpp)
- 引き続きMutex作成を無効。
- syscall/Mutexの移植が未実施のため。
- 引き続きMutex作成を無効。
移植概要
オリジナルの割り込み発生からハンドラ処理までの大まかな流れは以下の様になっています。
- 予め行うIDTの初期化で、handlers配列をIDTRに登録
- handlersはhandler.asmに記述された割り込み・例外ハンドラのテーブル
- irq発生でvector番号に対応したhandlers配列のハンドラに飛ぶ
- アセンブリからCのハンドラ本体(handlers.cpp)を呼ぶ
これに対して、基本的な流れはそのままに以下の様な構成としました。
- vectorテーブルのirqのオフセットに割り込みハンドラへのブランチ命令を記述(astart.S)
- irq発生でvectorテーブルの当該のハンドラに飛ぶ
- アセンブリからCの割り込み処理共通関数(handlers.cpp)を呼ぶ
- 割り込みに対応したハンドラを実行
x86とは異なり、ARMではirqのvectorが1本に丸められるので、共通の割り込み処理で発生した割り込みソースを確認した後、対応したハンドラ本体を実行します。
また、速度面からcommon_isrは廃止予定で、オリジナル同様に割り込み要因番号に対応したハンドラテーブルを準備して、アセンブリから直接飛べるようにしたいと思います。
コンテキストスイッチ概要
まずはオリジナルです。インターバルタイマの割り込みが発生して(必要であれば)コンテキスト切り替えが行われるまでの流れを追ってみます。
割り込みが発生するとDPL>=CPL*2となって、予めTSSに登録しておいたスタックに切り替わります。そしてハンドラにてスタック上にコンテキストを保存した後、g_CurrentThreadが指し示すThread構造体に格納します。
その後、必要であればコンテキストスイッチが発生して、g_CurrentThreadが切り替わり、その先のコンテキストをCPUとスタック上に復元して割り込み処理から復帰します。
上記はコンテキストスイッチに的を絞って処理を追っているため、実際にはCPLの確認や、切り替え先のCPLの違いによるコンテキストの復元の仕方が若干異なる部分は省略しています。
上記の流れを以下のように移植しました。
まず、ARMでは計7つものCPUモードがあります。
モード | 用途 | 特権 | 例外 |
---|---|---|---|
usr | ユーザ向け | × | × |
sys | OS向け | ○ | × |
svc | ソフトウェア割り込み発生時 | ○ | ○ |
abt | プリフェッチアボートまたはデータアボート発生時 | ○ | ○ |
udf | 未定義命令実行時 | ○ | ○ |
irq | irq発生時 | ○ | ○ |
fiq | fiq発生時 | ○ | ○ |
CPUモードはcpsrのbit0:4で表現され、例外モードは当該の例外が発生した際にCPUがcpsrを変更します。
特権モードであればcpsr(ステータスレジスタ)の設定によって各モードを自由に行き来する事が可能です。
また例外モードでは、それぞれに独立した以下のレジスタ(バンクレジスタ)が設けられています。*3
レジスタ | バンク | 使われ方 |
---|---|---|
r0〜r12 | fiqモードのみr8〜r12がバンクされる | abiによってまちまち |
r13 | ○ | sp |
r14 | ○ | リンクレジスタ(lr)。サブルーチンコール呼出時、pc+4がCPUによってセット |
r15 | - | pc |
cpsr | - | ステータスレジスタ |
spsr | ○ | 例外モードのみ。例外発生時のcpsrの値が格納される |
lrは例外時、例外が発生したPC+4が格納されます*4。
バンクレジスタは例外発生時に、CPUが自動的に切り替えてくれる寸法です。
但し、spは前回そのモードであったときの値が復元されるので、予め初期化しておく必要があります。
従って、予め明示的に例外モードになってバンクレジスタを設定しておけば例外発生時に切り替わります。
spにおいてはまさに、x86のTSSのようなメカニズムです。
CPUモードとバンクレジスタを踏まえて、上図のレジスタをスタックに積む処理(astart.S)を細かく補足すると、
- irq発生(CPUモードがirqモードに)
- r0からr5をスタック(irqモード)に退避(r0-r5を一時的に利用するため)
- spの位置は変更しない
- r3にr14から4を引いた値(割り込み発生時のpc)を格納
- r4にspcr(割り込み発生時のcpsr)を格納
- r5に(irqモードでバンクされた)spを格納
- 現在のプロセッサモードを割り込みが発生時のモード(4.のspcr)に変更
- r1にspを保存(割り込み発生時のsp)
- r2にlrを保存(割り込み発生時のlr)
- 現在のプロセッサモードをsvcモードに変更
- r1からr4までをスタック(svc)に保存
- (irqモードのspである5.で取得した)r5の内容のアドレスから、5レジスタ分(1.で退避した)をr0からr5に復帰
- スタック(svc)に割り込み発生時のバンク以外のレジスタr0からr12までを退避
- コンテキスト保存処理へ
という手順になります。
動作確認
冒頭でも記載したとおり、必要最小限のコンテキスト切替までの移植としています。
具体的には、
- 2つの特権モード(sysモード)のスレッドを生成
- 1つ目のスレッドは1秒毎にメッセージ表示。その間はビジーループ。(既存のmainProcessを修正)
- 2つ目のスレッドは3秒毎にメッセージ表示。その間はビジーループ。(既存のmonaIdleを修正)
- 1秒毎に交互にコンテキストスイッチ
として、確認しました。
ただ、一点問題があり、本当のビジーループだとコンテキストスイッチしないため、ループ中にシリアル出力(文字は無し)を行っています*5。原因解析中…。
実行すると2つのスレッドが切り替わるのが確認できます。
% ./runqemu Mona version.0.3.0Alpha9 on ARM920T(S3C2440A/Bishop) $Date:: 2008-04-29 11:35:00 +0900#$ Setting PIC [OK] System Total Memory 64[MB]. Paging on mainProcess[0] mainProcess[1] monaIdle[0] mainProcess[2] mainProcess[3] mainProcess[4] mainProcess[5] monaIdle[1] mainProcess[6] mainProcess[7] monaIdle[2] mainProcess[8] mainProcess[9] mainProcess[10]
気になった点
オリジナルでは、コンテキストスイッチが発生する際、iretはコンテキスト復元処理の仕上げに行っています(場所はarch_switch_threadx)。
移植もこれに沿っていますが、個人的には、iretは割り込みハンドラからの復帰で一本化し、iretの点在は避けるべきと考えます…。
x86に詳しくないので背景が不明ですが、そうせざるを得ない理由が無い限り、Monarmでは、割り込み復帰でコンテキストが切り替わるように変更したいと思います。
長文で
大げさにしていますが、あまり大幅に進んでいないような…
次は、Process/Schedulerを仕上げたいと思います。