2.1-Infomation_Storage

컴퓨터는 8bits 단위, 즉 단위(주소로 표현 가능한 가장 작은 단위인) 로 읽는다.

machine-level 의 프로그램은 메모리를 기다란 바이트 배열, 로 본다.
모든 바이트는 로 특정된다.
또한, 이러한 가능한 의 모임을 라고 한다.
위의 가상 주소 공간은 하나의 개념적인 이미지로 표현되며, 실제 구현은 DRAM(), flash memory, disk 등등으로 이루어진다.

뒤에서, 컴파일러와 실행중인 시스템이 서로 다른 프로그램 객체(프로그램 데이터, 명령어, 컨트롤 정보) 를 저장할 수 있는 관리 가능한 단위로 다루기 위해 메모리를 나누는지 살펴본다.

다양한 메카니즘이 프로그램의 다른 저장공간을 할당하고, 다룬다.
이 관리는 virtual address space 에서 이루어진다.
C언어에서의 포인터 값은 해당 데이터의 첫번째를 가르키는 virtual address 이다.

C언어 컴파일러는 type 정보와 함께, 적절한 machine-level 코드를 작성한다.
컴파일러는 type 정보를 인식하지만, machine-level 코드는 그렇지 않다.
단지, 바이트 블럭으로, 바이트의 배열로 볼 뿐이다.

2.1.1 Hexadecimal Notation

2진법으로 표현하기에는 너무 길기 때문에, A~F 를 10~16 에 대응하여 16진법으로 표현하곤한다.
2진법에서 16진법은 4개를 묶어서 하나로 표현하면 되며,
반대로 16진법에서 2진법은 해당 값을 4bit 로 표현하면 된다.

2진법에서는 꼴은 0이 개, 1이 1개 오는 비트 배열로 표현가능하다.
16진법에서는 일 때, 개의 0이 오고, 값에 따라 마지막 숫자가 달라진다.

2.1.2 Data Sizes

모든 컴퓨터에는 가 존재한다. 이는 포인터 데이터의 사이즈를 나타낸다.
따라서, word size 가 일 때, 의 주소가 가능하다.

최근에는 32-bit에서 64-bit 로 넘어가며, 주소 공간이 4GB 에서 바이트로 늘어났다.
대부분의 64-bit 머신은 32-bit 머신을 위한 프로그램을 동작시킬 수 있다.

C언어에서 자료형에 따라 사용되는 Byte는 컴퓨터 시스템이 32bit인지, 64bit인지에 따라 다르다.
ISO C99 에서는 시스템에 따라 bit 가 달라지는 것을 방지하기 위해,
int32_t, int64_t 라는 고정된 비트를 사용하는 자료형을 만들었다.

신기한 점: unsigned long, unsigned long int, long unsigned, long unsigned int 모두 가능하다.

개발자는 환경에 무관한 프로그램을 위해 노력한다.
이를 위한 한가지 방법은 환경에 무관한 데이터 타입의 사이즈일 것이다.
그러나, 하한선이 보장되어있지만, 상한선이 보장되어있지 않아, 32bit에서 64bit로 넘어갈 때
일부 버그가 발생하곤 했다.
그 이유 중 하나로, 32bit 에서의 int 를 포인터를 저장하는 용도로 사용했다는 것이다.
이것은 64bit 시스템에서는 오류가 발생한다.

2.1.3 Addressing and Byte Ordering

: MSB(Most Significant Bit) 부터 LSB(Least Significant Bit) 순으로 저장
: LSB(Least Significant Bit) 부터 MSB(Most Significant Bit) 순으로 저장
ex) 0x1234567 을 저장할 때, 작은 비트부터 Big Endian 은 01 / 23 / 45 / 67 로 저장한다.
반대로 Little Endian 은 67 / 45 / 23 / 01 로 저장한다.

