티스토리 뷰



* CHAPTER 4 - C언어와 화면 표시의 연습


#1 C언어로 메모리에 쓰기 (harib01a)


 3일차 까지는 화면을 새카맣게 만드는 것까지 성공했다. 이번엔 화면에 뭔가를 그려보는 실습을 해보자. 뭔가를 그리기 위해선 VRAM에 뭔가를 쓰면 된다. 하지만 C언어에는 그런 기능이 존재하지 않는다. 그래서 그런 기능을 가진 함수를 만들어봤다.

/*************************************************
 ** 3일차까지 완성되었던 naskfunc.nas 파일에 몇 줄을 추가함
*************************************************/

; naskfunc
; TAB = 4
[FORMAT "WCOFF"]; 오브젝트 파일을 만드는 모드
[BITS 32]; 32비트 모드용의 기계어를 만든다.

; 오브젝트 파일을 위한 정보
[FILE "naskfunc.nas"]; 소스 파일명 정보
GLOBAL _io_hlt; 이 프로그램에 포함된 함수명

; 이하는 실제의 함수
[SECTION.text]; 오브젝트 파일에서는 이것을 쓴 후에 프로그램을 쓴다.
_io_hlt : ; void io_hlt(void);
       HLT
       RET

; 이하는 추가된 부분
_write_mem8 : ; void write_mem8(int addr, int data);
       MOV           ECX, [ESP + 4]; [ESP + 4]에 addr이 들어있으므로 그것을 ECX에 읽어 들인다.
       MOV           AL, [ESP + 8]; [ESP + 8]에 data가 들어있으므로 그것을 AL에 읽어 들인다.
       MOV[ECX], AL
       RET

/*************************************************
 ** End Line
*************************************************/

 이것은 write_mem8(0x1234, 0x56); 처럼 사용하는 함수로 MOV BYTE[0x1234], 0x56을 명령을 한 것이다. 덧붙여서 addr은 'address'의 약어로 번지 혹은 주소라는 뜻이다. C언어로 write_mem8이 사용되면 여기 _write_mem8로 점프한다. 그리고 그때 지정된 숫자는 메모리에 메모를 해두었고, 각각

  • 첫 번째 것: [ESP + 4]
  • 두 번째 것: [ESP + 8]
  • 세 번째 것: [ESP + 12]
  • 네 번째 것: [ESP + 16]
    (이하 생략)
 에 써져있다. 여기서는 지정된 0x1234나 0x56을 받고 싶으므로, MOV로 레지스터에 쓰고 있다. 이미 CPU는 32비트 모드로 되어 있으므로 32비트 레지스터를 사용하고 있다. 16비트 레지스터를 사용할 수도 있지만, 기계어의 바이트 수가 증가하고 실행속도도 느려지는 등 성능에 문제가 생긴다.

 메모리의 번지 지정에서, 16비트 레지스터를 사용한 경우에는 [CX]라든가 [SP]의 지정이 에러였지만, 32비트 레지스터를 사용하는 경우에는 [ECX]라든가 [ESP] 등도 전부 가능하여, 기본적으로 사용할 수 없는 레지스터는 없다. 번지를 지정할 때는 레지스터뿐만 아니라 레지스터에 정수를 더하거나 빼거나 한 식을 사용할 수도 있다.

 C언어와 연계할 경우 자유롭게 사용해도 되는 레지스터와 그렇지 않은 레지스터가 있다. 자유롭게 사용해도 되는 것은 EAX, ECX, EDX 3개 뿐이다. 다른 레지스터에 관해서는 값을 이용하는 것은 좋지만 변경해서는 안된다. 쓰기는 안되고 읽기만 된다. 그것들은 중요한 값을 기억시키기 위하여 사용하고 있기 때문에 조심스러울 수밖에 없다. 그래서 여기서는 EAX와 ECX만을 사용하도록 한다.

 이번에는 naskfunc.nas에 1행을 더 추가했다. INSTRSET 명령이다. 이 프로그램이 486용이라고 nask에 알려주는 일을 한다. nask에게 'EAX라는 단어를 보면 레지스터라고 해석해!'라고 알려주는 것이다.
/*************************************************
 ** naskfunc 파트에 추가된 INSTRSET, 딱 한 문장이다. 
*************************************************/

; naskfunc
; TAB = 4
[FORMAT "WCOFF"]; 오브젝트 파일을 만드는 모드
[INSTRSET "i486p"]; 
[BITS 32]; 32비트 모드용의 기계어를 만든다.

/*************************************************
 ** End Line
*************************************************/

 만약 아무것도 지정하지 않는다면 8086이라는 매우 오래된, 16비트 레지스터밖에 가지고 있지 않은 CPU용으로 작성된 프로그램으로 분류되어버린다. EAX라는 단어를 만나도 단순히 '레이블'로만 해석해버린다. 이는 8086시대에 작성된 프로그램 중에는 EAX라는 레이블을 사용한 것이 가끔 잇었기 때문으로, 당시에는 설마 이 단어가 장래에 레지스터명이 될 것이라 생각도 못했다고 한다.


 486용이라고 써도 486에서만 움직이는 기계어가 나오는 것은 아니고, 단순히 단어 해석의 문제다. 486용 모드라도 16비트 레지스터만을 사용하고 있으면 8086에서도 정상작동하는 기계어가 된다. 갑자기 숫자 모델에 대한 얘기가 나와 헷갈릴 지 모르겠다. 아래의 PC의 CPU 가계도(인텔의 경우)를 살펴보자.


8086 -> 80186 -> 286 -> 386 -> 486 -> Pentium -> PentiumPro -> Pentiumll -> Pentiumlll -> Pentium4 -> ...


 286까지는 16비트 CPU, 386 이후는 32비트 CPU이다. 그럼 어셈블리 쪽 사전 작업을 마쳤으므로 이제는 C언어 쪽을 수정해보자. 이번에는 변수를 도입했다.

/*************************************************
 ** 새롭게 수정된 bootpack.c
*************************************************/

