「PC」カテゴリーアーカイブ

BIOSと戯れてみる(3)

PCをレガシーブートさせてマシン語で遊んでみようというお話です。一応BIOSをメインにするつもりですが、どうなるかわかりません。
環境はAMD64+Windows11+VMware Workstation 17 Player+Debian 12.4です。

システム・タイマー・カウント・リード

BIOSの機能一覧とかは、例えばここなんかに詳しいんですけれど、色々と機能があって遊び甲斐がありそうです。文字の色を変えたりとか、ビデオモードを変えたりとか、目に見える形で遊べるものもたくさんあります。
とはいえ、そのあたりは皆さんの自由に任せるとして、今回はSystemTimerCountReadというBIOSコールを試してみようと思います。

このBIOSコールはとても単純な構造をしていて、引数とか全くなしで呼び出すだけです。
で、CF(キャリーフラグ)に成功か失敗か、ALにオーバーフローしたかしないか、CXとDXに起動してからのカウントが返ってきます。当然動かし続ければ32Bitだって超えちゃうんですが、そこはオーバーフローしたかどうかで大小判定するという仕様です。
見やすくするとこんな感じ。

CF:成功0
失敗 1
AH:0固定
AL:オーバーフローなし 0
  オーバーフロー   1
CX:タイマカウントの上位2バイト
DX:タイマカウントの下位2バイト

たったこれだけです。呼び出して帰ってきた値を参照するだけです。
さて、コードはどんな感じになるかな?
はい。ドン。

;>===========================
;>      BIOSで遊ぼっ!
;>===========================

section .data

    _c_seg          equ 0x07c0
    _c_ex_area_addr equ 0x200
    _c_seg          equ 0x07c0
    _c_ex_area_addr equ 0x200

section .text

boot:
    ; set segment register
    mov ax, _c_seg
    mov ds, ax

    ; disk read

    mov ax, _c_seg
    mov es, ax
    mov bx, _c_ex_area_addr

    mov ah, 0x02 ; Read Sectors From Drive
    mov dl, 0x80 ; Drive
    mov al, 0x20 ; Sectors To Read Count ;
    mov ch, 0x00 ; Cylinder
    mov cl, 0x02 ; Sector(starts from 1, not 0) ; set 2. becouse not need MBR
    mov dh, 0x00 ; Head

    int 0x13     ; Execute disk read

    ; ビデオモードの設定
    mov ah, 0x0
    mov al, 0x3    ; 16色テキスト、80x25
    int 0x10

    jmp main

;********************************
; ブートセクタ終端までゼロで埋める
;********************************

times 510-($-$$) db 0

;********************************
; ブートセクタシグネチャの書き込み
;********************************

db 0x55
db 0xAA

;>===========================
;> main
;>===========================

main:
    ; システムカウンタ
    ; BIOSコールの実行
    mov ah, 0x00
    int 0x01a

    ; 以下、結果の表示

    jnc ._cf_nomal
    ; キャリーが立っていた場合
    mov ah, 0x01
    mov [._cf], ah

._cf_nomal:
    mov [._of], al

    ; CFの表示
    mov ax, ._s_hdr_cf
    call disp_str
    mov al, [._cf]
    call disp_byte_hex
    call disp_nl

    ; over fllow の表示
    mov ax, ._s_hdr_of
    call disp_str
    mov al, [._of]
    call disp_byte_hex
    call disp_nl

    ; cxの表示
    mov ax, ._s_hdr_cx
    call disp_str
    mov ax, cx
    call disp_word_hex
    call disp_nl

    ; dxの表示	
    mov ax, ._s_hdr_dx
    call disp_str
    mov ax, dx
    call disp_word_hex
    call disp_nl
    call disp_nl

    ; 処理終了

    call _hlt

._cf: db 0x00
._of: db 0x00

._s_hdr_cf: db ' CF        : ', 0x00
._s_hdr_of: db ' over flow : ', 0x00
._s_hdr_cx: db ' cx        : ', 0x00
._s_hdr_dx: db ' dx        : ', 0x00
._s_crlf:       db 0x0d, 0x0a, 0x00

;>===========================
;>      サブルーチン
;>===========================
;********************************
; bin_nibble_hex
;       4bit整数を16進文字に変換する(下位4Bit)
;       0~15 -> '0'~'f'
; param  : al : 変換する数値
; return : bl : 変換された文字
;******************************
bin_nibble_hex:

        and al, 0x0f
        cmp al, 0x09
        ja .gt_9
        add al, 0x30
        jmp .cnv_end
.gt_9:
        add al, 0x37

.cnv_end:
        mov bl, al
        ret

;********************************
; bin_byte_hex
; param  : al : 変換したい数値
; return : bx : 変換した2文字の16進文字
;********************************
bin_byte_hex:
    push cx
    push dx

    mov cl, al
    sar al, 4
    and al, 0x0f
    mov ah, 0
    call bin_nibble_hex
    mov dh, bl

    mov al, cl
    and al, 0x0f
    mov ah, 0
    call bin_nibble_hex
    mov dl, bl

    mov bx, dx

    pop dx
    pop cx

    ret

;********************************
; disp_byte_hex
;      1バイトの数値を16進で表示する
; param  : al : 表示したい数値
;********************************
disp_byte_hex:
    push ax
    push bx

    call bin_byte_hex
    mov ah, 0x0e
    mov al, bh
    int 0x10
    mov al, bl
    int 0x10

    pop bx
    pop ax

    ret

;********************************
; disp_word_hex
;       2バイト(1ワード)のデータを表示する
; param : ax : 表示するword
;********************************
disp_word_hex:

    push ax
    push bx

    mov bx, ax
    mov al, bh
    call disp_byte_hex

    mov al, bl
    call disp_byte_hex

._end:

    pop bx
    pop ax

    ret

;********************************
; disp_str
;       display null-terminated string.
; param : ax : addr of mem where string is set.
;********************************
disp_str:

    push ax
    push si

    mov si, ax
    mov ah, 0x0E

._loop:
    lodsb
    or al, al
    jz ._loop_end
    int 0x10
    jmp ._loop

._loop_end:
    pop si
    pop ax

    ret

