QEMUでARMの未定義命令例外

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です。各テーブルは４バイトです。大抵はハンドラへのジャンプ命令を書きます。
ベクタテーブルには合計８つの例外をサポートします。

実際のコードです。

```
.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を退避します。
次に、下位５ビットを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の５乗の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に変更できました。