주로 인텔 계열 시스템이 Little Endian 을, IBM, Oracle 계열이 Big Endian 을 사용한다.
"주로" 인 이유는 다 그렇지는 않기 때문이다.
최근의 프로세서는 , 양쪽을 다 지원한다. 하지만, 각 OS에서는 한쪽만 지원하기 때문에
주로 고정된다.

이 두 방법에는 일방적인 우위가 존재하지 않기 때문에 선택은 임의적이다.
유래가 걸리버 여행기 ㅋㅋ;;

같은 방식을 쓰는 시스템 사이에서는 Byte Ordering 이 문제가 되지 않는다.
그러나, Network 등을 통해 서로 다른 방식을 쓰는 시스템간의 통신에서는 문제가 생길 수 있다.

이를 해결하기 위해, 네트워크를 사용하는 프로그램은
송신 측은 "해당 시스템에서 네트워크 표준을 맞추는 변환"을 따라야하며,
수신 측도 "네트워크 표준을 해당 시스템으로 맞추는 변환" 을 갖춰야한다.

다음으로는 정수 표현에서 생길 수 있다.
이는 주로 machine-level 프로그램에서 관찰된다.
Little-Endian 으로 생성된 machine-level 코드에서 종종 일어난다.
정수 표현의 일반적인 방법은 왼쪽에서 오른쪽으로 쭉 읽는 것이나, Little-Endian 은 그것과는 다르기 때문이다.

마지막으로는 프로그램이 일반적인 타입 시스템을 우회할 때이다.
C언어는 , 으로 가능하다.
생성된 변수의 타입을 임의로 바꿔 사용하는 방법이지만, 이는 일반적으로 권장되지 않는다.
단, system-level 프로그래밍에서는 유용하고 필요한 경우가 있다.

int, float 등의 자료형의 값을 출력하기 위해서,
show_bytes 라는 함수에 매개변수만 casting 해서 출력하는 방법이 있다.
이러한 방법을 서로 다른 시스템에 적용할 때, Endian 에 따라 순서가 다르거나
32bit, 64bit 시스템이냐에 따라 포인터의 값이 달라진다.

	typedef unsigned char *byte_pointer;

	void show_bytes(byte_pointer start, size_t len) {
		int i;
		for(i = 0; i < len; i++) printf(" %.2x", start[i]);
		printf("\n");
	}

	void show_int(int x) {
		show_bytes((byte_pointer) &x, sizeof(x));
	}

그리고... 같은 값에 대한 intfloat 간에 비트패턴에 있어서 13개가 겹치는데,
이 이유에 대해서는 후술한다.(우연이 아님)

2.1.4 Representing Strings

대부분은 ASCII 인코딩 방식을 사용한다.
숫자 표현의 경우, 정수 x에 대해, 0x3x 로 인코딩된다.
예를 들어, 숫자 5는 ASCII 로 0x35 로 표현된다.

이는 byte ordering 이나 word 사이즈 관습과 무관하다.
그래서 이진 데이터보다 시스템 독립적이다.

여담으로 ASCII 는 영어 위주다보니, 더 넓은 범위로서 Unicode 가 등장했다.
32bit를 사용하는 방법으로 UTF(Universal Character Set) 이 존재한다.
이는 ASCII 의 superset으로 ASCII 를 포함한다. 이에 따라 가변적으로 1~2byte 로도 해석한다.

machine-level 프로그래밍에서는 주로 machine-dependent 하다.
이진 코드를 해석하는 방법이 machine 마다 다른 경우가 많기 때문이다.

2.1.6 Introduction to Boolean Algebra

컴퓨터가 이진 데이터(0 과 1) 을 다루기 때문에,
0과 1을 다루는 Boolean Algebra 에 대한 지식은 중요하다.
대표적인 연산들로 ~, &, |, ^ 이 있다.
또한 XOR 라는 연산은 어느 한쪽만 True 일때, 결과값이 참이다.(= 두 값이 서로 다를 때)
해당 연산은 0과 1로 이루어진 비트문자열에도 비슷하게 확장가능하다.