;****************************
; disp_nl
;   改行する
;****************************
disp_nl:

    push ax

    mov ax, _s_crlf
    call disp_str

    pop ax

    ret

;>****************************
;> hlt
;>****************************
_hlt:
    hlt
    jmp _hlt

;==============================================================
; ファイル長の調整
;==============================================================
_padding:
    times 0x100000-($-$$) db 0

PlayWithBIOS.3.1

なんと250行を超えてしまいました。実際の実行は61行目と62行目、この2行なんですが実行しただけでは何が起こったのかわからないので、結果を表示しています。その表示部分が200行とかあるわけです。いゃぁ、アセンブリ言語って楽しいですね♪
冗談はともかくとして、実行するとこんな感じになります。

ストップウォッチもどき

見出しでネタバレかもしれませんが、今回SystemTimerCountReadを題材に選んだのはこのためなんです。開始時のSystemTimerCountと終了時のSystemTimerCountをとれば、処理にどのくらいの時間がかかったかわかるじゃないですか。それで処理速度を比べてみたかったんですね。
なんの処理速度を比べたかったか。そうアレです。

cmp bx, 0 と
or bx, bx  の処理速度です。

果たして声を大にして叫ばなければいけないほど処理速度に差があるんでしょうか?
実際に試してみたかったんです。

cmp bx, 0 と or bx, bxの速度比較

さて速度比較なんですが生半可な回数だと繰り返しても一瞬で終わってしまうので、32Bit整数回以上回したいと思います。具体的には0x1000000000回繰り返してみます。
8086CPUでは整数の最大値が0xffffつまり65535でしかないので、16bitフルを2連にしてそれを16回繰り返します。変更点はこんな感じ。

;>=========================== ;> main ;>=========================== main: ; システムカウンタの取得と表示 call SystemCounter mov bx, 0xffff mov cx, 0xffff mov dx, 0x000f ._Loop1 ._loop2 ._loop3 dec bx cmp bx, 0 jne ._loop3 dec cx or cx, cx jne ._loop2 dec dx or dx, dx jne ._loop1 ; システムカウンタの取得と表示 call SystemCounter ; 処理終了 call _hlt

cmp bx, 0

さて、cmp bx, 0はこんな結果になりました。

開始が0x0003064Bで終了が0x00030916Cなので、0x2CBつまり715カウントかかったことになります。

or bx, bx

ではor bx,bxのほうはどうでしょうか。先ほどのソースの真ん中あたりかな?
cmp bx, 0をor bx, bxに変えて実行してみます。するとこんな結果になりました。



開始が0x00038561で終了が0x00038827ですから0x2C6ということで710カウントかかっています。

つまり?

cmpを使う場合とorを使う場合の比較ですが、見ての通りほとんど差がないという結果になりました。本来こういった速度比較をする場合、数十回とか数百回行って平均をとって比べるとかすべきですし、そもそもVMwareという仮想化ソフト上で動かしているわけですから実際のCPUとまるっきり一緒ということもないのかもしれません。
けれど、仮想環境で、なおかつ勉強や趣味のために動かしてている状況では、orを使おうがcmpを使おうがどっちでも構わないといえると思います。
少なくても、ですが、cmpを使っていて「そこはorを使うんだ。素人はこれだから…」とかしたり顔で言われる筋合いはないということです。(結局これが言いたかっただけ)

さて、ちょうどキリもいいので今回はここまでです。

《2024/07/06 12:45:23》

BIOSと戯れてみる(2)

OSを書く:初歩から一歩ずつ』を一通りなめてみたのですが、実は練習問題の「キーボードから読み込む」の自分の回答が少し不満でした。今回はそのあたりのお話を。
環境はAMD64+Windows10+VMware Workstation 17 Player+Debian 12.4です。

キー押下情報を取り込む

BIOSのキーボードサービスの中からキーボード入力読み込みサービスを使いました。
ですがこれ、1文字入力なんですよね。1部のキーを除いて押した瞬間に取り込んでしまう。複数文字を得るためには複数回呼ばなければならない。あまつさえ、入力待ちの間は処理がブロックされて他に何もできない。
せめて1回で複数文字を入力したい。その文字列を編集できなくてもいいから、まとまった文字を入力したい。
とまぁそんなことを考えたわけです。

複数文字入力

バイオスの説明とかつらつらと眺めていたら、キーボードステータス取得というのがありました。説明を見たら要するに「キーボード入力の有り無しをフラグで返す」らしいです。

最初見た時、これ何の役に立つんだろうって思いました。だって読みに行けばデータの有り無しは(ブロックされるかされないかで)はっきりわかるのに、なんで必要なんだろうって。
とはいえ、考えてみたら1文字ごとに処理がブロックされるってことは、ブロックされている間、他の処理をしないわけですから、もったいないオバケが大量発生してしまいます。そう考えると入力があったら読み込み、なかったら他の作業をするってのは理にかなっているかなぁと。
こんな感じ?

文字列読み込み

説明を見る限り、このサービスを使えば読み込むデータも取れそうなんですが、注意書きでこのサービスではクリアされない、入力サービスでクリアされるとわざわざ書いてあるので、このサービスで判定して、必要な時に読みに行くのが正解なんでしょう。

何が欲しいか、何がいらないか

まず譲れないのは表示できる文字は取り込みたいですね。表示できない文字は基本的に要らないことにしましょう。ただ一つだけ例外なのはリターン。これが押されたら入力の終わりと判断して処理を抜けましょう。
そういう方針で出来たのがこんなの。

;>===========================
;>      BIOSで遊ぼっ!
;>===========================

section .data

    _c_seg          equ 0x07c0
    _c_ex_area_addr equ 0x200

section .text

boot:
    ; set segment register
    mov ax, _c_seg
    mov ds, ax

    jmp main

;>****************************
;> hlt
;>****************************
_hlt:
    hlt
    jmp _hlt

_m_buf_str: times 128 db 0x00

section .text

;****************************
; get_str_ascii
;   キーボードから文字列を取り込んでアドレスをaxに返す
;****************************
get_str_ascii:

    mov si, _m_buf_str