void io_hlt(void);
void write_mem8(int addr, int data);


void HariMain(void)
{
	int i; /* 변수 선언. i 변수는 32비트의 정수형 */

	for (i = 0xa0000; i <= 0xaffff; i++) {
		write_mem8(i, i & 0x0f); /* MOV BYTE [i], 15 */
	}

	for (;;) {
		io_hlt();
	}
}

/*************************************************
 ** End Line
*************************************************/

 for 구문이 처음 등장한다. 반복 명령을 쓰기 위한 구문으로 { } 로 감싼 범위를 반복한다. 괄호 안에는 반복에 관한 조건 등을 쓰는데 이 조건들은 세미콜론(;)을 사용하여 3개로 나눈다. 첫 부분이 초기 설정문이다. i에 0xa0000 이라는 값을 대입하고 있다. 이 부분은 어떤 내용이라도 반드시 실행되는 것이 C언어의 규칙이다.


 다음 부분의 'i <= 0xaffff'는 반복 조건이다. 이 조건이 충족되면 반복을 계속하고, 충족하지 않는다면 반복을 멈춘다. 이 조건문은 첫 실행 때도 체크되므로 경우에 따라서는 { } 안의 내용을 한 번도 실행하지 않을 수 있다. 그러나 이번에는 i가 0xa0000이므로 조건을 충족하기에 반복 부분은 제대로 실행될 것이다.


 최후의 'i++'는 i = i + 1의 생략형으로, i에 1을 더하라는 명령이다. 이 명령은 { } 안의 실행이 한 번 끝날 때마다 반드시 실행되고, 반복 조건(i <= 0xaffff)을 체크한다. 맨 아래 io_hit( ) 을 감싸고 있는 for문의 경우 초기문도 조건문도 반복문도 없다. 3개가 다 생략된 형태로, 무한 루프를 의미한다. 아무 조건 없이 계속 실행하기 때문이다. 


 지금까지 한 결과를 토대로 실행을 시켜보면, 3일차에서 만났던 새카만 화면이 아닌 새하얀 화면을 만나게 된다. VRAM 전체에 15를 써 넣었기 때문인데, 모든 화소의 색을 15번째의 색으로 했다는 뜻이다. 하얀 화면이 나왔으므로 15번째 색은 물론 하얀색이다.



#2 줄무늬 (harib01b)


이번에는 흰 화면이 아닌 눈에 더 잘 띄는 줄무늬로 화면을 바꿔보자. bootpack.c를 조금만 조작하면 된다.

/*************************************************
 ** bootpack.c에 추가된 코드
*************************************************/

for (i = 0xa0000; i <= 0xaffff; i++) {
	write_mem8(i, i & 0x0f);
}

/*************************************************
 ** End Line
*************************************************/

write_mem8 부분이 달라졌다. 번지는 바뀌지 않았지만, 쓰는 값이 15가 아니고 i & 0x0f라는 값이 되어 있다. 해석하자면 이렇다. &라는 건 AND 연산이라고 불리는 것으로, 수학에서는 나오지 않는 종류의 연산자다. 앞서 CPU는 수치 데이터만을 다루는 것이 아니고 그림 데이터 등도 다룬다고 했었다. 그림 등을 다룰 경우 수학적인 계산 기능, 즉 더하거나 빼거나 곱하는 등의 계산은 거의 의미가 없다. 왜냐하면 다루고 있는 데이터는 분명 2진수겠지만 그것은 숫자로 사용하고 있는 것보다는 0과 1의 나열 그 자체가 중요하기 때문이다.


 이러한 그림 데이터에 대해서는 특정 비트를 1로 한다든가, 0으로 한다든가, 반전 시키는 연산기능이 필요하다. 먼저, 특정 비트를 1로 하는 기능부터 알아보자. 보통 OR 연산을 이용한다. 아래와 같은 연산이 가능하다.


0100 OR 0010 -> 0110

1010 OR 0010 -> 1010

 

 A OR B를 계산하는 것은 각각의 자릿수에 대하여, A나 B의 어느 쪽인가가 1이면 결과를 1로 하고, 그렇지 않으면 0으로 하는 것이다. 즉, 변수 i의 하위 2번째 비트에 0이 들어 있으면 i에 0010을 OR로 함으로써 하위 2번째 비트를 1로 만들 수 있다. 다른 비트에는 전혀 영향을 주지 않고서 말이다. i의 하위 2번째 비트가 본래 1이었으면 i는 바뀌지 않는다.


 이번에는 특정 비트를 0으로 하는 기능이다. AND 연산을 이용하여 계산할 수 있다.


0100 AND 1101 -> 0100

1010 AND 1101 -> 1000


 A AND B를 계산할 때에는 각 자릿수에 대하여, A와 B의 양쪽 모두가 1이면 결과를 1로하고, 하나라도 0이면 그 결과를 0으로 한다. 즉 변수 i의 하위 3번째 비트에 0이 들어 있으면, i에 1101을 AND함으로써 하위 3번째 비트를 0으로 만들 수 있다. 다른 비트에는 전혀 영향을 주지 않고서 말이다. i의 3번째 비트가 본래 0이었다면 i는 바뀌지 않는다.


 마지막으로, 특정 비트를 반전시키는 기능이다. 배타적 OR, (=XOR)연산을 사용하여 계산한다.


0100 XOR 0010 -> 0110

1010 XOR 0010 -> 1000


 A XOR B를 계산할 때에는 각 자릿수에 대하여 A와 B의 값이 서로 다르면 결과를 1로 하고, 그렇지 않으면 결과를 0으로 한다. 즉 정당한 수치가 변수 i에 들어 있다고 하면, i에 0010을 XOR하는 것으로, 비트를 반전시킬 수 있다. 다른 비트에는 전혀 영향이 없다. i에 모든 비트가 1인 데이터(즉, 0xffffffff)를 XOR하면 모든 비트가 반전된다.


 마지막으로 AND를 보자. 번지 값에 0x0f를 AND하면 어떻게 될까? 번지의 하위 4비트만 그대로 남고 다른 비트가 모두 0이 된다. (f는 16진수로 4비트기 때문에, 하위 4비트만 남는 것이다.)그래서 써지는 값은 결국


