読者です 読者をやめる 読者になる 読者になる

#temanote

Linux大好き。だけど進捗ありません。

寿 司 を 回 す

この記事はFUN Advent Calendar 2016の15日目の記事です。
www.adventar.org


昨日の担当はRuby大好きマンことなかさんでした。
ponpon-pain.hatenablog.com


自己紹介

てまーと呼ばれている者です。
現在学部1年で、いろいろあってとある研究スペースにお邪魔させていただいています。

Linuxが好きで、趣味でよく環境を構築したり吹き飛ばしたりしています。

本題


皆様回転寿司はご存知でしょうか。

そう、ターミナル上で回すあの寿司です。


OS上で動くターミナルでなら、OSが提供するライブラリを扱えるので、C言語なり最近のイケイケな言語で書けます。
でもそれじゃ面白くないし、寿司を回すためだけにOS立ち上げるのはちょっと勿体無いですよね。

やはり電源を入れれば寿司が回っている状態が望ましいのではないでしょうか。

そういうわけで、今回はOSの力を借りずに寿司を回してみます。


今回はアセンブリ言語*1で書きます。ぼくは初心者ですので、ネット上にあるあらゆる情報を駆使して寿司を回します。

アセンブリ入門

アセンブリとは、機械語を人間にわかりやすい形にしたものです。低水準言語なので、コンピューターの動作を直接書いたものに近いです。
基本的には「メモリに対してどのような処理をするか」をひたすら書いていく感覚です*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

asm.sourceforge.netより引用


大抵の言語は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

実行すると動画のようになります。


画面クリアを代用したせいか、ひたすら点滅していて目に悪い感じです。



参考

まとめ

今回は未来大生らしからぬ感じで、BIOSで動く回転寿司を作ってみました。
僕自身アセンブリ書くの初めてだったのでおかしい点も多々あるとは思いますがご容赦ください。

これで少しでも未来大に低レイヤーに興味を持ってくれる人がいると嬉しいなぁ*4


明日はleo_isaacさんです。

*1:アセンブラと呼ばれることも多いですが、本稿ではアセンブリで統一します。

*2:あくまで初心者の感想です

*3:最近のイケイケなPCはUEFIが立ち上がるらしいですが、僕の端末は8年前のものなのでよくわかりません

*4:多分ない