._loop:

    ; キーボードの状態を確認する
    mov ah, 0x11
    int 0x16
    jne ._loop

    ; キーボードから1文字取り込む
    mov ah, 0x10
    int 0x16
    mov bx, ax

    ; なんだこれ?
    cmp bl, 0x20
    jg ._skip

    ; Ctrl+Retの場合終了
    cmp bx, 0x1c0a
    je ._exit

    ; Retの場合終了
    cmp bl, 0x0d
    je ._exit

    ; 空白以下ならスキップ
    cmp bl, 0x20
    jle ._skip

    ; ~以上ならスキップ
    cmp bl, 0x7e
    jg ._skip

    mov ax, bx

    cmp si, _m_buf_str
    je ._loop

    mov [si], al
    inc si

    jmp ._loop

._skip:

    mov ax, bx

    mov ah, 0x0e
    int 0x10

    mov [si], al
    inc si

    jmp ._loop

._exit:

    mov ax, _m_buf_str

    ret

;>===========================
;> main
;>===========================

main:
    ; set segment register
    mov ax, _c_seg
    mov ds, ax

    ; 改行を入れて画面を整える
    mov ah, 0x0e
    mov al, 0x0d
    int 0x10
    mov al, 0x0a
    int 0x10

    ; キーボード入力処理
    call get_str_ascii
    mov bx, ax

    ; 返ってきた文字列を表示する
    mov ah, 0x0e
    mov al, 0x0d
    int 0x10
    mov al, 0x0a
    int 0x10

    mov si, bx

_loop:
    lodsb
    or al, al
    je _exit

    int 0x10

    jmp _loop

_exit:

    ; 処理終了
    jmp _hlt

._bun: db 0x0d, 0x0a, 0x0d, 0x0a, 0x00

times 510-($-$$) db 0
db 0x55
db 0xAA

PlayWithBIOS.2.1

もっと機能とか追加したかったのですが、これ以上だと複雑になりすぎるかなぁ。
かなり短いですけど今回はここまでです。
お疲れさまでした。
次は何しましょうか。

《2024/6/3 2:06:20》

BIOSと戯れてみる

前にx86系のPCをブートさせて遊んでみる『ブートで遊ぼっ!』というのを7回ほど書いてみました。テキストは『OSを書く:初歩から一歩ずつ』で、練習問題も最後までやりました。もう思い残すところはない!と言いたいところなんですが、書き足りないところとか、遊び足りないところもあるので、少し続けてみようと思います。
環境はAMD64+Windows10+VMware Workstation 17 Player+Debian 12.4で変わりません。

ディスク読み込み

前回シリーズではさらっと済ませたような気がするんですが、ディスク読み込みのお話です。

呼び出しパラメータ

最初、これを見た時には頭がくらくらしました。「なにをどうすればいいの?」って。
調べてみたらさらに頭が痛くなりました。
シリンダって何?ヘッドって何?ファイル構造を知らないとダメ?FAT?太ってないよ?
結局欲しい情報は
・読み込み対象装置
・読み込むセクタ(位置)
・セクタ数
らしいということが何となくわかりました。装置はHDD、今回の場合はアセンブルしたファイルそのもので0x80を指定すればいいそうです。セクタはMBRが1セクタ使ってるので、その先である2以降。セクタ数については作ったファイルが2048Byteなのでトータル4セクタ、先にも書いたけど1セクタをMBRに使っているので残り3セクタかな。
ここでシリンダとヘッダについてなのですが、HDDで最初の1セクタ読んだらヘッダ、シリンダが変更される、なんてことはまず考えられない。今回トータルでも4セクタしかないので、最後まで読んでもヘッダとシリンダが変わることはないだろう。という予想のもと、両方0で実装しました。

読み込みアドレス

そもそもブートローダーが512Byteとかケチ臭いことを言わず、ザクっと全部読み込んでくれたらBoot時点で苦労はしないのです(暴論)
nasm君は512Byteを超えようが知らん顔してバイナリ作ってるし。
このあたり、詳しい解説とかは他の方に任せます。いろんな方がいろんな書き方で解説してますし。
で、この時点で何が問題かというと、こんな感じかな

nasmでアセンブルした時、とある命令は512番地(正確には0x7c00+512番地)に居る前提で動こうとするんだけど、泣き別れで読み込まれてしまうと、前提が違うので動かない、という状況です。
こうならないために、セグメントレジスタを調整とかするんですが、ヘッダ、シリンダのお話や、ファイルシステムのお話、セグメントのお話なんかがいっぺんに来るので、なんだか難しい解り難いものになっちゃうわけです。
で、このシリーズは「遊びよ。遊びっ♪」と割り切っているので、面倒なことはとりあえず置いといて、動くようにしました。

ハードディスク側は、1セクタの512byteの先に、続きが書かれているのはまず間違いないです。PC側は512番地の後に続きがあれば動く。
ならば、2セクタから先を512番地に読み込めばいいじゃない。
そういうことなのでした。

ディスク書き込み

読み込みのお話が出たので、書き込みのお話もしましょう。

呼び出しパラメータ

書き込み操作のパラメータは以下です。

何のことはない、読み込みとほぼ一緒です。ならば同じように使えるんじゃないかな?
メモリに何かを書きこんで、それをディスクに書く、で行けるんじゃないかな?

; bios.asm

mov ax, 0x07c0
mov ds, ax
mov es, ax

mov ah, 0x0
mov al, 0x3
int 0x10

; set data to top of 2nd sector
mov si, _change_msg2
mov bx, 0x0200
loop2:
lodsb
or al, al
je loop2_end
mov [bx], al
add bx, 1
jmp loop2

loop2_end:

; set data to top of 3rd sector
mov si, _change_msg3
mov bx, 0x0400
loop3:
lodsb
or al, al
je loop3_end
mov [bx], al
add bx, 1
jmp loop3
loop3_end:

; set data to top of 4th sector
mov si, _change_msg4
mov bx, 0x0600
loop4:
lodsb
or al, al
je loop4_end
mov [bx], al
add bx, 1
jmp loop4
loop4_end:

; write disk

mov bx, 0x200   ; Destination address to write

mov ah, 0x03    ; Write Sectors To Drive
mov dl, 0x80    ; Drive
mov al, 0x03    ; Sectors To Write Count ;
mov ch, 0x00    ; Cylinder
mov cl, 0x02    ; Sector(starts from 1, not 0) ; set 2. becouse not need MBR
mov dh, 0x00    ; Head

