標準TTLだけ(!)でCPUをつくろう!(組立てキットです!)
(ホントは74HC、CMOSなんだけど…)
[第195回]

●2進数の減算について、もう少し…

前回は減算命令のうち、演算前のキャリー(ボロー)を無視するSUB命令について、その計算の仕方を考えました。
今回はボローを含めて減算を行う、SBB命令について説明をするつもりだったのですが、もう少し2進数の減算について、考えてみたいと思います。

前回、2進数の減算の例で、負数を減算するという例について、それは実はフェイクである、というお話をしました。
2進数では、負数を扱うこともできるのに、減算命令については、実は負数を引くことはできなくて、符号無しの数として減算をするのです、と説明しました。

なんだか、わかったような、わからないような、説明でしたが、「とにかくそういうことなのです」でおしまいにしてしまいました。

●2進数では負数を引くことができないのはなぜでしょうか?

今回は、そのあたりにしつこくこだわってみたい、と思います。

2進数の減算では、負数を引くことはできない、のはなぜでしょう?

それは、2進数の負数の約束事に、その理由があるのです(と思います)。
2進数の負数については、[第14回]で説明をしました。

たとえば8ビットの2進数を、符号付の数として考えた場合、その最上位ビット(ビット7)を1にした数、80〜FFが負数になります。
このときの80〜FFは10進数の−128〜−1に相当します。
ビット7だけが1で、あとが全部0である数80(10進数では−128)は、ちょっと特殊な数なので、それは除外することにして、その他の81〜FFが8ビットの符号付の数のうちの、普通の負数です。
ではその負数と、正の数とはどのような関係にあるかというと、正の数の「2の補数」が、その正の数を負数にしたものになります。

00000001は正の数(10進数の1)です。
この数の各ビットの1と0を全部反対にしたものが、00000001の「1の補数」です。11111110です。
その「1の補数」に1を加えたものが、「2の補数」です。11111111になります。
これが、符号付2進数の−1です。

ところで16ビットの−1はどうなるでしょうか。
同じように考えてみます。
こんどは16ビットですから、00000000 00000001が10進数の1です。
すると、11111111 11111110が「1の補数」になります。
それに1を加えて「2の補数」を作ります。11111111 11111111です。これが16ビットの−1です。

8ビットの符号付の数は、最上位のビット7が1であるときに、負数になります。
16ビットの符号付の数を考えると、今度は、最上位のビット15が1のときに、負数になります。
たとえば、8ビットの符号付の数では、11111111は負数(−1)ですし、00000001は正数(+1)です。
では、16ビットの符号付の数として、10000000 00000001は正の数かというと、下位8ビットは8ビットのときの+1と同じですが、最上位の15ビットが1なので、これは負数なのです(10進数の−32767)。

これとは逆に、01111111 11111111を符号付の数として考えた場合、下位8ビットは−1と同じ11111111ですが、最上位の15ビットが0なので正の数になります(10進数の+32767)。

コンピュータが扱う数は8ビットとは限りません。16ビット、24ビット、32ビットというように、大きな数の計算をしなければならないこともあります。
しかし、8080はそのような大きな桁の計算(特に減算)をするときでも、一度に8ビットしか計算をすることができません。
加算でも減算でも、下位から順に8ビットずつ計算を行っていきます。

もう、おわかりいただけたことと思います。
下位パイトの減算を行っているときには、それはまだ途中の計算なので、FFだから負数、01だから正数というように断定して計算するわけにはいきません。
したがって16ビットとか24ビットとかというようにたくさんのビットが並んでいる数の途中の計算をするためには、符号付ではない数(つまり普通の数)として計算するしかないのです。

ずっと減算についての例として説明をしてきましたが、このことは加算についても当てはまります。
前回までの説明はたまたま8ビットの数を使っての説明だったため、負数の加算についても目立った矛盾はでてきませんでした。しかし上位ビットへのキャリーが正しく発生するかどうかと言う点に注目して考えてみると、加算についても、8ビットの部分だけを切り取って、それを無理矢理、負数にしてしまって加算すると、おかしなことになってしまいます。

