「x86、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》