본문 바로가기

OS 개발일지

[OS개발] 6일차 - 인터럽트 처리의 시작

6일차

충공깽의 5일차 이후 이어서 2연타를 날리는 6일차. 6일차에는 소스를 분할하여 관리가 쉽도록 코드를 다듬었고, 본격적으로 인터럽트 처리를 시작했다.


# 우선 코드의 분할이다. bootpack.c에 몰빵되어 있던 코드를 용도별로 나누어 분리하였다.

  • graphic.c : 그림 그리기 관련
  • dsctbl.c : GDT, IDT 등의 descriptor table 관련
  • bootpack.c : main과 기타 등등

그리고 각 코드에서 정의 하는 함수들의 프로토타입과 상수들을 bootpack.h로 묶어 이것만 정의하면 어느 파일에서든 사용할 수 있게 하였다.

/* asmhead.nas */
struct BOOTINFO { /* 0x0ff0-0x0fff */
	char cyls; /* ブートセクタはどこまでディスクを読んだのか */
	char leds; /* ブート時のキーボードのLEDの状態 */
	char vmode; /* ビデオモード  何ビットカラーか */
	char reserve;
	short scrnx, scrny; /* 画面解像度 */
	char *vram;
};
#define ADR_BOOTINFO	0x00000ff0

/* naskfunc.nas */
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 load_gdtr(int limit, int addr);
void load_idtr(int limit, int addr);

/* graphic.c */
void init_palette(void);
void set_palette(int start, int end, unsigned char *rgb);
void boxfill8(unsigned char *vram, int xsize, unsigned char c, int x0, int y0, int x1, int y1);
void init_screen8(char *vram, int x, int y);
void putfont8(char *vram, int xsize, int x, int y, char c, char *font);
void putfonts8_asc(char *vram, int xsize, int x, int y, char c, unsigned char *s);
void init_mouse_cursor8(char *mouse, char bc);
void putblock8_8(char *vram, int vxsize, int pxsize,
	int pysize, int px0, int py0, char *buf, int bxsize);
#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

/* dsctbl.c */
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);
void set_segmdesc(struct SEGMENT_DESCRIPTOR *sd, unsigned int limit, int base, int ar);
void set_gatedesc(struct GATE_DESCRIPTOR *gd, int offset, int selector, int ar);
#define ADR_IDT			0x0026f800
#define LIMIT_IDT		0x000007ff
#define ADR_GDT			0x00270000
#define LIMIT_GDT		0x0000ffff
#define ADR_BOTPAK		0x00280000
#define LIMIT_BOTPAK	0x0007ffff
#define AR_DATA32_RW	0x4092
#define AR_CODE32_ER	0x409a



# 우선 GDT와 IDT의 남은 부분을 마저 공부하였다.
이 두 개념은 사실상 같은 원리이므로 GDT하나만 설명이 되어있다. 일단 지난 시간에는 GDT의 메모리상에서 영역을 정하고 구조체에 데이터를 넣은 뒤 이 영역 상에 등록하는 부분까지 진행하였다.

지금부터는 구조체에 정보를 넣는 set_segmdesc를 좀 더 분석하고 이렇게 설정해둔 세그먼트 영역을 GDTR에 올리는 과정을 살펴본다.


먼저 구조체를 만드는 과정부터 이해해보자. descriptor 구조체는 아래의 코드에 보이는 것처럼 2개의 short와 4개의 char 변수의 모음으로 총 8바이트짜리 구조체이다. limit, base, access_right는 각각 세그먼트의 크기, 번지수, 관리속성을 나타낸다.

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

왜 굳이 이렇게 따로따로 만들어두었냐 하면 80286언제적이냐 시대의 프로그램과의 호환성을 위해 설계한 결과라고 한다.(덕분에 구조체만드는 과정이 귀찮다...)

책에서는 글로만 설명되어있어서 그림을 그려가며...ㅠ 찬찬히 살펴보았다. 참고로 <<, >>, &, | 는 비트 연산자로서 비트를 앞뒤로 밀어내거나 or연산을 하는 역할이다.
Ex) 0x73 | 0x0f을 하면 01110011 & 00001111 = 00000011과 같이 하위 4비트를 얻을 수 있다.

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

