キャリーフラグとオーバーフロー
65816には、キャリーフラグと呼ばれるフラグがあり、これが立つ条件は主に下のいずれかです。
- 足し算した結果、繰上りが起こった場合
- 引き算した結果、繰下がりが起こらなかった場合
- SEC命令によって強制的に立てた場合
さて、キャリーフラグは何のために存在しているのでしょうか?キャリーが立っていればさらに1を足すとかいうのは面倒な気がしますが、これにはきちんとした理由があります。
○キャリーフラグと足し算
キャリーフラグはレジスタ長よりも大きい値を扱いやすくするために存在します。ここでは符号なし4bit整数だけを扱うことができるCPUを考えてみます。
たとえば、このCPUに%0111+%1001
(7+9)を計算させて、最後に右シフトしてみましょう。答えが8のなるのは分かりきっていますが、4bitしか扱えないのでキャリーがないと下のようになります。
0111
+1001
0000
%0000>>1=%0000
残念ながら途中で桁があふれたので答えは0になりました。では、キャリーフラグがあるとどうなるかというと、次のようになります。
0111
+1001
10000
%10000>>1=%1000
今度は正しく8と計算されました。これは繰り上がりであふれたbit5がキャリーフラグに保存され、右シフト時にキャリーフラグを併せた5bitで計算が行われたためです。
でもこれだけではなぜキャリーを足したり引いたりするのか分かりませんね。ですが実はキャリーを足し引きするという行為は皆さんも小学生のときから実はやっているはずです。
どういうことかというと、キャリーを足し引きするのは筆算と全く同じことなのです。
先ほどと同じ符号なし4bit整数だけを扱えるCPUを考えます。今度はこのCPUに%00111010+%00011101
(58+29)を計算させてみましょう。答えは87になるはずですが、そもそも58も29もこのCPUには扱えない数のはずです。
まず、小学生は58+29をどう計算するでしょうか。こうするはずです。
1
58
+29
87
8+9=17、5+2=7、繰上りがあるので7+1=8、答えは87
ごくごく普通の筆算の考え方です。それでは本題。このCPUでは43+29をこのように計算します(させます)。
②
①
0011
1010
+0001
+1101
0100+1=0101
10111
まず58と29の下位4bitずつを足します。%1010+%1101=%10111なので、bit5がキャリーフラグにセットされます。
次に58と29の上位4bitずつを足します。このとき、キャリーがセットされているのでさらに1を足します。%0011+%0001+1=%0101
計算し終わった4bit値を並べてみましょう。%01010111となります。これを10進数に直せば確かに87となります。
キャリーを足すというのはレジスタ長よりも大きい値を計算するときに繰り上がりを適切に表現するためのものだったのです。そしてそれは筆算で「繰り上がりの1」を足すのと本質的になんら変わりないということです。
実際のプログラムでは単に足し算がしたいだけで、繰り上がりはいらないということがあるかもしれません。その場合にはCLC命令でキャリーを強制的にクリアすれば問題ありません。
○キャリーフラグと引き算
足し算をするときキャリーを足す理由は分かりました。今度は引き算です。引き算の場合は「キャリークリア時にさらに1を引く」となっていますが、直感に反しているようにも見えます。なぜ「キャリークリア時」なのでしょうか?これにはCPUの性質と符号付整数の表し方に理由があります。
65816は引き算は出来ません
えっ、と思うかもしれませんが本当に65816は引き算は出来ません。足し算しか出来ません。
じゃあどうやってSBC命令で引き算を行っているかというと、2の補数を足しています。引き算ではなく足し算で計算をしています。
たとえば%01111111-%00101101という引き算は65816の内部処理的には%01111111+%11010011という足し算に置き換えられて処理されています。
それではキャリーフラグの謎について調べてみましょう。例によって4bitのCPUで考えますが、今度は符号付4bitの数しか扱えないCPUです。扱える範囲は-8~+7です。
%0101-%0011 (5-3)という計算は、このCPUでは次のように行います。このCPUも例によって足し算しか出来ないと考えてください。
0101
0101
-0011
-> +1101
????
10010
引き算が出来ないので内部的に5+(-3)という足し算に置き換えて計算されます。-3は2の補数表現で%1101となりますので%0101+%1101という計算が行われることになります。
今回の例では%0101+%1101で繰り上がりが発生しました。足し算で繰り上がりが発生したらキャリーが立ちますので5-2を計算するとキャリーが立ちます。
今度は%0101-%0110 (5-6)を計算させてみます。次のようになります。
0101
0101
-0110
-> +1010
????
1111
引き算が出来ないので内部的に5+(-6)という足し算に置き換えて計算されます。-6は2の補数表現で%1010となりますので%0101+%1010という計算が行われることになります。
今回の例では%0101+%1010で繰り上がりが発生しません。繰上りがないですのでキャリーは立ちませんでした。
この2つの例で、気づいたことは無いでしょうか?
「繰下がりのない引き算は、内部的に繰り上がりのある足し算であり、
繰下がりのある引き算は、内部的に繰り上がりのない足し算である」
ということが設計上必然となります。必ずこうなります。
したがって繰り下がりのない引き算でキャリーが立ち、繰下がりのある引き算でキャリーがクリアされるのです。
SBC命令ではキャリークリア時にさらに1を引きますが、これも筆算の考え方です。具体例は省略しますが、引き算でキャリークリアということは繰下がりがあった場合ですので、筆算で言うと桁借りが発生しています。筆算で桁借りしたときはさらに1引くのと同じです。
余分に1を引きたくない場合はSEC命令で強制的にキャリーを立てれば問題ありません。
○オーバーフロー
ADCやSBC命令ではキャリーフラグのほかにオーバーフローフラグが変更される場合があります。
オーバーフローフラグはサインドオーバーフローがあった場合に変更されると書いてありますが、サインドオーバーフローがそもそも分からないと困ります。
65816では基本的に数値を符号付で扱っており、bit7あるいはbit15は符号で、残りの7ないし15ビットで数値を表現します。
ということは8bitならば-128~+127、16bitならば-32768~+32767の範囲の値しか扱うことが出来ません。
この状態でたとえば64+64をさせるとどうなるでしょうか?65816は8bitモードだとします。
普通なら64+64=128なのですが、これを2進数に直すと%10000000となります。これは65816的には-128という値にしか見えません。
これがサインドオーバーフローです。足し算や引き算をした結果、符号が意図したものと逆になってしまった状態です。
サインドオーバーフローが起きた場合は、桁あふれ自体は起こっていないのでキャリーは変更されません。その代わり計算が正しく行われなかったという意味でオーバーフローフラグが立ちます。
繰上りが起こるなどして桁あふれが発生した場合にはキャリーが立ち、オーバーフローフラグは立ちません。キャリーを加えた9または17ビットで見れば正しく計算が行われているためです。
サインドオーバーフローの起こる条件
・2つの正数を足したら結果がマイナスになった (例:64+64=-128)
・2つの負数を足したら結果がプラスになった
(例:-65+(-65)=+126)
・負数から正数を引いたら結果がプラスになった (例:-65-65=+126)
・正数から負数を引くと結果がマイナスになった (例:64-(-64)=-128)
結局はサインドオーバーフローも2の補数表現の弊害なんですけれどね…
オーバーフローフラグが立った場合、正常な計算が行われなかった可能性が高いですので十分注意が必要です。
BVSまたはBVC命令で処理を分岐させるのが一番無難です。いわゆる例外処理です。
有名な事例ですが、1996年のアリアン5ロケット爆発事故は、オーバーフローを適切に取り扱わなかったことが原因といわれています。