00 01 02 03 04 05 06 07 08 09 0A 0B 0C 0D 0E 0F 00 01 02 03 04 05 06 ...


 이라는 식이 되어, 16화소마다 색 번호를 반복하게 된다. make run을 해 보면, 그 결과로 줄무늬 창이 나오는 것을 확인할 수 있다.



#3 포인터에 도전 (harib01c)


이전에도 언급했지만 C언어에는 직접 메모리 번지를 지정하여 쓰기 위한 명령이 없다. 하지만 대체 표현은 존재한다.


write_mem8(i, i & 0x0f); 에 대체되는 표현은 *i = i & 0x0f ; 이다.


 이 표현으로 바꾼 후 'make run'으로 실행해보면, 'invalid type argument of unary *' 라는 에러가 나온다. 분명 포인터를 올바른 방법으로 사용한 것 같은데 에러가 나온다. 하지만 C컴파일러의 입장이 되어 생각해보면 다르다. 아래의 어셈블러를 살펴보자.


MOV [0x1234], 0x56


 이 문장은 에러일까? 에러는 맞다. 그 이유가 중요하다. 메모리 설정이 BYTE인지, WORD인지, DWORD인지 컴파일러로선 알 수 없기 때문에 에러가 난다. BYTE 등을 생략해도 될 때는 상대가 레지스터일 때일 뿐이다. 그 외의 경우는 생략하면 안된다. 마찬가지 이유로 컴파일러는


MOV [i], (i & 0x0f)


 에 해당하는 기계어를 어떻게든 만들려고 생각했지만, [i]가 BYTE인지, WORD인지, DWORD인지 알 수 없다. 그래서 에러가 나는 것이다. 어떻게 하면 BYTE로 전달할 수 있을까?


char *p; // 이것은 변수 p가 메모리 번지 전용 변수라는 의미다.


 라는 변수를 선언하여 이 p에 i와 동일한 값을 넣어서, *p = i & 0x0f; 라고 해주면 된다. 그러면 C컴파일러는 'p는 메모리 번지 전용 변수이며, char로 써져 있으니 BYTE구나'라고 알게 된다.


char *p; // BYTE용 번지의 경우

short *p; // WORD용 번지의 경우

int *p; // DWORD용 번지의 경우


 이번에는 1바이트씩 쓰기 위해 char를 선택했다.


 char i; AL과 같은 1바이트의 변수이고, short i;AX와 같은 2바이트 변수이고, int i;EAX와 같은 4바이트 변수이다. 더욱이 char *p에서도, short *p, int *p에서도 변수 p자체의 크기는 4바이트다. 왜냐하면 이 변수 p는 '메모리 번지'를 기억하기 위한 변수이기 때문이다. 어셈블러에서도 번지는 ECX와 같이 4바이트의 레지스터로 지정했다.


 이것으로 준비는 다 되었으니 아래의 내용으로 'make run'을 진행해보자.

/*************************************************
 ** 변형된 bootpack.c
*************************************************/

void io_hlt(void);

void HariMain(void)
{
	int i; /* 변수 선언. i라는 변수는 32비트의 정수형 */
	char *p; /* p라는 변수는 BYTE [...]용도의 메모리번지저장을 위한 변수 */
	
	for (i = 0xa0000; i <= 0xaffff; i++) {
		p = i; /* 번지를 대입 */
		*p = i & 0x0f;
		
		/* 이것으로 write_mem8(i, i & 0x0f);대신 사용할 수 있다. */
	}

	for (;;) {
		io_hlt();
	}
}

/*************************************************
 ** End Line
*************************************************/

 write_mem8을 전혀 사용하지 않았는데도 줄무늬가 나오는 걸 확인할 수 있다. 그런데 콘솔 화면을 보면,


warning: assignment makes pointer from integer without a cast


 라는 경고가 나와 있다. 의미는 '대입 명령이 캐스트 없이 정수에서 포인터를 만들고 있다'이다. 캐스트라는 건 수치의 형(타입)을 바꾸는 명령이다. 대개는 하나하나 캐스트를 의식할 필요가 없지만, 이번 같은 명령에서는 명시적으로 해 줄 필요가 있다. cast형태를 맞춘다는 뜻이다.


 포인터메모리 번지를 나타내는 수치를 의미한다. C언어에서는 번지라는 말은 사용하지 않고 포인터라는 말을 사용한다. C언어는, 일반적인 수치와 메모리 번지는 근본적으로 다른 것이라는 생각으로 설계되어 있다. 사실 값이라는 면에서 보면 완전히 동일하지만, 일단 C에서는 그렇다. 위에 나온 경고를 제거하기 위해서는,


p = (char *) i;


 라고 쓰면 된다. 이것으로 i는 메모리 번지를 나타내는 정수로 캐스트되어 대입된다. (i가 기존에 가지고 있던 값 자체는 전혀 달라지지 않는다. 다만, C컴파일러에게 이것은 메모리 주소를 담을거야 라고 알려주는 것뿐이다.) 이제 기존의 write_mem8 함수가 필요없게 되었으니 naskfunc.nas 에서 삭제해주도록 하자.


 * 캐스트 응용


p = (char *) i;


 라는 것이 있다면, 이것을


*p = i & 0x0f; 에 대입하여


*( (char *) i) = i & 0x0f ; 라고 사용할 수 있다.


 가독성이 떨어지지만, 이렇게 사용하면 p라는 변수를 사용하지 않고도 정상적으로 메모리 번지값을 저장할 수 있다. 위에서 사용한 것과 BYTE[i] = i & 0x0f; 는 구조가 비슷하다. C는 어셈블러로부터 비롯된 것이니 당연한 이치다.


* 포인터 설명


