Closed33

CS:APP 第二章 情報の表現と操作

bayamasabayamasa

C及びC++は同様の演算結果を示す。Javaは少し異なるが、厳格な規格が存在している。
Cは後方互換性が担保されているので、C11で動くものがC99では動かないといったことがほとんど存在しない。
C89とC90はほとんど同じ意味

bayamasabayamasa

10進数の62を2進数に変換するときに効率的なやり方。
まず64 = 2^6ということを覚える。
62は64 - 2なので、64以下の桁で構成される値 かつ 2の桁は0だということがわかる。
つまり、2^5 ~ 2~0までの桁のうち、2の桁と1の桁(偶数なので)以外の値は1だということがわかる。
よって答えは111100

まぁプログラミング電卓でやれば良い話なんだが

bayamasabayamasa

暗記しておきたい累乗
16^2 = 256 = 2^8
16^3 = 4096 = 2^(4 + 4 + 4) = 2^12

10進数を16進数に変換するのは一回2進数に変換してからの方がはやいかも

16進数ABCDEFは奇数番目が偶数になる

bayamasabayamasa

m1のエンディアンの話
https://note.com/njrecalls/n/nf7f5e48c255f

bayamasabayamasa

M1はaarch64ベース。これはバイエンディアンでありエンディアンは処理系に依存する。
だからx86_64をm1に以降する際にエンディアンの変換処理は発生していない。

ただmemory-orderingと呼ばれる、メモリへのアクセス順序などがx86とaarchでは異なる。
これをrosetta 2で保管しているので、x86で動くアプリをエミュレータによって動かすことでシンプルにm1でアプリを動かすよりもオーバーヘッドが少ないアプリ実行を実現出来る。
http://www.nminoru.jp/~nminoru/diary/2002/10.html#20021030p1

bayamasabayamasa

エンディアンが問題になる場合の話
各CPUでエンディアンの処理が異なることは問題ない。なぜならそれは内部で処理が完結しているからである。
問題はネットワーク通信を行う場合
この場合、ネットワーク上でバイナリデータを送受信するときにエンディアンがちがうCPU同士で通信を行ってしまうと正しい数値が受け取れなくなってしまう。

なのでネットワーク側で規則を一意に定めてそのエンディアン方式に変換することが求められる。
例えばTCP/IPでは(というかネットワーク規則では)ビッグエンディアンを採用している。

そのため、x86系のPCにおいてはバイトオーダーをビッグエンディアンに変換してから出力するような実装が必要になってくる。

bayamasabayamasa

virtualbox内でcpuの情報を確認する方法。
cat /proc/cpuinfo
Corei5を使っている...!

bayamasabayamasa

2-6
2進数表記
3510593→001101011001000101000001
3510593.0→0x4A564504→01001010010101100100010100000100
001101011001000101000001
21bitが一致。
値が一致した部分だけを切り取った10進数表記 1413441
一致しない部分は最初に一致する桁の一つ前の桁

bayamasabayamasa

Unicodeを表現するには32bit必要だが、UTF-8でascii文字で記載可能な文字を扱うときは、ascii文字と同様に1byteとして処理することが出来る。

またこの辺もガッツリ調べたい。

bayamasabayamasa

マスキング操作
あるbitで一部の情報だけを扱いたい場合、ISA(Instruction Set Architecture)はそのような命令を持っていない。なので演算などを用いてbitを抽出する必要がある。
https://ja.wikipedia.org/wiki/マスク_(情報工学)

ex)
0x32181231に対して操作を行う。

  • 最下位バイトだけ操作したい。
    0x32181231 & 0xFF = 0x31
    & 0xFFの部分がマスクになっており、下位バイトだけ出力できるブール操作をしている。

  • 最下位バイトだけ値を残し、他を全部補数としたい
    0x32181231 ^ ~0xFF
    0xFFの否定により最下位バイト以外がFになり、最下位バイトが0になる。
    これによりXORで最下位バイトのみ値が残る。

  • 最下位バイト以外はそのままで、最下位バイトを全て1とする。
    0x32181231 | 0xFF

bayamasabayamasa

シフト演算の話
シフト演算は主に掛け算/割り算をビット演算で表現したいときに使う。
1bitずらすごとに 2× or 2÷になる。