int 0x13        ; Execute disk write

jmp hang

; end of proccess
;
hang:
jmp hang

_change_msg2: db 'All out!', 0x0d, 0x0a, 0x00
_change_msg3: db 'Pull the throttie!', 0x0d, 0x0a, 0x00
_change_msg4: db "All right Let's Go!", 0x0d, 0x0a, 0x00

times 510-($-$$) db 0

db 0x55
db 0xAA

_second_sector:
    db 'Forth Gate Open!', 0x0d, 0x0a, 0x00

    times 0x0400 -($-$$) db 0

_third_sector:
    db 'Quickly!', 0x0d, 0x0a, 0x00

    times 0x0600-($-$$) db 0

_fourth_sector:
    db 'Forth Gate Ooen!', 0x0d, 0x0a, 0x00

    times 0x0800 -($-$$) db 0

PlayWithBIOS.1

今までみたいにmakeとだけ打つと実行までされてしまいます。今回は実行前のファイルと実行後のファイルを比べたいのでこういう風に。

これで実行しないで止まるので、中を覗きます。


実行後に値が変わっていれば書き込み成功です。

はい。確かに2セクタ3セクタ4セクタが書き換わっています。

さて、今回はこれでお終いです。次のネタどうしよう…。

《2024/5/28 18:00:20》

ブートで遊ぼっ!(7)

86系のPCをブートさせて遊んでみよう、というお話の7回目になります。テキストは『OSを書く:初歩から一歩ずつ』です。
環境はAMD64+Windows10+VMware Workstation 17 Player+Debian 12.4です。
テキストの練習問題もこれで最後です。
頑張りましゅっ(噛んだ)

6. BIOSからキー押下情報を取り込む。

キーボードからの入力を得ることができれば、いろんな事が出来そうです。
期待が膨らみます。
使うBIOSコールは0x16。設定値は無し。キーが押されるとahとalに値が入って戻ってくるみたいです。ソースはこんな感じ?

; boot.asm
;
mov ax, 0x07c0
mov ds, ax

mov ah, 0x0
mov al, 0x3
int 0x10


mov ah, 0x00
int 0x16

mov cx, ax
mov ch, 0x00
mov bx, 10

loop:

mov dx, 0
mov ax, cx
div bx
mov cx, ax
mov ah, 0x0e
mov al, dl
add al, 0x30
int 0x10

cmp cx, 0
jne loop


jmp hang

[text]
; end of proccess
;
hang:
jmp hang

times 510-($-$$) db 0

db 0x55
db 0xAA

PlayWithBoot.6.1

実行すると、こんな感じ。

これはエンターを押した場合のalの値ですね。
なんだか簡単に終わってしまった?
一応、練習問題はこれで終わりですけど、これだけだと寂しいので、少し遊んでみましょう。

おまけ

せっかくなので練習問題の1から6まで、一つにまとめてみました。

; boot.asm
;
mov ax, 0x07c0
mov ds, ax

mov ah, 0x0
mov al, 0x3
int 0x10


; practice 1 (print other)

mov ax, p1_title
call print_str

mov al, 1

mov ah, 0x0e
add al, 0x30
int 0x10

mov ax, crlf
call print_str
call print_str

; practice 2 (add 2 num)

mov ax, p2_title
call print_str

mov ax, p2_msg
call print_str

mov bh, 1
mov bl, 3
add bl, bh

mov ah, 0x0e
mov al, bl
add al, 0x30
int 0x10

mov ax, crlf
call print_str
call print_str

; practice 3 (add 1 to 100 and print it)

mov ax, p3_title
call print_str

mov bx, 0
mov ax, 0

add_loop:

add ax, bx
add bx, 1

cmp bx, 100
jle add_loop

mov bx, ax

print_loop:

mov dx, 0
mov ax, bx
mov bx, 10
div bx
mov bx, ax
mov ah, 0x0e
mov al, dl
add al, 0x30
int 0x10

cmp bx, 0

jne print_loop

mov ax, crlf
call print_str
call print_str


; practice 4 (print valu in address)

mov ax, p4_title
call print_str

mov ax, 0x0000
mov bx, _test
mov byte al, [bx]
mov bx, ax

p4_loop:

mov dx, 0
mov ax, bx
mov bx, 10
div bx
;mov cx, dx
mov bx, ax
mov ah, 0x0e
mov al, dl
add al, 0x30
int 0x10

cmp bx, 0

jne p4_loop

mov ax, crlf
call print_str
call print_str

; practice 5 (read disk)

mov ax, p5_title
call print_str

mov ax, 0x07c0
mov es, ax
mov bx, 512

mov ah, 0x02 ; Read Sectors From Drive
mov dl, 0x80 ; Drive
mov al, 0x01 ; Sectors To Read Count ;
mov ch, 0x00 ; Cylinder
mov cl, 0x02 ; Sector(starts from 1, not 0) ; set 2. becouse not need MBR
mov dh, 0x00 ; Head

int 0x13     ; Execute disk read

mov ax, 512
call print_str

mov ax, crlf
call print_str
call print_str

; practice 6 (key read)

mov ax, p6_title
call print_str

mov ax, p6_msg
call print_str

mov ah, 0x00
int 0x16

mov bx, ax
mov bh, 0

p6_loop:

mov dx, 0
mov ax, bx
mov bx, 10
div bx
mov bx, ax
mov ah, 0x0e
mov al, dl
add al, 0x30
int 0x10

cmp bx, 0

jne p6_loop

jmp hang


; end of proccess
;
hang:
jmp hang

p1_title:
	db 'practice 1 (print other)', 0x0a, 0x0d, 0x0a, 0x0d, 0x00

p2_title:
	db 'practice 2 (add 2 num)', 0x0a, 0x0d, 0x0a, 0x0d, 0x00

p2_msg:
	db '1 + 3 : ', 0x00

p3_title:
	db 'practice 3 (add 1 to 100 and print it)', 0x0a, 0x0d, 0x0a, 0x0d, 0x00