MOV BYTE [i], (i & 0x0f) 를 C언어로 만든다고 생각하면,


 '메모리의 i번지에 i & 0x0f의 계산 결과를 쓰고 싶다'는 의미를 그대로 옮기면 된다. 쓰는 방법은 아래와 같다.


int i;

char *p;


p = (char *) i;

*p = i & 0x0f;


  이 4행은 위에서 기술한 MOV 명령 대신에 있는 것이고, 가장 중요한 대목이다. 좀 더 깊이 들어가서 살펴보도록 하자.


p = (char *) i;*p = i & 0x0f; 라는 2개의 대입문의 차이가 뭘까?


 이 질문은 어셈블러를 이해하면 자연스레 해결된다. 만약 p라는 것이 ECX에 대응한다고 하면 다음과 같다.


MOV ECX, i

MOV BYTE [ECX], (i & 0x0f)


 이 차이는 매우 분명하다. 즉, ECX라는 '레지스터'에 대입하는 것인지, ECX 번지의 '메모리'에 대입하는 것인지, 이것은 전혀 다른 것이다. 기억하는 반도체 쪽에서 보면 완전히 다른 의미다. CPU 안인가, 메모리 칩 안인가? C언어에서는 'p'와 '*p'라는 한 문자의 차이밖에 없지만 이렇게 의미가 다른 것이다.


/* C언어의 포인터 부분을 별도로 공부하고 오는 걸 추천합니다. 제 블로그 좌측 검색창에서 포인터라고 검색하시면 만날 수 있습니다. */



#4 포인터 응용(1) - harib01d


 줄무늬를 그리는 부분을 다음과 같이 기술할 수도 있다.

/*************************************************
 ** 변형된 bootpack.c
*************************************************/

p = (char *) 0xa0000; /* 번지를 대입 */

for (i = 0; i <= 0xffff; i++) {
	*(p + i) = i & 0x0f;
}

/*************************************************
 ** End Line
*************************************************/

 하고 있는 일은 본질적으로 달라지지 않았다. 메모리의 번지수는 0xa0000 부터 0xa0001, 0xa0002, 0xa0003 순으로 계속 1씩 증가할 것이다. 그리고 그 주소에다가 i & 0x0f 값을 써주는 것이다. i의 값은 0x0f 와 AND연산 처리되어 01 02 03 04 ... 0f 01 02 순으로 1씩 증가하는 주소에 각 저장될 것이다. 주소 1당 1BYTE를 저장할 수 있다고 생각하자.



#5 포인터 응용(2) - harib01e


 C언어에서는 *(p + 1)이라는 걸 p[i] 라는 형식으로 다시 쓸 수 있도록 되어 있다.

/*************************************************
 ** 변형된 bootpack.c
*************************************************/

p = (char *) 0xa0000; /* 번지를 대입 */

for (i = 0; i <= 0xffff; i++) {
	p[i] = i & 0x0f;
}

/*************************************************
 ** End Line
*************************************************/

잘못된 C언어 교과서엔 p[i]는 p라는 배열의 i번째 요소라고 쓰여 있을 것이다. 엄밀히 말하면 잘못됐다. p[i]라는 건 *(p+1)과 완전히 같은 의미로, 길게 쓰는 것이 귀찮을 경우 사용하는 것에 불과하다. 상황에 맞추어 둘을 번갈아 가며 사용하기도 한다.


 배열의 의미라던가 그런 건 애초에 없다. 여기 하나의 예를 보자. 덧셈은 더하는 순서를 바꿔도 좋으므로 *(p + i)를 *(i + p)라고 바꾸어 쓸 수 있다. 그래서 p[i] 를 i[p]라고 써도 된다. a[2]를 2[a]라고 써도 된다. 2라는 배열의 a번째의 요소라고 이걸 해석할 수 있을까? 전혀 그렇지 않기 때문에 본질적으로 배열과 포인터는 아무 연관이 없다.



#6 색 번호 설정 (harib01f)


OS화면 구성을 시작해보자. 그 전에 해야할 작업은 색에 대한 작업이다. 이번에 사용하고 있는 320x200의 8비트 컬러 모드는 색 번호가 8비트, 즉 0~255밖에 사용할 수 없다. 이것은 매우 적은 수의 색이다. 보통 PC에 색을 지정한다고 하면, #ffffff 같은 것을 말한다. 이것은 RGB 설정으로 16진수 6자릿수, 즉 24비트다. 8비트로는 부족하다. 그렇다면 #ffffff의 색을 지정하려면 어떤 방법을 사용해야할까?


 8비트 컬러 모드에서는 0~255의 색을 프로그래머가 마음대로 정할 수 있다. 즉, 색 번호 25는 #ffffff의 뜻이며, 색 번호 26은 #123456의 뜻으로 설정하여 사용하는 것이다. 이러한 구조를 팔레트(palette)라고 한다. 지금처럼 프로그램에서 아무런 색도 설정하지 않으면, 0번이 #000000, 15번이 #ffffff이 된다.


 색을 정하는 건 자기 마음이지만, 일단 이 책에서는 16가지 색만있으면 충분하다고 생각하고 0~15로 이 색들을 설정했다.


#000000: 검은색    #00ffff: 밝은 청색    #000084: 군청색    #ff0000: 밝은 적색    #ffffff: 흰색    #840084: 어두운 보라색

#00ff00: 밝은 녹색    #c6c6c6: 밝은 회색    #008484: 어두운 청색    #ffff00: 밝은 노란색    #840000: 어두운 적색

#848484: 어두운 회색    #0000ff: 밝은 청색    #008400: 어두운 녹색    #ff00ff: 밝은 보라색    #848400: 어두운 노란색


  위의 색 정보들을 바탕으로 bootpack.c를 재구성 해보았다.

/*************************************************
 ** 변형된 bootpack.c
*************************************************/

void io_hlt(void);
void io_cli(void);
void io_out8(int port, int data);
int io_load_eflags(void);
void io_store_eflags(int eflags);

/* 같은 소스 파일 내에 있더라도 정의한 후에 사용해야 한다.
	다음과 같이 미리 선언해둔다. */