Boolean 연산은 전반적으로 일반적인 정수 연산과 비슷한 특징을 가진다.
ring 이라는 대수적 구조는 group이라는 구조의 확장이다.
ring은 더하기에 대해서 group이 성립하는데, 여기에 곱하기라는 연산이 추가되며,
곱하기에 대해 결합법칙과 항등원이 존재한다.
실제로 Boolean Rings 에서 더하기는 XOR 에 대응하고, 곱하기는 & 에 대응한다.
또한, Boolean Rings 는 를 만족하는 특수한 ring 이다.

비트 문자열은 집합의 다른 표현으로 생각해볼 수 있다.
예를 들어 A = {0, 3, 5, 6} 을 로 생각하고, B = {0, 2, 4, 6} 을 로 생각할 수 있다. 이에 대해 &, | 연산자가 모두 집합의 교집합, 합집합에 대응한다.
관련된 알고리즘으로 "비트마스킹"이 있다.

2.1.7 Bit-Level Operation in C

C언어에서의 bit operation은 위에서 언급한 기호를 그대로 사용한다.
흥미로운 에제로 두 변수의 값 교환이 있다.

void inplace_swap(int *x, int *y) {
	*y = *x ^ *y;
	*x = *x ^ *y;
	*y = *x ^ *y;
}

이것이 가능한 이유는 이기 때문이다. 각 라고 설정하고 따라가면 확인 가능하다.

이것을 응용해서 다음 함수의 문제점을 찾아보자

void reverse_array(int a[], int cnt) {
	int first, last;
	for(first = 0, last = cnt - 1; first <= last; first++, last--) {
	inplace_swap(&a[first], &a[last]);
	}
}

길이가 홀수일 때, 배열의 가운데 값이 0인 문제가 생긴다.
그 이유는 일때, 에서 inplace_swap(&a[k], &a[k]) 라는 연산을 하며,
이기 때문에 정보가 없어진다.
따라서, 해결책은 가운데에서 즉, index 가 같을 때는 바꾸지 않는 것이다.
first <= last => first < last

bit-level 에서는 마스킹 연산이 자주 사용된다.
마스킹 연산은 특정 구간의 값만을 취하는 연산이다.
예를 들어, 에서 0, 1, 2, 3 위치의 값이 무엇인가? 라고 하면 그 결과는 이다.
이는 원하는 구간만큼 1로 채우고 나머지는 0으로 채운 비트문자열과 & 연산을 취함으로서 구할 수 있다.

2.1.8 Logical Operations in C

Boolean Operation 과는 유사하지만, 다르게 작용한다.
예를 들어, ! 의 결과값은 0 아니면 1만 가능하다.
그 외로 &&|| 연산자가 있는데, 이들은 첫번째 operand 로 전체 결과가 결정되면, 2번째 operand 는 고려하지 않는다.
ex) a && 5/adivided by zero 가 뜨지 않는다. 또한, p && *p++ 도 null pointer 참조가 일어나지 않는다.

2.1.9 Shift Operations in C

<< 은 비트를 왼쪽으로 밀면서, 새로운 공간을 0으로 채운다.
ex) 이 된다.

그러나, >> 의 경우는 조금 다르게 작동한다. 크게, , 2가지가 있다.
: 새로운 공간을 으로 채운다.
: 새로운 공간을 MSB 와 동일한 값으로 채운다. 이는 signed int 에서 유용하다.

C언어 표준은 Logical, Arithmetic 을 특정하지 않는다.
단, 대부분의 컴파일러가 signed data 에 대해 Arithmetic 을 사용하고, 개발자도 그렇게 받아들인다.
하지만, unsigned data 에 대해서는 Logical 을 사용해야만 한다.
Java 에서는 >> 을 Arithmetic 으로, >>> 을 Logical 로 이용한다.