그림을 보면 대입 방법은 거의 이해될것이라 믿는다. 여기서 부가 설명을 하자면, limit_hight에 대해서이다. limit_low는 그대로 받았지만 limit_high는 limit에서 4비트 만큼만 떼오는 모습이다. 왜냐하면 limit를 4바이트로 써버리면 base가 4바이트이므로 구조체가 꽉차서 관리속성이 들어갈 자리가 없기 때문이다. 그래서 limit_low(16) + limit_high하위(4) = 20비트를 limit가 사용하고 access_right(8) + limit_high상위(4) = 12비트를 관리속성이 사용한다.


이렇게 하면 또 다른 문제가 생기는데, 세그먼트의 크기를 나타내는 limit가 20비트 밖에 안되니 약 1MB안습까지 밖에 크기를 지정할수 없다는 점이다. 이에 인텔에서 고안한 방법이 G(Granularity) bit이다. 관리속성의 상위 4비트를 GD00형태로 하여 G플래그가 1이라면 세그먼트의 페이지를 4KB로 설정하는 것이다.

페이지는 세그먼트내에서 쓰이는 단위인데, 페이지가 없을때는 바이트단위로 주소를 지정하지만 페이지를 설정함으로써 정밀도는 떨어지는 대신 1MB x 4KB = 약4GB까지 세그먼트 크기를 뻥튀기하는게 가능해진다. 코드에서 보면 if문에서 0xffff(1MB)를 넘어가게 되면 관리속성의 최상위 비트를 1로 설정하고 limit의 크기를 0x1000(4KB)로 나눈다.

D플래그는 0이면 16bit, 1이면 32bit 모드를 뜻한다. 보통은 1로 설정한다.

관리속성은 방금 말한 GD00와 확장엑세스 권한을 뜻하는 8비트를 구조체에 3바이트로 저장해두고 사용할 때는 GD00 0000 확장엑세스(8) 형태로 사용한다. 책에 소개된 확장엑세스 속성만 적어두자면,

00000000 (0x00) : 미사용의 디스크립터 테이블
10010010 (0x92) : 시스템 전용의 읽기 쓰기 기능한 세그먼트. 실행 안 됨.
10011010 (0x9A) : 시스템 전용의 실행 가능한 세그먼트. 읽기 가능, 쓰기 불가능.
11110010 (0xF2) : 애플리케이션용의 읽기 쓰기 가능한 세그먼트. 실행 안 됨.
11111010 (0xFA) : 애플리케이션용의 실행 가능한 세그먼트. 읽기 사능, 쓰기 불가능.


이렇게 GDT의 설정을 마치고, load_gdtr함수를 이용해서 이 영역의 크기와 주소를 넘겨 GDTR에 등록한다. load_gdtr함수를 살펴보자. 물론 어셈블리어다.

_load_gdtr:		; void load_gdtr(int limit, int addr);
		MOV		AX,[ESP+4]		; limit
		MOV		[ESP+6],AX
		LGDT	[ESP+6]
		RET

왜 이런 코드가 나오냐 하면은 GDTR은 6바이트 짜리 레지스터로서 하위 2바이트에는 limit가 들어가고 상위 4바이트에는 addr가 들어가야한다. 그런데 인자가 들어오는 모습을 보면 인자가 4바이트 단위로 들어오게된다.(어셈블리어 특성상 int단위로 밖에 전달이 안되나보다)

예로 0xFFFF과 0x00270000을 인자로 주면 0x0000FFFF과 0x00270000이 들어오므로 메모리 위에는 다음과 같이 등록이 된다.(리틀엔디언)

[ESP+4]->FF FF [ESP+6]->00 00 00 00 27 00

여기서 LGDT에 한번에 넣으려면 FF FF 00 27 00 00의 나열이 필요하므로 AX에 FF FF를 저장했다가 2칸 옆에 옮겨넣음으로써 다음과 같이 메모리를 조작한다.

FF FF FF FF 00 00 27 00

그리고 마지막으로 주소 [ESP+6]를 LGDT로 전달하면 끝!

LGDT <- FF FF 00 00 27 00



# IDT도 같은 방법으로 설정할 수 있으므로 이제 인터럽트가 들어오는 규칙을 등록을 해보...고 싶지만!훼이크다 먼저 PIC의 선학습이 필요하단다.

PIC(Programmable Interrupt Controller)는 CPU가 인터럽트를 받는 회로가 하나 뿐인 단점을 해결하기 달게된 시작한 보조장치이다. 기본적인 구조는 다음과 같다.

