x86リアルモードでモニターを作ろうシリーズです。Windows11+wsl2で構築しています。
前回は BCD ライブラリを「導入しただけ」で終わりました。
おなか一杯とか言って終わりにしました。
……が、さすがに導入だけで終わると「で、何ができるの?」というお話になるので、今回は 計算のコア をやります。
やることは単純で、
- PHS(packed) ⇄ DHS(unpacked)
- 計算は DHS で筆算
- 最後に PHS に戻す
という流れ。前回の変換関数(pack/unpack)はそのまま使います。
方針:符号つき四則演算は “絶対値” に落として処理する
BCD筆算は「桁列」を扱うので、符号まで混ぜると面倒になります。
なので基本方針はこれ。
- 符号だけ先に剥がす(signを別に持つ)
- 絶対値で add/sub/mul/div
- 最後に 符号を付け直す
これをやるだけで、気持ちがだいぶ楽になります。
まずは比較:cmp_abs(絶対値比較)
加減算は「どっちが大きい?」がないと破綻します。
DHS は 下位桁が index 0 の “リトルエンディアン桁列” として扱うことにします(0桁目が1の位)。
; ----------------------------------------
; dhs_cmp_abs
; 絶対値比較
; 入力:
; DS:SI = DHS A
; ES:DI = DHS B
; 出力:
; AX = 1 (A>B), 0 (A==B), -1 (A<B)
; 破壊: BX,CX,DX
; ----------------------------------------
dhs_cmp_abs:
; 桁数で先に判定
mov cx, [si + DHS_LEN]
mov dx, [es:di + DHS_LEN]
cmp cx, dx
ja .a_gt
jb .a_lt
; 同じ桁数なら最上位桁から比較
test cx, cx
jz .eq
dec cx ; cx = last index
.loop:
mov al, [si + DHS_VAL + cx]
mov bl, [es:di + DHS_VAL + cx]
cmp al, bl
ja .a_gt
jb .a_lt
test cx, cx
jz .eq
dec cx
jmp .loop
.a_gt:
mov ax, 1
ret
.a_lt:
mov ax, -1
ret
.eq:
xor ax, ax
ret
絶対値加算:add_abs(桁上がりだけ)
これは筆算なので素直です。
A+B を計算して、carry を次桁へ。
; ----------------------------------------
; dhs_add_abs
; C = |A| + |B|
; 入力:
; DS:SI = DHS A
; ES:DI = DHS B
; SS:BP = DHS C (出力)
; 破壊: AX,BX,CX,DX
; ----------------------------------------
dhs_add_abs:
; c を 0 クリア(val 40 / len / sign)
; (クリア関数は省略。前回同様にベタループでOK)
; maxlen = max(a.len, b.len)
mov cx, [si + DHS_LEN]
mov dx, [es:di + DHS_LEN]
cmp cx, dx
jae .len_ok
mov cx, dx
.len_ok:
xor bx, bx ; i=0
xor dl, dl ; carry=0
.loop:
cmp bx, cx
jae .done_digits
mov al, [si + DHS_VAL + bx]
add al, [es:di + DHS_VAL + bx]
add al, dl ; +carry
; al は 0..19 くらいになる
cmp al, 10
jb .no_carry
sub al, 10
mov dl, 1
jmp .store
.no_carry:
xor dl, dl
.store:
mov [ss:bp + DHS_VAL + bx], al
inc bx
jmp .loop
.done_digits:
; 最後に carry が残ったら 1 桁追加
test dl, dl
jz .set_len
mov [ss:bp + DHS_VAL + bx], dl
inc bx
.set_len:
mov [ss:bp + DHS_LEN], bx
ret
絶対値減算:sub_abs(借りだけ)
A-B(ただし |A|>=|B| を前提)です。
; ----------------------------------------
; dhs_sub_abs
; C = |A| - |B| (|A|>=|B|)
; 入力:
; DS:SI = DHS A
; ES:DI = DHS B
; SS:BP = DHS C
; ----------------------------------------
dhs_sub_abs:
mov cx, [si + DHS_LEN] ; a.len
xor bx, bx ; i
xor dl, dl ; borrow=0
.loop:
cmp bx, cx
jae .finish
mov al, [si + DHS_VAL + bx]
sub al, dl ; -borrow
mov dl, 0
mov ah, [es:di + DHS_VAL + bx]
cmp al, ah
jae .ok
add al, 10
mov dl, 1 ; borrow
.ok:
sub al, ah
mov [ss:bp + DHS_VAL + bx], al
inc bx
jmp .loop
.finish:
mov [ss:bp + DHS_LEN], cx
; 正規化(上位 0 を削る)
; → 前回の phs_normalize 相当を DHS でもやる
; (ここでは dhs_normalize を呼ぶことにする)
mov si, bp
call dhs_normalize
ret
ここで “符号つき add/sub” を組む
符号の組み合わせは結局こうです。
- same sign:絶対値加算、符号はそのまま
- different sign:大きい方から小さい方を引いて、符号は“大きい方”
これで 符号付き加算 ができます。
; ----------------------------------------
; phs_add
; PHS A + PHS B -> PHS C
; 入力: DS:SI=A, ES:DI=B, SS:BP=C
; ----------------------------------------
phs_add:
; A,B を unpack して DHS に
; work_dhs_a / work_dhs_b / work_dhs_c を使う(固定領域)
; sign が同じ?
mov al, [si + PHS_SIGN]
mov bl, [es:di + PHS_SIGN]
cmp al, bl
jne .diff
.same:
; c = |a| + |b|, sign = a.sign
; dhs_add_abs(...)
; c.sign = al
jmp .pack
.diff:
; |a| と |b| を比較
; 大きい方 - 小さい方
; c.sign = big.sign
; (cmp_abs → sub_abs の順)
.pack:
; DHS -> PHS pack
ret
(この辺は “書くのは面倒だけど考え方は小学生とか中学生なので、コードの全体は github に置くことにします)
乗算:まずは「1桁 × 多桁」(mul_digit)
いきなり多桁×多桁をやると泣くので、最初はこれ。
- C = A * digit(0..9)
; ----------------------------------------
; dhs_mul_digit
; C = |A| * digit(0..9)
; 入力: DS:SI=A, AL=digit, SS:BP=C
; ----------------------------------------
dhs_mul_digit:
mov cx, [si + DHS_LEN]
xor bx, bx
xor dl, dl ; carry=0
.loop:
cmp bx, cx
jae .finish
mov ah, [si + DHS_VAL + bx]
; ax = ah * digit + carry
; ここで8bit mulを使う(AL=digit, AH=digit?)ので、適当に退避する
; 実装は各自の流儀で(私は泥臭くやります)
; 結果を 10 で割って carry と digit を作る
; (div は 16bit で余裕)
inc bx
jmp .loop
.finish:
; carry が残ったら append
ret
この mul_digit ができると、次は「筆算の掛け算」がそのまま書けます。
(つまり “各桁の mul_digit をずらしながら足す”)
除算:今回は 割り算の入口だけ(div_digit)
本当は除算まで行きたいところですが、除算は一気に面倒になります。
なので今回は入口として、
- A ÷ digit(0..9)で 商と余り
を作って終わります。
これがあると「10進→2進」「2進→10進」の補助にも使えて便利です。
上位桁から順に余りを *10 倍して足し、割り算を行う筆算そのままの流れで商を構築します。
; ----------------------------------------
; dhs_div_digit
; C = |A| / digit
; R = remainder
;
; 入力:
; DS:SI = DHS A
; AL = digit (1..9) ※0は呼ぶ側で弾く
; SS:BP = DHS C (商)
;
; 出力:
; AH = remainder (0..digit-1)
;
; 破壊:
; BX,CX,DX
; ----------------------------------------
dhs_div_digit:
mov cx, [si + DHS_LEN] ; 桁数
xor dx, dx ; remainder = 0
xor bx, bx ; i = 0
; 商の長さは一旦 A.len にしておく
mov [ss:bp + DHS_LEN], cx
test cx, cx
jz .zero ; A==0
dec cx ; 最上位桁 index
.loop:
; dx = remainder * 10 + current_digit
mov dl, 10
mul dl ; AX = remainder * 10
mov dl, [si + DHS_VAL + cx]
add al, dl ; AX = remainder*10 + digit
; AX / digit
div byte [digit_tmp] ; AL=quotient, AH=remainder
; 商の桁を書き込む(同じ位置)
mov [ss:bp + DHS_VAL + cx], al
; remainder は AH に残る
mov dl, ah
test cx, cx
jz .done
dec cx
jmp .loop
.done:
mov ah, dl ; remainder を返す
; 上位0桁を削る
mov si, bp
call dhs_normalize
ret
.zero:
xor ah, ah
ret
実行できること
この段階でできることは、
- 符号つき加算/減算(PHS)
- 1桁乗算(DHS)
- 1桁除算(DHS)
です。
「64bitが欲しい」から始まったのに、気づいたら 10進20桁 を手に入れつつあります。
現代の感覚だと遠回りすぎますが、リアルモードだとこれが近道です。たぶん。
そんなこんなで
まだ “おなか一杯” にはなってません。
むしろ、ここからが地獄です。
次回は多分、
- 多桁×多桁(筆算の掛け算)
- 正規化の地獄(len と leading zero)
- そして、みんな大好き「ゼロの符号問題」
あたりをやります。やる……はず。
今回はここまでです。お付き合いいただきありがとうございました。
《2026/1/13 12:32:06》