見出し画像

"Hello World!" が内部でホントにしていること

"Hellow World!" を表示するだけのプログラムのつまらなさ。

的な話を最近見て、ふと思ったことが。そりゃ "echo Hello World!" 的なことを各プログラミング言語で書いたとして、面白味はたしかにないな、なんて思ってしまう。C言語で、ぱっぱと書いてしまうと、以前にもお見せしたように。

#include <stdio.h>
int main(int argc, char **argv) {
   printf("Hello World\n");
}

こんなもんで済んでしまう。スクリプト言語だと、例えば Node.js ではこんなもん。

#!/usr/bin/env node
console.log("Hello World!");

1行目のおまじないを放置したら、ほんの1行。C言語だって、`main` 関数が呼ばれないと実行されないという仕様なだけで、実質は "stdio" ライブラリの `printf` 関数に食わせてるだけ。こんなの、そりゃ楽しくはない。文字出力の標準的な使い方(方言)を知ることができる、という程度にしかならない。

コンピュータというものは、一般的に人の代わりに何かを制御するものであって、「インプット-処理-アウトプット」を司ってます。この "Hello World!" プログラムは、そもそもインプット部分もなければ、処理している部分もほとんどないので、面白みにかけるという点については、大いに同意。とは言え、内部的にこの "printf()" 関数が処理をしているんだとかいう話であれば、それはまた話は別。

ところで。この "Hello World!" がコンピュータ上で何をしているのか、を正しく理解できているかどうかで言ったら、このプログラムからだけでは、まったく理解できないだろうと思う。コンピュータさんが文字列を勝手に認識して、画面に勝手に表示しているわけではなく、実際はライブラリや言語のランタイムがよしなにやってくれているだけです。

そこで色々考えた結果、アセンブラで書いてみれば、コンピュータ上で何が行われているか、より正確に理解できるのではと思い、Qiita を漁ってみました(8086系のアセンブラはね、苦手なの、ごめんね。読めるだけなの)。

OS に影響を受けると、OS のライブラリやデバイスドライバの影響を受けるので、簡単になりすぎてあれだなと思って探してみたら、ブートストラップで表示するという、鬼な内容を拝見して悶え死にました。こちら。

いいですね、こういうの大好きです。ということで、ソースコードを拝借します。

; hello.asm
[BITS 16]           ; リアルモード
[ORG 0x7C00]        ; 開始位置

start:
mov   si, msg       ; msg の先頭位置を SI レジスタに設定する。
call  print_string

fin:
hlt
jmp   fin

print_char:
mov   ah, 0x0E      ; BIOSに一文字表示を伝える
mov   bh, 0x00
mov   bl, 0x07      ; 文字色(白)

int   0x10          ; BIOSの機能を呼び出す。Call video interrupt.
ret

print_string:
next:
mov   al, [si]      ; 文字列から一文字を取得し AL レジスタに設定する
inc   si            ; SI レジスタをインクリメントする
or    al, al
jz    exit
call  print_char
jmp   next
exit:
ret

msg   db 'Hello World!', 0

times 510 - ($ - $$) db 0 ; ブートシグニチャまで0埋めする
dw 0xaa55                 ; ブートシグニチャ

うん。素晴らしい。

ブートストラップ固有の部分があるにせよ、メイン部分はそこから排除して考えられます。開始位置や最後のブートシグネチャ用のおまじないはプログラムの実態とは外して考えてみましょう。そうすると "start:" ラベルから "print_string:"ラベルのサブルーチン後の "msg db 'Hello World!', 0" までが、表示するためのメインルーチンです。紐解きましょう。

"start:" ラベル内では、"print_string:" ラベルではじまるサブルーチンをコールしているだけです。実態を伴ってはいません。

"print_string:" ラベルからのサブルーチンは、"SI" レジスタで指示された文字列 → "msg" 部分の 'Hello World!' の最初の文字の位置を示しています。このメモリ上に配置された一文字を "AL" レジスタへ読み込みます。この時、メモリから CPU内のレジスタへデータがロードされた状態になります。CPU ってのは厄介なもので、メモリ上にあるものを、そのまま純粋に処理することは苦手です。苦手ですというか、メモリがおそすぎるので一般的に CPU内のレジスタへストアするのが決まりみたいなものです。今の時代でもそうだと思います(よく知らんのだけど 680x0系はメモリダイレクト処理できて、86形はできなかったという記憶。最近はどうなの)。

