x86のデバッグレジスタを使ってみたらQEMUが固まる

# はじめに

江添さんのブログ「[OpenBSD、1985年に追加されたIntelの最新の誇大広告された機能を使わないことにより脆弱性を華麗に回避](https://cpplover.blogspot.jp/2018/05/openbsd1985intel.html)」で、x86にデバッグレジスタというものがあることを今更知りました。それを使って見ようとQEMUにFreeDOS入れて触ってみたらQEMUが固まった、という話です。


# デバッグレジスタとは

デバッグする時に、プログラムの指定した場所に来たら動作を止めたり(ブレークポイント)、メモリの監視をして、ある場所の値の読み書きを検知(ウォッチポイント)したい時がよくあります。これをハードウェア的に支援するのがX86のデバッグレジスタです。デバッグレジスタは`DR0`から`DR7`まで8本ありますが、`DR4`と`DR5`は使いません。`DR0`から`DR3`の4本は、監視対象のアドレスを入れます。`DR7`はデバッグコントロールレジスタと呼ばれ、どのような条件で止めるかを指定します。`DR6`はデバッグステータスレジスタで、どのブレークポイントで止まったか等が指定されます。

これらのレジスタへの読み書きは特権命令となっており、通常のOSでは読み書きが許されません。ちょっとやってみましょう。デバッグレジスタ`DR7`に`

```test.cpp
int
main(void) {
  __asm__("movq %dr7, %rax");
}
```

これをMac上のg++でコンパイル、実行してみましょう。

```shell-session
$ g++ test.cpp
$ ./a.out
zsh: segmentation fault  ./a.out
```

つれないお返事です。

ついでに、Windows環境のDEBUGでやってみま・・・あれ？DEBUGが無い！

DEBUGとはMSDOS時代から連綿とサポートされて来た、Microsoft純正のデバッガですが、なんと[Windows7から搭載されなくなった](https://srad.jp/story/09/05/13/1516240/)そうです。わりと便利なのに残念。まぁどうせ、DEBUGで実行したとしてもSIGSEGVかSIGILLが出るだけでしょうが。

# QEMU+FreeDOSの環境構築

現代のOSを使っていると、ユーザから直接特権命令を触ることはできません。触れなくもないですが、かなり面倒なことになります。

「俺も特権命令を使ってみたい！でも面倒なことはしたくない！」

そういう場合は、リングプロテクションの無いOSを使うのが最も簡単です。具体的にはDOSを使います。QEMUというプロセッサエミュレータを使い、FreeDOSというMS-DOS互換のフリーDOS実装を使って遊んでみましょう。

## QEMUのインストール

brewで一発です。

```shell-session
$ brew instal qemu
```

## FreeDOSのダウンロード

[FreeDOS本家](http://www.freedos.org/)からISOイメージを取ってきます。「CDROM “standard” installer」を落とすのが良いでしょう。本稿執筆時点のバージョンは1.2です。

## ディスクイメージの作成

FreeDOSをインストールするハードディスクのイメージを作りましょう。qemu-imgを使います。サイズは200MBもあれば良いと思います。

```shell-session
$ qemu-img create -f raw freedos.img 200M 
```

## FreeDOSのインストール

ダウンロードしたISOイメージと先程作成したディスクイメージを使ってQEMUを起動します。

```shell-session
$ qemu-system-i386 freedos.img -cdrom FD12CD.iso -boot d
```

これは、「i386の石をエミュレートし、freedos.imgをハードディスクイメージ(Cドライブ)とし、FD12CD.isoをCD-ROM(Dドライブ)に入れた状態で、Dドライブから起動しなさい(`-boot d`)」という意味です。

あとは[本家サイトの説明](http://wiki.freedos.org/install/)に従ってインストールするだけです。基本的に「Y」とか「1」とか「ESC」とか押すだけです。

インストールの終了後は

```shell-session
$ qemu-system-i386 freedos.img 
```

でFreeDOSが実行できます。起動後、どういうオプションで起動するか1〜4で聞かれますが、なんでも良いです。なにもドライバを読み込まない4で起動するといろいろ安定しているという噂ですが、僕の環境では1で問題が起きたことはありません。起動するとプロンプトが出てきます。

![image.png](https://qiita-image-store.s3.amazonaws.com/0/79744/56920f87-bf3e-54df-b67b-2601c05a0d5c.png)

おおー、昔懐かしいDOS画面！

# DEBUGの使い方

さて、早速DEBUGコマンドを使ってみましょう。コマンドプロンプトでdebugと打ち込むとDEBUG.COMが起動します。

```
C:\> debug
-
```

この「-」がDEBUGのプロンプトです。例えば現在のレジスタの状態を表示してみましょうか。「r」　と打ち込んでみます(大文字小文字は区別されません)。

![image.png](https://qiita-image-store.s3.amazonaws.com/0/79744/fd54448e-e0d6-5b83-a2c3-ac3439eccf46.png)

AXやらBXが全て0になっていますね。コードセグメントは083Fです。
Rの後ろに特定のレジスタを指定すると、そのレジスタの中身が表示されます。これで例えば`eax`のような32bitレジスタの値も表示できます。

さて、何かコードを書いてみましょう。例えばAXに1を代入してみましょうか。プログラムを書くのは「a」です。通常、オフセットアドレス100から書き込みますので「a 100」とします。

```
- a 100
083F:0100 
```

と、コードセグメント083F、オフセットアドレス0100のところに何か書けるようになりました。`mov ax,1`[^1]と入力し、改行を二回入れてみましょう。

[^1]: おそらく普段つかうアセンブリと代入の順番が逆なのに注意。

```
- a 100
083F:0100 mov ax,1
083F:0103
-
```

さて、プログラムが入力されたかどうか、ダンプしてみましょう。ダンプコマンドは「u」です。100から102までダンプしてみます。

```
- u 100 102
083F:0100 B80100 MOV AX,0001
-
```

ちゃんと入力されたようです。

次は実行してみましょう。オフセットアドレス100から103まで実行します。コマンドは「g=100 103」です。スタートアドレスは「=」で指定します。キーボードが正しく設定されていないと「=」が見つからないので注意しましょう。うちのMacの場合は「~」のキーに「=」がいました。

![image.png](https://qiita-image-store.s3.amazonaws.com/0/79744/db3a4031-e932-4e37-eaa0-35b1c1cb6cbb.png)

正しく`MOV AX,1`が実行され、`AX`に1が代入されました。

では、いよいよデバッグレジスタを使ってみましょう。`DR7`の`EAX`の値に読み出し、その値を表示させます。

![image.png](https://qiita-image-store.s3.amazonaws.com/0/79744/aba7ac50-46c1-2caa-c1d7-612167992879.png)

`EAX`の値として「00000400」が入りました。これが`DR7`の持つデフォルトの値です。特権命令である、「デバッグレジスタへの読み書き」がちゃんとできました。これで我々も特権階級の仲間入りです。

# デバッグレジスタの使用

**注意** 少なくともうちの環境では、以下のコードを実行するとQEMUが固まり、Macも操作不能になります。実行は自己責任でお願いします。

デバッグレジスタによりブレークポイントをいれるためには

* コントロールレジスタ`CR4`の当該フラグを立てる
* デバッグレジスタ`DR0`に、止めたい場所の**リニアアドレス**を代入する
* デバッグコントロールレジスタ`DR7`に、`DR0`で指定されたアドレスに来たら止めることを指示する

という作業をする必要があります。

## コントロールレジスタのデバッグフラグを立てる

x86には制御レジスタ(Control Register)として`CR0`から`CR4`が存在します。このうち、CPUの拡張機能の利用を指示するのが`CR4`です。それぞれのビットが何を意味するのかは、例えば[OS Project Wiki](http://www.wiki.os-project.jp/?x86%2FRegister#kdc81e7f)を参照していただくことにして、とりあえずビットの3番(0スタートなのに注意)がDebug Extensionです。これを有効にしましょう。

```nasm
mov eax,cr4
bts eax,3
mov cr4,eax
```

`CR4`の値を`EAX`に読み込んで、3ビット目を立てて、その値を`CR4`に返しています。これでDebug Extensionが有効になります。

## デバッグレジスタに止めたい場所のリニアアドレスをいれる

今回はとりあえず`DR0`を使うことにしましょう。ただし、コードを入力している段階では止めたい場所のアドレスがわからないため、とりあえず0を代入して、後で書き直すことにします。

```nasm
mov eax,0
mov dr0,eax
```
## デバッグコントロールレジスタにフラグをセット

デバッグレジスタの7番、`DR7`は、デバッグコントロールレジスタです。どのビットが何を意味するのかは、例えば[osdev.orgのWiki](https://wiki.osdev.org/CPU_Registers_x86#DR7)を参照していただくことにして、とりあえず0番目のビットを立てれば、`DR0`が示すアドレスの命令が実行されるタイミングで止まります。

```nasm
mov eax,dr7
bts eax,0
mov dr7,eax
```

`DR7`の値を`EAX`に代入し、0ビット目を立ててから`DR7`に書き戻しているだけです。`DR7`には、4つのデバッグレジスタごとに止める条件を「メモリの読み書きにする」もしくは「そのアドレスを実行しようとした時」を選ぶビットがありますが、デフォルト(00)では「そのアドレスの命令を実行しようとしたとき」が条件となります。

## リニアアドレスの計算

ここまでの内容を入力してみましょう。ついでに`nop`をいくつか入れて、最後に`int 3`を入れておきます。`int 3`まで来るとプログラムが止まるため、いちいち実行アドレスの終了アドレスを指定しなくて良いので便利です。

入力した画面がこちらです。

![image.png](https://qiita-image-store.s3.amazonaws.com/0/79744/23e623d9-f3a9-8dfd-4b7c-992fbb3243e4.png)

さて、このオフセットアドレス0120のnopにブレークポイントを置くことにしましょう。いま、セグメンテーションによるアドレスが表示されています。この「0B12:0120」のアドレスをリニアアドレスに変換しなければいけません。このうち「0B12」がセグメントアドレス、「0120」がオフセットアドレスで、セグメントアドレスは環境や実行状況により異なります。

リニアアドレスの計算はいろいろ面倒で、僕もきちんと理解している自信が無いのですが、とりあえず今回はセグメントを16倍してオフセットに足せばOKです。

```rb
(0x0b120+0x0120).to_s(16)
=> "b240"
```

というわけで、先程仮に0を代入していたところにb240を代入しましょう。オフセット10Bのところを書き直します。

```
-a 10B
0B12:010B mov eax,b240 (改行)
0B12:0111 (改行)
-
```

これでプログラムが完成しました。表示させて見ましょう。

![image.png](https://qiita-image-store.s3.amazonaws.com/0/79744/bfde8280-e755-39e5-0c1b-34c5d1c4d65e.png)

これが、セグメントアドレス「0B12」において、オフセットアドレス「0120」にブレークポイントを置いたコード・・・のはずです。

「はず」というのは、これをそのまま実行すると、QEMUがホストのMacごと固まるからです。

もし、Macで以下を実行しようとする人がいる場合は、そのMacに別のマシンからSSHでリモートログインしておいてください。

```
- g=100  # オフセットアドレス0100から実行　(固まる)
```

実行後、全く操作できなくなりますが、リモートログインしたマシンから

```shell-session
$ ps aux |grep qemu
$ kill -KILL (番号)
```

でqemuのプロセス番号を調べて殺せば復活できます。

# まとめ

「QEMUでデバッグレジスタを使って特権階級の仲間入りだぜ」とかやろうとしたらQEMUが固まりました。僕が何か間違ってるのか、それともQEMUが何かおかしいのかはわかりません。だれか詳しい方のフォローをお待ちしております。

どうでもいいですが、中学〜高校生の頃、仲間とMS-DOSでゲームを作って遊んでいました。当時、「アセンブラ[^2]を使えない」というのがわりとコンプレックスでした。当時のマシンは非力で、ライブラリも整備されていなかったため、アセンブラを使わないと画像表示などは遅くて使い物になりません。僕が使っていたのはQuick Basic、そしてC言語に移行しましたが、アセンブラは使えなかったので、他の仲間がアセンブラでバリバリゲームエンジンを組んでいました。何度かアセンブラに挑戦しましたが、VRAMにちっちゃい線を描画するところまでで挫折しました。

[^2]: たまに「アセンブラ」か「アセンブリ」か、という話題になります。言語としては「アセンブリ」、アセンブルするのが「アセンブラ」というのが一般的ですが、たとえばIBMなんかは「アセンブラ言語」と呼んでおり、どちらでもかまわないようです。僕は今は「アセンブリ言語」と呼びますが、当時は言語のことを「アセンブラ」と呼んでいたので、本稿では一貫性のない使い方になっています。

アセンブリを「読む」ようになったのは修士の頃で、DEC Alphaのマシンを与えられてからです。C/C++でプログラムを組んだのですが、どうも性能が思ったよりよかったり悪かったりしたのでアセンブリを出力して見ると、コンパイラがそれはそれは頑張って最適化しているのを見て感心したのを覚えています。以来、コンパイラの最適化能力に興味を持ち、アセンブリを読むようになりました。アセンブリを「書く」ようになったのはつい最近です。気がつくと当時中1だった仲間がバリバリアセンブリを書いているのを羨ましそうに見ていた時から、もう○○年も過ぎていました。

こうしてエミュレータでDOS動かしてDEBUGを使っていると、当時を思い出してなんとも言えない気分になります・・・

# 参考

* [OS Project Wiki](http://www.wiki.os-project.jp)
* [OSDev Wiki](https://wiki.osdev.org/)
* [［スクリプト言語しか書けないあなたへ］ FreeDOSとdebugコマンドで8086アセンブラ入門](https://qiita.com/usk83/items/c97066c3c663c5007658)