OSが書けなくても画面にお絵描きがしたい

PCでお絵描きがしたい、けど…

冬です

外は寒いのでこたつに入ってPCでお絵描きでもしたいところです

しかし、筆者のパソコンはOSもペイントソフトも入っておらずお絵描きをすることができません

どうしてもお絵描きがしたいので、今日は頑張ってPCの画面にお絵描きできるようにします

この記事はWORDIAN Advent Calendar 2018 - 23日目の記事です

戦略

さて、PCを動かすためにまずはブートローダが必要です。ブートローダBIOSの仕様に沿って実装する訳ですが、アセンブリを書いてプログラムがメモリの0x7c00にロードされ...32bitプロテクトモードに移行して...のように非常に難解なことをしないといけません。冬が終わってしまいます。世の中は便利なものでGRUBと呼ばれる素晴らしいブートローダが存在するため、ありがたく利用することにします。GRUBはよろしくやってくれた後にフレームバッファの場所を教えてくれるのでそこに色を書き込むことでお絵描きができます。

あ、GRUBを用意したりプログラムをコンパイルしたりするためのPCは別途用意することにしました。

GRUBを用意する

PCにGRUBをインストールします。ArchLinuxの人は

sudo pacman -S grub

で入ります。

GRUBで読み込めるプログラムの形式について

GRUBではmultibootと呼ばれるプログラムの形式を使用することができます。お絵描きするために最低限必要な事項は本記事で記述しますが、詳しい仕様についてはMultiboot Specification version 0.6.96をご確認ください。

Multiboot Specificationの3.1 OS image formatではプログラムの形式についての規定がされています。いろいろ書いてありますが、どうやらプログラムにはMultiboot headerが必要でファイルの先頭8192バイトに格納されている必要があるようです。3.2 Machine stateと3.3 Boot information formatではmultibootが読み込まれた後にGRUBが返してくれるマシンの状態に関する記述があります。これについても後述します。

Multibot headerの形式

Offset Type Field Name Note
0 unsigned 32bit magic required
4 unsigned 32bit flags required
8 unsigned 32bit checksum required
12 unsigned 32bit header_addr if flags[16] is set 今回は使わない
16 unsigned 32bit load_addr if flags[16] is set 今回は使わない
20 unsigned 32bit load_end_addr if flags[16] is set 今回は使わない
24 unsigned 32bit bss_end_addr if flags[16] is set 今回は使わない
28 unsigned 32bit entry_addr if flags[16] is set 今回は使わない
32 unsigned 32bit mode_type if flags[2] is set
36 unsigned 32bit width if flags[2] is set
40 unsigned 32bit height if flags[2] is set
44 unsigned 32bit depth if flags[2] is set
  • magic
    マジックナンバーです。必ず0x1BADB002を格納します。

  • flags
    bit 0,1,2,15を使います。bit 2を1にするとビデオモードを設定することができます。今回はとりあえず1にしましょう。 bit 0,1,15はそれぞれ1,1,0にしておきます。お絵描きに関係ないので説明は割愛します。

  • checksum
    チェックサムです。-(magic+flags)を格納します。

  • mode_type
    ビデオモードの希望です。グラフィックモード(0)かテキストモード(1)のどちらかに設定することができます。お絵描きするにはグラフィックモードのほうが嬉しいので今回は0に設定しておきます。

  • width,height
    画面のサイズの希望を設定することができます。お絵描きにおけるキャンパスサイズとなります。とりあえず最初はwidthを640、heightを480くらいにしておくことにします。

  • depth
    ビット深度の希望です。32にします。

  • その他
    今回は使わないため説明は割愛します。値は適当に0にしておきましょう。

multibootが読み込まれた後のマシンのレジスタの状態

色々と記述がありますがとりあえずEBXとついて気にしておけば良さそうです。

GRUBから渡されるブート情報について

