CS:APP 第二章 情報の表現と操作
C及びC++は同様の演算結果を示す。Javaは少し異なるが、厳格な規格が存在している。
Cは後方互換性が担保されているので、C11で動くものがC99では動かないといったことがほとんど存在しない。
C89とC90はほとんど同じ意味
16進数のfは15だったという衝撃
10進数の62を2進数に変換するときに効率的なやり方。
まず64 = 2^6ということを覚える。
62は64 - 2なので、64以下の桁で構成される値 かつ 2の桁は0だということがわかる。
つまり、2^5 ~ 2~0までの桁のうち、2の桁と1の桁(偶数なので)以外の値は1だということがわかる。
よって答えは111100
まぁプログラミング電卓でやれば良い話なんだが
暗記しておきたい累乗
16^2 = 256 = 2^8
16^3 = 4096 = 2^(4 + 4 + 4) = 2^12
10進数を16進数に変換するのは一回2進数に変換してからの方がはやいかも
16進数ABCDEFは奇数番目が偶数になる
m1のエンディアンの話
M1はaarch64ベース。これはバイエンディアンでありエンディアンは処理系に依存する。
だからx86_64をm1に以降する際にエンディアンの変換処理は発生していない。
ただmemory-orderingと呼ばれる、メモリへのアクセス順序などがx86とaarchでは異なる。
これをrosetta 2で保管しているので、x86で動くアプリをエミュレータによって動かすことでシンプルにm1でアプリを動かすよりもオーバーヘッドが少ないアプリ実行を実現出来る。
エンディアンが問題になる場合の話
各CPUでエンディアンの処理が異なることは問題ない。なぜならそれは内部で処理が完結しているからである。
問題はネットワーク通信を行う場合
この場合、ネットワーク上でバイナリデータを送受信するときにエンディアンがちがうCPU同士で通信を行ってしまうと正しい数値が受け取れなくなってしまう。
なのでネットワーク側で規則を一意に定めてそのエンディアン方式に変換することが求められる。
例えばTCP/IPでは(というかネットワーク規則では)ビッグエンディアンを採用している。
そのため、x86系のPCにおいてはバイトオーダーをビッグエンディアンに変換してから出力するような実装が必要になってくる。
このコマンドで自分のPCのエンディアンを確認することができる。
virtualbox内でcpuの情報を確認する方法。
cat /proc/cpuinfo
Corei5を使っている...!
2-6
2進数表記
3510593→001101011001000101000001
3510593.0→0x4A564504→01001010010101100100010100000100
001101011001000101000001
21bitが一致。
値が一致した部分だけを切り取った10進数表記 1413441
。
一致しない部分は最初に一致する桁の一つ前の桁
Unicodeを表現するには32bit必要だが、UTF-8でascii文字で記載可能な文字を扱うときは、ascii文字と同様に1byteとして処理することが出来る。
またこの辺もガッツリ調べたい。
これ立ち読みしたい
マスキング操作
あるbitで一部の情報だけを扱いたい場合、ISA(Instruction Set Architecture)はそのような命令を持っていない。なので演算などを用いてbitを抽出する必要がある。
ex)
0x32181231に対して操作を行う。
-
最下位バイトだけ操作したい。
0x32181231 & 0xFF = 0x31
& 0xFF
の部分がマスクになっており、下位バイトだけ出力できるブール操作をしている。 -
最下位バイトだけ値を残し、他を全部補数としたい
0x32181231 ^ ~0xFF
0xFFの否定により最下位バイト以外がFになり、最下位バイトが0になる。
これによりXORで最下位バイトのみ値が残る。 -
最下位バイト以外はそのままで、最下位バイトを全て1とする。
0x32181231 | 0xFF
ISAとは
シフト演算の話
シフト演算は主に掛け算/割り算をビット演算で表現したいときに使う。
1bitずらすごとに 2× or 2÷になる。
シフト演算ではビットのずらしが発生するため、端のビットに対して値を埋めていかなければならない
例えば8bit 11001001
のような値に対して、左シフトを1すると1001001?
最後の?の部分を埋め合わせないと値が特定できない。この位置に対して0 or 1の値のどちらを埋めるかというところで、論理シフト/算術シフトが扱われる。
-
論理シフト
常に0で埋める -
算術シフト
もし値が負数の場合
- 左シフト
最上位ビット(符号ビット)を残したまま、0埋めをする - 右シフト
1埋めをする
これにより負数の維持を実現出来る。(もし0埋めをしてしまうと符号逆転などが起こる)
基本的にコンパイラなどは負数のビットシフトにおいては算術演算を行っているが、処理系動作である。
2の補数計算
最上位bitをwだとすると-2 ^ (w -1 )
として後は通常の進数変換を行う。
ex)
1001 → (-1 * 2^3) + (0 * 2^2) + (0 * 2^1) + (1 * 2^0) = -7
つまり最上位ビットだけ符号転換して、後は通常の進数計算をするだけでよし。
C言語において、符号付き整数と符号なし整数で演算を行う場合、符号付き整数を符号なし整数に暗黙的にキャストして演算を行う。
#include <stdio.h>
int main()
{
if (-1 > 0U)
{
printf("負数は0Uより大きいと評価される");
}
else
{
printf("負数は0Uより小さいと評価される");
}
}
❯ ./a.out
負数は0Uより大きいと評価される
ゼロ拡張/符号拡張
範囲が小さい型から範囲が大きい型にキャストする場合、その数値が符号を持つかどうかによってbitの値が変わる。
#include <stdio.h>
typedef unsigned char *byte_pointer;
void show_bytes(byte_pointer start, size_t len)
{
size_t i;
for (i = 0; i < len; i++)
printf(" %.2x", start[i]);
printf("\n");
}
int main()
{
char a = -2;
int b = a;
long c = a;
char d = 2;
int e = d;
long f = d;
// 負の値
show_bytes((byte_pointer) &a, sizeof(char));
show_bytes((byte_pointer) &b, sizeof(int));
show_bytes((byte_pointer) &c, sizeof(long));
// 正の値
show_bytes((byte_pointer) &d, sizeof(char));
show_bytes((byte_pointer) &e, sizeof(int));
show_bytes((byte_pointer) &f, sizeof(long));
}
リトルエンディアンであることに注意
❯ ./a.out
fe
fe ff ff ff
fe ff ff ff ff ff ff ff
02
02 00 00 00
02 00 00 00 00 00 00 00
C言語において、符号付き整数と符号なし整数で演算を行う場合、符号付き整数を符号なし整数に暗黙的にキャストして演算を行う。
ex)
int a = -90000
unsigned int b = 20000
printf("ans = %d", a + b);
4bitのみで構成される値
5bit目から切り詰める
5 + 2 = 7
0101 0010 = 0111 = 7
8 + 10 = 18
1000 1010 = 10010 = (1)0010 = 2
if (x + y > 2^4)
x + y = (x + y) - 2^4
-8 + -7 = -15
1000 + 1001 = 10001 = (1)0001 = 1
-1 * 2^3 = -8
if (x + y < -(2^3))
x + y = (x + y) + (2^4)
unsinged char 1byte
0 ~ 255
unsinged char a = 220 = 11011100
unsinged char b = 100 = 01100100
a + b = 330 = 101000000 = 01000000 = 64
330 - (256)2^8 = 64
-INT_MIN = INT_MIN
01111111111111111111111111111111 + 1 = 1000000000000000000000000
5 0101
-5 1011
乗算オーバーフローの脆弱性
size_tの危険性
mallocを使用する際に、以下のような引数をもって計算する場合があるとする。
void *malloc_new(int ele_cnt, size_t ele_size)
{
void *result = malloc(ele_cnt * ele_size);
}
これを32bit用にコンパイルした際に問題が起きやすい。
size_tはコンパイルオプションによって最大値が決まる。
そのため、コンパイルオプションを32bitなどで行うと、size_t_max = uint_maxとなり極端に扱える数が小さくなる。
そのため、乗算オーバーフローにより想定していない数でのmallocが発生してしまい、結果として脆弱性を引き起こす要因になる。
乗算オーバーフローは常に意識しておくべきである。
整数の乗算はコンパイル側からすると高コスト(10クロックサイクル以上)かかる
しかし、加算/減算/ビットシフトにおいては(1クロックサイクル)で収まるため、コンパイル時に整数乗算はビットシフトに置き換えられる。
ex)
x * 14 = x * (2^3 + 2^2 + 2^1) = (x<<3) + (x<<2) + (x<<1)
このように書き直すことが可能になる
このようにすることでコンパイラがサイクルの効率化を図ることで、動作を速くする。
-6の乗算は(x<<1) - (x<<3)で行われる。
全ての整数はこのビットシフト同士の加減とxの加減で表現することが出来る
右シフトによりアンダーフローを起こしてしまった整数型は、0に近づくように丸めを行う。
そのときの定義
ある整数の絶対値|a|はa' ≦ a < a'+1
で表される数のa'となる。
つまり、aが3.14のとき
3 ≦ a < 3'+1
となるので |a| = 3となる。
また、aが-3.14のとき
-4 ≦ a < -4+1
となるので|a| = -4となる。
コンピューターの世界では、整数型の丸めはこのように定義されている。
つまり値が正の場合は切り捨て、負の場合は切り上げを行う。
もし負の値を切り捨てしたい場合はバイアスを付加する。
バイアスとはビットシフト数分の最大値を付加してから計算を行うこと。
例えば-12340
という値を4ビット右シフトを行う場合の計算は以下のようになる。
-12340 / 16 = -771.25 →丸め = -772
これを2進数で行うと
1100111111001100 >> 4 = 1111110011111100 = -772
しかし切り上げを行いたくない場合、ビットシフト分の最大を付加
つまり、4ビットの最大値15を付加する。そのため以下のようになる。
(1100111111001100 + 1111) >> 4 = 1111110011111101 = -771
これにより負の値であっても切り捨てを行うような計算を実現することが出来る