void init_palette(void);
void set_palette(int start, int end, unsigned char *rgb);

void HariMain(void)
{
	int i; /* 변수 선언, i라는 변수는 32비트의 정수형 */
	char *p; /* p라는 변수는 BYTE [...] 용의 번지 */

	init_palette(); /* 팔레트 설정 */

	p = (char *) 0xa0000; /* 번지 대입 */

	for (i = 0; i <= 0xffff; i++) {
		p[i] = i & 0x0f;
	}

	for (;;) {
		io_hlt();
	}
}

void init_palette(void)
{
	static unsigned char table_rgb[16 * 3] = {
		0x00, 0x00, 0x00,	/*  0: 검은색 */
		0xff, 0x00, 0x00,	/*  1: 밝은 적색­ */
		0x00, 0xff, 0x00,	/*  2: 밝은 녹색 */
		0xff, 0xff, 0x00,	/*  3: 밝은 노란색 */
		0x00, 0x00, 0xff,	/*  4: 밝은 청색 */
		0xff, 0x00, 0xff,	/*  5: 밝은 보라색 */
		0x00, 0xff, 0xff,	/*  6: 밝은 청색 */
		0xff, 0xff, 0xff,	/*  7: 흰색 */
		0xc6, 0xc6, 0xc6,	/*  8: 밝은 회색 */
		0x84, 0x00, 0x00,	/*  9: 어두운 적색 */
		0x00, 0x84, 0x00,	/* 10: 어두운 녹색 */
		0x84, 0x84, 0x00,	/* 11: 어두운 노란색 */
		0x00, 0x00, 0x84,	/* 12: 군청색 */
		0x84, 0x00, 0x84,	/* 13: 어두운 보라색 */
		0x00, 0x84, 0x84,	/* 14: 어두운 청색 */
		0x84, 0x84, 0x84	/* 15: 어두운 회색 */
	};
	set_palette(0, 15, table_rgb);
	return;

	/* static char 명령은 데이터밖에 사용할 수 없지만, DB 명령에 대응된다. */
}

void set_palette(int start, int end, unsigned char *rgb)
{
	int i, eflags;
	eflags = io_load_eflags();	/* 인터럽트 허가 플래그의 값을 기록한다. */
	io_cli(); 			/* 허가 플래그를 0으로 하여 인터럽트를 금지한다. */
	io_out8(0x03c8, start);
	for (i = start; i <= end; i++) {
		io_out8(0x03c9, rgb[0] / 4);
		io_out8(0x03c9, rgb[1] / 4);
		io_out8(0x03c9, rgb[2] / 4);
		rgb += 3;
	}
	io_store_eflags(eflags);	/* 인터럽트 허가 플래그를 본래 값으로 되돌린다. */
	return;
}

/*************************************************
 ** End Line
*************************************************/

프로그램의 앞쪽에 많은 외부 함수명이 선언되어 있다. 이것들은 나중에 naskfunc.nas에서 만들어야만 한다. 일단 이부분은 뒤에 다루기로 하고, 함수 HariMain을 살펴보자. 이것은 팔레트를 설정하기 위한 함수를 호출하는 문장이 1행 늘어난 것뿐이다. 중요한 사항은 아니니 다음으로 가보자.


 함수 init_pallete를 보자. 최초의 static 문장이 조금 길지만, 결국은 정수 table_rgb를 선언하고 있을 뿐이다. 이것을 간략히 쓰면 다음과 같다.

/*************************************************
 ** 간소화된 init_palette
*************************************************/

void init_palette(void)
{
	table_rgb 선언;
	set_pallete(0, 15, table_rgb);
	return;
}

/*************************************************
 ** End Line
*************************************************/

즉, 선언 이외에는 특별히 어려운 게 없다. 선언 부분만 설명하고자 한다. C언어에서는


char a[3];


 이라고 쓰면, a는 정수가 된다. 어셈블러에서 말하는 레이블이다. 레이블의 값은 물론 번지를 의식하고 있다. 그리고 더욱이 'RESB 3'이 준비되어 있다. 여기까지 정리하면 위의 문장은,


a:

RESB 3


 에 상응하는 것이다. nask에서의 RESB는 내용이 0이 되는 것이 보증되어 있지만, C언어에서는 그 보증이 없다. 적당한 쓰레기 값이 들어 있을지도 모른다. // RESB는 1일차에 등장한 내용으로, 'reserve byte'의 약자이다. 10바이트 정도 띄어놓는다는 의미로 RESB 10과 같은 방식으로 사용한다. 10바이트는 예약의 의미다. 그러나 nask에서는 단순히 띄어놓는 정도가 아니고 띄어놓는 부분을 0x00으로 채우는 것이 되므로, 1일차에서는 0x00을 많이 써 넣는 대용으로 사용했었다.


 더욱이 위와 같은 선언 뒤에는 { ... } 를 사용하여 데이터의 초기 값을 쓸 수 있다. char a[3] = {1, 2, 3}; 이런 식이다. 우리가 다룰 팔레트함수의 table_rgb는 초기화 할 때 대입해야할 값이 무려 16x3 = 48개나 된다. 1개의 대입으로 적어도 3개의 명령어가 필요할 것이고 결국 3바이트는 소비할 예정으로, 48 x 3 = 약 150 바이트를 소비해야할 지도 모른다. 따라서 아래와 같이 사용하는 것이 훨씬 합리적이다.


table_rgb:

