2.1 16진수와 2진수


컴퓨터 프로그래밍의 원리적인 이해를 위해 2진법과 16진법에 대해 알아 본다. 16진수(hexadecimal)는 0~9의 숫자와, a~f의 6개의 문자를 사용한다. C 언어에서는 16진수의 상수를 표현할 때, 10진수의 상수표현과 구분하기 위해, 상수 앞에 0x를 붙인다.

2진수

(binary)
16진수(hexadecimal)
10진수(decimal)

0000
0
0

0001
1
1

0010
2
2

0011
3
3

0100
4
4

0101
5
5

0110
6
6

0111
7
7

1000
8
8

1001
9
9

1010
a
10

1011
b
11

1100
c
12

1101
d
13

1110
e
14

1111
f
15




위 표에서 알 수 있듯이, 16진수의 1자리는 2진수의 4자리 즉 4비트로 표현되는 것을 알 수 있다. 따라서 16진수와 2진수는 서로 상호 변환이 쉽게 이루어진다. 예를 들어 8비트 2진수 10110110이란 수는 이를 하위의 4비트씩 그룹으로 묶으면, 1011과 0110의 2그룹으로 나눌 수 있다. 그리고 각각의 그룹에 위의 변환표를 적용한다. 즉 1011은 16진수에서 b이고, 0110은 16진수에서 6이다. 따라서 10110110은 16진수로 b6가 된다. 이제 왜 위의 표를 암기해야 하는지 알 수 있을 것이다. 아무리 16비트, 32비트 등 8비트 이상의 많은 비트의 2진수도 이와 같은 방법으로 16진수 간단히 변환될 수 있다. C 언어방식대로, 16진수 앞에는 0x를 붙여 표기한다. 따라서 10110110의 16진수 표기는 0xb6이 된다. 이번에는 반대로 16진수를 2진수로 바꾸어 본다. 예를 들어 0x8a라는 16진수를 2진수로 변환해 본다. 이 때, 0x8a에서 접두어 0x를 빼고, 8a에 대해, 각각의 16진수 한자리씩 위의 변환표를 이용하여, 2진수로 변환한다. 8은 1000, a는 1010이므로, 이 2개의 2진수 1000과 1010을 붙여 쓰면, 10001010의 8비트 2진수 표현이 된다.



2.2 비트와 바이트


컴퓨터는 하드웨어적으로 수많은 스위치들의 결합으로 이루어진다. 스위치는 회로적 연결상태(ON)와 회로적 단락상태(OFF)의 2가지 논리적 상태(logical state)로 동작한다. ON상태가 논리적인 1을 의미하며, OFF상태가 논리적 0을 의미한다. 엄밀히 말하면, 하나의 스위치도 컴퓨터라고 말할 수 있다. 다만 이 컴퓨터는 0 또는 1값만을 저장할 수 있는 하나의 메모리만 가지고 있을 뿐이다. 동작 또한 단지 그 값을 0으로 또는 1로 바꾸는 메모리 쓰기 동작과 현재의 상태를 알려주는 읽기 동작 밖에 할 수 없는 것이다. 단 하나의 스위치 이지만, 여기서 중요한 정의를 내릴 수 있다. 첫째, 메모리라는 정적기능와 메모리 읽기 쓰기라는 동적기능이 그것이다. 여러분에 C를 설명하기 전에 컴퓨터의 구조에 대해 설명하고자 한다. 이의 이해를 통해 보다 체계적으로 컴퓨터 메모리 구조와 연산원리를 이해할 수 있으리라 기대된다. 이 하나의 스위치를 비트(bit)라 한다. 이 하나의 비트로 저장할 수 있는 데이터는 0~1범위의 데이터를 단 하나 저장할 수 있을 뿐이다. 이제 이를 좀 더 확장하여, 이 비트가 2개 있다고 가정한다. 이 경우 2가지 선택이 있을 수 있다. 먼저 이를 각각 메모리로 활용하면, 즉 서로 다른 데이터로 구분하면, 2가지 데이터를 기억할 수 있다. 각각 하나의 데이터는 0~1의 범위만을 가질 뿐이다. 이 경우를 2개의 1 비트 데이터 저장용량을 갖는다고 말할 수 있다. 다른 방법으로는 이 2개의 비트를 하나로 묶어 하나의 데이터로 만드는 경우이다. 이 때 2개의 비트가 가질 수 있는 경우의 수는 4가지로 늘어난다. 이 두개의 비트를 b0와 b1이라 할 때, 그 경우의 수는 다음 표와 같다