CPU에 직접 붙어있는 PIC가 '마스터 PIC'이고 그 아래 딸린 PIC를 '슬레이브 PIC'라고 부른다. 설계상 슬레이브는 반드시 마스터의 IRQ(InteRupt reQuest) 2번에 붙어있어야 한다. 다음의 코드는 PIC를 초기화하는 코드이다. int.c에 새로 작성하였다. 외부장치이므로 색상 팔레트의 설정 때 사용했었던 IN-OUT을 이용한다.

#define PIC0_ICW1		0x0020
#define PIC0_OCW2		0x0020
#define PIC0_IMR		0x0021
#define PIC0_ICW2		0x0021
#define PIC0_ICW3		0x0021
#define PIC0_ICW4		0x0021
#define PIC1_ICW1		0x00a0
#define PIC1_OCW2		0x00a0
#define PIC1_IMR		0x00a1
#define PIC1_ICW2		0x00a1
#define PIC1_ICW3		0x00a1
#define PIC1_ICW4		0x00a1

void init_pic(void)
/* PIC의 초기화 */
{
	io_out8(PIC0_IMR,  0xff  ); /* 모든 인터럽트를 받아들이지 않는다 */
	io_out8(PIC1_IMR,  0xff  ); /* 모든 인터럽트를 받아들이지 않는다 */

	io_out8(PIC0_ICW1, 0x11  ); /* edge trigger 모드 */
	io_out8(PIC0_ICW2, 0x20  ); /* IRQ0-7은, INT20-27으로 받는다 */
	io_out8(PIC0_ICW3, 1 << 2); /* PIC1는 IRQ2에서 접속 */
	io_out8(PIC0_ICW4, 0x01  ); /* non buffer모드 */

	io_out8(PIC1_ICW1, 0x11  ); /* edge trigger 모드 */
	io_out8(PIC1_ICW2, 0x28  ); /* IRQ8-15는, INT28-2 f로 받는다 */
	io_out8(PIC1_ICW3, 2     ); /* PIC1는 IRQ2에서 접속 */
	io_out8(PIC1_ICW4, 0x01  ); /* non buffer모드 */

	io_out8(PIC0_IMR,  0xfb  ); /* 11111011 PIC1 이외는 모두 금지 */
	io_out8(PIC1_IMR,  0xff  ); /* 11111111 모든 인터럽트를 받아들이지 않는다 */

	return;
}

각 포트에 해당하는 상수값은 bootpack.h에 적어놓은 것인데 가만 보면 값이 동일한 녀석들이 다수 보인다...분신술 PIC의 설정은 순서가 정해져있기에 이런 구조가 가능하다고 한다. 각 포트를 전부 설명하고 싶지만 여백이 부족하여 중요한 애들만 보고 나머지는 책을 참조해보자.(나중에 여유가 되면 추가하겠다)

PIC0는 마스터, PIC1은 슬레이브를 의미한다. 우리가 마음대로 설정할 수 있는 부분은 ICW2인데, 여기서 각 회로(0~15)에서 오는 인터럽트를 어느 INT명령어로 처리 할 것인지를 정한다. 예제에서는 PIC0_ICW2에 0x20~0x27을 설정하였고 PIC1_ICW2에 0x28~0x2F를 설정해서 총 0x20~0x2F 16개의 인터럽트를 설정하였다. 만약에 IRQ9에 연결된 장치가 인터럽트를 발생시키면 CPU는 INT 0x29를 실행하는 식이다.

사족으로, 0x00~0x1F는 CPU의 시스템을 통제하기 위해 미리 정해놓은 번호라 사용할 수 없다.



# 이제부터는 정말로 인터럽트를 처리해보자.
먼저 키보드의 인터럽트 처리이다. PS/2 키보드는 IRQ1로 신호를 보내므로 0x21에 해당하는 함수를 작성한다.

void inthandler21(int *esp)
/* PS/2 키보드로부터의 인터럽트 */
{
	struct BOOTINFO *binfo = (struct BOOTINFO *) ADR_BOOTINFO;
	boxfill8(binfo->vram, binfo->scrnx, COL8_000000, 0, 0, 32 * 8 - 1, 15);
	putfonts8_asc(binfo->vram, binfo->scrnx, 0, 0, COL8_FFFFFF, "INT 21 (IRQ-1) : PS/2 keyboard");
	for (;;) {
		io_hlt();
	}
}

메세지를 표시하는 간단한 함수이다.


얘를 바로 등록하면 좋겠지만서도 어셈블리어로 해야될 부분이 있으므로 다음과 같은 함수를 어셈블리어로 작성한다.

