メモリ、バイト、レジスタ、エンディアン (2015-12-11)
アセンブリプログラミングでは、PythonやJavaScriptなどのスクリプト言語ではほとんど意識する必要がない、2進数やエンディアンといった概念を知っておく必要があります。 非常に基礎的な部分ですが、復習しておきましょう。
バイト
一般的なコンピュータのメモリの量を表す単位は 1 バイト(byte)です。1 バイトは下の表のように 8 ビット をひとまとめにした単位です。 1ビットとはコンピュータで扱うことのできる最小単位で 0 または 1 の 2つの状態を保持することができます。コンピュータの内部では電圧の高い/低いという状態で表現します。 2つの状態だけを扱うことで回路が簡単になるなどのメリットがあるため、中間の電圧を使ったりすることはありません。アナログという場合は中間の状態も扱う回路、デジタルというのは中間は無く飛び飛びの(この場合は0と1)値を使うものです。アナログテレビ、地上デジタルテレビのデジタル/アナログも同じ意味で使っています。このビットをすべての処理の最小単位とするため、コンピュータは2進数で処理を行うことになります。いくつかのビットを同時に扱うほうが便利なため、「8ビットを1バイトとする」と定義されています。
バイト | 1 | |||||||
---|---|---|---|---|---|---|---|---|
ニブル | 1 | 0 | ||||||
ビット | 7 | 6 | 5 | 4 | 3 | 2 | 1 | 0 |
メモリの内容 | 0 | 0 | 0 | 0 | 1 | 1 | 0 | 1 |
2進数と16進数
1ビットで2つの状態を表せますが、1バイト(8ビット)ではビットの組み合わせでいくつの状態を表わせるのでしょうか? 8ビットのすべての組み合わせを表にすると大きすぎるので、4ビットで試してみましょう。
10進 | 16進 | 2進 | |||
---|---|---|---|---|---|
3 | 2 | 1 | 0 | ||
0 | 0 | 0 | 0 | 0 | 0 |
1 | 1 | 0 | 0 | 0 | 1 |
2 | 2 | 0 | 0 | 1 | 0 |
3 | 3 | 0 | 0 | 1 | 1 |
4 | 4 | 0 | 1 | 0 | 0 |
5 | 5 | 0 | 1 | 0 | 1 |
6 | 6 | 0 | 1 | 1 | 0 |
7 | 7 | 0 | 1 | 1 | 1 |
8 | 8 | 1 | 0 | 0 | 0 |
9 | 9 | 1 | 0 | 0 | 1 |
10 | A | 1 | 0 | 1 | 0 |
11 | B | 1 | 0 | 1 | 1 |
12 | C | 1 | 1 | 0 | 0 |
13 | D | 1 | 1 | 0 | 1 |
14 | E | 1 | 1 | 1 | 0 |
15 | F | 1 | 1 | 1 | 1 |
1ビットで2種類、4ビットで16種類となりました。一般にNビットで 2 N 種類の組み合わせが表現できます。したがって、8ビットでは 2 8 で256になります。同じように16ビットで65536、32ビットで4294967296(約42億)、64ビットで18446744073709551616(約1844京)種類の組み合わせが表現できます。メモリの単位として8ビットを使いましたが、人が扱うには 256 種類の状態は区別するには多すぎます。そこで8ビットを4ビットずつの2つに分け 16x16 と考えることにします。最初の表で 4ビットの単位に「ニブル」という名前を付けています。最近はニブルという呼び方は使いませんが、4ビットを1桁とする16進数はコンピュータでは10進数よりよく使います。0から16までを1桁とすると、数字が足りません。そこで「9」より大きい数字を「A」「B」「C」「D」「E」「F」とアルファベットの文字を使って 16 種類の数字とします。このように4ビットを1桁とする16進数 が 10 進数と近く、慣れが必要かもしれませんが、扱いやすいためよく使われます。また、大きな数を考える場合はキロ、メガ、ギガ、テラという単位が使われますが、 2 10 が1024になることを使うと、10ビット単位でキロ、メガという1000倍を基準にする数え方とほぼ一致します。
名前 | 記号 | 乗数 |
---|---|---|
キロ | K | 210 = 1,024 |
メガ | M | 220 = 1,048,576 |
ギガ | G | 230 = 1,073,741,824 |
テラ | T | 240 = 1,099,511,627,776 |
ペタ | P | 250 = 1,125,899,906,842,624 |
エクサ | E | 260 = 1,152,921,504,606,846,976 |
数が大きくなると1000を基準とする場合と1024を基準とする場合でズレが大きくなってきますが、メモリは1024を基準とし、ハードディスクやDVDなどの記憶媒体は1000を基準(大きく見える)にしている場合が多いのでちょっと注意が必要です。1024を基準にする場合はキビ(Ki)、メビ(Mi)、ギビ(Gi) という表し方もありますが、ほとんど普及していません。
2の補数
2進数で負の値を表す場合に使われる方法です。 最上位ビットが1のときは 負の数として、加減算が楽になるように工夫された方法と理解してください。 この場合、正の数は1ビット分小さい数までしか表せません。表現できる数の 範囲の半分を負の数に割り振っているわけです。
ある数αの32ビットの2の補数表現という場合、
α+β= 232
が成立するβになります。 この場合、232 は 33ビットの数値 (100000000000000000000000000000000) となっていることに注意してください。
ある数 α の N ビットの2の補数表現 β は以下の式で求めることができます。
β= 2N - α
難解そうな定義ですが、2 の補数 (two's complement) は 1 の補数 に 1 を加えたものとして簡単に求めることができます。1の補数 (one's complement) は単に各ビットを反転させたものです。 最上位ビットが 0 の場合は正、1 の場合は負として扱います。
2 の補数表現を符号付表現とも呼びます。アセンブリ言語で「符号なし/符号付き」という表現は整数をすべて正として扱うか、2 の補数表現として扱うかをあらわしています。C の unsigned int と signed int と同じ意味です。
下の表は4ビットの2進数とその1の補数、2の補数、4ビットの2の補数として10進表現したものです。2 の補数が 24 = 16 (10000) から減算したものと同じとなることが確認できます。
10進 | 2進 | 1 の補数 | 2 の補数 | 2の補数の 10進表現 |
---|---|---|---|---|
0 | 0000 | 1111 | 0000 | 0 |
1 | 0001 | 1110 | 1111 | -1 |
2 | 0010 | 1101 | 1110 | -2 |
3 | 0011 | 1100 | 1101 | -3 |
4 | 0100 | 1011 | 1100 | -4 |
5 | 0101 | 1010 | 1011 | -5 |
6 | 0110 | 1001 | 1010 | -6 |
7 | 0111 | 1000 | 1001 | -7 |
8 | 1000 | 0111 | 1000 | -8 |
9 | 1001 | 0110 | 0111 | 7 |
10 | 1010 | 0101 | 0110 | 6 |
11 | 1011 | 0100 | 0101 | 5 |
12 | 1100 | 0011 | 0100 | 4 |
13 | 1101 | 0010 | 0011 | 3 |
14 | 1110 | 0001 | 0010 | 2 |
15 | 1111 | 0000 | 0001 | 1 |
以上のように2の補数は非常に重要な発明です。つまり、正負の整数を2の補数で表現することで以下の様なメリットがあります。
- 正負の整数の四則演算が、正の整数だけの演算として行う簡単な処理で扱える。
- ビット数の小さい数値をビット数の大きい数値に変換する符号拡張が、最上位ビットを拡張されたビット数分だけコピーするだけの処理となる。
メモリ
メモリはプログラムやデータを格納するために使います。 プログラム本体もプログラムが使用するデータも、同じようにメモリに1バイト単位で格納されます。 メモリには下の図のように1バイト単位で番地(アドレス)をつけます。0番地からメモリ全体に番地を振ります。特定のアドレスのメモリを指定するためにレジスタを使いますが、メモリ全体を指定できるだけの大きさが必要です。32bitのCPUの場合、レジスタも32ビットで、0から約40億までの番地を指定できます。これが使用できるメモリの量を制限します。したがって32bitのCPUのメモリ/a>空間の上限は4GBということになります。64bit の CPU では理論的な上限は16エクサバイトという非常に大きなメモリ空間が可能となります。64bitではメモリの上限は金額や物理的なサイズで制限されることになります。
メモリアドレス | メモリの内容 | |||||||
---|---|---|---|---|---|---|---|---|
16進数アドレス | 7 | 6 | 5 | 4 | 3 | 2 | 1 | 0 |
0000000000000000 | ||||||||
0000000000000001 | ||||||||
0000000000000002 | ||||||||
0000000000000003 | ||||||||
0000000000000004 | ||||||||
0000000000000005 | ||||||||
: : |
: : |
|||||||
FFFFFFFFFFFFFFF9 | ||||||||
FFFFFFFFFFFFFFFA | ||||||||
FFFFFFFFFFFFFFFB | ||||||||
FFFFFFFFFFFFFFFC | ||||||||
FFFFFFFFFFFFFFFD | ||||||||
FFFFFFFFFFFFFFFE | ||||||||
FFFFFFFFFFFFFFFF |
レジスタ
コンピュータの中でメモリはどのように使われるのでしょうか? 「メモリはプログラムやデータを格納するために使います。」と書きましたが、メモリの内容は レジスタ という数値を格納することのできる CPU の領域にコピーします。 その後、レジスタと他のレジスタや別アドレスのメモリとの間で計算したり、比較したりといった処理が行われます。その結果をレジスタからメモリに書き戻すこともできます。 また、メモリ番地を指定するにもレジスタを使います。インストラクションポインタ(プログラムカウンタともいう)というレジスタは実行する命令が格納されているメモリアドレスを保持することで、命令の実行順序をコントロールしています。
下の図は64ビットのレジスタを示します。レジスタは64ビットの数値(double word)だけではなく、8ビット(byte)、16ビット(half word)、32ビット(word)の数値をレジスタの下位を使って扱うこともできます。
byte 7 | byte 6 | byte 5 | byte 4 | byte 3 | byte 2 | byte 1 | byte 0 | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
7 | 6 | 5 | 4 | 3 | 2 | 1 | 0 | 7 | 6 | 5 | 4 | 3 | 2 | 1 | 0 | 7 | 6 | 5 | 4 | 3 | 2 | 1 | 0 | 7 | 6 | 5 | 4 | 3 | 2 | 1 | 0 | 7 | 6 | 5 | 4 | 3 | 2 | 1 | 0 | 7 | 6 | 5 | 4 | 3 | 2 | 1 | 0 | 7 | 6 | 5 | 4 | 3 | 2 | 1 | 0 | 7 | 6 | 5 | 4 | 3 | 2 | 1 | 0 | ||||||||||||||||
Double Word (64bit) | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
Word (32bit) | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
Half Word (16bit) | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
Byte(8bit) |
エンディアン
レジスタの内容をメモリに格納する方法に2種類の方法があり、エンディアンと呼びます。 上の表で「byte 0」「byte 1」の番号をメモリのアドレスとした場合、レジスタの内容の小さい桁側(右)をメモリアドレスの小さい側に格納する場合をリトルエンディアンと呼びます。
例えば 10進数で「4,822,678,189,205,111」という数値を16進数で 表すと「0x0011223344556677」という値になります。この64ビットの 数値をメモリに格納することを考えます。2種類の方法があって、 数値をメモリの小さいほうから並んでいる順に格納するビッグエンディアンと、 大きい桁はメモリアドレスの大きいほうに格納するリトルエンディアンです。
エンディアンは CPU に依存していて、x86 や x86-64 はリトルエンディアンです。 ARM と PowerPC はバイエンディアンになっていて、設定によってどちらのエンディアンにも切り替えられます。例えば CPU が PowerPC の玄箱はビッグエンディアンです。ARM64 はリトルエンディアンを採用しています。
ビッグエンディアン
素直に考えると、最初の桁、つまり数値として大きい側のデータ(00)から順にメモリに格納するように考えます。
メモリアドレス | 7 | 6 | 5 | 4 | 3 | 2 | 1 | 0 |
---|---|---|---|---|---|---|---|---|
0000 | 00 | |||||||
0001 | 11 | |||||||
0002 | 22 | |||||||
0003 | 33 | |||||||
0004 | 44 | |||||||
0005 | 55 | |||||||
0006 | 66 | |||||||
0007 | 77 |
ビッグエンディアンではメモリダンプ(メモリの内容のリスト)が メモリを区切る単位にかかわらずに、左から右に素直に並ぶというメリットが あります。
00 11 22 33 44 55 66 77 // byte 0011 2233 4455 6677 // half word 00112233 44556677 // word 0011223344556677 // doubleword
リトルエンディアン
一方で、桁の大きい方をメモリアドレスの大きい側に格納するほうが自然と言われれば、それはそれで納得します。
メモリアドレス | 7 | 6 | 5 | 4 | 3 | 2 | 1 | 0 |
---|---|---|---|---|---|---|---|---|
0000 | 77 | |||||||
0001 | 66 | |||||||
0002 | 55 | |||||||
0003 | 44 | |||||||
0004 | 33 | |||||||
0005 | 22 | |||||||
0006 | 11 | |||||||
0007 | 00 |
リトルエンディアンではメモリダンプ(メモリの内容のリスト)が メモリを区切る単位によって逆転するように見えます。 大きい桁を左側に書くため、区切り方によって複雑に変化するように 感じます。 「前の桁が後ろに格納される」ということを 十分に理解する必要があります。
77 66 55 44 33 22 11 00 // byte 6677 4455 2233 0011 // half word 44556677 00112233 // word 0011223344556677 // doubleword