표 2 2비트의 표현

b1
b0
2진수
10진수

0
0
00
0

0
1
01
1

1
0
10
2

1
1
11
3




이 표로부터 2비트 데이터로 표현할 수 있는 수의 범위는 10진수로 0~3인 것을 알 수 있다. 그리고 또한 스위치 하나의 추가가 2진법에서 자리 수의 증가를 의미하는 것을 알 수 있다. 따라서 만약 스위치가 8개로 늘어나고, 이 8개의 스위치가 하나의 데이터를 표현한다면, 이 8개의 비트가 표현할 수 있는 경우의 수는 256이 된다. 따라서 8비트 데이터가 표현할 수 있는 수의 범위는 10진수로 0~255인 것을 알 수 있다. 이 8개의 비트(bit)를 묶은 것을 바이트(byte)라 한다. 실제 컴퓨터에서는 바이트 단위로 데이터가 저장된다.



2.3 메모리 용량과 주소


이제 좀더 컴퓨터의 스위치의 개념을 확장하여, 2,048개의 비트를 갖는 컴퓨터를 가정한다. 1,024개 비트의 용량을 1K라 할 때, 이 컴퓨터는 2K 메모리 용량을 갖는다. 이를 바이트 단위로 환산하면, 256바이트의 메모리 용량을 갖는다. 다시 말해, 0~255까지의 수를 표현할 수 있는 바이트 데이터를 256개의 종류로 저장할 수 있다. 컴퓨터는 데이터를 구분하기 위해 메모리 주소(memory address)를 사용한다. 이제 이 256개의 메모리에 0에서 255까지의 번호를 붙여 데이터를 구분할 수 있다. 이 번호를 메모리 주소라 한다. 메모리 주소는 좀 더 많은 수를 표현하기 위해 16진법을 사용하여 표현한다.

이제 다시 메모리 이야기로 되돌아 가자. 2,048개의 비트를 갖는 메모리는 256바이트의 메모리 용량을 갖는다고 하였다. 이 때 이 메모리의 주소는 0번지에서 255번지를 갖게 되는데, 이를 16진수로 표현하면, 0x0번지에서 0xff번지의 주소를 갖는다라고 말한다. 이제 비트 수를 늘려나가 보자. 이제 주소를 기준으로 메모리의 용량을 계산하여 보자. 만약 메모리 주소가 0xffff의 4자리의 16진법으로 표현될 수 있다면, 이 주소번지로 구분할 수 있는 메모리 용량은 256*256=65,536개의 바이트가 된다. 이를 우리는 줄여서 64K라 한다. 컴퓨터 역사와 비교해 볼 때, 1980년대 초 애플컴퓨터 시대의 메모리 용량이 이러하였다. 이제 메모리주소의 16진법의 자리수와 메모리 크기와의 관계를 아래의 표에 정리해 본다.



표 3 메모리 주소와 메모리 용량

메모리 주소 범위
메모리 용량(bytes)
표기(bytes)
시기/컴퓨터

0~0xf
16



0~0xff
256



0~0x3ff
1,024
1K


0~0xfff
4,096
4K


0~0xffff
65,536
64K
1980's/ Apple

0~0xfffff
1,048,576
1,024K or 1M
1984/IBM-XT

0~0xffffff
16,777,216
16M
1990's/IBM-AT

0~0xfffffff
268,435,456
256M
2000's/Pentium

0~0xffffffff
4,294,967,295
4,096M or 4G
near future




이러한 메모리 주소 범위와 메모리 용량을 10진수로 표현하는 프로그램을 다음과 같이 작성한다.



리스트 1 메모리 용량

1: #include



2: void main()