そして、読み込んだら、"SI" レジスタの示すアドレス番地(位置) を次の文字へ移します。次の "or al, al" が謎仕様に見えますが、分岐のための演算を行っています。"AL" レジスタには、メモリ上からロードされた一文字の文字コードが入っています。"msg" 部分をよく見てみると分かるように 'Hello World!' のあとに ", 0" とあります。これは、文字列の最後に "0" というバイトデータを保持しています。ここで、"or al, al" に戻ってみましょう。"or" とは論理演算子の "OR" です。細かい説明はしませんが、"AL" レジスタが '0' のときだけ演算結果が '0' になります。1bit でも符号が立っていれば必ず "0以外" になります。"OR" ってそういうやつです。

この演算結果は、CPU の持つフラグレジスタに反映されます。例えば演算結果が "0" になれば "ZF" というゼロフラグレジスタの bit が立つ、という仕組みです。このフラグレジスタを利用して、条件付き判断が可能になります。高級言語でいう、いわゆる "if文" です。

このフラグレジスタの状態を維持しつつ、次の "jz exit" に進みます。これは "ZF" フラグレジスタの bit が立っていたら、"exit" のラベルへジャンプせよという命令です。要するに直前の演算結果が 0 なら "exit" 部分へ飛べってことです。まだ "Hello World!" のいずれかの文字のバイト情報を持っていれば、ここはスルーで、文字列が終了したら "exit" へ飛べということです。"exit" の先は "ret" となっていて、呼び出し元へ戻りましょうとなっています。

なお、"call" と "jump" の違いについては呼び出し元に戻れるか戻れないかの違いです。適当に調べてください。

文字のバイトコードが入っている場合には "call print_char" ラベルをコールします。 "print_char:" ラベルを見てみると

mov   ah, 0x0E      ; BIOSに一文字表示を伝える
mov   bh, 0x00
mov   bl, 0x07      ; 文字色(白)

となってます。この辺はおまじないです。BIOS コールをするときに、必要なレジスタに情報を入れていると理解すればよいでしょう。"ah" は一文字出力を意味しているようです。"bl" レジスタは文字の色を示しています。0x00〜0x07 の8色相当ぽいですね。"bh" レジスタとこは、ビデオページ番号だそうです。 0x00 で 0枚目(1枚目)を指定しているんでしょうが、86系のBIOSはよく知らないのでごめんなさい。そして "al" レジスタに出力する文字列(アスキーコード)が入っていれば良いようです。これらのレジスタに値をセットして、次の "int 0x10" というシステム(BIOS)コールを行うと、画面に文字が表示されるという仕組みです。

これを文字列のある間繰り返し、最後に "exit:" ラベル部分へ戻ると "ret" されて "fin:" ラベル以後の終了処理(hlt) が実行されて、プログラムが終了します。

そうです、文字を表示するだけでもこんなにやってるんです。長い。

と思ったあなた、まだ甘いです。だって、これシステムコール使っているじゃないですか。文字を画面に表示する部分について、BIOS にすべてをお願いしちゃっています。この部分、どのようにして文字を表示しているかが抜けちゃっていますね。とは言え、今の 86系のコンピュータではさまざまなVIDEOカードが乗っかっていて、BIOS などで抽象化してあげないと、まぁ厳しいです。86系ではVIDEOカードに直接文字を表示するように命令を出すのは、ちょっと至難の業ではないかと推測します(よく知らない)。

ということで、わが青春の X68000 時代に戻ります。VIDEO 用の RAM仕様は公開されていましたので、個人のプログラマやゲーム会社のみなさま方は、VRAM をダイレクトに叩きまくって、いかに高速に画面を描画するかに力を入れていました。"hiocs.x" なんてその最たるものですね。DOSコールしているすべての描画を速くしてくれる天才的なデバイスドライバでした。

