QEMUでARMの未定義命令例外の処理について調べてみました。
普通にIRQの割り込みを使いたいだけなんですけど、うまくいかないのでIRQよりは簡単な未定義命令例外の動作を確認してみました。
未定義命令例外とは
ARMでは未定義命令を検出すると例外が発生します。この未定義命令例外をつかうと未対応の命令をソフトでエミュレートすることなどができます。
未定義命令例外の動作例
アセンブラで、0xff、0xff、0xff、0xffを配置する。この 0xff, 0xff, 0xff, 0xffは未定義命令なので未定義命令例外が発生する。
// occur undefined exception
.byte 0xFF, 0xFF, 0xFF, 0xFF
これを QEMUで動かしてトレースすると、0xff、0xff、0xff、0xffを検出した直後に、未定義例外のベクター 0x00000004に飛んで命令を実行する。
0x000080a4: e1540009 cmp r4, sb
0x000080a8: 3afffffc blo #0x80a0
0x000080ac: ffffffff .byte 0xff, 0xff, 0xff, 0xff
0x00000004: e59ff018 ldr pc, [pc, #0x18]
未定義命令例外を処理する。
未定義命令例外を処理するのに必要なこと
- ベクターテーブルを配置
- 未定義命令モードのスタックポインタを設定
- 例外ハンドラを用意する。
ベクターテーブルを配置
ARMは歴史的経緯で、ベクタテーブルのデフォルトの配置先は0x0です。各テーブルは4バイトです。大抵はハンドラへのジャンプ命令を書きます。
ベクタテーブルには合計8つの例外をサポートします。
実際のコードです。
.globl _start
_start:
ldr pc, reset_handler
ldr pc, undefined_handler
ldr pc, swi_handler
ldr pc, prefetch_handler
ldr pc, data_handler
ldr pc, unused_handler
ldr pc, irq_handler
ldr pc, fiq_handler
reset_handler: .word reset
undefined_handler: .word undef
swi_handler: .word io_halt
prefetch_handler: .word io_halt
data_handler: .word io_halt
unused_handler: .word io_halt
irq_handler: .word io_halt
fiq_handler: .word io_halt
reset:
// copy vector 8000 to 0.
mov r0, #0x8000
mov r1, #0x0000
ldmia r0!,{r2,r3,r4,r5,r6,r7,r8,r9}
stmia r1!,{r2,r3,r4,r5,r6,r7,r8,r9}
ldmia r0!,{r2,r3,r4,r5,r6,r7,r8,r9}
stmia r1!,{r2,r3,r4,r5,r6,r7,r8,r9}
この例では _startを0x8000にしています。 (Raspberry Piのブートローダの仕様のためです。)
8000番地から命令を実行し、まず、resetにジャンプし、0x8000-0x801Fにあるベクタテーブルを0x0000-0x001Fにコピーします。
未定義命令モードのスタックポインタを設定
ARMでは例外が発生すると、例外に応じたプロセッサモードに自動的に遷移します。そのときプロセッサモードに応じたスタックポインタ SPに自動的に切り替わります。
未定義命令例外が発生したら、プロセッサモードが未定義例外モードになります。
未定義命令例外発生時にスタックを使う必要があるなら、事前にスタックポインタSPを設定します。
コード例です。
// save cpsr
mrs r0, cpsr
// setup sp in undef mode
bic r1, r0, #0x1f
orr r1, r1, #0x1b
msr cpsr_c,r1
mov sp,#0x4000
// restore cpsr
msr cpsr_c, r0
cpsrの下位5ビットにプロセッサモードを設定すると、そのプロセッサモードに遷移します。
なので、まず、r0にcpsrを退避します。
次に、下位5ビットを0クリアして、未定義命令モード 0x1bを設定し、cpsr_cにwriteします。
こすうると、未定義命令モードに入ったので、SPに0x4000を設定しています。
後片付けとして、r0をcpsr_cに戻して、元のプロセッサモードに戻しときます。
例外ハンドラの作成
例外ハンドラの例です。
undef:
push {r0,r1,r2,r3,r4,r5,r6,r7,r8,r9,r10,r11,r12,lr}
bl c_undef_handler
pop {r0,r1,r2,r3,r4,r5,r6,r7,r8,r9,r10,r11,r12,lr}
movs pc, lr
r0からr12とlrをスタックに退避します。
C言語のハンドラを呼びます。
r0からr12とlrをスタックから戻します。
未定義命令例外の場合は、例外発生時にlrに次のPCが格納されているので、lrの値をpcに代入すると、次の命令に復帰します。
トレース
起動してから、未定義命令例外ハンドラ処理までのトレースです。
$ qemu-system-arm -M raspi2 -m 128 -serial mon:stdio -nographic -kernel kernel.elf -d in_asm
----------------
IN:
0x00008000: e59ff018 ldr pc, [pc, #0x18]
----------------
IN:
0x00008040: ee101fb0 mrc p15, #0, r1, c0, c0, #5
0x00008044: e2011003 and r1, r1, #3
0x00008048: e3510002 cmp r1, #2
0x0000804c: 1a00001d bne #0x80c8
----------------
IN:
0x000080c8: e320f002 wfe
0x000080cc: eafffffd b #0x80c8
----------------
IN:
0x00008050: e3a00902 mov r0, #0x8000
0x00008054: e3a01000 mov r1, #0
0x00008058: e8b003fc ldm r0!, {r2, r3, r4, r5, r6, r7, r8, sb}
0x0000805c: e8a103fc stm r1!, {r2, r3, r4, r5, r6, r7, r8, sb}
0x00008060: e8b003fc ldm r0!, {r2, r3, r4, r5, r6, r7, r8, sb}
0x00008064: e8a103fc stm r1!, {r2, r3, r4, r5, r6, r7, r8, sb}
0x00008068: e10f0000 mrs r0, apsr
0x0000806c: e3c0101f bic r1, r0, #0x1f
0x00008070: e381101b orr r1, r1, #0x1b
0x00008074: e121f001 msr cpsr_c, r1
----------------
IN:
0x00008078: e3a0d901 mov sp, #0x4000
0x0000807c: e121f000 msr cpsr_c, r0
----------------
IN:
0x00008080: e3a0d902 mov sp, #0x8000
0x00008084: e59f4044 ldr r4, [pc, #0x44]
0x00008088: e59f9044 ldr sb, [pc, #0x44]
0x0000808c: e3a05000 mov r5, #0
0x00008090: e3a06000 mov r6, #0
0x00008094: e3a07000 mov r7, #0
0x00008098: e3a08000 mov r8, #0
0x0000809c: ea000000 b #0x80a4
----------------
IN:
0x000080a4: e1540009 cmp r4, sb
0x000080a8: 3afffffc blo #0x80a0
----------------
IN:
0x000080ac: ffffffff .byte 0xff, 0xff, 0xff, 0xff
----------------
IN:
0x00000004: e59ff018 ldr pc, [pc, #0x18]
----------------
IN:
0x000080b8: e92d5fff push {r0, r1, r2, r3, r4, r5, r6, r7, r8, sb, sl, fp, ip, lr}
0x000080bc: eb00001a bl #0x812c
----------------
IN: c_undef_handler
0x0000812c: e12fff1e bx lr
----------------
IN:
0x000080c0: e8bd5fff pop {r0, r1, r2, r3, r4, r5, r6, r7, r8, sb, sl, fp, ip, lr}
0x000080c4: e1b0f00e movs pc, lr
----------------
IN:
0x000080b0: e59f3020 ldr r3, [pc, #0x20]
0x000080b4: e12fff33 blx r3
----------------
IN: kernel_main
0x00008120: e92d4010 push {r4, lr}
0x00008124: ebffffe7 bl #0x80c8
QEMU: Terminated
VBARを使って、ベクタテーブルの場所を指定
ARMv7-Aでは、VBAR (Vector Base Addess Register)を使ってみました。
VBARを使う場合、ベクターテーブルは32バイトアラインの場所に配置する仕様です。
VBARに設定してみました。
// set VBAR
ldr r0, =vector
mcr P15, 0, r0, c12, c0, 0
中略
.align 5
vector:
ldr pc, reset_handler
ldr pc, undefined_handler
ldr pc, swi_handler
ldr pc, prefetch_handler
ldr pc, data_handler
ldr pc, unused_handler
ldr pc, irq_handler
ldr pc, fiq_handler
reset_handler: .word reset
undefined_handler: .word undef
swi_handler: .word io_halt
prefetch_handler: .word io_halt
data_handler: .word io_halt
unused_handler: .word io_halt
irq_handler: .word io_halt
fiq_handler: .word io_halt
.alignが5だと、2の5乗の32バイトにアラインする指定です。
r0に設定したいアドレスを書き込み。
VBARはコプロセッサCP15にあるので、ARMv7-Aのアーキテクチャリファレンスマニュアルに記載のパラメータでmcr命令を使います。
r0の内容をVBARに書き込みます。
動作を試してみましたので、そのトレースです。
IN:
0x0000805c: ffffffff .byte 0xff, 0xff, 0xff, 0xff
----------------
IN:
0x000080a4: e59ff018 ldr pc, [pc, #0x18]
未定義命令例外のベクタが、0x000080a4に変更できました。