わらばんし仄聞記

南の国で引きこもってるWeb屋さん

原書で学ぶ64bitアセンブラ入門(2)

前回に引き続き、タイトルの勉強会でやった内容についてです。
勉強会毎の投稿ではなく、区切りと記事を書く労力の関係上、本書の1章ごとに書いていこうと思います。

chapter2 Numbers

二章は数値についてです。
コンピュータでは全ての情報をビット列として扱いますが、そのなかでも数値をビット列でどう保存しているかについて見ていきます。

この辺りはまだ序の口なので、まだまだ64bit特有な事は出てきません。ですので大体わかってる人は飛ばしてしまってもいいんじゃないかなとは思います。正直自分でも記事を書いていて、今更な事柄なんでとばしちゃいたい気持ちが少し…。

Binary numbers(2進数)

日頃使ってる10進数の数値は各桁の重みが10のn乗(n=0,1,2...)で表されるのと同様、2進数では各桁の重みが2のn乗となります。(位取り記数法)

  • 10進数の例
1024 = 10^3 * 1 + 10^2 * 0 + 10^1 * 2 + 10^0 * 4
     = 1000 + 0 + 20 + 4
     = 1024
  • 2進数の例
10101111 = 2^7 * 1 + 2^6 * 0 + 2^5 * 1 + 2^4 * 0 + 2^3 * 1 + 2^2 * 1 + 2^1 * 1 + 2^0 * 1
         = 128 + 16 + 8 + 4 + 2 + 1
         = 159

10進数から2進数への変換方法として、簡単なやり方は2での除算を繰り返して余りを最下位桁から順に当てはめていくやり方があります。
手順を逐一書いてみるとこんな感じ。

  • 741を2進数に変換
除算 剰余 ビット
741/2 370 1 1
370/2 185 0 01
185/2 92 1 101
92/2 46 0 0101
46/2 23 0 00101
23/2 11 1 100101
11/2 5 1 1100101
5/2 2 1 11100101
2/2 1 0 011100101
1/2 0 1 1011100101

というわけで、741は2進数で1011100101となります。このアルゴリズムは簡単なループや再帰で書けるのでお手軽ですね。

yasmアセンブラ用のコードを書く場合、定数の表現に2進数を使うことができます。
その際に10進数と見分ける為、末尾に"b"を付けて表す事になっています。

mov    rax, 1001    ; 1001をraxへ入れる
mov    rax, 1001b   ; 9をraxへ入れる

Hexadecimal numbers(16進数)

2進数では各桁の重みが2のn乗だったのと同様、16進数では16のn乗になります。つまり1つの桁で0~15までの数値を表さなければならないですが、当然普段使っている10進数の数字で10以上の数値を一文字で表すものはありません。なので、10以上にはアルファベット1文字を対応させることになり、その対応は以下のようになります。

10進数 2進数 16進数
0 0 0
1 1 1
2 10 2
3 11 3
4 100 4
5 101 5
6 110 6
7 111 7
8 1000 8
9 1001 9
10 1010 a
11 1011 b
12 1100 c
13 1101 d
14 1110 e
15 1111 f

yasm上で16進数の定数を表現するには"0x"を接頭辞として付けます。この書き方は他の言語やらでも大体一緒ですね。
さて、先程の2進数と同様、16進数を10進数に直してみます

  • 0xa1aを10進数にする
0xa1a = 16^2 * 0xa + 16^1 * 1 + 16^0 * 0xa
      = 256 * 10 + 16 * 1 + 16
      = 2586

その他、10進数から16進数を作る方法は2進数と同様にして出来ますので、この辺りは省略。

さて、コンピューター上では実際のデータは0か1の2進数で表現できるビット列で表されている事は先に述べた通りです。ただ、だからといって全ての数値などを2進数で書いていると、書くのも読むのも大変です。そこで16進数の出番となってきます。
2進数の4桁は丁度16進数の1桁として表せるため(16=2^4だから当然ですが)、相互に変換が容易になります。10進数を使おうとすると、桁が綺麗に合ってくれないので余計面倒になりますしね。
で、よく8ビット(=16進数2桁)の事を1バイトと呼びますが、この4ビットについても別名があり、「ニブル(nibble)」と呼ぶそうです。あんま聞かないですけどね。

Integer(整数)

z86-64での整数は1,2,4,8バイトの長さがあり、それらのとる値は以下の通りです

