やりたいこと
始めに動作結果から見せます。
※ノートパソコン実機での起動がどうしてもうまく行かなかったのでbochsという仮想環境で動作させています。
(仮想環境ではありますが、実質的には物理パソコンと同様に動作します。
つまり、起動直後にWindowsやLinuxが立ち上がることなく、いきなりピカチュウが表示される状態を再現しています。
今後、実家にある古いパソコンでの動作確認も予定しています。)
16bitモードでドット絵を表示
アセンブリのソース
次にこのピカチュウを描画するためのプログラムを以下に載せます。
org 0x7C00
bits 16
start:
; VGA 320x200 256色
mov ax, 0x0013
int 0x10
; ESへVRAMの開始アドレスを設定 (0xA000)
mov ax, 0xA000
mov es, ax
; ピカチュウ描画
call draw_pikachu
; CPU停止
jmp $
draw_pikachu:
; 0=白, 1=黒
mov si, pikachu_data
mov di, 100 * 320 + 152 ; 開始位置 (VGA画面: y=100, x=152)
mov cx, 16 ; 高さ16行
.y_loop:
push cx
mov cx, 16 ; 幅16列
.x_loop:
lodsb ; 画素をALレジスタへ読み込み
cmp al, 0
je .white ; AL=0: 白
cmp al, 1
je .black ; AL=1: 黒
jmp .next
.white:
mov byte [es:di], 15 ; 白
jmp .next
.black:
mov byte [es:di], 0 ; 黒
jmp .next
.next:
inc di
loop .x_loop
add di, 320 - 16 ; 改行
pop cx
loop .y_loop
ret
; ピカチュウ16×16ドット絵,0=白,1=黑
pikachu_data:
db 0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0
db 0,0,0,0,0,0,0,0,1,0,0,0,0,0,0,0
db 0,1,1,1,0,0,0,1,0,1,0,1,1,1,0,0
db 0,1,1,0,1,0,0,1,0,0,1,0,1,1,0,0
db 0,0,1,0,0,1,1,1,1,1,0,0,1,0,0,0
db 0,0,0,1,0,0,0,0,0,0,0,1,0,0,0,0
db 0,0,0,1,0,1,0,0,0,1,0,1,0,0,0,0
db 0,0,0,1,0,0,0,1,0,0,0,1,0,0,0,0
db 0,0,1,0,1,0,1,1,1,0,1,0,1,0,0,0
db 0,0,1,0,0,0,0,0,0,0,0,0,1,0,0,0
db 0,0,1,0,0,1,0,0,0,1,0,0,1,0,0,0
db 0,0,1,0,1,1,0,0,0,1,1,0,1,0,0,0
db 0,0,1,0,0,0,0,0,0,0,0,0,1,0,0,0
db 0,0,0,1,0,0,1,1,1,0,0,1,0,0,0,0
db 0,0,0,0,1,1,0,0,0,1,1,0,0,0,0,0
db 0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0
; 512バイトとなるように残りを0で埋める
times 510-($-$$) db 0
dw 0xAA55;ブートセクタの目印
コンパイル方法
nasm -f bin pikachu.asm -o pikachu.bin
bochsの設定ファイル
# Bochs 設定ファイル
megs: 32 # 32MB メモリ
romimage: file=$BXSHARE/BIOS-bochs-latest
vgaromimage: file=$BXSHARE/VGABIOS-lgpl-latest.bin
floppya: 1_44=pikachu.bin, status=inserted
boot: floppy # floppyから起動(実際は違う)
display_library: win32 # Windows はこの設定
log: bochs.log # ログ出力設定
※bochsはx86環境のエミュレータですが仕組み及び使用方法は他のサイトをご参考下さい。実体の無い仮想的なパソコンと考えて頂いて結構です。
コンパイルしたbinファイルを動かすためにbochsrc.txtという設定ファイルを用意する必要があります。
romimage、vgaromimageは実際にインストールしたbochsの中にあるファイルに合わせて設定して下さい。
起動
コマンドプロンプト等でpikachu.bin及びbochsrc.txtのあるフォルダへ移動し以下のコマンドを実行すると自動的にbochsが起動します。
bochs -f bochsrc.txt -q
プログラムを読むための予備知識
初めに
プログラムの目的
このプログラムの目的は、16ビットモードでピカチュウのドット絵をVGAモード(320x200、256色)で描画することです。
このプログラムはBIOSの割り込み(int 0x10)を使って、VGAモードに切り替え、VRAMにデータを書き込みます。
基本的な仕組みを簡単に説明しますが、より詳しく知りたい場合は検索してみてください。
ブートローダー(MBR)とは
パソコンの電源が入ると、最初にBIOSというプログラムが実行されます。BIOSはハードウェアの初期化や、起動する装置(HDD、USBメモリ、CD等)からOSを起動する準備を行います。
ブートローダーは、これらの記憶媒体の最初の512バイトに記録される特殊なプログラムです。BIOSは、ブートセクタが保存されている場所を確認し、そこからプログラムをメモリに読み込みます。BIOSは最初の512バイトのうち、最後の2バイトに0xAA55という目印が書かれていると、ブートローダーだと判断しその512バイトをメモリの0x7C00の位置に読み込みます。
BIOSは動作を終えると、0x7C00へ跳び、ブートローダーのプログラムを実行します。通常、ブートローダー内には、他のアドレスへ跳ぶ命令が記述されており、例えば、WindowsやLinux等のOSを起動します。しかし、このプログラムでは、512バイト以内で完結する小さなプログラムを作成しているため、ブートローダー内だけで動作します。
VGAモードとは
簡単に言うと、VGAモードは、PCがどのように画面に絵や文字を表示するかを決める基準のことです。この「決まりごと」のおかげで、PCが異なる種類のディスプレイでも同じように表示できるようになっています。
このような決まりがないと、各社がバラバラな仕様で製品を開発してしまい、「このソフトはA社の画面では動くけど、B社のでは真っ暗」みたいなことが起きてしまいます。
ピカチュウ画像の表現
ピカチュウの絵は16×16のドットで表現されています。ここでは、白を「0」、黒を「1」として、pikachu_data内で絵を定義しています。0と1の並びをじっと見ていると、ピカチュウの姿が浮かび上がってくる筈です。(浮き上がってこない場合は凝視したまま携帯を軽く振ってみて下さい。輪郭くらいなら何となく見えると思います)
パソコン上で文字を表示できるのも同じような原理です。
VRAMとは
VRAMとはGPU内に搭載されている専用メモリです。このメモリは、画面に表示するためのデータを格納し、GPUがそれを処理して映像を描画します。
通常のメインメモリ(RAM)と同じように、特定のアドレス範囲がVRAMとして割り当てられており、このプログラムでは0xA000番地がそれに該当します。ここに直接データを書き込むことで、画面上にピクセルが表示される仕組みです。
例えば、VRAM内の特定の領域は画面の各ピクセルに対応しており、プログラムがその位置にデータを書き込むと、画面にその内容が表示されます。これにより、VRAMのデータが最終的に画面に映像として描画される仕組みです。
※メモリのアドレスは連続しているように見えますが、実際にはメインメモリだけでなく、BIOSやVRAMといった異なる領域が存在し、全てがメインメモリな訳ではありません。
BIOS割り込みとは
16ビットモードで開発する場合、BIOSが予め用意している機能(=割り込み)を呼び出すことで、ハードウェア制御の手間を大幅に省くことができます。
これは高級言語におけるライブラリのような存在です。
今回使用した int 0x10
は、画面に文字やピクセルを表示するための割り込みです。
これを使わずに自分で描画する場合、VGAの仕様書を読み、GPUのレジスタを直接操作しなければならず、設定の間違いや表示エラーも起きやすく、「画面に文字を出すだけで一日かかる」なんてことも起こり得ます。
CPUが int 番号
命令を実行すると、BIOSはその番号に対応するアドレスに跳びます。
この「割り込み番号 → 処理アドレス」
の対照表が、割り込みベクタテーブル(IVT)
です。
例:int 0x10 の場合 (仮!)
割り込み番号 | IVT上の位置(仮) | 格納されているアドレス(仮) |
---|---|---|
0x10 | 0x10 × 4 = 0x40 番地 | 0xF000:0xFEA5(画面表示の処理先) |
ピクセル位置 (x, y) →VRAMアドレスへの変換式
VRAMにおける「画面上のピクセル位置 (x, y)」は、実際にはメモリの「アドレス」として管理されています。簡単に言うと、画面の左上のピクセル((0, 0))から始まり、右へ、下へと、1ピクセルずつメモリ上で順番に並んでいます。
例えば、VGAモード13h(320x200の256色モード)の場合、画面は320列、200行のピクセルで構成されています。この場合、メモリ上では1行あたり320バイトを使って、200行分のピクセルが順番に並ぶわけです。
変換式
ピクセル位置 (x, y) をVRAMアドレスに変換する式は以下の通りです:
$$
\text{アドレス(VRAM)} = 0xA000 + (y \times 320) + x
$$
例
例えば、(x = 5, y = 10) のピクセルのアドレスを計算する場合:
アドレス = 0xA000 + (10 * 320) + 5
= 0xA000 + 3200 + 5
= 0xA000 + 3205
= 0xA00D
このように、(x = 5, y = 10) のピクセルのVRAMアドレスは 0xA00D になります。
y×320で改行を表現しています。
16ビットモード メモリ分布
アドレス範囲 | 用途 | 補足 |
---|---|---|
FFFFF~F0000 | BIOS ROM領域 | POSTやINT 13hなどがここにある |
・・・ | ||
BFFFF~B8000 | VGAテキストモードVRAM | 文字表示 |
・・・ | ||
AFFFF~A0000 | VGAグラフィックモードVRAM | 自由に描画 |
・・・ | ||
9FBFF~07E00 | 使用可能 | |
07DFF~07C00 | ブートセクタ読み込み先 | BIOSがMBRをここに読み込む |
07BFF~00500 | 使用可能 | |
・・・ | ||
0x03FF~0x0000 | 割り込みベクタテーブル(IVT) | 256個×4バイトのINTアドレス表 |
コードの具体的な説明
org 0x7C00
アセンブラに対してこのプログラムは0x7C00に読み込まれるということを教えています。
mov ax, 0x0013
int 0x10
レジスタaxに0x0013を設定し、BIOS割り込み0x10を呼び出すことで
解像度320x200、256色を設定できます。
この2行の命令によって、PCはVGAモード13h(320x200、256色)に切り替わり、グラフィック表示のための準備が整います。その後、画面にドット絵などを描画することが可能になります。
(理由は説明できません。そういう決まりなのです。知りたければ当時のIBM技術者へ何故このように決めたのか聞くしかありません。)
mov ax, 0xA000
mov es, ax
VGAモード13h(320x200、256色)の場合はVRAMの領域が0xA000から始まります。
esにVRAMの開始位置を設定します。
; CPU停止
jmp $
ドル記号 $ は「現在実行中のアドレス」を意味します。
つまりこの命令は「自分自身に跳べ」という意味になります。
結果としてCPUはこの場所に留まり続け、プログラムの実行が無限ループ状態になります。
CPUを停止させないと実行を続けてしまい暴走します。
仮に jmp $ を入れなかったとすると、CPUはその後ろにある ピカチュウの画像データ や、文字列、何かの設定値などを「命令」と誤解して処理を続けます。そして当然意味不明な命令ばかりなので、フリーズしたり、暴走したり、最悪再起動がかかったりします。
CPUからすると「ここからがコードで、ここからがデータ」といった区別は一切つきません。
全てはバイナリ列(0と1の羅列)でしかなく、命令とデータを区別する知恵はCPUにはないのです。
draw_pikachu:
; 0=白, 1=黒
mov si, pikachu_data
mov di, 100 * 320 + 152 ; 開始位置 (VGA画面: y=100, x=152)
siにはpikachu_dataの開始位置(左上の'0'が入っているアドレス)を設定します。
diにはVRAM上でピカチュウを表示する位置を設定します。(ピカチュウの左上の画素を格納するVRAM上の位置)
.y_loop:
push cx
mov cx, 16 ; 幅16列
.y_loopは「縦方向のループ」で、画像の高さを16行として描画しています。最初にcxレジスタに16を設定して、1行ごとに描画します。
push cxは、cxレジスタの内容を保存しておくためです。後で復元するために必要です。
※ loop
命令は、cx
レジスタの値を1減らし、ゼロでなければ指定されたラベルにジャンプします。
.x_loop:
lodsb ; 画素をALレジスタへ読み込み
cmp al, 0
je .white ; AL=0: 白
cmp al, 1
je .black ; AL=1: 黒
jmp .next
.x_loopは「横方向のループ」です。1行のピクセルを1つずつ処理しています。
lodsbは、siレジスタが指すメモリ(画像データ)から1バイトをalレジスタに読み込みます。このalにはピクセルの色が格納されます。
al == 0の場合は「白」(VRAMの値15)。
al == 1の場合は「黒」(VRAMの値0)。
cmp al, 0 と cmp al, 1 で、alの値に応じて、白か黒かを判定します。
.white:
mov byte [es:di], 15 ; 白
jmp .next
.black:
mov byte [es:di], 0 ; 黒
jmp .next
whiteとblackのラベルでは、それぞれのピクセルをVRAMに書き込んでいます。
mov byte [es:di], 15 は、VRAM(es:di)の現在の位置に白色(15)を設定します。
mov byte [es:di], 0 は、黒色(0)を設定します。
.next:
inc di
loop .x_loop
.nextは、次のピクセルに移動する部分です。di(VRAM内での位置)を1つ進めます(inc di)。
loop .x_loopで、横方向の処理を16回繰り返します。
add di, 320 - 16 ; 改行
pop cx
loop .y_loop
ret
1行のピクセルを処理した後、add di, 320 - 16 で次の行に移動します。320 - 16で、1行あたり16ピクセルの幅だけ移動します(行末の改行分)。
pop cxで保存していたcxを復元します。
loop .y_loopで縦方向(行数)の処理を16行繰り返します。
; 512バイトとなるように残りを0で埋める
times 510-($-$$) db 0
dw 0xAA55;ブートセクタの目印
全体が512バイト、最後がAA55でないとBIOSが起動してくれません。
バイナリエディタで開くと最後2バイトがAA55となっていることが分かります。
ファイルの大きさは512バイトです。全体で512バイトになるように0で埋めるのです。
最後に
説明不足な点もたくさんあるかと思います。
間違い等あればご指摘頂けると幸いです。
技術書を読むときに説明が分かり辛い点が多く、なぜもっと上手く説明ができないのかとイライラすることが良くありますが、自分で書いてみて説明の難しさに気付きました。
参考文献
①李忠 (2023) 『x86彙編語言 從實模式到保護模式 』 電子工業出版社.