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 固定としています。

Cortex-M3

多くの制約があり枚挙に暇がないですが、例として例外関連では、

  • 後着
  • テール・チェーン
  • 優先度グループ

は未対応です。

おわりに

実機の代替にはなりませんが、TOPPERS/ASP for LPC1768 のちょっとした確認に使えるのではと思います。
尚、ソースコードは下記のリンク先の github 上に公開しています。

8/23 追記

リポジトリ名を変更しました。すでにcloneしている場合は、.git/config を修正してください。
旧: qemu-cortexm3
新: qemu-cortex-m3

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 = {
        &eth_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 にてログイン(パスワードなし)が可能です。

github

ソースと詳しいドキュメントは github 上で公開しています

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の移植
  • アボート例外の移植
    • astart.S(旧ihandler.asm)
  • Loader.cppの追加


以上から、前回から以下の事柄が可能になりました:

  • Loaderからプロセスを起動できるようになった
  • プロセス空間が切り替わるようになった
  • スレッドが規定のポリシーでスケジューリングされるようになった
  • デマンドページングが可能になった


これで積み残しの部分はほぼ解消しましたが、まだDMAページの設定が無効のままになっています。これに関してはデバイスドライバ移植までとっておきたいと思います。*1
また、細かい点では以下の修正・変更があります:

  • Kernelメモリを6MBに戻した(0x20000〜0x80000)
    • 常駐するカーネルページは8MBでオリジナルと同様
    • 当初64MBにしたのはカーネルメモリ内で物理ページをやりくりすると勘違いしていたため。実際はカーネルページ以降のページが割り当てられる。
  • システムメモリを64MBに(プラットフォーム上は128MB)
  • ページテーブルディスクリプタをファインページからコアースページに変更
    • テーブルエントリが1024→256に
    • 1ページテーブルあたりの仮想メモリ範囲は1MBで同様なので、同一アドレスの4回重複が無くなった*2
  • 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

またシステムコールの仕組みは割り込みやフォルトの流れとほとんど一緒です。

  • 現スレッド構造体にコンテキストを保存
  • システムコール本体を呼ぶ
  • 現スレッド構造体のメンバであるeaxの値をeaxレジスタに保存
  • 復帰

システムコールに関しても処理の流れ自体に変更はありません。

プロセスロードの仕組み

Loaderの追加によってプロセスの生成が可能になりました。
Loaderからプロセスを生成する流れを簡単に追ってみます。

INITプロセスの処理(青)

新規にプロセスを生成する側のプロセス(INIT)の処理:

  1. 0x80000000からサイズ0x32000を共有メモリ(Shared segment)として設定する
  2. 新しいプロセスを生成する(コンテキストの生成)
  3. 生成したプロセスのメモリ空間の0xA0000000からサイズ0x32000を共有メモリとして設定する
    • 共有メモリの識別IDを1.と同じ値にする
  4. 0x80000000にロードするプロセスのイメージをコピーする
  5. 0x80000000はマップされていないページのため、ページフォルトが発生する
    • 0x80000000は共有メモリとして設定されているため、Shared segmentのフォルトハンドラで対処
    • マッピングされていないため、新規に物理ページが割り当てられる
  6. ページングが行われて目的のイメージがメモリ上にコピーされる
Userプロセスの処理(紫)

起動される側のプロセス(User)の処理:

  1. 0xA0000000のコードをフェッチ(Userプログラムのエントリポイントは0xA0000000)
  2. 0xA0000000はマップされていないページのため、ページフォルトが発生する
    • 0xA0000000は共有メモリとして設定されているため、Shared segmentのフォルトハンドラで対処
  3. ページングが行われてコードがフェッチ可能となる

といった感じだと思います。
INITプロセスの0x80000000とUserの0xA0000000が同一の物理メモリ領域に対応するのは、共有メモリの識別IDが同一であるためです。

お次は

自分で書いたソース(特にアセンブリ)をきれいにしつつ、各サーバプロセスの起動に移ろうと思います。

*1:それ以外に忘れものがあるかも・・・

*2:はじめからそうしていれば

*3:優先度順、VMSAv6以降はもう少し種類がある

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プロセスの判断は無効。
  • Scheduler::SetNextThread関数(Scheduler.cpp)
    • runqからの次スレッドの線形探索を無効。
      • 2つのスレッドを1秒毎に交互に切り替わるように。
  • irqHandler_0関数(タイマ割り込みハンドラ:ihandlres.cpp)
    • Scheduleを無効。
      • 無条件にSetNextThreadを実行するように。
  • startKernel関数(kernel.cpp)
    • 引き続きMutex作成を無効。
      • syscall/Mutexの移植が未実施のため。

移植概要

オリジナルの割り込み発生からハンドラ処理までの大まかな流れは以下の様になっています。

  • 予め行うIDTの初期化で、handlers配列をIDTRに登録
    • handlersはhandler.asmに記述された割り込み・例外ハンドラのテーブル
  • irq発生でvector番号に対応したhandlers配列のハンドラに飛ぶ
  • アセンブリからCのハンドラ本体(handlers.cpp)を呼ぶ

これに対して、基本的な流れはそのままに以下の様な構成としました。

  • vectorテーブルのirqのオフセットに割り込みハンドラへのブランチ命令を記述(astart.S)
  • irq発生でvectorテーブルの当該のハンドラに飛ぶ
  • アセンブリからCの割り込み処理共通関数(handlers.cpp)を呼ぶ
  • 割り込みに対応したハンドラを実行

x86とは異なり、ARMではirqvectorが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)を細かく補足すると、

  1. irq発生(CPUモードがirqモードに)
  2. r0からr5をスタック(irqモード)に退避(r0-r5を一時的に利用するため)
    • spの位置は変更しない
  3. r3にr14から4を引いた値(割り込み発生時のpc)を格納
  4. r4にspcr(割り込み発生時のcpsr)を格納
  5. r5に(irqモードでバンクされた)spを格納
  6. 現在のプロセッサモードを割り込みが発生時のモード(4.のspcr)に変更
  7. r1にspを保存(割り込み発生時のsp)
  8. r2にlrを保存(割り込み発生時のlr)
  9. 現在のプロセッサモードをsvcモードに変更
  10. r1からr4までをスタック(svc)に保存
  11. (irqモードのspである5.で取得した)r5の内容のアドレスから、5レジスタ分(1.で退避した)をr0からr5に復帰
  12. スタック(svc)に割り込み発生時のバンク以外のレジスタr0からr12までを退避
  13. コンテキスト保存処理へ

という手順になります。

動作確認

冒頭でも記載したとおり、必要最小限のコンテキスト切替までの移植としています。
具体的には、

  • 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を仕上げたいと思います。

*1:または0xFFFF0000に設定可能

*2:値ではなく意味として

*3:逆に、例外以外のモードはバンクされない

*4:プリフェッチアドレス

*5:nop4発でもNG