p4_title:
	db 'practice 4 (print valu in address)', 0x0a, 0x0d, 0x0a, 0x0d, 0x00

p5_title:
	db 'practice 5 (read disk)', 0x0a, 0x0d, 0x0a, 0x0d, 0x00

p6_title:
	db 'practice 6 (key read)', 0x0a, 0x0d, 0x0a, 0x0d, 0x00

p6_msg:
	db 'ascii code : ', 0x00

crlf:
	db '', 0x0a, 0x0d, 0x00

_test:
	db 0x15, 0x00

print_str:

        push ax
        push si

        mov si, ax
        mov ah, 0x0E

loop:
        lodsb

        or al, al
        jz loop_end

        int 0x10

        jmp loop

loop_end:

        pop si
        pop ax

        ret


times 510-($-$$) db 0

db 0x55
db 0xAA

top_of_2nd_sector:
	db 'Hello Sector No.1', 0x0d, 0x0a, 0x00

times 1024-($-$$) db 0

PlayWithBoot.6.2

実行すると、こんな感じです。

これで一通りやり切しました。
他にもやりようはあるし、数値の表示を左からきちんと表示するとか、手を加えられるところも沢山あると思います。
そのあたり、気になった部分はそれそれ直してみていただければと。

一連のソースですが、githubに上げてみました。素の設定なので公開されている筈です。よろしければ覗いてやってください。

https://github.com/cbwb-inc/software/PlayWithBoot

かなり時間がかかってしまいましたが、『ブートで遊ぼっ!』これにて終了です。
物凄く楽しかった♪

《2024/5/9 12:30:24》

ブートで遊ぼっ!(6)

86系のPCをブートさせて遊んでみよう、というお話の6回目になります。テキストは『OSを書く:初歩から一歩ずつ』です。
環境はAMD64+Windows10+VMware Workstation 17 Player+Debian 12.4です。
今回はテキストの練習問題の4からです。

4. あるメモリアドレスの内容をプリントする

とかすると、axには_testのアドレスが入るわけで、あとはその中身を取り出して…取り出して?
どうやって?
というのがこの課題なんでしょう。
今までやってきたように、axとかに入っている値を扱いたい場合は上の書き方をします。で、それをアドレスとしてその中身を扱いたい場合[]で囲います。例えば今回の「_test」のアドレスの中身を取り出したい場合

とかして、_testのアドレスを取得した上で

とするわけです。1
欲しいのは0x15という値(41行目の_testのあとにdb命令?で定義しています)
alは1Byte。
mov命令は気が利かないので、alが1Byteなんだからbxのアドレスから1Byteだけ読むなんてことをしてくれません。なのでbxのアドレスから([bx])1Byteをalに移送してくれ(move byte alの部分)と指示します。
読み込めたら後は今まで通り、表示するだけ。

; boot.asm

mov ax, 0x07c0
mov ds, ax

mov ah, 0x0
mov al, 0x3
int 0x10

mov ax, 0x0000
mov bx, _test
mov byte al, [bx]
mov bx, ax

loop:

mov dx, 0
mov ax, bx
mov bx, 10
div bx
mov cx, dx
mov bx, ax
mov ah, 0x0e
mov al, dl
add al, 0x30
int 0x10

cmp bx, 0
jne loop


jmp hang

; end of proccess
;
hang:
jmp hang

_test: db 0x15, 0x00

times 510-($-$$) db 0

db 0x55
db 0xAA

PlayWithBoot.5.1

実行すると、こんな感じです。

5. BIOSを使ってディスクから何かを読み込んでみる

わたしはここで挫折しかかりました。ディスク読み込みのBIOSコールは調べられたんですが、パラメータが良くわかりませんでした。
ていうか、端的に言って、シリンダー、ヘッダー、セクターが理解できません。
なんか情報が錯綜しているというか、混じりあってる?
その上、ブートでセクタにはパーティションデーブルがあって、パーティションを特定したらFATがあって、みたいな?
お詫びと開き直りをするまで、結構かかりましたとも。ええ。
で、思ったわけです。
あのね?
わたしはディスクから読み込めればいいの!
FATなんて関係ないの!
と。
そもそもブートセクタは読めているわけで(だから動いてるし)、それはheaderが0、cylinderが0、sectorが1(sectorは1オリジンだそうです)以外にあり得ない。
ならば、それはheaderが0、cylinderが0、sectorを2にすれば読めるんじゃない?
さすがに2sectorの1KByteでheaderもcylinderも変わることないでしょ?
そういう予測のもとにソースを書きます。

; boot.asm

mov ax, 0x07c0
mov ds, ax
mov es, ax

mov ah, 0x0
mov al, 0x3
int 0x10


mov bx, 0x200   ; Destination address to read

mov ah, 0x02    ; Read Sectors From Drive
mov dl, 0x80    ; Drive
mov al, 0x01    ; Sectors To Read Count ;
mov ch, 0x00    ; Cylinder
mov cl, 0x02    ; Sector(starts from 1, not 0) ; set 2. becouse not need MBR
mov dh, 0x00    ; Head

int 0x13        ; Execute disk read

mov si, 0x0200
mov ah, 0x0E

loop:

lodsb

or al, al
jz loop_end

int 0x10

jmp loop

loop_end:


jmp hang


; end of proccess
;
hang:
jmp hang


times 510-($-$$) db 0

db 0x55
db 0xAA

test:
	db 'Hello 2nd sector!', 0x0a, 0x0d, 0x00

_padding:
        times 0x0400-($-$$) db 0

PlayWithBoot.5.2

さて、動かしてみましょう。
こういうのってワクワクしますよねw

はい。無事に動きました。

今回はここまでです。
なんだか、何かをやり遂げた気がします^^

《2024/5/9 1:18:24》

  1. mov al, [_test]とか書けた気がする…。 ↩︎

ブートで遊ぼっ!(5)

86系のPCをブートさせて遊んでみよう、というお話の5回目になります。テキストは『OSを書く:初歩から一歩ずつ』です。
環境はAMD64+Windows10+VMware Workstation 17 Player+Debian 12.4です。
前回、練習問題の1をやったので続きを。

2. 2つの数値を加算する。

すでに終わっている気もしないでもないですが、改めて。
1と3を足してみましょう。

