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

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》