본문 바로가기

OS 개발일지

[OS개발] 4일차 - COLOR, COLOR, COLOR!

4일차

4일차에는 메모리를 조작하여 화면에 색을 표현하는 방법을 배웠다.


# OS는 화면을 표시하기 위해서 메모리를 사용한다. 메모리의 특정한 영역 (나의 경우 320x200 = 64,000 = 0xFA00 만큼의 메모리 주소가 필요한데, 0xA0000 ~ 0xAFA00의 메모리를 사용한다고 한다)에서의 각 주소는 화면의 화소와 일대일 대응을 이루고 있다. 이 영역의 메모리를 조작하여 화면의 표시를 바꿀 수 있다. (그래서 화면이 클 수록 메모리도 많이 먹는가 보다)


책에서는 메모리에 쓰는 어셈블리어 함수를 만들어서 해보는 과정이 있었지만 결국 포인터로 다시 코드를 바꾸므로 안쓰려 했으나... 어셈블함수에서 처음으로 인자를 받아 쓰는 과정이 있으므로 코드를 남긴다.

; iolib
; TAB=4

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

		GLOBAL	_io_hlt,_write_mem8

[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에 read한다
		MOV		AL,[ESP+8]		; [ESP+8]에 data가 들어가 있으므로 그것을 AL에 read한다
		MOV		[ECX],AL
		RET
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);
	}

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

보면 함수의 인자가 [ESP+4], [ESP+8], [ESP+12]... 순으로 전달되는 모습이다. 자유롭게 사용할 수 있는 32bit 레지스터인 EAX, ECX, EDX 중 ECX를 사용해서 addr값을 저장하고, AL에 data를 저장한다. AL을 굳이 사용하는 이유는 색 데이터인 8bit만큼의 데이터만 불러오기 때문이다. AX면 16bit, EAX면 32bit 만큼의 데이터를 가져오게 된다. 마지막으로 불러온 주소 값에 data를 저장한다.


이 과정을 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();
	}
}

char 포인터 변수 p를 선언하고, 최초 번지를 대입, 반복문으로 번지수를 돌려가며 p가 가리키는 곳에 값을 대입한다. (메모리 주소에 직접 대입하는 느낌이 겁나 새롭다...)

포인터 파트를 읽으면서 새롭게 안 사실이 p[5] = 5[p] 라는거...(!)
    p[5] = *(p+5)
    5[p] = *(5+p)
이렇게 표현되기 때문이라고 신박하다


이걸로 예쁜 색으로 바탕색도 바꿔보고,

줄무늬도 그려보고...얼쑤



# VRAM의 비디오 모드에서 8bit로 표현 가능한 기본 색상은 아래 표에 정리되어있다.


프로그래머가 이 팔레트를 조작하는 것이 가능한데, 여기서 디바이스 IN, OUT 명령이 등장한다. 팔레트를 조작하기 위해서 비디오 DA 컨버터에 특정 신호를 보내서 팔레트의 값을 변경한다. 보내는 정보는 RGB에 해당하는 3바이트.

방법은 다음과 같다.

  • 일련의 액세스 중에 인터럽트 등이 들어가지 않게 한다(예를 들어 CLI).
  • 0x03c8에 설정하고 싶은 팔레트 번호를 써 넣고, 이어서 RGB 순서로 0x03c9에 써 넣는다. 만약, 다음 팔레트도 이어서 설정하고 싶으면, 팔레트 번호의 설정을 생략하고, 계속해서 RGB 순서로 0x03c9에 써 넣는다.
  • 현재의 팔레트 상태를 읽어낼 때에는 먼저 0x3c7에 팔레트 번호를 써 넣고, 0x03c9를 3번 읽어낸다. 이어서 읽을 때 역시 쓸 때와 같다.
  • 최초에 CLI를 한 경우에는 마지막에 STI를 한다.

CLI는 CLear Interupt flag 로써, IF(Interupt Flag)를 내려서 비디오 컨버터와 통신 중 인터럽트가 일어나지 않게 방지한다. STI는 SeT Interupt flag 이므로 IF를 다시 올린다.

; 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
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;
}

C언어 코드에서 보이는 io_out8, io_cli 등 함수는 물론 어셈블함수이다.
어셈블리어 덕후라 그런지 C코더들과는 상이한 코드... rgb를 직접움직인다던가...


IN과 OUT코드에 대해 얘기하자면, IN의 경우 통신할 포트를 인자로 받아서 EDX에 저장하고, EAX를 초기화해서 준비해 놓은다음, IN명령어로 EAX에 불러온 값을 저장한다. 이때 AL, AX, EAX로 불러올 데이터의 크기를 지정할 수 있다. 그리고 RET(return)으로 EAX를 반환. (RET은 EAX를 반환하도록 정해져 있다)


다른건 괜찮은데 eflags의 존재가 거슬린다.

먼저 C코드를 보자. 이게 뭐냐하믄 32bit 레지스터인데, 앞에서 배운 CF(Carry Flag)같은 플래그들이 저장되어 있는 레지스터이다. 그런데 CLI로 플래그를 내렸다가 STI로 다시 돌릴때 다른 플래그에 영향을 주면 안되므로 io_load_efalgs로 eflag의 상태를 저장했다가 STI를 할 필요도 없이 io_store_eflags로 원래대로 돌리는 것이다.

어셈블코드를 보면 그냥 대입하면 좋을걸 굳이 PUSH와 POP을 사용하는 모습이다.
load의 경우 PUSHFD(push flags double-word)로 eflags의 내용을 스택에 집어 넣고, POP으로 EAX에 그 내용을 옮긴다. 그리고 RET으로 반환.
store의 경우 인자로 받은 eflags를 EAX에 저장하고, PUSH로 스택에 밀어넣는다. 그 후 POPFD(pop flags double-word)로 eflags에 저장.



# 점을 그리는 법을 배웠으니 선이나 사각형 그리는 건 시간문제.

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;
}



그림그리는 법을 배우니 윈도우 프로그래밍 하는 기분이다ㅋㅋㅋ 필자가 앞으로 사용할 윈도우 화면도 코드로 남겨주었다.