DB 0x00, 0x00, 0x00, 0xff, 0x00, 0xff, 0x00, 0xff, 0x00, ...


 DB만 사용해서 표현하는 것이다. 이렇게 하면 48바이트로 명령어가 끝난다. 그래서 C언어에서도 RESB 명령 대신 DB 명령을 사용하도록 지시하는 방법이 있다. 바로 static이다. unsigned의 경우는 '다루고 있는 데이터는 BYTE(char)이지만, 이것을 부호(sign)가 없는 수(= 0 혹은 양수)로 다루겠다" 라는 지시다. char형의 변수에는 3개의 타입이 있다. signed형, unsigned형, 특별히 지정되지 않은 형으로 분류된다.


 signed의 경우, -128 ~ 127의 정수를 계산에 사용한다. 음수도 다루는 편이 계산의 폭이 생겨 편리하지만, 그것과 바꾸어 다룰 수 있는 최대치가 약 반 정도롤 줄어든다. unsigned형에서는 0~255의 정수를 계산에 사용한다. 특별히 지정이 없을 경우는 그냥 컴파일러가 맘대로 정해도 된다는 뜻이다.


 여기서는 0xff라는 수치가 몇 번 나오는데, 255라는 최대 광도를 뜻하고 있다. 0xff를 -1로 해석하면 곤란하다. 그래서 unsigned 모드로 구성했다. 덧붙여, int나 short에도 signed나 unsigned가 존재한다. 이것으로 init_palette의 설명은 끝났다.


 set_palette에 대한 설명을 지금부터 살펴보자. 이 함수는 여러 가지 일들을 처리해주고 있는데, 우선 다소 간소화된 코드로 이 함수를 살펴보자.

/*************************************************
 ** 간소화된 set_palette
*************************************************/

void set_palette(int start, int end, unsigned char *rgb)
{
	int i;
	io_out8(0x03c8, start);
	for (i = start; i <= end; i++) {
		io_out8(0x03c9, rgb[0] / 4);
		io_out8(0x03c9, rgb[1] / 4);
		io_out8(0x03c9, rgb[2] / 4);
		rgb += 3;
	}
	return;
}

/*************************************************
 ** End Line
*************************************************/

이 함수는 함수 io_out8을 몇 번 호출하고 있을 뿐이다. io_out8은 추후에 naskfunc.nas에 작성하게 될 것이다. 기능은 장치 번호로 지정한 장치에 데이터를 보내는 함수다.


 CPU의 다리(핀) 끝에는 메모리가 이어져 있다고 이전에 설명했지만, 이어져 있는 것이 메모리뿐이라면 CPU는 계산과 기억밖에 하지 못한다. 실제로는 키보드의 입력에 반응하거나, LAN카드를 통해서 네트워크로부터 정보를 받아오거나, 사운드카드에 음악 데이터를 보내거나, 플로피디스크에 정보를 써 넣거나 하는 등의 동작을 하고 있다. 이것들은 모두 장치(device: 디바이스)이며, 이것들도 물론 CPU에 연결되어 있다.


 그리고 이어져 있는 한, 이것들에게 전기 신호를 보내거나, 이들로부터 정보를 받기 위한 명령이 있다. 장치로 전기 신호를 보내는 것이 OUT 명령이고, 장치에서 전기 신호를 받는 것이 IN 명령이다. 많은 메모리를 구별해서 사용하기 위해 번지라는 개념이 존재하듯, OUT 명령이나 IN 명령에서도 많은 장치를 구별하여 사용하기 위해 장치번호를 사용한다. 이 장치 번호를 보통 port(포트, =항구)라고 하고, 이것은 CPU에서 각 장치를 향해 전기 신호를 보내거나 받는 역할을 맡는다.


 C언어에서는 IN 명령이나 OUT 명령에 상응하는 명령이 없으므로, 어셈블러로 직접 만들어야만 한다. 프로그램을 보면 0x3c8 이라든가, 0x3c9라는 장치 번호가 갑자기 나와 있는데, 이것들은 VGA 사이트에 있는 문서의 '비디오 DA 컨버터'라는 항목을 참조한 것이다. 아래의 내용이 그 문서의 내용이다. 그리고 그 아래 정리한 내용이 있다.


  • 팔레트의 액세스 순서

  • 일련의 액세스 중에서 인터럽트 등이 들어가지 않게 한다. (예를 들어 CLI).

  • 0x03c8에 설정하고 싶은 팔레트 번호를 써넣고 이어서 R G B 순서로 0x03c9에 써넣는다. 만약 다음 팔레트도 이어서 설정하고 싶으면, 팔레트 번호의 설정을 생략하고 계속해서 R, G B 순서로 0x03c9에 써 넣어도 된다.

  • 현재의 팔레트 상태를 읽어낼 때는 먼저 0x3c7에 팔레트 번호를 써넣고, 0x03c9 3번 읽어낸다. 이것이 차례로 R, G, B가 된다. 만약 다음 팔레트도 읽어내고 싶다면 팔레트 번호의 설정을 생략하고 R, G, B 순서로 읽어내도 좋다.

  • 최초에 CLI 를 한 경우에는 마지막에 STI를 한다.


/*************************************************
 ** 간소화된 set_palette 중 CLI, STI 살펴보기
*************************************************/

