参考: 乗除算命令
(2024年度以降の授業で出題する課題は乗算・除算命令を使わなくても解けるが,参考のため解説する。乗算・除算命令を使って課題プログラムを作成しても構わない。)
乗算
98 + 7 − (6 × 5) + (4 × 3 × 2) + 1 を計算するプログラムを作ってみよう。加減算に加えて乗算が必要だ。
乗除算を行う論理回路を作るのは加減算回路よりずっと面倒なので,CPUによっては乗除算命令が存在しないこともある(例えばミクロン)。乗算命令がないCPUで乗算を行う方法は,後の章で考える。
前掲の命令表の通り,幸いi386には乗算命令 mul
がある。ただし,(おそらくハードウェアを簡素化するために)以下の制約がある。
mul
の引数はソースオペランド(乗数)のみ,かつ,即値をオペランドにできない。- 記述例)
mul ebx
(eaxにebxを乗じる)
- 記述例)
- 被乗数と代入先は,ソースオペランドのビット幅に応じて自動的に決まる(下表)。
ソースオペランド | 被乗数 | 代入先 | C言語風表現 |
---|---|---|---|
バイト (BL, BH, CL, CH, ...) | AL | AX | AX = AL * src; |
ワード (BX, CX, ...) | AX | DXAX | DXAX = AX * src; |
ダブルワード (EBX, ECX, ...) | EAX | EDXEAX | EDXEAX = EAX * src; |
(「DXAX」は「積の上位16ビットをDXに,下位16ビットをAXに格納する」という意味。 「EDXEAX」は「積の上位32ビットをEDXに,下位32ビットをEAXに格納する」という意味。 積のビット幅は,乗数と被乗数のビット幅の和になる。例えば,32ビット同士を掛けると積は64ビットになる。)
例. EAX = 6,EBX = 5 のときに mul ebx
を実行すると,EAX の値は 30,EDX の値は 0 になる。他のレジスタの値は変わらない。
ソースオペランドは EAX や EDX でもよい。EAX = 6 のときに mul eax
を実行すると,EAX の値は 36,EDX の値は 0 になる。C言語風に書けば EDXEAX = EAX *
src; で,srcが EAX や EDX であってもよい。現在の各レジスタの値を使って乗算を行い,結果をEDXとEAXに代入するだけだ。
以降,計算の途中結果を32ビットで扱うことにしよう(Javaのint型と同じ)。乗算は,32ビット同士の積を計算すればよい。結果は64ビットになるが,上位32ビットは単に無視することにする。
さて 98 + 7 − (6 × 5) + (4 × 3 × 2) + 1 の計算だが,mul
を使うと自動的に EAX と EDX を使うことになる。それ以外のレジスタを一つ選び,加減算の途中結果を常に保持するようにしよう。最終的に計算結果を EBX に入れたいので,加減算の途中結果も EBX に保持すれば無駄がない。6 × 5 や 4 × 3 × 2 は EBX を使わずに計算し,積が得られたらEBXから引いたりEBXに加えたりする。
- 参考:どの演算にどのレジスタを使うか決めるのもコンパイラの仕事の一つだ。途中結果を保持しているレジスタの値を壊さないように,やりくりしてレジスタを割り当てる必要がある。
例題 98 + 7 − (6 × 5) + (4 × 3 × 2) + 1 を計算して結果を出力するアセンブリ言語プログラムを作りなさい。
除算
乗算命令の次は除算命令を見てみよう。
i386の場合,除算命令 div
も,乗算と同様にオペランドに制約がある。
div
の引数はソースオペランド(除数)のみ,かつ,即値をオペランドにできない。- 記述例)
div ebx
(edxeaxをebxで割る)
- 記述例)
- 被除数と代入先は,ソースオペランドのビット幅に応じて自動的に決まる。
ソースオペランド | 被除数 | 商の代入先 | 剰余の代入先 | Python風表現 |
---|---|---|---|---|
バイト (BL, BH, CL, CH, ...) | AX | AL | AH | AL, AH = AX // src, AX % src |
ワード (BX, CX, ...) | DXAX | AX | DX | AX, DX = DXAX // src, DXAX % src |
ダブルワード (EBX, ECX, ...) | EDXEAX | EAX | EDX | EAX, EDX = EDXEAX // src, EDXEAX % src |
例. EAX = 215,EDX = 0,EBX = 7 のときに div ebx
を実行すると,EAX の値は 30,EDX の値は 5 になる。他のレジスタの値は変わらない。
被除数のビット幅が除数の2倍であることに注意。div ebx
を実行する場合,被除数は「EDXを上位32ビット,EAXを下位32ビットとする64ビットの数」。
32ビット同士の除算をしたい場合,DIVを実行する前にEDXを0にしないと,間違った結果になる。
「12356秒が何時間何分何秒か計算する」プログラムを考えよう。
最初に「時間」を求めてもよいし最初に「秒」を求めてもよいが,いずれにせよ除算は2回必要だ。div
命令1個で商と剰余の両方が求まる。
除算の前に被除数の上位ビット(除数が32ビットの場合はEDX)を0にしておくことを忘れずに。
被除数,商,剰余を格納するレジスタは固定なので,必要な情報を上書きして消してしまわないようにしなければならない。除算命令の前や後で,どのレジスタの値をどのレジスタに転送すればよいか,紙の上などで検討するとよいだろう。
結果の出力だが,終了コードを使う方法では(0〜255の範囲の)1個の数値しか出力できない。 ここでは(あまり意味のない計算だが)「12356秒が何時間何分何秒か計算し,時×10 + 分×5 + 秒 を出力する」ということにしよう。「12356秒は3時間25分56秒なので 3×10 + 25×5 + 56 (= 211) を出力する」ということだ。
例題 12356秒が何時間何分何秒か計算し,時×10 + 分×5 + 秒 を計算して出力するアセンブリ言語プログラムを作りなさい。
- 参考:以前実行したのと同じコマンドを実行する際は
!
記法を使うと便利。
$ nasm hms.s
$ ld hms.o
$ ./a.out ; echo $? -- ; は「複数のコマンドを順に実行」
56
$ !na -- 最後に実行した'na'で始まるコマンドを再実行
nasm hms.s
$ !ld -- 最後に実行した'ld'で始まるコマンドを再実行
ld hms.o
$ !./a -- 最後に実行した'./a'で始まるコマンドを再実行
./a.out ; echo $?
181
参考: 浮動小数点演算例外
除算命令でエラーが発生すると,WS室の計算機環境では「浮動小数点演算例外」というメッセージが出力され,プログラムが強制終了される(CPUがプログラムの実行を止め,OSに制御を移す)(div
命令は浮動小数点演算命令ではないのだが,CPUとOSの都合でこのような出力がされるようだ)。
div
命令で発生するエラーは以下の2種類:
- 0で割ろうとした。
- 商が格納先のビット幅を超えた(例えば 64ビット÷32ビット の演算結果が32ビットを超えた)。
特に2.は,被除数の上位桁(64ビット÷32ビット の場合は EDX)を0に初期化すべきときにそうするのを忘れるとよく発生する。
例えば,EAX = 215, EDX = 8, EBX = 7 のときに div ebx
を実行すると,浮動小数点演算例外が発生する。この div
命令で計算されるのは,215 ÷ 7 ではなく (8 × 232 + 215) ÷ 7 であることに注意。その結果は 4908534083 であるが,232を超えているのでエラーとなる。