3: {

4: printf("%x=%u\n",0xffff,0xffff);

5: printf("%x=%u\n",0xfffff,0xfffff);

6: printf("%x=%u\n",0xffffff,0xffffff);

7: printf("%x=%u\n",0xfffffff,0xfffffff);

8: printf("%x=%u\n",0xffffffff,0xffffffff);

9: }



이 프로그램의 실행결과는 여러분이 직접 확인해 보기 바란다.



2.4 데이터형과 메모리할당


이 절에서는 데이터형(data type)에 대해, 알아본다. 프로그램에서 데이터는 변수(variables)와 상수(constants)로 나뉘어 진다. 변수는 사용되기 전에 데이터형이 선언(declaration)되어야 한다. 데이터형을 좀 더 쉽게 설명하기 위해, 가장 작은 크기의 데이터형인 자료형인 char에 대한 설명부터 시작한다. 먼저 다음의 프로그램을 보자.



리스트 2 변수의 메모리

1: #include

2: void main()

3: {

4: unsigned char a;

5: printf("%x\n",&a);

6: }



리스트 2의 프로그램은 실제 코드는 4번과 5번의 단 2줄이지만, 이 코드에는 메모리의 개념을 이해하기 위한 많은 내용이 포함되어 있다. 먼저 이 프로그램의 최종 목적은, 8비트 변수 a를 하나 선언하고, 이 변수에 할당되어 있는 메모리 주소 값을 화면에 출력하는 것이다. 그런데 여기서 알아 두어야 할 것은, 변수에 대한 정의이다. 변수는 메모리의 특정 바이트를 이용해서 값을 갖는다. 이 프로그램과 같이 unsigned char형 변수는 1바이트의 메모리를 이용하여, -128~127까지의 정수를 표현한다. 자세한 변수의 데이터형에 대해서는 나중에 좀 더 자세히 알아 보기로 한다. 중요한 점은 이 변수에 대한 메모리 주소 할당을 사용자가 직접 할 수 없다는 사실이다.

하드웨어 프로그래밍, 즉 운영체제의 간섭을 받지 않는 낮은 수준의 하드웨어 제어용 시스템 프로그램에서는 사용자가 임의로 변수에 메모리의 주소를 지정할 수 있다. 그러나 운영체제하의 컴퓨터에서는 이러한 직접 메모리 접근(access) 금지되어 있다. 만약 이를 허용한다면, 기본적인 운영체제의 틀이 흔들리기 때문이다. 즉 운영체제는 모든 컴퓨터자원의 관리의 책임을 맡기 때문에, 사용자의 직접 메모리사용은 허용하지 않는 것이다.

리스트 2의 프로그램에서 4번줄은 unsigned char형 정수 변수 a을 선언하는 내용이다. 그러나 이는 소프트웨어 프로그래밍 측면에서 보면 다소 관념적 표현이다. 보다 하드웨어적인 면에서의 변수 a의 선언에 대한 정확한 표현은 다음과 같다. 즉 현재의 메모리(물론 그 용량은 컴퓨터 자원에 따라 달라진다) 중, unsigned char형 정수로 사용 가능한 메모리 1바이트를 할당하여, 그 할당된 주소의 메모리로 표현되는 값을 앞으로 a라고 쓰겠다고 선언한다. 메모리 1바이트는 32비트 운영체제에서는 4바이트의 주소(address)로 식별되며, 그 주소에는 1바이트의 데이타(data)가 기억된다.