; boot.asm
;
mov ax, 0x07c0
mov ds, ax

mov ah, 0x0
mov al, 0x3
int 0x10

mov ah, 0x0E

mov al, 1
mov bl, 3
add al, bl

add al, 0x30

int 0x10

; end of proccess
;
hang:
jmp hang

times 510-($-$$) db 0

db 0x55
db 0xAA

PlayWithBoot.4.1

説明の必要もないくらいですね。alとblにそれぞれ1と3を入れて、alとblを加算。
それだけです。
ただ、結果を確認できないのも何なので前回やった数値を表示するやりかたで、数値の1を文字の「1」に変換して表示しています。alに0x30を足すところですね。
これを実行すると、こうなります。


3. 1から100までの数を合計して解を画面にプリントする。

ネタばれしてるような気もしますが、こんな感じでソースを書いてみました。

; boot.asm
;
mov ax, 0x07c0
mov ds, ax

mov ah, 0x0
mov al, 0x3
int 0x10

mov ax, 0
mov bx, 0

loop:

add bx, 1
add ax, bx

cmp bx, 100
jl loop

add al, 0x30

int 0x10

; end of proccess
;
hang:
jmp hang

times 510-($-$$) db 0

db 0x55
db 0xAA

PlayWithBoot.4.2

実行すると、こうなります。

えっと、実は数値に0x30を足して文字に変換するって、1文字にしか対応していないんです。0~9は変換できるんですが、10から先には対応していません。
ではどうするか?
『1文字にしか対応できないなら、1文字ずつ表示すればいいじゃない』
はい、その通りです。例えば12を表示するとして、1と2を1文字ずつ表示すればいいんです。ではどうやって?
う~ん、中学受験の点取らせ問題ですね。
10で割った余りが1の位の数値になります。
答えを同じく10で割って余りが次の位の数値になります。
答えを…とまぁ数値の桁数だけ繰り返すわけです。
具体的にはこんな感じ。

12を例にしてソースを書いてみました。

; boot.asm
;
mov ax, 0x07c0
mov ds, ax

mov ah, 0x0
mov al, 0x3
int 0x10

mov cx, 12

mov dx, 0
mov ax, cx
mov bx, 10
div bx
mov cx, ax
mov ah, 0x0e
mov al, dl
add al, 0x30
int 0x10

mov dx, 0
mov ax, cx
mov bx, 10
div bx
mov cx, ax
mov ah, 0x0e
mov al, dl
add al, 0x30
int 0x10

; end of proccess
;
hang:
jmp hang

times 510-($-$$) db 0

db 0x55
db 0xAA

PlayWithBoot.4.3

割り算を行うdiv命令ってなんだか癖があって、axに入れた数値をbxで割って、答えがaxに、余りがdxに設定されます。
今回は10で割るので余りは確実に一桁。答えの下半分であるdlだけ見れば良いということになります。
1の位から上に向かって処理するので、12は21と表示されます。
こんな感じですね。

ここで大いなるお詫びと、胸を張った開き直りをしなければなりません。
12が対象なんだから、そのまま「12」と表示したいところです。
出来るんですけどね。ただものすごく少しだけ面倒くさいんです。
これがお仕事とかなら12と表示させるんですが、今回は処理の結果を確認したいだけなんです。なら21と表示されても、右から読めばいいじゃないですか。
というわけで、この場に限って「わたしはアラビア人~」と言い聞かせながら、右から読むことにします。

さて、元の問題に戻ります。ここでちょっとズルをします。1~100までを足すと答えは5050になります。つまり4桁です。4回処理をすればいいわけです。

; boot.asm
;
mov ax, 0x07c0
mov ds, ax

mov ah, 0x0
mov al, 0x3
int 0x10

mov cx, 12

mov dx, 0
mov ax, cx
mov bx, 10
div bx
mov cx, ax
mov ah, 0x0e
mov al, dl
add al, 0x30
int 0x10

mov dx, 0
mov ax, cx
mov bx, 10
div bx
mov cx, ax
mov ah, 0x0e
mov al, dl
add al, 0x30
int 0x10

; end of proccess
;
hang:
jmp hang

times 510-($-$$) db 0

db 0x55
db 0xAA

PlayWithBoot.4.4

ハイライトは1回分にしか入れてません。同じ処理の繰り返しだし。
で、実行するとこうなります。

無事に0505が表示されました。右から見て5050です。

さて、このソースもソフトウェアを作成する立場としては、ちょっと看過できないです。ほぼ同じコードがドカンドカンと4回もあります。繰り返しなのだからループにしちゃいましょう。
12は2桁なので2回、5050は4桁なので4回実行しています。どんな数値にも対応するためには繰り返しを終わらせる条件がわからなければいけません。
これも中学入試の点取らせ問題レベルかなぁ。
答えが0になったらどんな数で割っても余りは出ません。というわけで答えが0になったら終了すればいいわけです。

; boot.asm
;
mov ax, 0x07c0
mov ds, ax

mov ah, 0x0
mov al, 0x3
int 0x10

mov ax, 0
mov bx, 0

._loop

add bx, 1
add ax, bx

cmp bx, 100
jl ._loop

mov cx, ax

mov dx, 0
mov ax, cx
mov bx, 10
div bx
mov cx, ax
mov ah, 0x0e
mov al, dl
add al, 0x30
int 0x10

mov dx, 0
mov ax, cx
mov bx, 10
div bx
mov cx, ax
mov ah, 0x0e
mov al, dl
add al, 0x30
int 0x10

mov dx, 0
mov ax, cx
mov bx, 10
div bx
mov cx, ax
mov ah, 0x0e
mov al, dl
add al, 0x30
int 0x10

mov dx, 0
mov ax, cx
mov bx, 10
div bx
mov cx, ax
mov ah, 0x0e
mov al, dl
add al, 0x30
int 0x10

; end of proccess
;
hang:
jmp hang

times 510-($-$$) db 0

db 0x55
db 0xAA

PlayWithBoot.4.5

ハイライトは100まで足す部分と、1桁表示するところです。表示は繰り返しなので1回だけで。
実行するとこんな感じです。

今回はここまでです。
お疲れさまでした。

《2024/5/8 6:47:24》

