본문 바로가기

OS 개발일지

[OS개발] 5일차 - GDT? IDT...?

5일차

5일차는 이제 점도 찍을 수 있겠다, 본격적으로 문자를 표시해보고 GDT와 IDT에 대하여 공부했다.


# 일단 좀더 편한 출력을 위해서 화면에 관련된 설정값들을 불러오기로 했다.
BOOTINFO라는 구조체를 선언, asmhead에서 설정한 값들을 불러온다. 구조체라는 것은 일련의 변수들을 연속된 메모리 주소에 선언하는 것이므로, 불러오려는 값들이 연속된 주소상에 올라가 있다면 단순히 첫 주소를 대입하는 것으로 쉽게 구조체에 값들을 대입시킬 수 있다.그림판 재등장

struct BOOTINFO {
	char cyls, leds, vmode, reserve;
	short scrnx, scrny;
	char *vram;
};

vram은 char형 포인터로, 화면출력시 기준이 되는 0xA0000을 값으로 가지게 된다.


4일차 마지막에 보인 배경화면을 그리는 일을 init_screen에서 맡기고 bootinfo를 메인에서 설정하게 되었다.

struct BOOTINFO {
	char cyls, leds, vmode, reserve;
	short scrnx, scrny;
	char *vram;
};

void HariMain(void)
{
	struct BOOTINFO *binfo = (struct BOOTINFO *) 0x0ff0;

	init_palette();
	init_screen(binfo->vram, binfo->scrnx, binfo->scrny);

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

void init_screen(char *vram, int x, int y)
{
	boxfill8(vram, x, COL8_008484,  0,     0,      x -  1, y - 29);
	boxfill8(vram, x, COL8_C6C6C6,  0,     y - 28, x -  1, y - 28);
	boxfill8(vram, x, COL8_FFFFFF,  0,     y - 27, x -  1, y - 27);
	boxfill8(vram, x, COL8_C6C6C6,  0,     y - 26, x -  1, y -  1);

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

	boxfill8(vram, x, COL8_848484, x - 47, y - 24, x -  4, y - 24);
	boxfill8(vram, x, COL8_848484, x - 47, y - 23, x - 47, y -  4);
	boxfill8(vram, x, COL8_FFFFFF, x - 47, y -  3, x -  4, y -  3);
	boxfill8(vram, x, COL8_FFFFFF, x -  3, y - 24, x -  3, y -  3);
	return;
}

구조체 포인터의 값은 ->표기로 쉽게 접근이 가능하다.



# 이제 문자를 출력해볼 시간!
먼저 글자 하나를 출력하는 방법에 대해 생각해본다. 필자의 경우 폰트 배열이

00000000
00011000
00011000
00011000
00011000
00100100
00100100
00100100
00100100
01111110
01000010
01000010
01000010
11100111
00000000
00000000

이러한 8bit * 16 = 16byte {0x00, 0x18, 0x18, ... , 0xe7, 0x00, 0x00} 의 정보를 담고 있다면 다음과 같은 함수로 한 문자를 출력할 수 있을것이라고 한다.

void putfont8(char *vram, int xsize, int x, int y, char c, char *font)
{
	int i;
	char *p, d /* data */;
	for (i = 0; i < 16; i++) {
		p = vram + (y + i) * xsize + x;
		d = font[i];
		if ((d & 0x80) != 0) { p[0] = c; }
		if ((d & 0x40) != 0) { p[1] = c; }
		if ((d & 0x20) != 0) { p[2] = c; }
		if ((d & 0x10) != 0) { p[3] = c; }
		if ((d & 0x08) != 0) { p[4] = c; }
		if ((d & 0x04) != 0) { p[5] = c; }
		if ((d & 0x02) != 0) { p[6] = c; }
		if ((d & 0x01) != 0) { p[7] = c; }
	}
	return;
}

putfont8(vram, 화면 가로 크기, x위치, y위치, 색상, 폰트 배열) 과 같이 넘겨 받아 0이 아닌 부분에 점을 찍는다.


여기에 필자가 만든 OSASK에 쓰이는 hankaku라는 커스텀 폰트를 사용하게 된다. 폰트 파일은 txt이고

char 0x41
........
...**...
...**...
...**...
...**...
..*..*..
..*..*..
..*..*..
..*..*..
.******.
.*....*.
.*....*.
.*....*.
***..***
........
........

이런 형식으로 256자가 들어있다.
이 파일을 obj파일로 빌드해서 C파일에서 위에서 말한 형식대로 폰트 배열을 넘겨받을 수 있게 한다.

extern char hankaku[4096];

과 같이 선언후 아스키코드 순으로 선언되어 있으므로

hankaku + 'A'

처럼 사용하면 'A'에 해당하는 폰트 배열을 사용할 수 있다.


하여 만들어진 문자열 출력 함수

void HariMain(void)
{
	struct BOOTINFO *binfo = (struct BOOTINFO *) 0x0ff0;

	init_palette();
	init_screen(binfo->vram, binfo->scrnx, binfo->scrny);
	putfonts8_asc(binfo->vram, binfo->scrnx,  8+2,  8+2, COL8_000000, "ABC 123");
	putfonts8_asc(binfo->vram, binfo->scrnx, 30+2, 30+2, COL8_000000, "Haribote OS.");
	putfonts8_asc(binfo->vram, binfo->scrnx,  8,  8, COL8_FFFFFF, "ABC 123");
	putfonts8_asc(binfo->vram, binfo->scrnx, 30, 30, COL8_FFFFFF, "Haribote OS.");

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

void putfonts8_asc(char *vram, int xsize, int x, int y, char c, unsigned char *s)
{
	extern char hankaku[4096];
	for (; *s != 0x00; s++) {
		putfont8(vram, xsize, x, y, c, hankaku + *s * 16);
		x += 8;
	}
	return;
}

예쁘게 출력된 문자열, 바람직하다.



흐름을 타서 변수 내용까지 출력하여 디버거 역할을 시켜본다.
필자의 경우 sprintf를 사용해서 문자열을 별도로 저장 -> putfont8_asc로 출력하는 방법을 이용했다. sprintf는 미리 준비되어 있는데, OS에 의존하지 않도록 특별히 설계되었다고 한다.

void HariMain(void)
{
	struct BOOTINFO *binfo = (struct BOOTINFO *) 0x0ff0;
	char s[40];

	init_palette();
	init_screen(binfo->vram, binfo->scrnx, binfo->scrny);
	putfonts8_asc(binfo->vram, binfo->scrnx,  8, 8, COL8_FFFFFF, "ABC 123");
	putfonts8_asc(binfo->vram, binfo->scrnx, 31, 31, COL8_000000, "Haribote OS.");
	putfonts8_asc(binfo->vram, binfo->scrnx, 30, 30, COL8_FFFFFF, "Haribote OS.");
	sprintf(s, "scrnx = %d", binfo->scrnx);
	putfonts8_asc(binfo->vram, binfo->scrnx, 16, 64, COL8_FFFFFF, s);

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

스크린샷 테마가 자주 바뀌는건 기숙사컴이랑 병행해서 쓰다보니...



# 폰트와 비슷한 방법으로 마우스 커서까지 그려보았다.
먼저 마우스 커서 모양의 데이터를 준비하고 init_mouse_cursor8에서 이를 색상의 배열로 만든다. 그리고 색상 배열을 직사각형 영역에 출력하도록 만들어진 putblock8_8로  출력!

void init_mouse_cursor8(char *mouse, char bc)
/* 마우스 커서를 준비(16 x16) */
{
	static char cursor[16][16] = {
		"**************..",
		"*OOOOOOOOOOO*...",
		"*OOOOOOOOOO*....",
		"*OOOOOOOOO*.....",
		"*OOOOOOOO*......",
		"*OOOOOOO*.......",
		"*OOOOOOO*.......",
		"*OOOOOOOO*......",
		"*OOOO**OOO*.....",
		"*OOO*..*OOO*....",
		"*OO*....*OOO*...",
		"*O*......*OOO*..",
		"**........*OOO*.",
		"*..........*OOO*",
		"............*OO*",
		".............***"
	};
	int x, y;

	for (y = 0; y < 16; y++) {
		for (x = 0; x < 16; x++) {
			if (cursor[y][x] == '*') {
				mouse[y * 16 + x] = COL8_000000;
			}
			if (cursor[y][x] == 'O') {
				mouse[y * 16 + x] = COL8_FFFFFF;
			}
			if (cursor[y][x] == '.') {
				mouse[y * 16 + x] = bc;
			}
		}
	}
	return;
}

void putblock8_8(char *vram, int vxsize, int pxsize,
	int pysize, int px0, int py0, char *buf, int bxsize)
{
	int x, y;
	for (y = 0; y < pysize; y++) {
		for (x = 0; x < pxsize; x++) {
			vram[(py0 + y) * vxsize + (px0 + x)] = buf[y * bxsize + x];
		}
	}
	return;
}

대빵 큰 마우스


init_mouse_cursor8의 bc는 Background Color로써, '.' 부분은 뒷배경을 출력해서 투명효과를 준다.

putblock8_8(vram, 화면 가로 크기, 출력 이미지 가로 크기, 세로 크기, x위치, y위치, 출력 배열, bxsize)로 사용하는데, bxsize는 출력된 그림의 1라인이 몇 화소로 되어있는지를 나타내는데, 대부분 이미지 사이즈과 동일하나 간혹 다르게 쓸 필요가 있어 굳이 집어넣었다고 한다.



# GDT IDT... 갑자기 이상한 개념이 튀어나왔다...

일단 이것들을 알아야 하는 이유가 최종적으로 마우스 커서를 움직이게 하고 싶어서이다...만 아직은 왜 필요한지 모르겠다.


먼저 세그먼테이션에 대해서이다. 세그멘테이션은 메모리를 분할한 뒤, 각 블록의 처음 번지를 프로그램이 0이라고 인식하여 사용할 수 있게 하는 기능이다. 그러면 프로그램 마다 ORG로 메모리를 지정하지 않아도 된다.

세그멘테이션이 세그먼트 1개를 나타내기 위해서는 8바이트 크기의 다음의 정보들이 필요하다.

  • 세그먼트의 크기
  • 세그먼트의 시작 번지
  • 세그먼트의 관리 속성(쓰기 금지, 실행 금지, 시스템 전용 등)

16bit 세그먼트 레지스터는 원칙적으로 65,535개의 세그먼트를 다룰수 있으나 모종의 이유로 인해 하위 3비트를 사용하지 못하므로 8,191개의 세그먼트만 관리가 가능하다. 그래서 8바이트 x 8,191개 = 65,535바이트가 세그먼트 정보의 저장을 위해 필요한데 이를 어디에 저장하느냐...

팔레트에 색을 저장하는 방법과 동일하다. 다만 메모리에 작업하므로 IN이나 OUT은 사용하지 않는다. 단순 기록인 것이다. 그리고 이 64KB의 공간을 GDT(Global (segment) Descriptor Table)이라고 부른다.

이렇게 저장한 세그멘트를 GDTR(GDT Register) 레지스터에 불러들여 사용한다.


그러면 IDT는 뭐시냐, 하면 'Interrupt Decriptor Table'로써 어떤 인터럽트가 들어오는 경우에 어떤 함수로 처리할 것인지를 정해 놓는 테이블이다.

CPU가 모든 장치(키보드, 마우스 등등...)의 상태를 감시하게 된다면 그만큼 작업의 효율이 떨어지게 된다. 그래서 무슨 일이 생기면 인터럽트가 작동하도록 하여서 그때 그때 일을 처리하도록 하는 것이다.


그래서 나온 코드가 이것,

struct SEGMENT_DESCRIPTOR {
	short limit_low, base_low;
	char base_mid, access_right;
	char limit_high, base_high;
};

struct GATE_DESCRIPTOR {
	short offset_low, selector;
	char dw_count, access_right;
	short offset_high;
};
void init_gdtidt(void)
{
	struct SEGMENT_DESCRIPTOR *gdt = (struct SEGMENT_DESCRIPTOR *) 0x00270000;
	struct GATE_DESCRIPTOR    *idt = (struct GATE_DESCRIPTOR    *) 0x0026f800;
	int i;

	/* GDT의 초기화 */
	for (i = 0; i < 8192; i++) {
		set_segmdesc(gdt + i, 0, 0, 0);
	}
	set_segmdesc(gdt + 1, 0xffffffff, 0x00000000, 0x4092);
	set_segmdesc(gdt + 2, 0x0007ffff, 0x00280000, 0x409a);
	load_gdtr(0xffff, 0x00270000);

	/* IDT의 초기화 */
	for (i = 0; i < 256; i++) {
		set_gatedesc(idt + i, 0, 0, 0);
	}
	load_idtr(0x7ff, 0x0026f800);

	return;
}

void set_segmdesc(struct SEGMENT_DESCRIPTOR *sd, unsigned int limit, int base, int ar)
{
	if (limit > 0xfffff) {
		ar |= 0x8000; /* G_bit = 1 */
		limit /= 0x1000;
	}
	sd->limit_low    = limit & 0xffff;
	sd->base_low     = base & 0xffff;
	sd->base_mid     = (base >> 16) & 0xff;
	sd->access_right = ar & 0xff;
	sd->limit_high   = ((limit >> 16) & 0x0f) | ((ar >> 8) & 0xf0);
	sd->base_high    = (base >> 24) & 0xff;
	return;
}

void set_gatedesc(struct GATE_DESCRIPTOR *gd, int offset, int selector, int ar)
{
	gd->offset_low   = offset & 0xffff;
	gd->selector     = selector;
	gd->dw_count     = (ar >> 8) & 0xff;
	gd->access_right = ar & 0xff;
	gd->offset_high  = (offset >> 16) & 0xffff;
	return;
}

가장 먼저 GDT와 IDT를 나타낼 8바이트 짜리 구조체를 정의하고 있다.

그다음 각 기술자에 테이블이 위치할 번지수를 대입한다. 번지수는 필자가 임의로 정한것.
GDT(0x270000~0x27FFFF) IDT(0x26F800~0x26FFFF)

그후 for문으로 각 구조체의 정보를 초기화 시켜준다.

그리고 특별히 따로 설정해둔 2개의 세그멘트와 idt가 있고 load명령으로(어셈블함수) gdtr, idtr에 등록한다.
set_segmdesc(세그먼트 설정 번지, 크기, 시작 번지, 속성) 으로 사용한다.
load명령어는 아직 뭐 어떻게 써먹는지 모르겠다.



마지막에 GDT랑 IDT가 나와서 멘붕했으나(...) 앞으로 차차 알아갈것이라 믿는다...
어셈블리어 안하니까 되게 편하다