ビット数 バイト数 最小値 最大値
unsigned 8 1 0 255
signed 8 1 -128 127
unsigned 16 2 0 65535
signed 16 2 -32768 32767
unsigned 32 4 0 4294967295
signed 32 4 -2147483648 2147483647
unsigned 64 8 0 18446744073709551615
signed 64 8 -92233720368547750808 9223372036854775807

符号付き整数の負の数は2の補数表現で表されます。よく聞く2の補数の作り方は、全ビットを反転させて1を足したものという様に説明されているかと思います。さて、この「2の補数」。大体2進数を習うときに言葉と作り方だけ出てきて、一体どうしてこうなった的な説明が書いてない事が多いです。自分も習ったときはそれくらいで、根拠はスルーだったおかげで長いことちゃんと理解してなかったのでその辺りをここで捕捉しておこうかと。本書には別にこんな事書いてないんですけどね。

そもそも補数とは

n進法において、ある数aに対して足したときにn進法での桁が1つ上がる最小の数です。つまりある数aの桁数がmとすると

(補数) = n^m - a

となります。本当はもうちょっと細かい事があるんですが、その辺りはwikipedia先生(補数)を見てください。

  • 実際に例を挙げてみると

10進数で61の補数は

10^2 - 61 = 39

よって39ですね。さて、同様にして2進数について見てみますと、10101100(=172)の補数は

2^8 - 172 = 84
          = 01010100

となります。実際、01010100は10101100を反転して1足した数になってますね。しかしこの1を足すという挙動が腑に落ちない。なんなのコレ?
10101100(=172)について見てみると、この数値で使っている桁数で表現出来る最大値は11111111(=255)となる。で、先程補数を作るときに使った値は2^8(=256)。ということは、先程の式はこのよう変形できる

2^8 - 172 = 256 - 172
          = 255 - 172 + 1

ここでやっている255 - 172という行為。ちょっと考えてみればわかりますが、これは2進数上で考えると172のビットを反転していることと同じですね。こうして呪文のように言われている「反転して1を足せ!」という行為に裏付けがとれました。まー、この辺りは一通りwikipedia先生(2の補数)に書いてあるんですけどね。

  • なぜ補数を使うと負数の表現が出来るのか?

いやまぁ、計算したら実際にそうなるし、そういう風に考えられたとか言ってしまえばそれまでなんですが、これについても昔は気持ち悪かったので同じ境遇の人が居るかも知れないことを考えて書いておきます。
(ちゃんとした人が書いた書物で読んだ訳でなく、自分でそういうことかと思っただけなので間違いがあったら指摘お願いします)

話を簡単にするため、8ビットの場合について考えます。
有効桁を8ビットの符号無しとして見ると、表現出来る最大の数は255。256になると有効桁数を超えた所に桁が上がる事になりますね。有効桁が8ビットなので9ビット目以上はオーバーフローしてしてしまい、つまり、有効桁数に着目している限りでは256は0に戻ってしまいます。これは256を法とした剰余系とみなせます。

さて、整数において任意の数mは

m = qn + r
q = 商(quotient), r = 剰余(residue)

で表せます。先程の256を法とする剰余系を踏まえると、nを256とすればqにいかなる整数を入れてもmとrは合同になります。例としてm = 85、m = 183という2パターンについて考えると

 85 ≡ 0 * 256 + 85 (=85)
    ≡ 1 * 256 + 85 (=341)
    ≡ 2 * 256 + 85 (=597)

183 ≡ 0 * 256 + 183 (=183)
    ≡ 1 * 256 + 183 (=439)
    ≡ 2 * 256 + 183 (=695)

てな具合ですね。

ここで唐突に符号付きの状態に移行してみましょう。8ビットの場合、最上位ビットを符号ビットとして1の場合は負の値として扱いますが、これは符号無しの場合での0~127を正の数、128~255を負の値として扱うという区分けをしたことに他なりません。よって先程出した85と183という2値は、符号付き状態にしたとたん、正の数と負の数に分かたれてしまいました…。

まぁ、85の方はいいでしょう。元から正の数で分断後も正の数、つまり何も変わりありません。
一方183はいきなり負の値だと言われてしまいました。いきなりそんな事いわれたって…って感じですよね。

しかしここでいい方法があります。183は先程の256を法とした剰余系にいると考えると、負の値としての振る舞いも出来ることになります。任意の数mを表す式において、183という値は先程

