寿 司 を 回 す
この記事はFUN Advent Calendar 2016の15日目の記事です。
www.adventar.org
昨日の担当はRuby大好きマンことなかさんでした。
ponpon-pain.hatenablog.com
本題
皆様回転寿司はご存知でしょうか。
そう、ターミナル上で回すあの寿司です。
n番煎じ pic.twitter.com/vOg7m1ygc9
— 西崎芽依 (@Tkon_sec) September 21, 2016
OS上で動くターミナルでなら、OSが提供するライブラリを扱えるので、C言語なり最近のイケイケな言語で書けます。
でもそれじゃ面白くないし、寿司を回すためだけにOS立ち上げるのはちょっと勿体無いですよね。
やはり電源を入れれば寿司が回っている状態が望ましいのではないでしょうか。
そういうわけで、今回はOSの力を借りずに寿司を回してみます。
アセンブリ入門
アセンブリとは、機械語を人間にわかりやすい形にしたものです。低水準言語なので、コンピューターの動作を直接書いたものに近いです。
基本的には「メモリに対してどのような処理をするか」をひたすら書いていく感覚です*2。
例として
mov ax, 0x07c0
は、axに値0x07c0を格納します。
axとは、CPU内のレジスタと呼ばれるメモリの名前です。
この辺りが気になった方は是非アセンブリを勉強してみてください(そして僕に教えてください)。
参考までに、Hello,worldはこうなるみたいです。
section .text global _start ;must be declared for linker (ld) _start: ;tell linker entry point mov edx,len ;message length mov ecx,msg ;message to write mov ebx,1 ;file descriptor (stdout) mov eax,4 ;system call number (sys_write) int 0x80 ;call kernel mov eax,1 ;system call number (sys_exit) int 0x80 ;call kernel section .data msg db 'Hello, world!',0xa ;our dear string len equ $ - msg ;length of our dear string
大抵の言語はHello,worldの書き方はだいたい同じになると思いますが、アセンブリはいくつか種類があります。また、実行する環境によっても変わる点があるらしいです。
上記はLinux上で動作するHello,worldとのことです。
int 0x80という命令でLinuxのシステムコールを呼び出し、文字を出力したりアプリケーションを終了しています。ココが所謂「OSの力を借りている」部分になります。
OSの力を借りないプログラム
ではOSの力を借りないで寿司を回すためにはどうすればよいでしょうか。
目的は「電源を入れれば寿司が回っている」状態にしたいわけです。
PCは電源が投入された直後にOSが読み込まれるわけではないのはご存知ですよね。
PC/AT互換機の大雑把な順番は以下のとおりです。
1, BIOS*3が立ち上がる
2, ハードウェアの初期化が行われる
3, ドライブの先頭512B、通称MBR(Master Boot Record)がメモリにロードされる
4, MBR内のプログラムに従ってOSがロードされる
5, OS側の処理
流石にBIOSは飛ばせませんね。
OSの前で実行したいんだから、MBR領域に入れておけばハードウェアの初期化終了直後に実行してくれますね。
というわけでMBR領域に入れておけば動く回転寿司プログラムを作りましょう。
MBRでHello,world
mov ax, 0x07c0 mov ds, ax mov ah, 0x0 mov al, 0x3 int 0x10 mov si, msg mov ah, 0x0E print_character_loop: lodsb or al, al jz hang int 0x10 jmp print_character_loop msg: db 'Hello, World!', 13, 10, 0 hang: jmp hang times 510-($-$$) db 0 db 0x55 db 0xAA
MBR上でHello,worldを表示させるだけでこれだけ長くなります。内容としては、「msgに入ってる文字を一つずつ表示、最後の文字まで来たらハングアップさせる」というものです。
注目すべきは後ろの最初の2行と後ろの3行で、これらがこのプログラムをMBRプログラムであると宣言しているようなものです。
簡単に言えばおまじないです。
さて、さきほどLinux上でのHello,worldを見せたとき、int 0x80でLinuxのシステムコールを呼んでいると説明しました。ではこのMBR領域に入れると文字列を出力するプログラムはどのようにして文字を表示しているのでしょうか。
答えはint 0x10で、これはBIOS Interrupt callと呼ばれるものです。対応する動作をBIOSに命令しています。
BIOS interrupt call - Wikipedia
下の方に一覧が載っています。int 0x10だけではなく、様々な命令があることがわかりますね。0x10は画面に関わる命令のようです。
int 0x10の欄を見てみると、AHの値とそれに対応した処理が書いてあります。これはどういうことかというと、「int 0x10を実行すると、AHの値に対応した命令がBIOSによって実行される」ということです。
Cや多くの言語では引数を取ってそれに対応した処理をするのが多いので、なかなかイメージが沸かないかもしれませんが、この辺りがアセンブリ特有の面白さだと僕は思います。
今回のプログラムの場合、print_character_loop:の前でAHを0x0Eに設定しています。先程の表によると、Write Character in TTY Modeとありますね。つまりは画面に文字を表示させる命令みたいです。
でも、画面に文字を出力させる命令ならばその文字も指定出来ないとおかしいですよね。もう少し調べてみましょう。
INT 10H - Wikipedia
英語Wikipediaはすごいですね。リファレンスじゃないですか。
0x0Eを見ると、AL = Character, BH = Page Number, BL = Color (only in graphic mode)と細かく説明が載っていますつまりは
mov ah, 0x0e mov al, 'a' int 0x80
とすると、aが出力されるとのこと。あとは必要に応じてページ番号と色が設定できるらしいです。
だいぶ回転寿司に近づいてきました。
あと寿司を回すために必要なのは、「画面のクリア」と、「任意の場所に文字を出力する方法」と、ループですね。
ループは割愛します。気になった方は調べてみてください。
さて、画面のクリアも任意の位置への出力も、どちらもint 0x10の中にあります。
任意の位置への出力は、カーソル移動を使えばなんとかなりそうです。
AHに0x02を入れて、BLにページ番号、DHとDLにそれぞれ行と列を入れれば良いらしいです。
画面クリアについてはわからなかったので、0x00のビデオモード設定で代用します。
というわけで、初心者が頑張って書いた「MBR領域にかきこんでブートすると寿司が回るプログラム」が以下です。
; sushi.asm section .data loop_count db 0x00 section .text ; setup mov ax, 0x07c0 mov ds, ax ; clean display mov ah, 0x0 mov al, 0x3 int 0x10 mov cx, 0x0 print_sushi_loop: ; set cursor position mov al, [loop_count] add al, cl cmp al, 33 js cur_set sub al, 32 cur_set: cmp al, 28 jns LEFT cmp al, 16 jns UNDER cmp al, 12 jns RIGHT ; TOP mov dl, al mov al, 2 mul dl mov dh, 0 ; row mov dl, al ; column jmp SUSHI RIGHT: sub al, 11 mov dh, al ; row mov dl, 22 ; column jmp SUSHI UNDER: mov dh, 5 ; row mov dl, 27 ; column sub dl, al mov al, 2 mul dl mov dl, al jmp SUSHI LEFT: mov dh, 32 ; row sub dh, al mov dl, 0 ; column SUSHI: mov ah, 0x02 mov bh, 0x00 ; page int 0x10 ; print sushi mov ah, 0x0e mov bx, cx mov al, [sushi+bx] int 0x10 add cx, 1 ; if cx==5 cmp cx, 5 jnz sub_skip ; wait loop push cx mov cx, 0 ffor_begin: push cx mov cx, 0 for_begin: add cx, 1 cmp cx, 0xFFFF jnz for_begin pop cx add cx, 1 cmp cx, 0x02FF jnz ffor_begin pop cx add cx, 1 push cx mov cx, 0 ; counter and display clear mov ah, 0x00 mov al, 0x02 int 0x10 mov dl, [loop_count] add dl, 0x01 cmp dl, 32 jnz no_reset mov dl, 0 no_reset: mov [loop_count], dl sub_skip: jmp print_sushi_loop sushi: db 'sushi' times 510-($-$$) db 0 db 0x55 db 0xAA
実行すると動画のようになります。
— 西崎芽依 (@Tkon_sec) 2016年12月15日
画面クリアを代用したせいか、ひたすら点滅していて目に悪い感じです。
参考