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回繰り返します。変更点はこんな感じ。
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》