char라는 자료형은 이 1바이트의 데이터를 8비트 정수로 사용하겠다는 데이터 형태를 규정하는 것이다. 따라서 컴파일러는 이러한 문장을 만나면, 프로그램이 요구하는 메모리의 크기와 현재 유용 가능한 메모리 영역을 고려하여, 적당한 메모리주소를 변수 a에 할당한다. 현재의 예에서는 변수 a는 8비트 정수형의 데이터를 위한 것이므로, 컴파일러는 1바이트 크기의 메모리를 변수에 할당한다. 앞서 말했듯이, 이 주소 할당의 메카니즘은 컴파일러와 운영체제의 소관이므로, 프로그램에게는 개방되어 있지 않다. 따라서 어떤 주소의 메모리가 할당될 지는 할당되기 전에는 프로그래머가 예측할 수 없다. C 프로그램에서 메모리 할당(memory allocation)은 문법적 오류(syntax error)를 체크하는 컴파일(compile) 단계가 완료된 이후에, 링크(link and relocation)단계에서 수행된다. 링크단계를 거쳐야 비로소 실행가능한 파일(executable file)이 만들어지는데, 이 단계에서는 실제 특정변수가 어떤 메모리번지에 할당되었는지 알 수가 있다. 리스트 2의 5번줄이 이러한 변수의 할당된 주소를 알아보는 문장(statement)이다. 5번줄의 &는 변수에 할당된 주소를 알려주는 번지연산자(reference operator)이다. 그리고 5번줄의 %x는 printf()함수에서 해당 변수를 16진수 포맷으로 출력하라는 스트링이다. 이 프로그램의 실행결과는 여러분이 직접 알아보라.


이 결과를 통해, 변수a는 0x12ff7c라는 메모리 번지에 할당되어 있는 것을 알 수 있을 것이다. 리스트 2의 내용을 확장하여, 여러 가지 다른 데이터형의 변수들에 대해, 어떻게 메모리가 할당되는 지 알아본다. 먼저, 변수 a와 같은 데이터형의 변수 b를 추가로 선언하고, 역시 변수 b의 할당된 주소를 알아본다. 다음은 이를 위한 프로그램이다.



리스트 3 추가된 변수의 메모리 할당

1: #include

2: void main()

3: {

4: unsigned char a,b

5: printf("%x\n",&a);

6: printf("%x\n",&b);

7: }



이 프로그램에서 추가된 것은 4번줄에서, 변수 b를 unsigned char형 정수로 추가로 선언한 것과 6번줄의 변수 b의 할당된 메모리 주소를 화면에 출력하는 print()문이다. 이 프로그램을 실행 또한 여러분이 직접 해보시기를 바란다.


이 결과에서 변수a에 0x12ff7c번지의 메모리를, 변수 b에 0x12ff78번지의 메모리를 할당한 것을 알 수 있다. 변수a와 변수b의 크기기 각각 1바이트인 것을 고려하면, 이러한 메모리할당은 의외이다. 실제 1바이트 변수 2개밖에 이 프로그램에서는 사용되지 않았는데, 실제 할당된 주소는 연속적이지 않고, 4의 차이가 나는 것을 알 수 있다. 즉 메모리 할당이 비경제적인 것을 알 수 있다. 이로부터 단일변수의 선언은 비록 그 변수의 크기가 1바이트라 하더라도 4바이트의 간격으로 메모리 공간을 할당받는다는 사실을 알 수 있다. 이를 확인하기 위해, 리스트13-2의 프로그램에서, 변수 a와 b를 각각 16비트 정수형(short int), 32비트 정수형(long int), 32비트 실수형(float) 등으로 선언하여 보아도, 같은 간격의 메모리 주소 할당 결과를 갖는 것을 알 수 있다. 물론 64비트 변수인 64비트 실수형(double) 변수에 대해서는 다른 결과를 갖는다. 이를 확인하기 위해, 리스트13-2를 다음과 같이 변경하여 작성한다.



리스트 4 64비트 실수형의 메모리주소 할당

1: #include

2: void main()

3:{

4: double a,b;

5: printf("%x\n",&a);

6: printf("%x\n",&b);

7: }



리스트 4이 리스트 3와 다른 것은 4번줄의 자료형이 double로 바뀐 것이다. C프로그램에서 double형은 64비트 부동소수점 실수형이다. 이 데이터형에 대해서는 다음에 좀 더 설명한다. 이 프로그램의 실행은 여러분이 직접해보시길 바란다. 이 결과로 부터 64비트형 변수의 주소할당은 정확히 8바이트씩 차이가 나는 것을 알 수 있다. 아 사실로부터 다음과 같은 결론을 내릴 수 있다.



