「フレームバッファ」を使用してディスプレイへの描画を試してみます。ディスプレイの各ピクセルには、メモリ空間上に対応する領域があり、それが「フレームバッファ」と呼ばれる領域です。
poibootでは、kernel.binへジャンプする際、UEFIで取得したフレームバッファ情報を渡すようにしています。この章ではpoibootから渡されたフレームバッファ情報を取得し、取得したフレームバッファ情報を使用して、画面へドットを描画したり、塗りつぶしたりしてみます。
フレームバッファの各ピクセルは「B(青)」・「G(緑)」・「R(赤)」・「Reserved(予約領域)」の順に各1バイトずつ計4バイトで構成されています。フレームバッファ領域の最も若いアドレスが画面左上のピクセルに対応します。
まずは、フレームバッファ領域の先頭アドレスを取得してみます。
この節のサンプルディレクトリは「041_get_fb_addr」です。
フレームバッファに関する情報は、ブートローダー(poiboot)から渡されます。
poibootでは、フレームバッファに関する情報を「struct fb(リスト4.1)」という構造でメモリ上に配置しています。そして、kernel.binを実行する際、その先頭アドレスをRSIレジスタ*1へ格納した上で、kernel.binへジャンプします。
[*1] RSIレジスタは、関数の第2引数として使われるレジスタです。そのため、普通にC言語などでカーネルを書く際は、kernel.binの一番先頭の関数の第2引数でstruct fbの先頭アドレスを取得できます。
リスト4.1のコメントに記載の通り、フレームバッファの先頭アドレスは、先頭のメンバー変数「base」に格納されています。「先頭のメンバー変数のアドレス」は「構造体の先頭アドレス」と等しいので、RSIに格納されているアドレス(struct fbの先頭アドレス)は、メンバー変数baseのアドレスでもあります。そのため、RSIが指す先に「フレームバッファの先頭アドレス」があります。
「あるアドレスが指す先」のメモリを参照したい場合、例えばC言語では「ポインタ」がありますが、機械語(およびアセンブラ)では「レジスタ間接アドレッシング」(あるいは単に「レジスタ間接」)というオペランド指定方法があります。
これは、命令のオペランドとして「レジスタに格納されたアドレスの指す先」を指定するものです。アセンブラでは「(%レジスタ名)
」の様に書きます。ここでは、RSIに格納されたアドレス先のメモリ上のデータ(フレームバッファ先頭アドレス)を、64ビットレジスタRAXへコピーしてみます。対応するmov命令は表4.1の通りです。
アセンブラ | mov (%rsi),%rax |
---|---|
機械語 | 48 8b 06 |
レジスタ間接で取得した値を64ビットレジスタへ格納する構文は表4.2の通りで、構文内の「reg_dst_src
」は表4.3の通りです。
アセンブラ | mov (レジスタ(64bit)), レジスタ(64bit) |
---|---|
機械語 | 48 8b 00+reg_dst_src |
備考 | レジスタ間接がRSPの場合、末尾に0x24追加 |
レジスタ間接がRBPの場合、3バイト目に0x40加算、末尾に0x00追加 |
dst/(src) | (RAX) | (RCX) | (RDX) | (RBX) | (RSP) | (RBP) | (RSI) | (RDI) |
---|---|---|---|---|---|---|---|---|
RAX | 0x00 | 0x01 | 0x02 | 0x03 | 0x04 | 0x05 | 0x06 | 0x07 |
RCX | 0x08 | 0x09 | 0x0a | 0x0b | 0x0c | 0x0d | 0x0e | 0x0f |
RDX | 0x10 | 0x11 | 0x12 | 0x13 | 0x14 | 0x15 | 0x16 | 0x17 |
RBX | 0x18 | 0x19 | 0x1a | 0x1b | 0x1c | 0x1d | 0x1e | 0x1f |
RSP | 0x20 | 0x21 | 0x22 | 0x23 | 0x24 | 0x25 | 0x26 | 0x27 |
RBP | 0x28 | 0x29 | 0x2a | 0x2b | 0x2c | 0x2d | 0x2e | 0x2f |
RSI | 0x30 | 0x31 | 0x32 | 0x33 | 0x34 | 0x35 | 0x36 | 0x37 |
RDI | 0x38 | 0x39 | 0x3a | 0x3b | 0x3c | 0x3d | 0x3e | 0x3f |
表4.2の備考について、レジスタ間接に使用するレジスタがRSPあるいはRBPの場合、機械語のパターンが少し変わります。例としては表4.4と表4.5の通りです。
アセンブラ | mov (%rsp), %rax |
---|---|
機械語 | 48 8b 04 24 |
アセンブラ | mov (%rbp), %rax |
---|---|
機械語 | 48 8b 45 00 |
なお、一部の命令では、レジスタ間接に64ビット幅以外のレジスタを使う命令もあったりしますが、本書で使わないため省略します。
サンプルコードはリスト4.2の通りです。
フレームバッファの先頭アドレスをRAXへコピーした後、haltの無限ループで待機するようにしました。
QEMUで動作させて、QEMUモニターでRAXを見てみると、フレームバッファの先頭アドレスが格納されています。
(qemu) info registers RAX=00000000c0000000 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=0000000000110004 RFL=00000002 [-------] CPL=0 II=0 A20=1 SMM=0 HLT=1
この場合、フレームバッファの先頭アドレスは「0xc0000000」であったと分かります。
続いて、画面の幅と高さを取得します。
この節のサンプルディレクトリは「042_get_width_height」です。
画面の幅と高さもstruct fbの構造の中にあります。それぞれ、幅(hr)はstruct fbの先頭から16(0x10)バイト目、高さ(vr)は20(0x14)バイト目です。
これらのデータを参照する際、アドレスを格納したレジスタへオフセット分の値を都度足したり引いたりしても参照できるのですが、少し面倒です。
レジスタ間接アドレッシングには「ディスプレースメント(dsp)」と呼ばれるオフセット値を指定できるため、これを使うと「あるアドレスからNバイト先のデータ」へ1命令でアクセスできます。
ディスプレースメント付きの間接アドレスは、アセンブラでは「dsp(レジスタ名)
」の様に書きます。「RSIに格納されたアドレスの16(0x10)バイト先」であれば「0x10(%rsi)
」、「RSIに格納されたアドレスの20(0x14)バイト先」であれば「0x14(%rsi)
」です。
幅(hr)と高さ(vr)はそれぞれ32ビットです。ここでは、幅の値を32ビットレジスタEBXへ、高さの値を32ビットレジスタECXへコピーすることにします。それぞれのアセンブラと機械語コードは表4.6と表4.7の通りです。
アセンブラ | mov 0x10(%rsi),%ebx |
---|---|
機械語コード | 8b 5e 10 |
アセンブラ | mov 0x14(%rsi),%ecx |
---|---|
機械語コード | 8b 4e 14 |
それぞれ、末尾の1バイトがディスプレースメントです。
8ビットのディスプレースメント付きレジスタ間接の値を32ビットレジスタへ格納する構文は表4.8の通りで、構文内の「reg_dst_src
」は表4.9の通りです。
アセンブラ | mov dsp(レジスタ(64bit)), レジスタ(32bit) |
---|---|
機械語 | 8b 40+reg_dst_src dsp(8bit) |
備考 | レジスタ間接がRSPの場合、3バイト目に0x24追加 |
dst/(src) | (RAX) | (RCX) | (RDX) | (RBX) | (RSP) | (RBP) | (RSI) | (RDI) |
---|---|---|---|---|---|---|---|---|
EAX | 0x00 | 0x01 | 0x02 | 0x03 | 0x04 | 0x05 | 0x06 | 0x07 |
ECX | 0x08 | 0x09 | 0x0a | 0x0b | 0x0c | 0x0d | 0x0e | 0x0f |
EDX | 0x10 | 0x11 | 0x12 | 0x13 | 0x14 | 0x15 | 0x16 | 0x17 |
EBX | 0x18 | 0x19 | 0x1a | 0x1b | 0x1c | 0x1d | 0x1e | 0x1f |
ESP | 0x20 | 0x21 | 0x22 | 0x23 | 0x24 | 0x25 | 0x26 | 0x27 |
EBP | 0x28 | 0x29 | 0x2a | 0x2b | 0x2c | 0x2d | 0x2e | 0x2f |
ESI | 0x30 | 0x31 | 0x32 | 0x33 | 0x34 | 0x35 | 0x36 | 0x37 |
EDI | 0x38 | 0x39 | 0x3a | 0x3b | 0x3c | 0x3d | 0x3e | 0x3f |
表4.8の備考に記載の通り、レジスタ間接のレジスタがRSPの場合、機械語のパターンが変わります。例としては表4.10の通りです。
アセンブラ | mov 0x12(%rsp), %rax |
---|---|
機械語 | 8b 44 24 12 |
ここで、構文33で紹介したDSP無しの構文では、レジスタを示すバイトは「00+reg_dst_src」という構文でした。今回の場合と比較すると、レジスタを示すバイトに0x40を足すと「DSP付き」の意味になり、末尾に追加した1バイトがDSPとして扱われるようです。
構文34より、「0x12」のDSP付きでRAXのレジスタ間接で取得した値をEAXへ格納する機械語は「8b 40 12
」です。
試しに、2バイト目から0x40を引き、3バイト目のDSPを除いた「8b 00
」をdas_dumpで逆アセンブルしてみます。
$ das_dump 8b 00 /tmp/tmp.pYcO1hU5Ag/a.bin: ファイル形式 binary セクション .data の逆アセンブル: 0000000000000000 <.data>: 0: 8b 00 mov (%rax),%eax
「8b 00
」はDSP無しに対応する命令だと分かりました。これより、「レジスタ間接の対象レジスタを示すバイトに0x40を足すとDSP付きを示し、末尾の1バイトがDSPになる」と考えて良さそうです。
DSP無しの構文33の備考を改めて見てみると、『レジスタ間接のレジスタ(src)がRBPの場合、3バイト目に0x40を足し、末尾に「00」を追加』とありました。これは、RBPにはDSP無しの機械語が無いので、DSP無しのアセンブラからアセンブルすると、DSPを0とした機械語が生成されていたのですね。
以上を踏まえて、前節のsample.txtに幅と高さの取得を追加してみます(リスト4.3)。
QEMU上で実行し、QEMUモニターでレジスタ値を確認すると、EBXとECXにそれぞれ、フレームバッファの幅と高さが格納されています。
(qemu) info registers RAX=00000000c0000000 RBX=0000000000000320 RCX=0000000000000258 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=000000000011000a RFL=00000002 [-------] CPL=0 II=0 A20=1 SMM=0 HLT=1
EBXに格納されている「0x320」は10進数で「800」、ECXに格納されている「0x258」は10進数で「600」です。QEMUのフレームバッファ解像度はデフォルトで800x600なので、正常に取得できているとわかります。
前節までで、フレームバッファで画面描画を行うために必要な情報は揃いました。この節からはそれらを使って画面描画を行ってみます。まずは1ピクセルだけ画面に描画してみます。
この節のサンプルディレクトリは「043_draw_pixel」です。
前節までで確認したレジスタ間接による方法で、メモリ上にマップされたフレームバッファの領域へ書き込みを行います。
フレームバッファのピクセルフォーマットは「青(B), 緑(G), 赤(R), 予約(RSV)」が各1バイトずつで、1ピクセル当たり4バイトです。フレームバッファには画面左上から順にピクセルが並んでいるため、フレームバッファ先頭の4バイトに書き込みを行うと画面左上の1ピクセルを操作できます。
ここではフレームバッファ先頭の4バイトに即値で「B=0x00, G=0xff, R=0x00, RSV=0x00」を書き込んで、画面左上の1ピクセルを緑で塗ってみます。対応する機械語コードは表4.11の通りです。
アセンブラ | movl $0x0000ff00,(%rax) |
---|---|
機械語コード | c7 00 00 ff 00 00 |
即値をレジスタ間接で書き込むの場合、アセンブラ上では何ビットの書き込みかが分からない*2ため、書き込みのビット幅を明示する必要があります。
[*2] アセンブラでは即値で「0x0000ff00」と書いていても操作するビット幅に応じて上の位のビットを良しなに0(あるいは符号ビット)で補完します。そのため、「0x0000ff00(計32ビット)」と書いているからといって、「32ビットの即値書き込み」の意思がアセンブラへ伝わるわけではないです。「mov $0x0000ff00,(%rax)」でアセンブルしようとすると「サイズが分からない」といったエラーが出ます。
ここでは、32ビットアクセスであることを明示するためmov
に接尾辞として"l"*3を付けています。
[*3] 他にも、8ビットアクセスは"b"、16ビットアクセスは"w"、64ビットアクセスは"q"といった接尾辞があります。
機械語コードの末尾4バイトが書き込む即値データです。
構文をまとめると以下の通りです。
アセンブラ | movl 即値(32bit), (レジスタ(64bit)) |
---|---|
機械語 | c7 00+reg_ofs 即値(32bit) |
備考 | レジスタ間接がRSPの場合、3バイト目に0x24追加 |
レジスタ間接がRBPの場合、2バイト目に0x40加算、3バイト目に0x00追加 |
前節までのsample.txtへ、画面左上(0,0)のピクセルを緑で塗りつぶす処理を追加してみます(リスト4.4)。
GUIモードで実行すると、たった1ピクセルなので見難いですが、一番左上の1ピクセルが緑で塗られていることを確認できます。
また、QEMUモニターでフレームバッファのデータをダンプすることでも確認できます。
(qemu) x/4bx 0xc0000000 00000000c0000000: 0x00 0xff 0x00 0x00
筆者の場合、フレームバッファのアドレスは0xc0000000だったので、そこから1バイトずつ、4個分のデータを16進数でダンプしてみました。
「0x00(B) 0xff(G) 0x00(R) 0x00(RSV)」と、意図通りにフレームバッファへ書き込みができていることを確認できます。
前節で画面の左上(0,0)の1ピクセルを塗ることができました。フレームバッファ上の書き込み先を1ピクセル(4バイト)ずつずらしながら、全ピクセル(幅 * 高さ)分を書き込むことで、画面全体を単色で塗りつぶしてみます。
この節のサンプルディレクトリは「044_fill」です。
jmp命令を使うことでループを作ることはできるので「書き込み先を4バイトずつずらしながら、即値を書き込む」事はできそうです。
ただ、フレームバッファの領域を超えて書き込みを行ってしまうとフレームバッファ外の領域のメモリ破壊につながります。そのため、レジスタEDXを「書き込んだピクセル数を管理するカウンタ」、レジスタESIを「画面の総ピクセル数(幅 * 高さ)」とし、「EDX >= ESI」のときループを脱出するようにします。
ここで主に使用する命令が、数値比較を行う「cmp」命令と、「jbe」命令です。
jbeは「Jump if Below or Equal」の略です。既に登場した「je(Jump if Equal)命令」等と同じく条件付きジャンプ命令の一種です。
対応するフラグレジスタはキャリーフラグとゼロフラグで、いずれかがセットされていた場合にジャンプします。
キャリーフラグは、算術演算の結果、レジスタサイズを超える桁上がり(キャリー)あるいはボロー(桁借り)が発生した場合にセットされます。今回の場合、キャリーフラグがセットされるのは「ESI - EDXが負となる場合」で、ゼロフラグがセットされるのは「ESIとEDXが等しい場合」なので、「EDX >= ESI」のときジャンプすることになります。
jbe命令の構文は表4.13の通りです。
アセンブラ | jbe <ラベルあるいは"."> |
---|---|
機械語コード | 76 fe+rel_addr |
末尾の1バイトがジャンプ先を決める相対アドレスで、jmp命令等と同様、基準は0xfeです。
前節までのsample.txtへ画面塗りつぶし処理を追加してみました(リスト4.5)。
初出の構文については後述しますが、やっていることとしてはコメントに記載のとおりです。
気をつける点としては、1ピクセルの描画を行っている以下の命令です。
機械語の末尾4バイトには、描画したいピクセル情報がB(0x00)・G(0xff)・R(0x00)・RSV(0x00)の順に並んでいますが、この命令自体は「32ビット即値をレジスタ間接で書き込む」です。そのため、アセンブラでは、バイトオーダーをリトルエンディアンで並べると「B・G・R・RSV」の順になるように、「0x0000ff00」という即値を指定しています。
今回、「xor」・「mov」・「imul」・「cmp」・「add」・「inc」で新しい構文がでてきましたので、紹介しておきます。
32ビットレジスタを使う構文が新しく登場しました。
アセンブラ | xor レジスタ(32bit), レジスタ(32bit) |
---|---|
機械語 | 31 c0+reg_src_dst |
「reg_src_dst
」は以下の通りです。
src/dst | EAX | ECX | EDX | EBX | ESP | EBP | ESI | EDI |
---|---|---|---|---|---|---|---|---|
EAX | 0x00 | 0x01 | 0x02 | 0x03 | 0x04 | 0x05 | 0x06 | 0x07 |
ECX | 0x08 | 0x09 | 0x0a | 0x0b | 0x0c | 0x0d | 0x0e | 0x0f |
EDX | 0x10 | 0x11 | 0x12 | 0x13 | 0x14 | 0x15 | 0x16 | 0x17 |
EBX | 0x18 | 0x19 | 0x1a | 0x1b | 0x1c | 0x1d | 0x1e | 0x1f |
ESP | 0x20 | 0x21 | 0x22 | 0x23 | 0x24 | 0x25 | 0x26 | 0x27 |
EBP | 0x28 | 0x29 | 0x2a | 0x2b | 0x2c | 0x2d | 0x2e | 0x2f |
ESI | 0x30 | 0x31 | 0x32 | 0x33 | 0x34 | 0x35 | 0x36 | 0x37 |
EDI | 0x38 | 0x39 | 0x3a | 0x3b | 0x3c | 0x3d | 0x3e | 0x3f |
今回、xorはEDXレジスタをゼロクリアするために使用しています。mov命令で即値0を格納しても良いのですが、xorを使用しているのは機械語のバイト数が2バイトと小さいからです。
試しにas_dumpで確認してみます。
$ as_dump mov \$0,%edx /tmp/tmp.0Sw3Al6M9z/a.out: ファイル形式 elf64-x86-64 セクション .text の逆アセンブル: 0000000000000000 <.text>: 0: ba 00 00 00 00 mov $0x0,%edx
mov命令の場合、即値部分で4バイト使ってしまう5バイトの命令でした。
movも32ビットレジスタに関する構文が初出です。
アセンブラ | mov レジスタ(32bit), レジスタ(32bit) |
---|---|
機械語 | 89 c0+reg_src_dst |
「reg_src_dst」はXORと同じです。
imulも32ビットレジスタに関する構文です。
アセンブラ | imul レジスタ(32bit), レジスタ(32bit) |
---|---|
機械語 | 0f af c0+reg_dst_src |
「reg_dst_src
」は以下の通りです。
dst/src | EAX | ECX | EDX | EBX | ESP | EBP | ESI | EDI |
---|---|---|---|---|---|---|---|---|
EAX | 0x00 | 0x01 | 0x02 | 0x03 | 0x04 | 0x05 | 0x06 | 0x07 |
ECX | 0x08 | 0x09 | 0x0a | 0x0b | 0x0c | 0x0d | 0x0e | 0x0f |
EDX | 0x10 | 0x11 | 0x12 | 0x13 | 0x14 | 0x15 | 0x16 | 0x17 |
EBX | 0x18 | 0x19 | 0x1a | 0x1b | 0x1c | 0x1d | 0x1e | 0x1f |
ESP | 0x20 | 0x21 | 0x22 | 0x23 | 0x24 | 0x25 | 0x26 | 0x27 |
EBP | 0x28 | 0x29 | 0x2a | 0x2b | 0x2c | 0x2d | 0x2e | 0x2f |
ESI | 0x30 | 0x31 | 0x32 | 0x33 | 0x34 | 0x35 | 0x36 | 0x37 |
EDI | 0x38 | 0x39 | 0x3a | 0x3b | 0x3c | 0x3d | 0x3e | 0x3f |
先に紹介したxor・movとは行・列が異なるのでご注意ください。
cmpも32ビットレジスタに関する構文です。
アセンブラ | cmp レジスタ(32bit), レジスタ(32bit) |
---|---|
機械語 | 39 c0+reg_src_dst |
「reg_src_dst」は、xor・movと同じです。
addは8ビット即値を64ビットレジスタへ加算する構文が初出でした。
アセンブラ | add 即値(8bit), レジスタ(64bit) |
---|---|
機械語 | 48 83 c0+reg_ofs 即値(8bit) |
incも32ビットレジスタについてです。
アセンブラ | inc レジスタ(32bit) |
---|---|
機械語 | ff c0+reg_ofs |
GUIで実行すると、画面が緑一色で塗りつぶされることが確認できます(図4.1)。(白黒印刷なので印刷上はわからないですが。。)
各ピクセルを塗る色をランダムに決めることでカラーノイズを描画してみます。
この節のサンプルディレクトリは「045_color_noise」です。
実はx86 CPUには乱数を取得する命令があります。それが「rdrand命令」です。rdrandはオペランドにレジスタを指定することで、そのレジスタへ乱数を格納してくれます。
ここでは、レジスタEDIへ乱数を格納し、この4バイトレジスタの内容をピクセルデータとしてフレームバッファへ書き込んでみます。
rdrand命令の今回使用する構文は以下の通りです。
アセンブラ | rdrand レジスタ(32bit) |
---|---|
機械語 | 0f c7 f0+reg_ofs |
早速実装してみます(リスト4.7)。
rdrand命令で32ビットレジスタEDIへ乱数を格納し、それをmov命令でフレームバッファへ書き込むように変更しました。
なお、32ビットレジスタをレジスタ間接で書き込むmov命令は新しい構文なので、紹介しておきます。
アセンブラ | mov レジスタ(32bit), (レジスタ(64bit)) |
---|---|
機械語 | 89 00+reg_src_dst |
備考 | レジスタ間接がRSPの場合、末尾に0x24追加 |
レジスタ間接がRBPの場合、2バイト目に0x40加算、末尾に0x00追加 |
「reg_src_dst」は以下の通りです。
src/(dst) | (RAX) | (RCX) | (RDX) | (RBX) | (RSP) | (RBP) | (RSI) | (RDI) |
---|---|---|---|---|---|---|---|---|
EAX | 0x00 | 0x01 | 0x02 | 0x03 | 0x04 | 0x05 | 0x06 | 0x07 |
ECX | 0x08 | 0x09 | 0x0a | 0x0b | 0x0c | 0x0d | 0x0e | 0x0f |
EDX | 0x10 | 0x11 | 0x12 | 0x13 | 0x14 | 0x15 | 0x16 | 0x17 |
EBX | 0x18 | 0x19 | 0x1a | 0x1b | 0x1c | 0x1d | 0x1e | 0x1f |
ESP | 0x20 | 0x21 | 0x22 | 0x23 | 0x24 | 0x25 | 0x26 | 0x27 |
EBP | 0x28 | 0x29 | 0x2a | 0x2b | 0x2c | 0x2d | 0x2e | 0x2f |
ESI | 0x30 | 0x31 | 0x32 | 0x33 | 0x34 | 0x35 | 0x36 | 0x37 |
EDI | 0x38 | 0x39 | 0x3a | 0x3b | 0x3c | 0x3d | 0x3e | 0x3f |
備考について、例えばEAXからRSPのレジスタ間接で書き込む場合、以下のようになります。
アセンブラ | mov %eax, (%rsp) |
---|---|
機械語 | 89 04 24 |
また、EAXからRBPのレジスタ間接で書き込む場合は以下のようになります。
アセンブラ | mov %eax, (%rbp) |
---|---|
機械語 | 89 45 00 |
実行すると図4.2の様に、ランダム色の描画により、ノイズ画像のようなものが表示されます。(これも白黒印刷なのでわかりにくいですが)