185 ≡ 0 * 256 + 185

となるように表しました。そこで、さっき述べましたよね?この剰余系なら「qはどんな整数であっても剰余の値と合同になります」と。
ならばということで、こういう風にしちゃいます

186 ≡ 1 * 256 + (-71)

義務教育で習う商と余りの概念でいくと余りは正の数と勝手に思いこみがちですが、そもそも余りとは先程の m = qn + r のrの事に他なりません。なので、もちろんrが負であっても構いません。よって

186 ≡ -71

であるから、256を法とする剰余系では185と-71は合同です。
以上より、この剰余系においてはある数に185を足すことと71を引くことは同じ事になります。

さー これでスッキリしましたね。なぜ11111111なんてのが-1として扱われるか。
11111111は255であり、256を法とする剰余系では 255 ≡ -1 なのです。つまり有効桁数8bitの環境下においては255を足すことと1を引くことは同義なのです。よって、2の補数表現を使うことで加算で減算を表現でます。



かなり話が逸れました。
閑話休題。でもってIntegerの節はあと足し算したり掛け算したりしてるだけですのでこんなもので。

Floating point numbers(浮動小数点数

浮動小数点数とはどう扱われるべきか?そのあたりはIEEE754に書いてあります。
x86-64ではfloat、double、long doubleというパターンが使えますが(long doubleはIEEE754に載ってない)、いずれも基本的には指数部や仮数部やらに使うビット数が変わるだけなので、ビット数控えめのfloatについて見ていきます。

floatでは32ビットの内訳は以下のようになってます

位置 役割
32bit目 符号ビット
24~31bit目 指数部
1~23bit目 仮数

符号ビットはまぁいいですね。1ならその値は負の値であることを示します。

指数部には指数バイアスというものがあり、指数部8ビットに指定された値から127を引いた値が実際に指数として使われる値になります。つまり、指数部が10000011であるなら、131 - 127 = 4ということで、仮数部で示された値に2^4が掛けられる事になります。同様に01111101であるなら、125 = 127 = -2ということで、2^-2が掛けられます。

仮数部は小数点以下の値を表します。但し、この仮数部は整数部分を示す値はありませんが、1が暗黙の値として存在するものとして扱われます。つまり、仮数部が01100000000000000000000ならば、これはつまり1.01100000000000000000000であるとして処理されます。

これらを踏まえて考えると、このままでは0という値の表現ができないことに気づくと思います。指数部は2の指数を表すので、つまりここで表現できるいかなる値を指定しても0より大きくなります。仮数部もまた、暗黙の整数部が存在する以上、最小値は1になります。
そのため、これもまたルールとして、「すべてのビットが0ならば0として扱う」ということになっています。

他にもこのようなルールがあり、指数部の0x00と0xFFには特別な意味があります。0x00は仮数部も0の時は0となりますが、仮数部に値がある場合は非正規化数を表します。また、指数部、仮数部ともに0の時でも符号ビットが1の場合は-0を表現します。
0xFFの場合は無限大を表現し、これもまた符号ビットにより正の無限大と負の無限大を表します。

簡単な例

さて、実際に10進数から浮動小数点数を導く実例を見てみます。

  • 1.0の場合

この場合、仮数部には暗黙の1が存在するので、値はであれば1.0として見なされます。正の数だから符号ビットは0となり、あとは指数部。指数部から導かれる値、これをnとすると2のn乗が仮数部で求められた値に掛けられるので、つまり 2^n * 1.0 = 1.0 となればよい。つまり n = 0 であればいいわけで、指数部はそのビット列で示された値から127が引かれる事になるということは、127 - 127 = 0 を作れば良い。よって、指数部は127。
これをビット列で表すと

0             01111111     00000000000000000000000
↑符号ビット  ↑指数部     ↑仮数

見やすくすると

0011 1111 1000 0000 0000 0000 0000 0000 = 0x3F800000

となり、つまり浮動小数点数での1.0は0x3F800000となる。更にはintel系CPUではバイトオーダーはリトルエンディアンのため、この値はメモリ上に

00 00 80 3F

というように配置されます。リトルエンディアン=アドレスがリトルな位置に、バイト単位でのエンド(=末尾)を配置するパターンですね。

あと、本書には10進数から浮動小数点数の作り方なんてのも載ってますが、そこら辺は他の書籍でも沢山載ってるでしょうし割愛ということで。