(1 (1) 단일 변수는 변수의 크기가 32비트이하인 경우, 변수의 크기에 상관없이 4바이트 크기의 메모리를 할당받는다.
(2) 단일 변수의 크기가 64비트인 경우, 8바이트 크기의 메모리를 할당받는다.



그럼 실제 1바이트크기의 변수에 1바이트 크기의 메모리를, 2바이트 크기의 변수에 2바이트 크기의 메모리를 할당받기 위한 방법은 무엇일까? 그 방법은 변수를 배열로 선언하는 것이다. 배열변수란 동일한 데이터형을 갖는 복수개의 요소를 갖는 변수이다. 배열변수는 선언할 때, 변수명 뒤에 []를 붙이고, []안에 사용할 배열요소의 개수를 지정한다. 그리고 수식에서 이 변수가 사용될 때, 배열 변수 뒤에 []를 붙이고, []안에 사용할 배열요소의 번호를 지정한다. 배열요소의 번호는 0부터 시작한다. 예를 들어 10개의 배열요소를 갖는 배열변수a를 정수형으로 선언한다고 하면, 다음과 같이 선언한다.



int a[10];



여기서 a[0]는 배열변수 a의 첫번째 요소이고, a[9]이 배열변수 a의 마지막 요소가 된다. 이러한 배열변수는 벡터(vector)나 행렬(matrix)와 같이 복수개의 같은 데이터형의 데이터를 표현하고자 할 때 주로 사용된다. 배열변수에 대해서는 나중에 좀 더 자세히 다루기로 한다. 이 절에서는 배열변수로 선언된 변수의 메모리 할당에 대해서만 알아 보자. 다음은 이를 위한 프로그램 예이다.



리스트 5 배열변수의 메모리 할당(1)

1: #include

2: void main()

3: {

4: unsigned char a[10];

5: printf("%x\n",&a[0]);

6: printf("%x\n",&a[1]);

7: }



리스트 5의 4번줄에서 unsigned char형 정수 변수로 10개의 요소를 갖는 배열변수 a[10]을 선언하고, 5번줄에서, 그 변수들의 할당된 주소를 알아본다. 이 프로그램을 여러분이 실행시켜보라.
이 결과로부터 배열변수 a[0]과 배열변수 a[1]에 각각, 0x12ff74와 0x12ff75의 메모리가 주소가 할당된 것을 알 수 있다. 즉 두개의 변수가 연속적인 주소로 할당된 것을 알 수 있다. 그리고 또한, 배열변수의 요소번호가 증가할 때마다, 그 배열요소에 할당된 메모리주소도 증가하는 것을 알 수 있다. 이를 좀 더 확인하기위해, 다음의 프로그램을 작성해 본다.



리스트 6 배열변수의 메모리할당(2)

1: #include

2: void main()

3: {

4: unsigned char a[10];

5: for (char i=0;i<10;i++)

6: printf("a[%d]'s adress=%x\n",i, &a[i]);

7: }



리스트 6에서 5번줄에서 for()을 사용하여, 6번줄의 문장을 10번 반복시킨다. 그리고 반복시 마다 i=0에서 i=9까지 증가시켜나간다. 이러한 for()문에 대해서는 나중에 다시 설명하기로 한다. 여기서는 일단 위와 같이 반복시키기 위해 사용된다고만 알아두자. 그리고 6번줄의 print()문을 통해 a[0]에서 a[9]까지 각각의 변수에 할당된 메모리 주소를 알아본다. 이 프로그램을 실행시켜 보라.

이 결과를 통해, 다시 한번 알 수 있는 것은 배열변수는 연속적으로 메모리주소가 할당되며, 배열번호가 증가함에 따라, 할당된 주소도 선언된 데이터형의 크기 간격으로 증가함을 알 수 있다. 만일 배열변수가 16비트 정수형으로 선언되면, 할당된 메모리 주소는 2의 간격을 가지고 연속적으로 할당될 것이다. 다음은 이를 확인하기 위한 프로그램이다.



리스트 7 16비트 정수형 배열변수의 메모리 할당

1: #include

2: void main()

3: {

4: unsigned short a[10];

5: for (char i=0;i<10;i++)

6: printf("a[%d]'s address=%x\n",i, &a[i]);

7: }



이 프로그램 리스트에서는 4번줄에서 배열변수 a[]를 16비트형 정수로 선언해 주었다. 따라서 이 변수들에 각각 2바이트씩 메모리할당이 이루어진다. 이 프로그램을 실행시켜 보라.






이와 같이 변수들은 주어진 데이터형에 따라, 메모리에서 차지하는 크기가 달라진다.



2.5 프로그램의 저장영역


하나의 프로그램이 주 메모리에 로딩될 때, 운영체제는 프로그램을 코드(code)영역, 데이터(data)영역, 스택(stack)영역 등 3개의 영역으로 나누어 주메모리에 로딩한다. 코드영역에는 프로그램 실행코드가 저장되며, 데이터 영역에는 전역 데이터(global data)들이 저장되고, 스택영역에는 지역데이터(local data)들이 저장된다. 32비트 운영체제인 윈도즈는 4K바이트 크기를 갖는 페이지들을 이용하여 이러한 영역들을 생성한다. 다음은 프로그램의 main()함수가 저장되는 영역을 알아 보기 위한 프로그램이다.



리스트 8: 코드(code)영역

1: #include

2: void main()

3: {

4: printf("pointer of main()=%p\n",main);

5: }



위 리스트의 4번 줄에서 main은 main()함수의 포인터가 된다. 함수 포인터에 대해서는 나중에 좀 더 설명하기로 한다. 위 프로그램을 실행시켜 보라.


다음은 전역변수(global variables) 및 지역변수(local variables)의 저장영역을 알아 보기 위한 프로그램이다.



리스트 9: 전역변수 및 지역변수의 저장영역

1: #include

2: int a,b;

3: void main()

4: {

5: int a,b;

6: printf("adress of variables=%p %p\n",&a,&b);

7: printf("adress of variables=%p %p\n",&::a,&::b);

8: }



리스트 9의 3번 줄에서 정수형 전역변수 a와 b를 선언하고, 5번 줄에서 정수형 지역변수 a와 b를 선언한다. 6번 줄에서, 지역변수의 포인터(pointer)값을, 7번 줄에서 전역변수의 포인터(pointer) 값을 화면에 출력한다. 여기서 "&"는 변수의 포인터를 계산하는 번지 연산자이다. 포인터에 대해서는 나중에 자세히 다루기로 한다. "::"는 전역변수를 지정하는 descripter이다. 지역변수와 전역변수 또한 나중에 다시 다루기로 한다. 이 프로그램을 실행시켜 보라.



지역변수는 스택영역에 저장되므로, 변수가 선언될 때마다, 변수가 선언된 순서대로 스택영역의 높은 메모리 주소부터 저장된다. 그림 8의 결과를 보면, 0x12ff7c번지에 지역변수 a를 할당하고, 0x12ff78번지에 지역변수 b를 할당함을 알 수 있다. 즉 지역변수가 하나 선언될 할 때마다, 할당된 메모리 주소는 4씩 줄어드는 것을 알 수 있다. 반면에 전역변수는 데이터영역에 저장되고, 변수가 선언될 때마다, 변수가 선언된 순서대로 데이터영역의 낮은 메모리 주소부터 저장된다. 그림 8의 결과를 보면, 전역변수a는 0x4255e0번지에 할당되고, 전역변수b는 0x4255e4번지에 할당됨을 알 수 있다. 즉 전역변수가 하나 선언될 때마다, 할당된 메모리 주소가 4씩 증가함을 알 수 있다.



2.6 요약


이번 장에서는 컴퓨터 메모리와 변수와의 관계를 살펴 보았다. 메모리에 데이타가 저장된 방식과 변수가 선언될 때, 메모리 주소에 할당되는 영역등에 대해서도 알아 보았다. 이렇게 메모리에 대한 이해는 보다 폭넓고 전문적인 프로그램에서는 절대적으로 필요하며, 특히 하드웨어 자원이 제한적인 시스템에서의 프로그램에서 요구되어진다.

 


+ Recent posts