multibootがGRUBに読み込まれるとEBXにブート情報構造体のポインタが格納されます。お絵描きをするために必要なフレームバッファの情報もあります。

             +-------------------+
     0       | flags             |    (required)
             +-------------------+
     4       | mem_lower         |    (present if flags[0] is set)
     8       | mem_upper         |    (present if flags[0] is set)
             +-------------------+
     12      | boot_device       |    (present if flags[1] is set)
             +-------------------+
     16      | cmdline           |    (present if flags[2] is set)
             +-------------------+
     20      | mods_count        |    (present if flags[3] is set)
     24      | mods_addr         |    (present if flags[3] is set)
             +-------------------+
     28 - 40 | syms              |    (present if flags[4] or
             |                   |                flags[5] is set)
             +-------------------+
     44      | mmap_length       |    (present if flags[6] is set)
     48      | mmap_addr         |    (present if flags[6] is set)
             +-------------------+
     52      | drives_length     |    (present if flags[7] is set)
     56      | drives_addr       |    (present if flags[7] is set)
             +-------------------+
     60      | config_table      |    (present if flags[8] is set)
             +-------------------+
     64      | boot_loader_name  |    (present if flags[9] is set)
             +-------------------+
     68      | apm_table         |    (present if flags[10] is set)
             +-------------------+
     72      | vbe_control_info  |    (present if flags[11] is set)
     76      | vbe_mode_info     |
     80      | vbe_mode          |
     82      | vbe_interface_seg |
     84      | vbe_interface_off |
     86      | vbe_interface_len |
             +-------------------+
     88      | framebuffer_addr  |    (present if flags[12] is set)
     96      | framebuffer_pitch |
     100     | framebuffer_width |
     104     | framebuffer_height|
     108     | framebuffer_bpp   |
     109     | framebuffer_type  |
     110-115 | color_info        |
             +-------------------+

色々ありますが必要な部分はオフセット88からのフレームバッファに関する情報のみです。

Offset Type Field Name
88 unsigned 64bit framebuffer_addr
96 unsigned 32bit framebuffer_pitch
100 unsigned 32bit framebuffer_width
104 unsigned 32bit framebuffer_height
108 unsigned 32bit framebuffer_bpp
109 unsigned 32bit framebuffer_type
110-115 unsigned 32bit color_info
  • framebuffer_addr
    フレームバッファの先頭のアドレスです。64bitです。注意

  • framebuffer_pitch
    バイトのピッチです。単位はバイト

  • framebuffer_width , framebuffer_hight GRUBが設定したフレームバッファの縦横のサイズです。

  • framebuffer_bpp GRUBが設定したフレームバッファのビット深度です。

  • framebuffer_type GRUBが設定したフレームバッファの種類です。0だとインデックスカラー、1だとダイレクトカラー、2だとテキストモードになっています。また、0,1ではこの後のcolor_infoが使用されます。

  • color_info
    framebuffer_typeが1(ダイレクトカラーの場合)

             +----------------------------------+
     110     | framebuffer_red_field_position   |
     111     | framebuffer_red_mask_size        |
     112     | framebuffer_green_field_position |
     113     | framebuffer_green_mask_size      |
     114     | framebuffer_blue_field_position  |
     115     | framebuffer_blue_mask_size       |
             +----------------------------------+

32bit中に各色がどのように配置されるかが格納されています。positionが下位ビットから何bit目からかを、mask_sizeがビットの幅を表しています。 今回はダイレクトカラーに設定されることを前提とするため、インデックスカラーの場合の説明は割愛します。

実装

Multiboot Specificationの4.3 Example OS codeにはサンプルコードがあります。せっかくなので利用します。

プリプロセッサ定義や構造体が含まているmultiboot.hはそのまま使います。

multiboot.h (クリックで展開)

boot.SではMultiboot headerを配置してC言語コンパイルした関数を呼びます。サンプルから不要な部分を削除して最小限必要な部分のみを残しました。

画像データを描画するCプログラムです。ダイレクトカラーでdepthが32bitに設定されていることが前提です。とりあえず画面を緑色で塗りました。

コードが書けたら

$ gcc -m32 -ffreestanding -fno-common -fno-builtin -fomit-frame-pointer -O2 -c -o oekaki.o oekaki.c
$ gcc -m32 -ffreestanding -fno-common -fno-builtin -fomit-frame-pointer -O2 -c -o boot.o boot.S
$ ld  -melf_i386  -Ttext=0x100000 --oformat elf32-i386 -o Oekaki boot.o oekaki.o

のようにコンパイルとリンクを行います。テキストエディタgrub.cfgも作成しましょう。

menuentry "oekaki" {
    multiboot /boot/Oekaki
    boot
}

grub-mkrescueでブートイメージを作成します。

$ mkdir -p iso/boot/grub
$ mv Oekaki iso/boot
$ mv grub.cfg iso/boot/grub
$ grub-mkrescue -o oekaki.iso iso

作成したプログラムのブートイメージが作成されました。試しにqemuでブートしてみましょう。 f:id:akkkix:20181224021228p:plain

これでフレームバッファの中身を画面に出せるようになりました。後はソースコードを適当にいじれば画像等も表示することができます。

めでたしめでたし