x86 系コンピュータを動かす理論と実装 作って理解する OS の読書メモ。第 0 部「イントロダクション」をサクッと流し読みしつつ、気になりどころだけ自分向けにメモ。
本はこれ ↓
「天秤を使って 15g までの重さを測るために必要となる最小の分銅の数の問題」から 2 進数の話に持っていく部分が面白かった。
まず分解能を 1g と定めてそこから積み重ねていく形で必要な分銅の数が定まる。
- 0g: 分銅は必要なし
- 1g: 1
- 2g: 2
- 3g: 2 + 1
- 4g: 4
- 5g: 4 + 1
- 6g: 4 + 2
- 7g: 4 + 2 + 1
- 8g: 8
- 9g: 8 + 1
- 10g: 8 + 2
- 11g: 8 + 2 + 1
- 12g: 8 + 4
- 13g: 8 + 4 + 1
- 14g: 8 + 4 + 2
- 15g: 8 + 4 + 2 + 1
各重さと必要な分銅の対応関係に対して、 8g, 4g, 2g, 1g の分銅を 4 bit の各桁に対応させるとそのまま 2 進数に読み換えることができる。
2 進数の加算は 10 進数と同じようにやればよいが、減算はどうすれば良いだろうか。減算は負の数を導入すれば加算演算と考えられるので、それを可能にする負の数の表現を考えようという流れ。
負の数の演算がうまくできるような負数の表現を探っていく
- バイアス表現
- ざっくりいってしまえばゼロ点をずらずようなやつ
- 5 をゼロ点とすれば 100(2) = 4 を -1 として、110(2) = 6 を +1 として扱える
- しかし加算がうまくいかない: 100(2) + 110(2) = 1010(2) = 10 でバイアスを考慮すると 5 になってしまう
- 符号ビット表現
- MSB を符号として利用する
- 101(2) = -1 ていう感じ
- 100(2) = 000(2) = 0 となり、 +0, -0 と 2 つのゼロ表現が生じてしまう
- これについてもまた加算がうまくいかない: 101(2) + 001(2) = 110(2) = -2 となってしまうが、これは符号と値が独立しているからであり、この点を解消しないと課題を解決できそうな表現にはならなそうだ
- 1 の補数表現
- 1 の補数とは単純にビット反転した数で、01010(2) だったら、その 1 の補数は 10101(2)
- ある 2 進数 b(2) に対してその負の値を ~b(2) と表現しましょう的な話
- 0000(2) = 1111(2) = 0 で、この表現でも 2 つのゼロ表現が生じてしまう
- 5 + (-7) = 0101(2) + 1000(2) = 1101(2) = -2 で 計算自体はうまくいく
- 2 + (-1) = 010(2) + 110(2) = 1000(2) のようにケタ上がりが発生した場合は LSB に加算する必要がある、すなわち 1000(2) = 001(2) = 1 と考える
- 2 の補数表現
- 1 の補数とは単純にビット反転した数に 1 を足したもので、01010(2) だったら、その 1 の補数は 10110(2)
- ある 2 進数 b(2) に対してその負の値を ~b + 1(2) と表現しましょう的な話
- 0 = 0000(2) の 2 の補数は ~0000(2) + 1(2) = 1111(2) + 1(2) = 0000
- あらかじめ、符号あり/なしと値の範囲を定めておく必要がある
- 5 + (-7) = 0101(2) + 1001(2) = 1110(2) = -2 で 計算自体はうまくいく
C 言語で記述された命令はコンパイラによって CPU ごとに異なる命令に翻訳され、これを コンパイル という。 int
型には CPU が手間をかけずに計算できる最適なビット数が割り当てられ、このようなアーキテクチャによる実装の差異を 機種依存性 とよぶ。
- 変数宣言 を行うとメモリの一部がデータの保管場所として確保され、初期化を行うと該当の領域がその値でクリアされる
- アドレス はメモリ配列の位置を示す
- ポインタ はアドレスと型情報を持つ
- 間接演算子
*
によってオペランドのポインタの指し示す変数を取得できる
- アドレス演算子
&
によってオペランドの変数のポインタを取得できる
int* px
とすると px
はアドレスおよび int
型という情報を持つ
int x
と宣言された x
について px = &x
すると px は x のアドレスを指すようになる
void add(int* p, int x, int y) {
*p = x + y
}