ブートで遊ぼっ!(4)

86系のPCをブートさせて遊んでみよう、というお話の4回目になります。テキストは『OSを書く:初歩から一歩ずつ』です。
環境はAMD64+Windows10+VMware Workstation 17 Player+Debian 12.4です。
一応前回まででテキストの本文(?)については触れたので、今回からは練習問題について書いてみようかと思います。
まずは練習問題の1番です。

画面に他のものをプリントする。

これ、ものすごく面白い問題だなぁと。といいますか「他のもの」ってのが曖昧過ぎるなぁ。いろんな解釈が出来そう…。
思いつくままに作ってみましょう。

別の文字をプリントする。

これは簡単ですね。文字列の定義を変えるだけです。なのでこんな感じ。

; boot.asm

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, x86!', 13, 10, 0

hang:
jmp hang

times 510-($-$$) db 0

db 0x55
db 0xAA

PlayWithBoot.3.1

実行すると、こう。

文字列を追加してみる

さすがに前節で「練習問題クリア!」とかいうのは何なので、文字列を追加して2つの文字列を表示してみます。これも難しくないですね。
ドカンとコピペして該当箇所をちょこっと変えます。
こんな感じ。

; boot.asm
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 print_2nd_character

int 0x10

jmp print_character_loop

print_2nd_character:
mov si, msg2
mov ah, 0x0E

print_character_loop2:
lodsb

or al, al
jz hang

int 0x10

jmp print_character_loop2

msg:
db 'Hello, World!', 13, 10, 0

msg2:
db 'Hello, x86!', 13, 10, 0

hang:
jmp hang

times 510-($-$$) db 0

db 0x55
db 0xAA

PlayWithBoot.3.2

22行目から34行までがコピーした部分です。設定する文字列のアドレスだけ変更してます。39行目と40行目は追加した文字列の定義ですね。
実行するとこう。

サブルーチンにしてみる

一応、練習問題としてはクリアなのかもしれませんが、ソフトウェアを作る立場としては、ほとんど同じで少しだけ違うコードって気持ちが悪いわけです。
こういう場合、どんな言語でも処理を共通化して、呼び出す形にします。
今回もそうしてみましょう。axに表示したい文字列のアドレスを指定して、サブルーチンを呼び出します。
こんな感じ。

; boot.asm
;
mov ax, 0x07c0
mov ds, ax

mov ah, 0x0
mov al, 0x3
int 0x10

mov si, msg
call print_character

mov si, msg2
call print_character

jmp hang

; sub routine
;
print_character:

mov ah, 0x0E

print_character_loop:
lodsb

or al, al
jz print_character_end

int 0x10

jmp print_character_loop

print_character_end:

ret

; define messages
;
msg:
db 'Hello, World!', 13, 10, 0

msg2:
db 'Hello, x86!', 13, 10, 0

; end of proccess
;
hang:
jmp hang

times 510-($-$$) db 0

db 0x55
db 0xAA

うん。すっきりしました。
実行するとこう。前と変わらないですね^^;
(というか変わったら困ります。それはバグです。)

数値を表示してみる

さて、今までは文字を表示していたんですが、『別のもの』を『文字列でないもの』と解釈することもできるわけです。実際『レジスタの中身を見たい』とか、数値を表示したくなるわけで。例えば数値の1を表示したいとか。
なにも考えずに書いたソースがこれ。

; boot.asm
;
mov ax, 0x07c0
mov ds, ax

mov ah, 0x0
mov al, 0x3
int 0x10

mov ah, 0x0E
mov al, 0x01
int 0x10

; end of proccess
;
hang:
jmp hang

times 510-($-$$) db 0

db 0x55
db 0xAA

PlayWithBoot.3.3

実行すると、こうなります。

あらまぁ、望んだ結果にはなりませんでした。
表示したいのは1で、表示できるのは”1″なんですよね。
文字の”1″を指定してあげないと1は表示されません。
結論から言うと文字の”1″は文字コード(これは数値です)の0x31になります。
なのでソースはこうなります。

; boot.asm
;
mov ax, 0x07c0
mov ds, ax

mov ah, 0x0
mov al, 0x3
int 0x10

mov ah, 0x0E
mov al, 0x01

add al, 0x30

int 0x10

; end of proccess
;
hang:
jmp hang

times 510-($-$$) db 0

db 0x55
db 0xAA

PlayWithBoot.3.4

実行してみます。

望み通り1が表示されました。

ここまでやれば、練習問題1はクリアしたと言っていいんじゃないかな。
なので、今回はここまでです。

《2024/5/3 17:07:24》

ブートで遊ぼっ!(3)

86系のPCをブートさせて遊んでみよう、というお話の3回目になります。テキストは『OSを書く:初歩から一歩ずつ』です。
環境はAMD64+Windows10+VMware Workstation 17 Player+Debian 12.4です。
前回はMakefileを作ったところで気力が尽きちゃったんだけど、今回はどうなるかなぁ(不安)
まぁ、最低でも文字を表示するところは終わらせるつもりですけど…。

スクリーンにプリントする

画面に文字を表示します。定番と言うか、「またお前か!」というか、何と言うか。
Hello World
です。
細かい説明は(不十分かもしれませんが)テキストにありますので、まずはそちらを参考にしてください。ざっくり言うと、表示したい文字列をメモリ上に定義して、その最初のアドレスから文字列が終わるまで1文字ずつ表示しています。
ていうか、これくらいしか言うこと無くない?これ以上何を言えと?
とか思ったり思わなかったり。
ともあれ、ソースを作成します。

; boot.asm
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

PlayWithBoot.2.1

アセンブル & 実行します。makeさん大活躍です。

make qemu

『Hello World!』が表示されました!

めでたしめでたし!
おしまい。

とか言いたいところなんですが、もし私が読み手としてこの記事をみたら
「ふざけんな!ボケぇ」
とか言いたくなるような気がするので、少しだけ追加です。

ahとal

ふと。
ahとalってaxレジスタのハイとロー、上位バイトと下位バイトじゃなかったっけ?
なら

mov ah, 0x0
mov al, 0x3

って

mov ax, 0x0003

とかにしても動くんじゃない?
というわけでソース修正。
こんな感じ。5行目が修正箇所ですね。