_asm_inthandler21:
		PUSH	ES
		PUSH	DS
		PUSHAD
		MOV		EAX,ESP
		PUSH	EAX
		MOV		AX,SS
		MOV		DS,AX
		MOV		ES,AX
		CALL	_inthandler21
		POP		EAX
		POPAD
		POP		DS
		POP		ES
		IRETD

코드를 보면 먼저 ES와 DS를 스택에 넣고, PUSHAD란 명령이 보이는데 사실상 백업기능이다. EAX, ECX, EDX, EBX, ESP, EBP, ESI 를 몽땅 PUSH 시킨다. 여기에다 ESP값까지 간접 PUSH.

이후에 SS값을 복사해서 SS, DS, ES가 모두 같은 값을 가지도록 만든다. 함수 실행 조건이란다.

그리고 위에서 만들었던 함수를 CALL 명령으로 호출한다.

그 다음엔 PUSH로 저장해놓았던 레지스터들을 복원시킨뒤에, (왜 ESP는 되돌려주지 않는걸까?)

마지막으로 인터럽트 처리 종료시 호출해야하는 IRETD를 실행한다.


이렇게 완성된 함수를 IDT에 등록한다.예헤이

/* IDT의 초기화 */
void init_gdtidt(void)
{
	struct SEGMENT_DESCRIPTOR *gdt = (struct SEGMENT_DESCRIPTOR *) ADR_GDT;
	struct GATE_DESCRIPTOR    *idt = (struct GATE_DESCRIPTOR    *) ADR_IDT;
	int i;

	/* GDT의 초기화 */
	for (i = 0; i <= LIMIT_GDT / 8; i++) {
		set_segmdesc(gdt + i, 0, 0, 0);
	}
	set_segmdesc(gdt + 1, 0xffffffff,   0x00000000, AR_DATA32_RW);
	set_segmdesc(gdt + 2, LIMIT_BOTPAK, ADR_BOTPAK, AR_CODE32_ER);
	load_gdtr(LIMIT_GDT, ADR_GDT);

	/* IDT의 초기화 */
	for (i = 0; i <= LIMIT_IDT / 8; i++) {
		set_gatedesc(idt + i, 0, 0, 0);
	}
	load_idtr(LIMIT_IDT, ADR_IDT);

	/* IDT의 설정 */
	set_gatedesc(idt + 0x21, (int) asm_inthandler21, 2 * 8, AR_INTGATE32);

	return;
}

인자 중 2*8은 세그먼트 번호 2번(system.hrb)에 이 함수가 존재하며 8배는 << 3 의 의미로 하위 3비트에 다른 의미가 있으므로 비워놓는 것이라고 한다.

AR_CODE32_ER은 인터럽트 처리용의 유효한 설정임을 나타내는 속성이다.


덤으로 이제 인터럽트 처리를 받을 수 있으니 Main함수에 STI를 추가해 언터럽트를 받을 수 있게 하였고 PIC0에 PIC1과 키보드(IRQ1)의 신호를 받도록 설정했다.

void HariMain(void)
{
	struct BOOTINFO *binfo = (struct BOOTINFO *) ADR_BOOTINFO;
	char s[40], mcursor[256];
	int mx, my;

	init_gdtidt();
	init_pic();
	io_sti(); /* IDT/PIC의 초기화가 끝났으므로 CPU의 인터럽트 금지를 해제 */

	init_palette();
	init_screen8(binfo->vram, binfo->scrnx, binfo->scrny);
	mx = (binfo->scrnx - 16) / 2; /* 화면 중앙이 되도록 좌표 계산 */
	my = (binfo->scrny - 28 - 16) / 2;
	init_mouse_cursor8(mcursor, COL8_008484);
	putblock8_8(binfo->vram, binfo->scrnx, 16, 16, mx, my, mcursor, 16);
	sprintf(s, "(%d, %d)", mx, my);
	putfonts8_asc(binfo->vram, binfo->scrnx, 0, 0, COL8_FFFFFF, s);

	io_out8(PIC0_IMR, 0xf9); /* PIC1와 키보드를 허가(11111001) */

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



GDI IDT가 이제 모두 이해되어서 머리가 개운해졌다! 키보드와 마우스를 곧 다룰 수 있다니 이 얼마나 즐거운 일인가! 과제랑 시험이 좀 적으면 좋을텐데...틀렸어 공대생은 꿈도 희망도 없어


참조: <OS 구조와 원리> - 카와이 히데미