たとえば、1+(−1)=0という計算は、上位桁へのキャリーは発生しません。
しかし、これを2進数で計算してみると、
  00000001
  11111111(
1 00000000

というように、上位桁へのキャリーが発生してしまいます。
これは、上で説明した減算での場合と同じで、ADD命令も実は、1+(−1)=0ではなくて、1+255=256という計算を行うために、上位桁(上位ビット)へのキャリーが発生するのです。

それじゃあ、負数を含む加算、減算ができないじゃないの?
という当然の疑問がでてきます。
ですから、[第14回]で、2進数を符号付の数とするか、符号無しの数として扱うかは、「プログラマの勝手」だと言ったのです。

プログラマは、ある2進数を符号付の数として扱うことは自由にできます。
しかし、CPUは基本的には、符号無しの数として加算または減算を行います。
ただ、その計算が符号付の数に対して行われた場合も考慮して、計算結果のビット7が1になったら、S(サイン)フラグが立つようにしてあるのです。
プログラマは、キャリーフラグとサインフラグを意味があるものと見るか、無視すべきものかを判断するようにプログラムを書くことで、その計算を結果的に負数を含む計算にすることもできるのです。

上の1+(−1)の2進数での計算例で、それが8ビットの符号付数の加算であった場合には、キャリーは意味の無いものとして捨てることになります。
もし上の計算が、符号無しの数、とりわけ8ビットよりも大きな数の加算の途中の過程であった場合には、キャリーは上位桁への桁上げを示す重要な情報ですから、捨てることはできません。

うーん。
2進数の計算は、なんと難しいことか。
とお考えのあなた。
実は10進数でも、同じことなのですよ。

●10進数なら、どうなの?

2進数の計算を理解するためには、誰でも普通に計算できる10進数で考えてみることが有効です。

いやあ、10進数の計算は、2進数のように面倒なことはないよ。

それは、小学校のときに習って、それからずうっとそのまま使ってきていますからね。慣れているだけなのですよ。
10進数だって、本当は「符号無し」の数しか計算できない仕組みなのですよ。

ちょっと小学校に戻ったつもりで、簡単な引き算をやってみましょう。

  6 5 8
  □□3(
  ??□

昔はこういうのを「虫食い算」と言いました。そのほかにも鶴亀算とか流水算、植木算、などいろいろな計算問題がありましたが、最近はそういうのは教えないようですね?
ただ、この「虫食い算」はちょっと欠陥問題で完全に正解することはできません。
でも一番下位の答えだけは、出せますね?
答えの一番下の位の□には、何が入るでしょうか?

そんなの、5に決まってるじゃないの?ばかにしないでよぉ。

ブ、ブーッ!!
間違いでーす。
正解は、1ですよー。

ほんとうはぁ、

  6 5 8
  −1 3(
  6 7 1

だったのですよぉ。
いやぁ、これはひどい。まるでドロンジョ達がよくやるインチキ問題そっくりです。

そうなのですよ。
負数の引き算はできないので、負数を引くときには、負号を取って絶対値?正の数?にしてから、それを加算する、というテクニックを教えられて、それで、何の疑問も感じないでずっときたわけなのですよね。
いや、最初にこういう計算方法を教えられたときに、「えー、なぜぇ?」って疑問に思った方もいらっしゃったのでは?
どうして、マイナスをマイナスするとプラスになるのだろう、って。
10進数の計算だって、そうやって、よーく考えてみると、なかなか難しいのです。

ともあれ、10進数の計算でも、負数の存在を考えると、全体を見ないで、下位桁のみを見ての加算、減算はできない、ということがわかりました。
負数を含む計算になると、途端に難しくなってきてしまうのは、2進数だけではなくて、10進数でも同じことなのです。

でも、でも、えーと、たしかに、658−(−13)は、658+13というように、引き算を足し算になおしてから、計算しているけれど…。
だから、下位桁だけの計算を考えると、負数の場合と正数の場合とでは、計算結果が違ってきてしまうというのはわかりました、よ。
でも、なんだか、2進数での計算方法とは、ちょっと違うような気がするんだけど…。

たとえば、423−142は、そのままちゃんと引き算をしているよ。
これは、足し算じゃあないよ。

●10進数の引き算でも、足し算にできる?

うーん。
そうなのですよね。
10進数の引き算も、負数の引き算は、足し算に直して計算しますけれど、正の数を引く場合には、そのまま引き算をしていますねえ。
2進数の場合には、負数(2の補数)に直してから、足し算しましたよねぇ。

あれ?
どうして、10進数の引き算は、423+(−142)というようにしても、足し算にならないのでしょう?

ここまで考えてきて、ちょいと面白いことを思いついてしまいましたよ。
ええっ?まてよ?ほんとうに、そうなのか?
最初は、思いついただけで、まさかねぇ、と思ったのですけれど、試しに計算してみたら、なかなか面白いことがわかりました。

2進数と同じように、「補数」を考えると、引き算が足し算になってしまうのです!
(もっとも「補数」というのが、実は「引き算」なのですけれどね)

  423
  142(
  281

この計算を、2進数のときと同じように考えてみましょう。
2進数では、引く数の補数を考えました。
1)まず、各桁(各ビット)の1と0をひっくり返して、「1の補数」を作りました。
2)次に、「1の補数」に1を加えて、「2の補数」を作りました。
3)その「2の補数」を引かれる数に加算します。
4)最後に、加算の結果、キャリー(桁上げ)が発生したときは、そのキャリーを無効にし、キャリーが発生しなかったら、逆にキャリーを発生させます(減算ですから、上位桁へのボローになります)。
以上で、引き算の答えが求まりました。