シフト演算ではビットのずらしが発生するため、端のビットに対して値を埋めていかなければならない
例えば8bit 11001001のような値に対して、左シフトを1すると1001001?最後の?の部分を埋め合わせないと値が特定できない。この位置に対して0 or 1の値のどちらを埋めるかというところで、論理シフト/算術シフトが扱われる。

  • 論理シフト
    常に0で埋める

  • 算術シフト
    もし値が負数の場合

  1. 左シフト
    最上位ビット(符号ビット)を残したまま、0埋めをする
  2. 右シフト
    1埋めをする

これにより負数の維持を実現出来る。(もし0埋めをしてしまうと符号逆転などが起こる)
基本的にコンパイラなどは負数のビットシフトにおいては算術演算を行っているが、処理系動作である。

bayamasabayamasa

自分でやってみたサンプルコード

#include <stdio.h>

int main()
{
	char a = 0x63;
	char b = 0x95;
	printf("%d\n", a);
	printf("%d\n", b);
	printf("%d\n", a >> 4); //正の整数なので論理シフト
	printf("%d\n", b >> 4); //負の整数なので算術シフト
}
bayamasabayamasa

C言語において、符号付き整数と符号なし整数で演算を行う場合、符号付き整数を符号なし整数に暗黙的にキャストして演算を行う。

#include <stdio.h>

int main()
{
   
	if (-1 > 0U)
	{
		printf("負数は0Uより大きいと評価される");
	}
	else
	{
		printf("負数は0Uより小さいと評価される");
	}
}
❯  ./a.out  
負数は0Uより大きいと評価される
bayamasabayamasa

この場合はこちらが出力される

#include <stdio.h>

int main()
{
	if (-1L > 0U)
	{
		printf("負数は0Uより大きいと評価される");
	}
	else
	{
		printf("負数は0Uより小さいと評価される");
	}
}
❯  ./a.out  
負数は0Uより小さいと評価される% 
bayamasabayamasa

このように符号なし整数はバグの温床になりやすい。
それによってCの後継の言語は符号なし整数をサポートしていない場合が多い。Javaなど。

ただ符号なし整数は整数としてではなく、ビット配列として価を見る場合はとても有効である。

bayamasabayamasa

ゼロ拡張/符号拡張

範囲が小さい型から範囲が大きい型にキャストする場合、その数値が符号を持つかどうかによって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
bayamasabayamasa

C言語において、符号付き整数と符号なし整数で演算を行う場合、符号付き整数を符号なし整数に暗黙的にキャストして演算を行う。
ex)

int a = -90000
unsigned int b = 20000

printf("ans = %d", a + b);
bayamasabayamasa

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

bayamasabayamasa

-INT_MIN = INT_MIN
01111111111111111111111111111111 + 1 = 1000000000000000000000000

5 0101
-5 1011

bayamasabayamasa

乗算オーバーフローの脆弱性
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が発生してしまい、結果として脆弱性を引き起こす要因になる。

乗算オーバーフローは常に意識しておくべきである。

bayamasabayamasa

整数の乗算はコンパイル側からすると高コスト(10クロックサイクル以上)かかる
しかし、加算/減算/ビットシフトにおいては(1クロックサイクル)で収まるため、コンパイル時に整数乗算はビットシフトに置き換えられる。

ex)
x * 14 = x * (2^3 + 2^2 + 2^1) = (x<<3) + (x<<2) + (x<<1)
このように書き直すことが可能になる

このようにすることでコンパイラがサイクルの効率化を図ることで、動作を速くする。

bayamasabayamasa

-6の乗算は(x<<1) - (x<<3)で行われる。

全ての整数はこのビットシフト同士の加減とxの加減で表現することが出来る

bayamasabayamasa

右シフトによりアンダーフローを起こしてしまった整数型は、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となる。

コンピューターの世界では、整数型の丸めはこのように定義されている。
つまり値が正の場合は切り捨て、負の場合は切り上げを行う。

bayamasabayamasa

もし負の値を切り捨てしたい場合はバイアスを付加する。
バイアスとはビットシフト数分の最大値を付加してから計算を行うこと。

例えば-12340という値を4ビット右シフトを行う場合の計算は以下のようになる。
-12340 / 16 = -771.25 →丸め = -772
これを2進数で行うと
1100111111001100 >> 4 = 1111110011111100 = -772

しかし切り上げを行いたくない場合、ビットシフト分の最大を付加
つまり、4ビットの最大値15を付加する。そのため以下のようになる。
(1100111111001100 + 1111) >> 4 = 1111110011111101 = -771
これにより負の値であっても切り捨てを行うような計算を実現することが出来る

このスクラップは2021/08/20にクローズされました