中身でやっていることの闇の深さについては、ここでは語りませんが、いかに高速にVRAMへデータを転送するかのお勉強するには、最高のプログラムです。CPUキャッシュなし68000MPU とCPUキャッシュあり68030MPU(68020以降)とでVRAMへのデータ転送方式の考え方が、まったく違うのでCPUキャッシュの考え方にも繋がって素晴らしすぎるんですが、ここで書くには紙面が短すぎる。

ということで、X680x0 最強の内部仕様書、ここで「inside X68000」です。こちらのp.20〜22あたりにグラフィック用のVRAM($C00000〜$DFFFFF)とテキストVRAM($E00000〜$E7FFFF)が割り当てられていることが分かります。

X68000の画面構成については、こちらから見られますので、こちらもどうぞ。

とりあえず、文字を表示するにはテキストVRAMへダイレクトに吐き出せば良さげです。よく読むと分かるんですが、X68000 のテキストVRAMは文字を表示させるにはちょっと難解で、テキスト画面なのにどっとコントロールが可能な仕様となっています。さらには、4面のテキスト画面を持っていて、それらを重ね合わせることによって、各ドットの色が制御される(4^2 = 16色)仕組みとなっています。頭おかしい。なので、なんとなくテキスト用のVRAM へ文字コードを配置すれば、それだけで文字が表示できるんではと思った諸氏、世の中はそんなに甘くありませんでした。

ただこのテキストVRAM。文字を高速に表示する機能にはかろうじて特化していて、1ワード(=16bit) に bit を定義してテキストVRAM へ転送すると、横16dot 分を1度に制御できるようになっています。これらを各テキスト画面(=4面)に制御すれば色も制御できれば、ドットコントロールも可能なテキスト画面に、文字を配置することができる仕組みです。なので 4度 VRAMへ書き込みすれば、16dot分済ませることができ、1dot = 1ワードなグラフィックVRAM よりも最低でも4倍高速(複数ページ同時書き込み制御も可能なので、最大16倍)に制御可能となっています。固定化されたフォントではなく、好きなフォントを表示することもできるので、PC9801やPC/AT などのような固定ROMフォントしか利用できなかったMS-DOS(DOS/Vより前)よりも先進的でした。というか、頭おかしい。

では、そんな X68000 。文字フォントはどうしていたかというと、先程のメモリマッピング上に "CGROM($F00000〜$FBFFFF)" と呼ばれる領域があります。

この "CGROM"に 8x8〜24x24までの6種類(英語4種、2バイト漢字2種)の文字フォントデータが含まれています。ですから、文字を表示するに当たっては、まず文字フォントのbit構成情報を読み出してから、テキストVRAM へ文字情報を書き出すという、実におせっかいでややっこしい仕組みになります。その代わり表現度が高いという X680x0 独特のアーキテクチャの考え方があります。ほんとに独特かどうかは、他と比較したことないので知りません、ごめんなさい。

ということで、ソースコード準備しようとしたら "hiocs.x" くらいしかこのへんまで載せているソースコードに巡り会えず、自分の作ったコードはもうパージされてしまったので、今日は諦めます。やり方としては、文字コードや文字サイズに合わせて、"CGROM" からフォントデータを読み込み、それをテキストVRAMへ出力するという流れになります。

過去のソースコード殲滅されてるの、ホント悲しいけど、今は必要ないと考えて前向きに生きていくことにするとともに、ほんの "Hello World!" を表示するだけでも、実態のコンピュータはたくさんの色々な処理をしていることを理解いただければ幸いです。

ちなみに、C言語にはじまるライブラリやランタイム絡みでいくと、エラー処理含めて内部処理はとんでもないレベルで、もっと多くのことをやっています。なので、アセンブラでマシン語化するとほんの数十KBでも、C言語でコンパイルすると数百KBとかになるのは、そのへんの差です。

貴方がサポートしてくれると、私が幸せ。 私が幸せになると、貴方も幸せ。 新しいガジェット・ソフトウェアのレビューに、貴方の力が必要です。