; boot.asm
mov ax, 0x07c0
mov ds, ax

mov ax, 0x0003
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

PlayWithBoot.2.2

で、実行。

無事に動きました。

or al, al の謎

まぁ、謎っていうほどのモノじゃないんですが、最初にこれを見た時に非常に悩みました。だってね

or X, Y

ってXとYのorをとって結果をXに設定する、という命令なわけで。
普通のプログラム言語っぽく書くなら

X = X or Y

なわけじゃないですか。
今回はXもYもalなので

al = al or al

になります。で、

同じ値のorをとると、元の値になります。
なので

al = al

になるという。ということはコードとしては

mov al, al

と同等になるという。
意味あるの?

ここでアセンブラを使ってるととても重要な機能と意味を持つフラグレジスタのお話がちょろっと出ます。
アセンブラの場合、全ての命令ではないのですが命令の実行結果によってフラグレジスタが更新されます。雑な言い方で申し訳ないんですが、アセンブラのコーディングでは
命令の実行
フラグの確認
といったスタイルが多くなります。

or al, al
jz hang

がこのパターンですね。
jz命令はゼロフラグが立っていたら(言い換えれば結果がゼロだったら)指定先に飛べという動作です。
表にするとこんな感じ。

lodsbで読
んだalの値
or la, alの結果ゼロフラグ文字
 1回目0x480x480H
 2回目0x650x650e
 3回目0x6c0x6c0l
 4回目0x6c0x6c0l
 5回目0x6f0x6f0o
: :: :: :: :: :
 13回目0x210x210!
 14回目0x0d0x0d0CR
 15回目0x0a0x0a0LF
 16回目0x000x001NULL
alの値とゼロフラグ

定義した文字列で一番最後の0x00の時だけ、ゼロフラグが1になります。
なので、最初から文字を1文字ずつ表示して0x00まで来たらhangに飛ぶという処理となり、めでたく『Hello World!』が表示されるというわけです。

蛇足

『alがゼロだったら~』という処理なんだから素直に

cmp al, 0x00
je hang

じゃいけないの?
という向きもあると思います。なので動かしてみましょう。
ソースはこれ。15行目、16行目を修正してます。

; boot.asm
mov ax, 0x07c0
mov ds, ax

mov ah, 0x0
mov al, 0x3
int 0x10

mov si, msg
mov ah, 0x0E

print_character_loop:
lodsb

cmp al, 0x00
je  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

PlayWithBoot.2.3

ちゃんと動きます。

う~ん、どっちを使うかなんですが、or al, al を使った方がいいことあるんでしょう、きっと。知らないけど。

《2024/4/27 15:25:24》

ブートで遊ぼっ!(2)

86系のPCをブートさせて遊んでみよう、というお話の2回目です。テキストは『OSを書く:初歩から一歩ずつ』です。
環境はAMD64+Windows10+VMware Workstation 17 Player+Debian 12.4です。
今回はMakefileを作るのと、余裕があれば文字を表示するところまでかな。届くかな?

ビルドプロセスを短縮する

Makefileを作ってビルド…というかアセンブルから実行までを簡単にしましょう、というお話。
『今時make?』という気もしなくはないんですが、アセンブラでboot周りのことをやるのなら『やっぱmakeよね!』とも思います。
やっていることというか、やりたいことは凄く単純で

と、2回コマンドを打っていたのを

で済まそうということ。
いちいちだらだらタイプしたくないし、ヒストリから拾うのも面倒だし、という怠け者の発想ですね。怠けるためにメンドクサイ仕込みをするとか、世の中よくあることです。
makeの使い方とかMakefileの書式とかを知りたい方は、ネットで調べてもらえば沢山記事があると思いますので、そちらに任せます。

テキスト通りMakefileというファイルを作成します。


実行してみます。



ん?
なにこれ?

って、どういうこと?

ちょっと調べてみたら、Makefileではコマンドの前にタブがないといけないみたいです。
何て言うか…『年寄りはこれだから困る』とか言いたくなってみたり?
気を取り直してMakefileを修正します。

2行目、5行目、8行目の先頭にタブを入れます。
で、改めて実行します。

ちゃんと動きました。めでたしめでたし。

ここで気力が尽きました。文字列の表示は次回だなぁ…。

《2024/4/19 11:14:24》

ブートで遊ぼっ!

x86系のPCをブートさせて遊んでみようと思います。
OSを作ろうとか、大それたことは言いません。技術力も知識も能力も全く足りないですから。
そもそもOSを作るのって、とっても

テキストは『OSを書く:初歩から一歩ずつ』です。ざっと目を通してみたんですが、それほどボリュームないし、解説もそれなりにあるし、これなら1日あれば確認できるかなと。チュートリアルだしねw

前提条件

テキストがDebianを前提にしているので、それに乗っかります。
今回のわたしの環境は

Intel Core i7
Windows10
VMwera Workstation 17 Plyer
Debian 12.4

です。Debianはこのために新規で入れました。ほとんど、まっさらです。

テキストで『nasm』『build-essential』『qemu』が必要だというのでインストールします。
Debianを起動してターミナルエミュレーターを実行し、以下を入力します。

む!

とか返ってきた。
どうしよう、どうする?

テキストは2016年とか2017年に書かれたものだし、今は2024年だし、8年もあれば生まれた子供も小学校3年生?になるし、パッケージ構成が変わっててもおかしくはないなぁ。
ともあれ、これでお終いにするってのは、あまりに情けなさすぎる。どうしたものか。

少し調べてみたら、色々な情報がありました。余計なものをインストールしても邪魔にならなければいいんだけれど、できるなら最小限の構成にしたいなぁ。

なので、これでどうだっ!

インストールは成功。
ではソースを書きましょう。
ファイル名は『boot.asm』です。

; boot.asm
hang:
jmp hang

times 510-($-$$) db 0

; This is a comment

db 0x55
db 0xAA

PlayWithBoot.1

コードの解説はテキストに任せて、書かれたとおりにオペレーションします。

問題ないですね。

動きました。こんな感じです。


これで前提条件が終了です。

なんか、思ったより先が長そう…

《2024/4/17 0:04:09》