void set_palette(int start, int end, unsigned char *rgb)
{
	int i, eflags;
	eflags = io_load_eflags();	/* 인터럽트 허가 플래그의 값을 기록한다. */
	io_cli(); 			/* 허가 플래그를 0으로 하여 인터럽트를 금지한다. */
	// 이미 위에 설명된 부분
	io_store_eflags(eflags);	/* 인터럽트 허가 플래그를 본래 값으로 되돌린다. */
	return;
}
/*************************************************
 ** End Line
*************************************************/

 '팔레트의 액세스 순서' 중, CLI와 STI라는 것이 나온다. CLI인터럽트 플래그(interrupt flag)를 0으로 만드는 명령이다(clear interrupt flag). STI는 이 인터럽트 플래그를 1로 만드는 명령이다(set interrupt flag). 플래그라는 것은 이전에 나온 캐리 플래그와 같은 것으로, CPU에는 여러 가지 플래그가 존재한다. 인터럽트 플래그는 CPU의 인터럽트 처리와 관계가 있다. CPU에 인터럽트 신호가 왔을 때, 인터럽트 반응 회로가 동작하는가(인터럽트 플래그가 1), 혹은 일단 무시하는가(인터럽트 플래그가 0)를 나타낸다. 즉, 이 플래그를 통해서 CPU의 동작을 설정할 수 있는 것이다.


 다음으로 EFLAGS라는 특별한 레지스터를 살펴보도록 하자. FLAGS라는 16비트의 레지스터확장된 32비트 레지스터가 EFLAGS다. FLAGS라는 것은 캐리 플래그나 인터럽트 플래그 등으로 구성된 레지스터다. 캐리 플래그에 관해서는 JC, JNC 등의 점프 명령으로 0인지 1인지를 알아낼 수 있지만 인터럽트 플래그에 관련해서는 JI 라든가 JNI 같은 명령이 없다. 알아보는 방법은 EFLAGS를 읽어 들여 9번째 비트가 0인지, 1인지를 체크하는 것이다. 참고로 캐리 플래그는 EFLAGS의 0번째 비트다.

 set_palette에서 하고 싶은 것은 팔레트를 설정하기 전에 CLI 를 실행하는 것이다. 따라서 처리가 끝나면 인터럽트 플래그를 원래 위치로 되돌려 놓아야 한다. 되돌려 놓으려면 원래 값을 기억해야 하므로 io_load_eflags( )라는 함수를 만들어서 처음에 eflags의 값을 읽어두는 것이다. 그리고 처리가 끝난 후에 eflags의 내용을 보고 STI를 실행할지 말지를 정하면 된다. 단순히 생각하면 eflags를 EFLAGS에 대입하면 인터럽트 플래그는 원위치 된다. io_store_eflags라는 함수로 그 처리를 해주는 것 뿐이다.


 CLI, STI, EFLAGS의 읽기, EFLAGS로 쓰기 등의 그 어느것도 C로는 할 수 없다. 그래서 어셈블리로 할 수밖에 없는 것이다. 이것으로 bootpack.c의 설명을 마치기로 하고 이제 naskfunc.nas를 살펴보도록 하자.

/*************************************************
 ** naskfunc.nas in harib01f
*************************************************/

; naskfunc
; TAB=4

[FORMAT "WCOFF"]				; 오브젝트 파일을 만드는 모드
[INSTRSET "i486p"]				; 486의 명령까지 사용하고 싶다.
[BITS 32]					; 32비트 모드용의 기계어를 만든다.
[FILE "naskfunc.nas"]				; 소스 파일명 정보

		GLOBAL	_io_hlt, _io_cli, _io_sti, io_stihlt
		GLOBAL	_io_in8,  _io_in16,  _io_in32
		GLOBAL	_io_out8, _io_out16, _io_out32
		GLOBAL	_io_load_eflags, _io_store_eflags

[SECTION .text]

_io_hlt:	; void io_hlt(void);
		HLT
		RET

_io_cli:	; void io_cli(void);
		CLI
		RET

_io_sti:	; void io_sti(void);
		STI
		RET

_io_stihlt:	; void io_stihlt(void);
		STI
		HLT
		RET

_io_in8:	; int io_in8(int port);
		MOV		EDX,[ESP+4]		; port
		MOV		EAX,0
		IN		AL,DX
		RET

_io_in16:	; int io_in16(int port);
		MOV		EDX,[ESP+4]		; port
		MOV		EAX,0
		IN		AX,DX
		RET

_io_in32:	; int io_in32(int port);
		MOV		EDX,[ESP+4]		; port
		IN		EAX,DX
		RET

_io_out8:	; void io_out8(int port, int data);
		MOV		EDX,[ESP+4]		; port
		MOV		AL,[ESP+8]		; data
		OUT		DX,AL
		RET

_io_out16:	; void io_out16(int port, int data);
		MOV		EDX,[ESP+4]		; port
		MOV		EAX,[ESP+8]		; data
		OUT		DX,AX
		RET

_io_out32:	; void io_out32(int port, int data);
		MOV		EDX,[ESP+4]		; port
		MOV		EAX,[ESP+8]		; data
		OUT		DX,EAX
		RET

_io_load_eflags:	; int io_load_eflags(void);
		PUSHFD		; PUSH EFLAGS라는 의미
		POP		EAX
		RET

_io_store_eflags:	; void io_store_eflags(int eflags);
		MOV		EAX,[ESP+4]
		PUSH	EAX
		POPFD		; POP EFLAGS라는 의미
		RET

/*************************************************
 ** End Line
*************************************************/

 MOV EAX, EFLAGS 같은 명령이 있으면 어려운 일이 아무것도 없었겠지만, CPU에는 그러한 명령이 없다. EFLAGS를 읽거나 쓰기 위해 사용할 수 있는 것은 'PUSHFD'와 'POPFD'라는 명령이다. PUSHFD는 'push flags double-word'의 약어로, 플래그를 더블워드로 스택에 밀어넣는다는 의미다. 결국 하고 있는 일은 'PUSH EFLAGS' 뿐이다. POPFD는 'pop flags double-word'의 약어로, 플래그를 더블워드로 스택으로부터 빼낸다는 의미다. 이것도 결국 하는 일은 'POP EFLAGS'뿐이다.


 스택은 자료구조의 한 형태고, 스택에 정보를 등록하는 것을 push라고 한다. 스택으로부터 등록했던 정보를 빼내는 건 pop이라 한다. 결국 PUSHFD POP EAX는, 먼저 스택에 EFLAGS를 밀어 넣고, POP으로 나온 데이터를 EAX에 대입시키고 있는 것이다. 그래서 MOV EAX, EFLAGS 대신 사용하게 된다. 한편, PUSH EAX POPFD는 그 반대 방향이 되므로 MOV EFALGS, EAX 대신에 사용한다.


 마지막으로, io_load_eflags는 처음으로 값을 리턴하는 함수의 예이지만, C언어의 규약에서는 RET했을 때 EAX에 들어 있던 값이 함수의 값으로 간주된다. 위의 소스코드에서는 지금 당장은 필요하지 않은 함수도 있다. 하지만 앞으로 필요할 것 같다는 판단하에 만들어둔 것이다. 이제 'make run'으로 실행을 해보면, 우리가 위에 설정했던 팔레트 색들을 토대로 화면에 줄무늬들이 그려지는 걸 확인할 수 있다. 좌측이 기존의 실행 콘솔창 화면이고, 우측이 색을 별도로 설정한 후 실행한 콘솔창 화면이다.