それと同じことを10進数について、考えてみます。
1)まずは、「1の補数」です。2進数では、1と0をひっくり返しましたが、10進数ではどうすればよいでしょうか?
10進数では「9の補数」を考えればよいことがわかりました(そんな言葉があるかどうかは知りません。ふと思いついたネーミングです)。
足すと9になる数だから、「9の補数」です。1に対しては8です。4に対しては5です。2に対しては7です。
ですから、142の「9の補数」は、857になります。
2)その「9の補数」に1を足します。857+1=858です。これは「10の補数」です(これも、私が思いつきでつけた名前です)。
3)そのようにして作った「10の補数」を、引かれる数に足してみましょう。
果たして、結果は?

  423
  858(
1 281

最後に、4)の仕上げです。
上位桁への桁上げが発生したときは、それを無効にします。

おお。すると、結果は281になるではありませんか。
やっぱり、なんだか手品のようですね。

●下位桁からの桁上げがある場合の減算は?

ところで、上の計算例では、引く数142をひとまとめにして、「10の補数」を作って、それを423に加算しました。
でも、その計算は、1桁ごとに行うこともできるはずです。
たとえば最下位に注目すると、3−2の計算になります。「10の補数」の加算に変換すると、3+8=11になります。
ですから、最下位桁の答えは1です。
上位桁への桁上げ(キャリー)が発生しますが、4)のルールでそのときは逆にキャリーを無効にします。

さて、では、この先、その上の桁の計算はどうなるのでしょうか?

実は、この上の桁の計算は、SUBではなくて、SBBのルールで行わなければなりません。
SBBのルール、つまり下位桁からのキャリー(ボロー)を含めた減算のルールです。
本当は、それを今回、説明するつもりだったのですが、また道草を食ってしまいました。
次回は、その説明をすることにいたします。
2009.4.1upload
2009.4.3一部加筆

前へ
次へ
ホームページトップへ戻る