本書では、C言語やアセンブラをコンパイル/アセンブルした実行バイナリを調べることで、機械語における各命令の書き方(構文)を一つ一つ理解します。そして、理解した構文を使って小さな機械語プログラム(=実行バイナリ)を書いて実行してみます。
流れをまとめると以下の通りです。
この章では以降、上記の流れを、環境構築を行いながら簡単なプログラムを使って紹介します。
本書では、実行バイナリから機械語構文を確認する際、「objdump」というコマンドを使って逆アセンブルを行います。逆アセンブル結果を機械語とアセンブリ言語で並べて表示してくれるため、機械語で書いた場合のバイナリ列(すなわち構文)はどのような表現になるのかを確認するのに役立ちます。
objdumpコマンドは「binutils」パッケージに入っています。まずはこのパッケージをインストールします。
$ sudo apt install binutils
以下のようにバージョンが表示できればOKです。(バージョンは違っていても構いません。)
$ objdump -v GNU objdump (GNU Binutils for Debian) 2.28 Copyright (C) 2017 Free Software Foundation, Inc. This program is free software; you may redistribute it under the terms of the GNU General Public License version 3 or (at your option) any later version. This program has absolutely no warranty.
objdumpを使ってみます。試しにC言語で「無限ループだけのプログラム」を書いて、逆アセンブルしてみることにします。
C言語のプログラムはリスト1.1の通りです。お好きなエディタで書いてみてくだい。
なお、このサンプルプログラムは、本書のサンプルプログラムをまとめているリポジトリ*1内の「011_jmp」ディレクトリ内に置いています。
[*1] サンプルリポジトリのURLは「はじめに」をご覧ください。
ここでは「loop.c」というファイル名で作成したとします。
これをコンパイルします。本書ではコンパイラにはGCCを使用しますので、まだインストールされていなければインストールしてください。
$ sudo apt install gcc
以下のようにバージョンが表示できればOKです。(本書で扱う範囲では、バージョンは違っていても構いません。)
$ gcc -v Using built-in specs. COLLECT_GCC=gcc COLLECT_LTO_WRAPPER=/usr/lib/gcc/x86_64-linux-gnu/6/lto-wrapper Target: x86_64-linux-gnu Configured with: ../src/configure -v ・・・省略・・・ Thread model: posix gcc version 6.3.0 20170516 (Debian 6.3.0-18+deb9u1)
それでは、コンパイルします。「-g」を付けて、実行バイナリにデバッグ情報も付加しています。
$ gcc -g -o loop loop.c
これを逆アセンブルしてみます。
$ objdump -S loop | less ・・・省略・・・ 0000000000000660 <main>: int main(void) { 660: 55 push %rbp 661: 48 89 e5 mov %rsp,%rbp while (1); 664: eb fe jmp 664 <main+0x4> 666: 66 2e 0f 1f 84 00 00 nopw %cs:0x0(%rax,%rax,1) 66d: 00 00 00 ・・・省略・・・
「-S」は「デバッグ情報があればCのソースコードも併せて表示」するオプションです。
また、行数が多いので、スクロールできるようにパイプでページャ(less)に繋いでいます。
出力をスクロールしていくと、「int main(void)」があり、そこより下の行にmain関数の実装があります。
少し下の行に行くと「while (1);」があり、その直後の行が、無限ループを実現している機械語、およびアセンブラのコードです。該当の行のみ抜粋するとリスト1.2の通りです。
objdumpの出力は、各行で機械語とアセンブラが対応しています。この場合、アセンブラが「jmp 664 <main+0x4>
」で、その機械語が「eb fe
」です。たった2バイトで無限ループを実現できるのですね。
一番左にコロン区切りで書かれている「664:」は実行バイナリ先頭からのバイト数です。「jmp 664
」の「664」はこの値を示しており、jmp命令自身と同じ場所にジャンプしてくるため、無限ループとなります。
そんなわけで、1つ目の命令は「jmp」命令です。もう少し詳しく見てみます。
それでは、たった2バイトの命令の中に「先頭から何バイト目」という絶対アドレスが埋め込まれているのでしょうか?このような無限ループを、例えば先頭から0バイト目に配置すると機械語コードも変わるのでしょうか?
実験してみましょう。ループをjmp命令で実現できることはわかっているので、ここでは1行〜数行のアセンブラコードを機械語に変換してみることで、jmp命令のアセンブラの表現と機械語の対応を理解します。
アセンブリ言語で書かれたソースコードを機械語(実行バイナリ)へ変換するにはアセンブラを使用します。ですが、たった1行〜数行のコードで実験して見るためだけにわざわざコードを書いて、アセンブルして、実行バイナリを16進数でダンプする、というのは面倒です。本書では、それらを行うコマンドを、「as_dump
」というシェルスクリプトで用意しています。
as_dumpはサンプルリポジトリの「tools
」ディレクトリに入っています。
as_dumpはアセンブルに「asコマンド(GNUアセンブラ)」を使用します。asはbinutilsに入っているので、objdumpを使用する際にインストールしていれば問題ありません。
それでは、as_dumpを使って簡単なアセンブラを機械語コードに変換してみます。
jmp命令のオペランド*2にはラベルが使えます。そして、「今この場所」を示すラベルには「.(ドット)」があり、「jmp .
」で無限ループを実現できます。
[*2] 命令へ与えるパラメータのこと。jmp命令の場合、先程の「4」がオペランドに相当します。
as_dumpを使って「jmp .
」の1行だけのアセンブラコードを機械語に変換してみます。
$ as_dump jmp . /tmp/tmp.hdumfEdnIQ/a.out: ファイル形式 elf64-x86-64 セクション .text の逆アセンブル: 0000000000000000 <.text>: 0: eb fe jmp 0x0
このように、as_dumpコマンドは引数にアセンブラを与えることで、1行のアセンブラをその場で機械語に変換し、結果をobjdumpによる逆アセンブル結果として表示します。
結果に関して、jmp命令以外には何もないアセンブラコードなので、jmpのオペランドが「0x0(先頭から0バイト目)」なのは意図通りです。ただ、今回の場合も機械語コードは「eb fe
」となりました。どうやら、機械語コードにはジャンプ先は相対アドレスで埋め込まれていそうです。それを逆アセンブラのツール(objdump)が逆アセンブルの際、よしなに絶対アドレスで表示してくれていたのでしょう。
それでは、「eb fe
」のどこに相対アドレスが埋め込まれていたのでしょうか。今度はラベルを使って少し先へジャンプさせてみましょう。
as_dumpを引数なしで実行すると入力を受け付ける状態になりますので、その場で2行のアセンブラコードを書きます。「Ctrl-D
」で抜ければ、書いたアセンブラコードを機械語へ変換します。
$ as_dump jmp loop loop: jmp . <Ctrl-Dで抜ける> /tmp/tmp.V4twRVogBv/a.out: ファイル形式 elf64-x86-64 セクション .text の逆アセンブル: 0000000000000000 <loop-0x2>: 0: eb 00 jmp 2 <loop> 0000000000000002 <loop>: 2: eb fe jmp 2 <loop>
2つ目のjmp命令の地点に「loop」というラベルを付けました。
1行目のjmp命令は自身の命令の場所から自身の命令長である2バイト先にジャンプすることになります。その場合、機械語コードは「eb 00
」になりました。自身の命令と同じ場所へジャンプする場合は、これまで確認した通り「eb fe
」です。
どうやら、機械語コードの「eb
」が「jmp
」命令であることを示し、続く2バイト目がジャンプ先の相対アドレスを示しているようです。
また、2バイト目に関して、「今この場所」へジャンプする際は「fe
」で、「2バイト目」へジャンプする際は「00
」でした。「0xfe + 2 = 0x100」で、1バイトからオーバーフローした分を捨てると「0x00」になることから、eb命令(jmp命令)のオペランドは0xfeを基準とした相対アドレスであることがわかります。
as_dumpの逆に、その場で逆アセンブルする「das_dump」コマンドもサンプルディレクトリのtoolsディレクトリに入れてあります。
少し追加で実験してみます。
先ほど、jmp命令の機械語である「eb XX」命令のオペランドは、「fe」を基準として、それに足し合わせた数値の分がジャンプ先の相対的なアドレスになることを確認しました。
それでは、「fe」から値を引いた場合はどうなるのでしょうか?
例として、「fe」から1を減算した値である「fd」をオペランドとした「eb fd」を逆アセンブルしてみます。
$ das_dump eb fd /tmp/tmp.qQjOC52hIN/a.bin: ファイル形式 binary セクション .data の逆アセンブル: 0000000000000000 <.data>: 0: eb fd jmp 0xffffffffffffffff
das_dumpコマンドも、使い方はas_dumpコマンドと同様です。コマンドライン引数に逆アセンブルしたい機械語を1バイトずつ半角スペース区切りで16進数で指定すると、それをobjdumpで逆アセンブルした結果を表示します。
逆アセンブルの結果、jmpのオペランドは「0xffffffffffffffff」となりました。この値は2の補数で「-1」を示します。「基準feから引いた値を指定するとその分だけマイナスの方向へジャンプする」ということだと分かります*3。
[*3] プラス方向へのジャンプについてfeからの増分が2以上の場合、00へラップ(1周)するので、プラス方向へのジャンプの最大値とマイナス方向へのジャンプの最大値はどこかでぶつかります。それがどこなのかもdas_dumpで適当にオペランドを変えて逆アセンブルしてみることで確認できます。興味があれば実験してみてください。
相対アドレス指定でジャンプするjmp命令の機械語コードとアセンブラの構文を表1.1にまとめます。
アセンブラ | jmp <ラベルあるいは"."> |
---|---|
機械語 | eb fe+rel_addr |
表1.1の「アセンブラ」の表記について、objdumpではjmpのオペランドを実行バイナリ先頭からのバイト数あるいは相対アドレスで表示していましたが、一般的にアセンブラを書く際はジャンプ先にラベルを置いて、jmpのオペランドにはラベルを指定します。
また「機械語」の欄の「+rel_addr
」は相対的にジャンプさせたいバイト数を足し合わせる事を示しています。
以降では、新たな構文を見つける/あるいは紹介する度に、このような表にまとめます。
ここまでで、jmp命令の相対アドレス指定での構文を理解しました。
次は、理解した構文を使って、実際に動作する実行バイナリを作ってみます。
バイナリを書く際、一般的にはバイナリエディタを使うかと思うのですが、そもそも「バイナリを書く」事が一般的ではないためか、0から書いていくような場合に使い勝手のよいバイナリエディタがなかなか無い印象です。
そこで、本書では、16進数が羅列されたテキストファイルをバイナリへ変換するスクリプト「t2b」を用意して、機械語でのコーディング作業はテキストエディタで行うことにします。
t2bもシェルスクリプトで、サンプルリポジトリのtoolsディレクトリにあります。
標準のUNIXコマンドしか使っていないため、動作のために新たに必要なパッケージは特にありません。
それでは、コードを書いてみます。
前述の実験を少し変更し、2つのjmp命令を並べて、交互にジャンプするようにしてみました(リスト1.3)。
内容は「sample.txt」というファイルで保存することにします。また、サンプルリポジトリの中の、「011_jmp」ディレクトリの中にも置いてあります。
なお、t2bは行頭・行中どちらでも、「#」以降の記述をコメントとして扱います。そのため、リスト1.4の様にコメントを付けることが可能です。機械語と「#」の間はタブ文字でもスペースでもどちらでも構いません。
また、「:(コロン)」より左側の記述も無視するため、リスト1.5の様に先頭からのバイト数などを付け加えても構いません。
t2bを使用して以下のようにバイナリへの変換が行えます。
$ t2b sample.txt > code.bin
odコマンドでバイナリをダンプしてみると、意図したとおりに書かれていることを確認できます。
$ od -t x1 code.bin 0000000 eb 00 eb fc 0000004
「-t x1」は1バイト区切りで表示するオプションです。
アドレスの大きい方へ2バイト先にジャンプする「eb 00
」と、アドレスの小さい方へ2バイト前へジャンプする「eb fc
」が書かれており、意図通りのバイナリであることを確認できました。
ここまでで機械語命令を格納したバイナリは生成できました。ただ、「実行バイナリ」とするにはヘッダが足りません。
「はじめに」でも説明した通り、本書では手で書いた実行バイナリを、QEMU上でOSレスで実行します。なお、QEMUにしろ実機にしろ、電源を入れてから(QEMUの場合はエミュレータを立ち上げてから)最初に実行されるのはBIOS*4です。ただ、本書で作るバイナリはBIOSのお作法に従ったものではないため、作ったバイナリをロードし実行を開始してくれる「ブートローダー」が必要です。本書では「poiboot」という独自のブートローダーを使用します。ブートローダーが関わるのはバイナリの実行開始のみで、その後はOSレスで直接ハードウェアを制御します。
[*4] 現代はUEFI BIOS
ブートローダー「poiboot」が実行できるバイナリフォーマットはシンプルで表1.2の通りです。
ヘッダ | bss領域の先頭アドレス(8バイト) |
---|---|
(先頭16バイト) | bss領域のサイズ(8バイト) |
本体 | 実行バイナリ本体(機械語コード) |
(17バイト目以降) | ※ poibootはロード後、本体の先頭(ヘッダの直後)へジャンプする |
bssとは、C言語での初期値なし(あるいは初期値0)のグローバル変数やstatic変数等で、プログラム実行開始時にゼロで初期化しておくべき領域です。poibootは実行バイナリ先頭の16バイトの内容からbss領域のアドレスとサイズを確認し、本体の実行開始前にその領域をゼロで初期化します。
poibootが事前にやることはこれだけです。初期化が完了したら、実行バイナリ本体の先頭へジャンプします。
bssについては、ヘッダの「bss領域のサイズ」が0の場合、poibootは何もしないです。本書では特にbss領域は使わないので、全て0の16バイトのバイナリを、機械語コードの先頭にくっつければ良いだけです。
ddコマンドで全て0の16バイトのデータ「head.bin
」を生成し、前項で作成した「code.bin
」と結合します。
$ dd if=/dev/zero of=head.bin bs=1 count=16 16+0 レコード入力 16+0 レコード出力 16 bytes copied, 0.000514814 s, 31.1 kB/s $ cat head.bin code.bin > kernel.bin
poibootは「kernel.bin
」という名前の実行ファイルを認識するので、この名前で生成しました。
以下のようにQEMUをインストールします。
$ sudo apt install qemu-system-x86 ovmf
poibootは、旧来のBIOS(レガシーBIOS)ではなく、「UEFI」という近年一般的に使われているBIOSを前提としているのですが、QEMUにはUEFIのファームウェアが付属しないため、オープンソースのUEFIファームウェアである「OVMF
」を併せてインストールしています。
OVMFについては、ホームディレクトリ直下に「OVMF
」というディレクトリを作成し、そこへファームウェアバイナリを配置しておきます。
「ovmf」パッケージによってインストールされたファイルの一覧は以下のコマンドで確認できます。
$ dpkg -L ovmf /. /usr /usr/share /usr/share/OVMF /usr/share/OVMF/OVMF_CODE.fd /usr/share/OVMF/OVMF_VARS.fd /usr/share/doc /usr/share/doc/ovmf /usr/share/doc/ovmf/changelog.Debian.gz /usr/share/doc/ovmf/copyright /usr/share/ovmf /usr/share/ovmf/OVMF.fd /usr/share/qemu /usr/share/qemu/OVMF.fd
「OVMF_CODE.fd
」と「OVMF_VARS.fd
」が本書で使用するファームウェアバイナリです。
以下のようにホームディレクトリ直下にディレクトリを作成し、ファイルをコピーしておいてください。
$ mkdir ~/OVMF $ cp /usr/share/OVMF/OVMF_CODE.fd ~/OVMF/ $ cp /usr/share/OVMF/OVMF_VARS.fd ~/OVMF/
poibootはGitHub上で公開しています。
本書のブートローダーとして動作確認した時点は「x86_64_ml_20190922
」でリリースしており、リリースページのURLは以下の通りです。
poibootのコンパイル済み実行バイナリは、上記のリリースページにて「poiboot_x86_64_ml_20190922.zip」というzipアーカイブで取得できます。
ダウンロードし、展開しておいてください。以下の2つのファイルを使用します。
必要なものは全て用意できました。作成した実行バイナリをQEMU上で実行するために、リスト1.6のようにファイルを配置してください。
ここでは、「fs
」という名前の作業ディレクトリへファイルを配置したとします。
この階層構造について簡単に説明すると、これはUEFIファームウェア(QEMU上で実行する場合OVMF)が実行ファイルを見つけるためのファイル配置です。UEFIはレガシーBIOSとは違い、FATファイルシステムを認識できます。そして、起動ディスク上のFATファイルシステムに「efi/boot/bootx64.efi
」という名前で実行ファイルが配置されていた場合、それを実行してくれます。
bootx64.efiは「poiboot」です。poibootはFATファイルシステム上の「kernel.bin
」を実行するため、作成した実行ファイルが実行される、という流れです。
作成した実行バイナリ「kernel.bin」が実行されるまでの流れをまとめると以下の通りです。
ファイルを配置できたので、QEMUで実行してみます。
fsが存在するディレクトリと同じ階層で以下のコマンドを実行してください。
$ ls -d fs fs ← 「fs」が見える階層(fsと同じ階層)で実行 $ qemu-system-x86_64 \ -m 4G -enable-kvm \ -drive if=pflash,format=raw,readonly,file=$HOME/OVMF/OVMF_CODE.fd \ -drive if=pflash,format=raw,file=$HOME/OVMF/OVMF_VARS.fd \ -drive dir=fs,driver=vvfat,rw=on
KVMが無い環境の場合、「-enable-kvm
」のオプションは外してください。
「-drive dir=fs,driver=vvfat,rw=on
」のオプションで、前項で作成した「fs」ディレクトリをFATファイルシステムとして扱うように指定しています。QEMUを実行する際のカレントディレクトリがfsが存在するディレクトリとは異なる場合、「dir=fs
」の「fs
」を良しなに変更してください。(相対パスあるいは絶対パスで指定すれば良いです。)
実行すると、GUIでQEMUの画面が表示されたかと思います。ただ、今回は何も画面に表示するプログラムではないので、見た目上は何も変化はありません。
このQEMUコマンドは今後手で打つにはしんどいくらいに長いので、「run_qemu
」という名前でシェルスクリプト化してあります。他のツールと同様にサンプルリポジトリのtoolsディレクトリに入れてありますので、今後はこちらを使います。
先ほどのコマンドを「run_qemu」を使って実行する場合、以下の通りです。
$ run_qemu fs
run_qemuは、引数に指定したディレクトリを、ブートローダーや実行バイナリが配置されたディレクトリとして使用します。
なお、QEMUはCUIで実行することもできます。QEMUに「-nographic」オプションを追加するとCUIでQEMUを起動できます。run_qemuは2つ目以降のコマンドライン引数をQEMUへの追加オプションとして扱いますので、以下のようにオプションを追加すると、CUIモードで起動できます。
$ run_qemu fs -nographic
QEMUにはQEMUの実行状態を確認できる「QEMUモニター」という機能があります。
QEMUモニターの画面へ切り替えてみましょう。切り替え方はGUI/CUIのそれぞれ、表1.3の通りです。
GUIの場合 | |
---|---|
QEMUモニターへ切り替え | Ctrl-Alt-2 |
元の画面に戻る | Ctrl-Alt-1 |
CUIの場合 | |
QEMUモニターへ切り替え | Ctrl-a押下のあとcキー押下 |
元の画面に戻る | Ctrl-a押下のあとcキー押下 |
ヘルプ表示 | Ctrl-a押下のあと?キー押下 |
CPUの演算やその結果は基本的に「レジスタ」というCPU専用の高速な記憶領域に格納されます。「info registers
」コマンドで現在のCPUのレジスタ値を確認できます。
QEMU 2.8.1 monitor - type 'help' for more information (qemu) info registers RAX=0000000000110000 RBX=00000000bef67f98 RCX=0000000000000000 RDX=0000000000000000 RSI=0000000000407180 RDI=00000000bfe9b018 RBP=00000000bff0eb10 RSP=0000000000210000 R8 =00000000bff0e9dc R9 =0000000000000078 R10=00000000bfe6e080 R11=00000000e7dc4657 R12=00000000bef69540 R13=00000000bef69548 R14=00000000bff24b60 R15=00000000bef6a018 RIP=0000000000110002 RFL=00000002 [-------] CPL=0 II=0 A20=1 SMM=0 HLT=0 ...
GUIの際、出力が1画面に収まらず流れてしまった際は「Ctrlを押しながら上下キー」でスクロールできます。
ここで注目すべきは「RIP=
」の値です。これは、今CPUが実行している命令のアドレスです。poibootは「kernel.bin」内の実行バイナリ本体(ヘッダより後の17バイト目以降)を、メモリアドレス「0x110000
」から配置するので、RIPが「0x110000」から大きく離れていると異常です。今回の場合、1つ目のjmp命令のアドレスは「0x110000」で、2つ目のjmp命令のアドレスは「0x110002」となりますので、RIPがこのいずれかの値になっていれば正常です。
1つ目と2つ目のjmp命令を繰り返しているはずなので、何度かinfo registers
コマンドを繰り返すと「RIP」が「0x110000」になったり「0x110002」になったりします。
また、メモリの内容を表示させることもできます。例えば、実行バイナリ本体が配置されている0x110000以降の内容を逆アセンブルして命令2個分を表示させるには以下の通りです。
(qemu) x/2i 0x110000 0x0000000000110000: jmp 0x110002 0x0000000000110002: jmp 0x110000
「x/2i
」の「x
」が「引数に指定されたメモリアドレスの内容を表示する」というコマンドで、「/
」以降の「2i
」がフォーマット指定です。「i
」が「逆アセンブルしてアセンブラ表示」という表示形式を示しており、「2
」はそれを「2個分」表示することを示しています。なお、もちろん16進数等で表示させることもできます。詳しくはQEMUのドキュメントをご覧ください*5。
このようなビジーループのプログラムを実行しているとホストPC側のCPU使用率が上昇していきます。レジスタ値やメモリの内容を確認する際は一旦QEMUの実行を「stop
」コマンドで止めておくと良いです。「cont
(短縮表記c
)」コマンドで再開できます。
<一時停止> (qemu) stop <再開> (qemu) cont <再開(短縮表記)> (qemu) c
確認後、QEMUを終了させる際は、QEMUモニター上では「quit
」コマンドで終了できます。GUIの場合はウィンドウの[X]ボタンでも構いません。また、CUIの場合は「Ctrl-aのあとx」でも終了できます。
(qemu) quit
以上で、「機械語の構文理解」から「実行バイナリ作成」、「QEMUでの動作確認」までのフルセットの流れが完了です。今後は、命令の簡単さや章ごとのテーマによって、フルセットで都度確認したりはしないですが、気になる場合はここで確認したように色々と実験してみてください。
この章では最後にあと2つ、無限ループプログラムに関連した命令を紹介します。
今後のためにこの章ではあと2つ、簡単な命令をさくっと紹介します。
まずは、「nop
」命令です。サンプルディレクトリは「012_nop」です。
nop命令は「何もしない命令」です。前節ではjmp命令のジャンプ先の相対アドレスを色々と変えてみるためにjmp命令自体を間に入れてみたりしていましたが、そのような場合はこの命令を使うと良いです。
as_dumpを使ってこの命令の機械語を確認します。
$ as_dump nop /tmp/tmp.MrEmEUlxOC/a.out: ファイル形式 elf64-x86-64 セクション .text の逆アセンブル: 0000000000000000 <.text>: 0: 90 nop
オペランドを取らないnop命令のアセンブラ/機械語構文は表1.4の通りであると分かりました。
アセンブラ | nop |
---|---|
機械語 | 90 |
例えば、「3バイト分戻るジャンプ」を実験として試してみる際は、nopを使ってリスト1.7のように書けます。
この章で紹介する最後の命令は「hlt
」命令です。サンプルディレクトリは「013_hlt」です。
hlt命令はシャットダウン等の意味合いで使われる「halt」の略です。この命令自体はシャットダウンという程ではなく、割り込みがあるまでCPUをスリープさせます。
本書で行うようなOSレスのプログラミングの場合、戻る先が無いので、プログラムの最後では必ず無限ループを設置して処理を止めておく必要があります。その際、ビジーループだとCPU負荷が上がってしまうので、hlt命令でCPUを休ませるようにすると良いです。
hlt命令もas_dumpで機械語を確認します。
$ as_dump hlt /tmp/tmp.5dlph8ymMx/a.out: ファイル形式 elf64-x86-64 セクション .text の逆アセンブル: 0000000000000000 <.text>: 0: f4 hlt
オペランドを取らないhlt命令のアセンブラ/機械語構文をまとめると表1.5の通りです。
アセンブラ | hlt |
---|---|
機械語 | f4 |
例えばリスト1.8のような感じで使います。
hlt命令によるCPUのスリープ状態からは、割り込み等で起床するので、完全にやることがなくなった際はhlt命令を実行し続けるようにjmp命令でループを入れておきます。