#7 사각형 그리기 (harib01g)


 색이 갖추어졌으므로 이번에는 그림을 그려보도록 하자. 먼저, VRAM과 화면상의 점과의 관계부터 알아보자. 지금 화면 모드에서는 화면에 320 x 200 = (64,000)개의 화소가 있다. 왼쪽 위의 좌표를 (0, 0), 오른쪽 아래의 좌표를 (319, 199)라고 하면, 화소 좌표 (x, y)에 대응하는 VRAM의 번지는


0xa000 + x + y * 320


 으로 계산된다. 다른 화면 모드에서도 0xa0000이라는 개시 번지와 y에 곱셈하는 320이라는 숫자가 바뀌는 정도로, 기본적으로는 동일하다. 이 식을 사용해서 화소의 번지를 계산해서 그 메모리에 색 번호를 기억시키면 화면상의 그 위치에 지정한 색이 나오게 된다. 이것이 한 점의 그림이 된다. 이 작업을 x를 증가시키면서 반복하면 옆으로 긴 수평의 직선을 그을 수 있다. 또한 여러 개의 직선을 이용하면 사각형을 그릴 수 있다.


 위에서 언급한 방식으로 boxfil8 이라는 함수를 만들었다. bootpack.c에 존재한다. 그리고 HariMain에서는 줄무늬 모양을 그리지 않고, 이 함수를 3번 사용하여 3개의 사각형을 그려보았다.

/*************************************************
 ** 변경된 bootpack.c 중 일부 (in harib01g)
*************************************************/

void boxfill8(unsigned char *vram, int xsize, unsigned char c, int x0, int y0, int x1, int y1);

#define COL8_000000		0
#define COL8_FF0000		1
#define COL8_00FF00		2
#define COL8_FFFF00		3
#define COL8_0000FF		4
#define COL8_FF00FF		5
#define COL8_00FFFF		6
#define COL8_FFFFFF		7
#define COL8_C6C6C6		8
#define COL8_840000		9
#define COL8_008400		10
#define COL8_848400		11
#define COL8_000084		12
#define COL8_840084		13
#define COL8_008484		14
#define COL8_848484		15

void HariMain(void)
{
	char *p; /* 변수 p는 BYTE [...]용의 번지 */

	init_palette(); /* 팔레트 설정 */

	p = (char *) 0xa0000; /* 번지 대입 */

	boxfill8(p, 320, COL8_FF0000,  20,  20, 120, 120);
	boxfill8(p, 320, COL8_00FF00,  70,  50, 170, 150);
	boxfill8(p, 320, COL8_0000FF, 120,  80, 220, 180);

	for (;;) {
		io_hlt();
	}
}

void boxfill8(unsigned char *vram, int xsize, unsigned char c, int x0, int y0, int x1, int y1)
{
	int x, y;
	for (y = y0; y <= y1; y++) {
		for (x = x0; x <= x1; x++)
			vram[y * xsize + x] = c;
	}
	return;
}

/*************************************************
 ** End Line
*************************************************/

 위 소스에서 보면 #define이라는 걸 사용하고 있는데, 정수 선언을 하기 위함이다. 어느 색 번호가 어느 색인지를 기억하는 것이 귀찮으므로 알기 쉽게 번호로 지정했다.



#8 완성 (harib01h)


 4일차의 막바지다. HariMain만 좀 수정했다.

/*************************************************
 ** 수정된 HaribMain (in bootpack.c in harib01h)
*************************************************/

void HariMain(void)
{
	char *vram;
	int xsize, ysize;

	init_palette();
	vram = (char *) 0xa0000;
	xsize = 320;
	ysize = 200;

	boxfill8(vram, xsize, COL8_008484,  0,         0,          xsize -  1, ysize - 29);
	boxfill8(vram, xsize, COL8_C6C6C6,  0,         ysize - 28, xsize -  1, ysize - 28);
	boxfill8(vram, xsize, COL8_FFFFFF,  0,         ysize - 27, xsize -  1, ysize - 27);
	boxfill8(vram, xsize, COL8_C6C6C6,  0,         ysize - 26, xsize -  1, ysize -  1);

	boxfill8(vram, xsize, COL8_FFFFFF,  3,         ysize - 24, 59,         ysize - 24);
	boxfill8(vram, xsize, COL8_FFFFFF,  2,         ysize - 24,  2,         ysize -  4);
	boxfill8(vram, xsize, COL8_848484,  3,         ysize -  4, 59,         ysize -  4);
	boxfill8(vram, xsize, COL8_848484, 59,         ysize - 23, 59,         ysize -  5);
	boxfill8(vram, xsize, COL8_000000,  2,         ysize -  3, 59,         ysize -  3);
	boxfill8(vram, xsize, COL8_000000, 60,         ysize - 24, 60,         ysize -  3);

	boxfill8(vram, xsize, COL8_848484, xsize - 47, ysize - 24, xsize -  4, ysize - 24);
	boxfill8(vram, xsize, COL8_848484, xsize - 47, ysize - 23, xsize - 47, ysize -  4);
	boxfill8(vram, xsize, COL8_FFFFFF, xsize - 47, ysize -  3, xsize -  4, ysize -  3);
	boxfill8(vram, xsize, COL8_FFFFFF, xsize -  3, ysize - 24, xsize -  3, ysize -  3);

	for (;;) {
		io_hlt();
	}
}

/*************************************************
 ** End Line
*************************************************/

 태스크 바가 좀 크게 보이는 건, 화소 수가 적은 탓이다. OS의 기본적인 느낌은 난다. 현재 haribote.sys는 1,216 바이트이다. 1.2KB 정도로 매우 작지만, 이 정도의 일은 할 수 있다는 걸 보여줬다.

댓글
공지사항
최근에 올라온 글
최근에 달린 댓글
Total
Today
Yesterday