개인적인 일과 OS 만드는 과정에서 너무 지식이 부족함을 느끼고.. 한동안 공부만 했다.

이해 안되는 개념은 이해 될때까지 예제와 문서 읽으면서 공부를 하긴 했는데 역시나 OS 만드는 것에 대한 지식이 너무 방대함을 느끼고…좌절(?).

오래된 노트북 책장속에 꼽아 놓고, 가끔 켤 때 마다 보이는 바탕화면에 OS 만들기 폴더를 볼 때 마다 내 자신이 한심해지는 것 같아 더더욱 의도적으로 외면하게 되었다.

그러다가 죽은 노트북 살리면서 생각지도 않는 의욕이 불타 올라 , 다시 시작.


우선 페이징을 설명하기 전에 가상 메모리에 대해서 설명이 필요하다.

가상 메모리 단어에서 어느 정도 무엇을 내포된 의미를 알 수 있듯이 메모리를 가상화 한다는 거다.

?????? 메모리??? 내컴퓨터에 달려있는 램???

그렇다. 이 램을 가상화 한다는 거다. 가상화 라는 단어가 어려울진 모르겠는데 쉽게 내컴퓨터에 2G짜리 램 하나 달려 있는데 OS에서 4G 달려 있는 것처럼 속인다(?)라는 거다. 사기???? 맞다.. 사기치는 거다. 짜임새 있고 논리적으로…. 프로세스(프로그램)며 OS까지 전부 다 개별적으로 4G를 쓸 수 있게 해주는 황금 알을 낳게 해주는 기술이다. (배를 갈라보자…)

그럼 왜 사기 치는 걸까???
(당연하지 않나? 돈이 없어서.. ㅋㅋㅋㅋㅋ)

점점 소프트웨어들의(프로그램,OS) 기능이 커짐에 따라 메모리도 많이 소모하게 되었는데 이게 물리적인 메모리 램은 한정되어 있는 상황에서 서로 메모리 가져오기 전쟁을 벌어야 했기 때문이다.
MS-DOS 시절 한글 띄워 보겠다고 한글 램 상주 프로그램 실행 후 게임을 실행할 때를 생각해 보자… 메모리 부족으로 실행되지 않는다. 그러면 램 상주 프로그램 종료 시키고… 이것저것 최적화 해서 게임 실행 했었던 시절을 떠 올려 보니 이해가 쉬워 진다. 프로그램들은 메모리를 쓰고 싶은데 딴놈이 그놈의 메모리를 차지 하고 있으니 사용자가 간섭해서 종료 시키고 최적화 시켰던 그 시절…(추억이 새록새록하다.)

유한한 메모리 공간이 가상 메모리 기술을 사용한다고 해서 갑자기 없던 메모리가 3차원 우주공간에 생성되어 그 공간을 사용하는 그런 공상 과학적인 그런건 아니다. 앞에서 말했던 것처럼 속이는 거다. 어떻게????

우선 가상메모리는 개별 프로그램 및 OS에게 너에게 주어진 메모리 공간은 4G이다.(난 32bit OS를 목표로 하기 때문에 4G로 표기한다.) 라고 넉넉한(?) 메모리 공간을 제시한다. 그러면 좋아라 하겠지…. 마구마구 메모리 할당하고 쓸것이다. 지지고 볶고 . 어라 저놈도 메모리 크게 잡네?? 나도 잡자 … 마구 마구 할당 하면 그래 버블이 발생한다. 버블이 발생하면? 죽는거지 뭐… (갑자기 옆길로……샜다.)

가상 메모리는 개별 프로그램들에게 메모리 공간 4G와 독립적 메모리 공간을 약속한다. 4G공간은 물론 가상 공간인 것일 테고 독립적 메모리 공간이라 함은 각 프로그램들의 메모리 공간은 서로 접근할 수도 없고 독립적으로 메모리 0번지 ~0xFFFFFFFF 까지 나만의 공간을 만들어 준다는 것이다.
예를 들어 A프로그램이 메모리 0x100000에 어떤 데이터를 써 놨다고 하자. 그게 온라인 커뮤니티 아이디 , 패스워드라고 한다면 B라는 프로그램은 A프로그램이 0x100000 메모리 아이디, 패스워드를 저장해 놓은 걸 알고 있는 상황에서 메모리 0x100000 를 읽어 보면???? 아무것도 없다.  이는 각 프로그램마다 제공된 메모리 공간은 독립적으로 타 프로그램과 공유하지 되지 않는다는 의미인 것이다.

(독립된 가상 메모리는 실제 컴퓨터 메모리상의 주소와 일치 하지 않는다. A프로그램 0x100000 가상 메모리 주소는 실제 메모리 0x100000에 적재되지 않는다. 가상 메모리상의 주소는 실제 메모리 주소가 결정되지 않는 논리적 가상의 주소일 뿐이다. 이렇게 되지 않는다면 A프로그램과 B프로그램이 0x100000을 사용하게 된다면 메모리가 독립적 사용이 불가능 하게 된다. 이는 페이징에서 좀더 설명한다.)

그리고 가상 메모리를 프로그램 별 4G로 준다는 의미는 무엇인가? 분명히 앞서 설명한 바에 의하면 각 프로그램은 실제 램 2G가 설치된 컴퓨터에도 사기친(?) 4G를 제공한다고 했다. 그렇다면 위에서 예를 들었던 A,B 프로그램 두개가 독립적인 메모리 공간으로 4G를 가지고 있으므로 4G X 2 = 8G 라는 공간이 컴퓨터에 발생이 필요 할 텐데 가상 메모리는 물리적 메모리 2G로 어떻게 8G라는 공간을 만들어 놓을까?? (사기는 그럴듯 해야 사기빨이 받는 법이다…)

이런저런 아키텍처를 연구하는 사람들이 가만히 프로그램들을 살펴보니 4G가 공간을 주더라도 0x00000000 ~ 0xFFFFFFFF 메모리 공간에 차곡차곡 메모리를 쌓아 쓰지 않는 점을 밝혀 냈단다. 또한 프로그램 이라는게 4G메모리를 전체 할당해서 사용한다고 해도 메모리를 사용(CPU가 사용 시점)하는 시점에는 전체가 아닌 부분만 사용한다는 패턴을 찾아 냈다.  그러니까 메모리를 4G 전체 할당 한 상태에서 실제 CPU가 사용하는 메모리는 극히 작은 부분만 필요하다는 것이다.

   
image

즉, A,B 프로그램에서 8G를 사용할 경우, 실제 CPU가 사용하는 부분만 메모리에 올리고 나머지 부분은 우리가 흔히 OS사용하면서 보았던 페이징 파일로 디스크에 저장해 놓는다. 즉 실제 컴퓨터 메모리 2G는 A프로그램과 B프로그램 전체의 메모리를 저장하지 않고 페이징 파일로 저장해 놨다가 필요 할 때 실제 컴퓨터 메모리로 필요한 만큼 올리는 것이다. (필요할 때 필요한 만큼 꺼내 쓴다….역시나 실제 메모리에 올려 놓은 데이터가 더이상 쓰지 않거나 당분간 쓰지 않을 것 같을 때는 다시 파일로 보낸다.) 이렇게 실제 메모리와 디스크에 메모리 데이터를 서로 교환 작업함으로써 각 프로세스가 독립된 4G 공간의 메모리를 확보 할수 있게 되는 것이다.


페이징이란 앞서 설명한 가상 메모리를 구현 할 수 있게 만드는 메모리를 관리 기법을 말한다. 위 가상 메모리 설명에서 CPU가 필요로 할 때 페이징 파일에서 실제 메모리로 필요한 만큼 꺼내 쓴다라고 설명했다. 이때 실제 메모리로 페이징 파일에서 데이터를 옮기는 데이터량의 최소 단위를 페이지 말한다. 다시 말하면 메모리의 크기를 페이지 단위로 나눠서 데이터를 이동시킨다 라고 할 수 있다. 여기서 메모리를 페이지 단위로 나눠서 관리하는 기법을 페이징 이라고 말한다고 한다. (메모리를 페이지 단위 쪼개는 작업이라고 볼 수 있다.) 페이지는 2 제곱 값으로 지정 될 수 있으며 보통 4096byte(4kb) 으로 설정된다. (리눅스나 윈도우도 4KB)
메모리를 페이지 단위로 나누는 작업 자체가 페이징이라고 했지만 좀더 심화된 작업이 있다. 위에 가상 메모리 4G 를 구현하기 위해서는 페이징 이라는 작업을 해줘야 내 OS에 4G 가상 메모리 공간을 만들 수 있다. 조금 많이 난해하고 어렵다. 가상 메모리라는 용어하고 페이징이 결합하면서 실제 주소, 가상 주소 단어가 등장하게 된다. 이 단어가 글에서 왕복된 설명이 나오는 순간.. 머리가 글을 읽는 눈을 쫒아 가지 못하고 … 퍼진다. (그래도 진행해 보자….)

각 프로세스별 4G 가상 메모리 공간을 만들기 위해선 가상 메모리 4G 공간을 페이지 단위로 나눈다. 이 나눠진 페이지들을 표로 만들고 각 페이지 마다 번호를 붙여 보면 0부터 1048574 까지 번호가 부여 될 것이다. (4294967295(4G) ÷ 4096(페이지) = 1048575가 나온다. ) 1048575개의 페이지는 1MB 공간을 뜻하는 것이다. 즉 가상 메모리 4G를 구현하기 위해서는 실제 메모리 1MB의 할당이 필요해지며 여기에 가상 메모리 4G를 실제 메모리에 연결하기 위해서 각 페이지 마다 연결 참조 포인트 4Byte를 추가하면 4M 바이트의 공간이 각 프로세스 마다 고정적으로 생긴다는 결혼이다.

image
image

프로세스 마다 4MB 공간을 필요로 하다는 말은 프로세스 마다 고정적인 4MB의 메모리를 사용한다는 말인데 이건 정말 큰 메모리이다. 위 계산은 한 프로그램이 4G를 다 사용 할 때의 계산으로 작은 메모리(100kb 미만 메모리 할당)만 사용하는 프로그램도 4M의 고정적인 메모리를 할당 하고 실행된다.
이에 작은 메모리를 사용하는 프로그램과 많은 메모리를 사용하는 프로그램들의 독립적 메모리 공간과 4G의 가상 메모리를 제공하고 각각의 사용 메모리별 형평성(?), 계산 속도 최적화를 밤낮 연구하다가 방법을 만들어 냈다. (밤낮 연구했는지 그냥 알고 있는 내용을 구현했는지 난 모른다. 똥 싸다가 생각해 냈는지도 모른다. 또는 그냥 어렵게 만들려고 했을지도 모르겠고.. 그냥 이런 방식이 있다는걸 제안했다..)

우선 4바이트를 가진 1024개의 배열을 만든다. 총 크기는 4kb 이다. 이걸 프로그램 실행 때마다 할당 한다. 4M에서 4kb로 1024배로 줄었다. 오~~~~~~~~~~~. 다이어트를 제대로 해 버렸다. 이건 다이어트 수준이 아니라 병수준이다… 흠… 여튼… 4M를 4kb로 줄이는 대신 복잡해 졌다. 뭐든 복잡한게 간단해지면 그 반대 급부로 무언가가 발생한다…(절대량은 변하지 않는다??? ㅋ)

자 , 이 4kb가 어떻게 4G를 공간을 표현하는지 살펴 보자.

우선 4byte 크기로 1024 개의 배열을 만든다. 이 배열의 각 인덱스(요소) 들은 또다른 4byte크기의 배열의 시작 주소를 가르킨다. 어렵지~~~ ㅋㅋㅋㅋㅋ… 1024개의 배열이 있는데 배열의 각 인덱스는 4byte크기의 1024 배열을 가지고 있다 라고 표현해도 어려울 것이다. 이건 그림으로…

image
1024개의 배열의 각 인덱스는 1024개의 배열의 첫 시작 주소를 가지고 있다. 1024의 배열의 크기가 4096 이고 각 배열 인덱스가 4byte 1024 배열을 가지고 있으므로 4096 + (1024 x 4096) 은??? 약 4Mb이다. 기존 4MB 고정 보다 4G 전체 할당 했을 시 보다 4kb 더 많긴 하지만 이렇게 하면 1Mb의 작은 메모리를 할당하는 프로그램들은 4kb + 4kb 즉 8kb만 프로그램 할당 되므로 뭐… 나름 괜찮다고 볼수 있다. 
왜 8kb 냐고??? 음… 프로그램 실행때 기본 생성되는 4kb와 첫번째 배열 인덱스 하나는 총 4Mb까지의 메모리를 가르킨다. 주소를 가르키는 4byte가 1024개 있으므로 4MB이다.
즉 첫번째 배열의 각각의 인덱스는 메모리 주소 4MB 제곱으로 메모리 주소를 가르킨다라고 할 수 있다.


이제 4G 주소 공간 표현을 개념적으로 알아봤으니 실제로는 어떻게 계산되는지를 알아보자.

주소를 나타내는 포인터는 4바이트이다. (당연하지 않겠나… 4G까지 가르킬려면 4바이트가 필요하다) 이 주소를 32비트로 표현해서 비트 23번째 비트부터 32번째 비트를 첫번째 배열 인덱스값. 13번째 부터 22번째 비트까지를 첫번째 배열이 가르키는 배열의 인덱스값으로 구분한다. 그리고 나머지 1번째 부터 12번째 비트는 오프셋 값으로 구분하면 된다.

image
이해를 돕기 위해 그림을 그렸다. 위 그림은 가상 주소 0x1234가 어떻게 실제 메모리를 가르키게 되는지를 표현한 그림이다.
1. 23bit~32bit가 0 이므로 첫번째 배열(주황색) 인덱스 0을 가르킨다.
2. 첫번째 인덱스 0은 두번째 배열(연두색)의 주소를 가르키고 있다.
3. 13bit~22bit는 1 번째 인덱스를 가르킨다.
4. 그러면 첫번째 배열 인덱스0이 가르키는 두번째 배열 1번째 인덱스를 가르킨다고 볼수 있다.
5. 두번째 배열 1번째 인덱스는 실제 메모리 주소를 가르키고 있다. 어느 주소인지는 모른다. 할당 할 당시에 주소이기 때문에 임의의 주소일 것이다. 이 임의의 주소에서 564번째를 가르킨다.
실제 메모리도 4096 단위로 나눠져 있는 주소 단위이다. 두번째 배열 1이 가르키고 있는 주소는 4096 단위로 정렬된 주소이므로 이 주소에서 564 오프셋 만큼 떨어진 지점을 가르키고 있다.


이 가상 주소 변환 작업은 고맙게도 CPU가 해준다고 한다. 아니 CPU안에 있는 MMU 라는 놈이 해준단다. 그리고 위 첫번째 배열 이니 두번째 배열이니 하는 배열도 각자 용어가 따로 존재한다. 
난 이해를 돋기 위해 배열 이라고 사용했다. (뭐…..프로그램상으로 배열로 처리…) 이는 배열이 아닌 테이블이라고 불린다. 또한 첫번째 배열이라고 칭했던건 Page Directory Entry(PDE)라고 하며 첫번째 배열 PDE가 가르키는 배열(테이블)은 Page Table Entry(PTE) 라고 불리고 있다.
각 프로그램마다 첫번째 배열. 즉 PDE 4kb가 할당되어져 있다. 이를 CPU가 어느 프로그램을 선택해서 실행이라는 나쁜(?)짓을 할려고 할 때 이 PDE를 CPU에 등록한다. 즉 CPU가 작업 하는 프로그램을 변경하는 시점(컨텍스트 스위칭 이라고 하는데…..후에….)에 PDE 테이블 주소만 설정해 주면 알아서 다 해 준다.




이로써 개념 설명은 끝났다.

지금까지 바람 OS는 보호모드로 진입후 키보드 처리 하는 단계까지 구현되어 있는데 이제 가상 메모리 처리를 함으로써 명실상부한 32비트 OS로 거듭날 예정이다. 물론 구현에서 제대로 된 동작까지는 ……휴…갈길이 멀다… 어떤일이 펼쳐 질지는… 또 한 2년 잠수할지도….





'프로그래밍 > OS 만들기' 카테고리의 다른 글

OS 만들기 #19 키보드 처리 및 쉬어가기.  (0) 2015.08.16
OS 만들기 #18 IDT - 3  (0) 2015.08.12
OS 만들기 #17 IDT - 2  (0) 2015.08.10
OS 만들기 #16 IDT - 1  (0) 2015.07.30
OS 만들기 #15 커널-키보드 입력  (0) 2015.02.09

IDT까지 어찌어찌 해서 처리는 해 놓은 것 같다. 그런데 갈수록 함수가 늘고 이것저것 변수들도 늘고 이러니 메인 파일이 지저분 하다. “뭐… 언제 시간 나면 한번 정리 해야지..~

그런데… 다음에 할 내용이 조금 무겁고 어렵다.. 페이징. 많이 어렵다. 이걸 구현 하느리 기존 소스 정리나 하는 게 나을 것 같다. 쉴겸…..~~

우선.. StartKernel() 함수를 좀더 간단하게 정리했다.

int  StartKernel(int argc, char *argv[])
{
	char *hello = "Hello OS!";
	char *complete = "complete.";
	BYTE* vidmem = (BYTE*)0xB8000;
	BYTE data = 0;
	int b = 5;
	int c = 0;


	kprint(hello);

	InitPIC();
	Init_IDT();
	Init_kbd();
	
	while(1) ;
	return 0;
}

InitPIC() PIC 초기화 함수

Init_IDT() IDT 초기화 함수

Init_kbd() 키보드 관련 초기화 함수.

많이 간단해졌다. 

printf 함수도 다시 구현하였다… 커널에서 프린트 한다는 의미로 kprint (많은 OS 들도 kprint 라고 하더라 ^^ ㅋㅋㅋ. 그래서 이름을 차용했다.)로 함수명을 바구고.. 문자열만이 아닌 가변 인자를 받아 출력 할 수 있도록 수정하였다.

문자열 관련 함수들 . strlen, strcpy, strcmp, memcpy, memcmp, memset 이런 함수들은 그냥 기존 코드들을 가져왔다. 이런걸로 시간 뺏기기 싫어서..(라고 쓰고… 만들 줄 모르고… 다시 작성한다는게 귀찮아서~^^)..^^

그리고 IDT 최종 키보드 입력 부분까지 구현했었는데..

문제는 대문자와 특수문자( !~@#등) 출력이 되지 않았다… 그래서 키보드 테이블을 대문자와 특수문자를 출력 할 수 있도록 SHIFT키와 CAPS LOCK키를 지원하도록 수정했다.

#include "stdio.h"
#include "keyboard.h"
#include "8042.h"
#include "asm.h"

BOOL IsLShift = FALSE;
BOOL IsRShift = FALSE;
BOOL IsCapslock = FALSE;
BOOL IsAlt = FALSE;
BOOL IsCtrl = FALSE;
BOOL IsNumLock = FALSE;


KeyboardInfo kbd_scancode[] = 
{
	{0x00,	0x00, 0x00},	// ESC
	{'1','!',	0x00, 0x00},
	{'2','@',	0x00, 0x00},
	{'3','#',  	0x00, 0x00},
	{'4','$', 	0x00, 0x00},
	{'5','%', 	0x00, 0x00},
	{'6','^',	0x00, 0x00},
	{'7','&',	0x00, 0x00},
	{'8','*',	0x00, 0x00},
	{'9','(',	0x00, 0x00},
	{'0',')',	0x00, 0x00},
	{'-','_',	0x00, 0x00},
	{'=','+',	0x00, 0x00},
	{0x00,0x00, 	0x00, 0x00},	// BACKSPACE
	{0x00,0x00, 	0x00, 0x00},	// TAB
	{'q','Q', 	0x00, 0x00},
	{'w','W', 	0x00, 0x00},
	{'e','E', 	0x00, 0x00},
	{'r','R', 	0x00, 0x00},
	{'t','T', 	0x00, 0x00},
	{'y','Y', 	0x00, 0x00},
	{'u','U', 	0x00, 0x00},
	{'i','I', 	0x00, 0x00},
	{'o','O', 	0x00, 0x00},
	{'p','P', 	0x00, 0x00},
	{'[','{', 	0x00, 0x00},
	{']','}', 	0x00, 0x00},
	{0x00,0x00, 	0x00, 0x00},	// ENTER
	{0x00,0x00, 	0x00, 0x00},	// CTRL
	{'a','A', 	0x00, 0x00},
	{'s','S', 	0x00, 0x00},
	{'d','D', 	0x00, 0x00},
	{'f','F', 	0x00, 0x00},
	{'g','G', 	0x00, 0x00},
	{'h','H', 	0x00, 0x00},
	{'j','J', 	0x00, 0x00},
	{'k','K', 	0x00, 0x00},
	{'l','L', 	0x00, 0x00},
	{';',':', 	0x00, 0x00},
	{'\'','"', 	0x00, 0x00},
	{'`','~', 	0x00, 0x00},
	{0x00,0x00, 	0x00, 0x00},	// Left Shift
	{'\\','|', 	0x00, 0x00},
	{'z','Z', 	0x00, 0x00},
	{'x','X', 	0x00, 0x00},
	{'c','C', 	0x00, 0x00},
	{'v','V', 	0x00, 0x00},
	{'b','B', 	0x00, 0x00},
	{'n','N', 	0x00, 0x00},
	{'m','M', 	0x00, 0x00},
	{',','<', 	0x00, 0x00},
	{'.','>', 	0x00, 0x00},
	{'/','?', 	0x00, 0x00},
	{0x00,0x00,	0x00, 0x00},	// right shift
	{0x00,0x00, 	0x00, 0x00},	// PrtSc
	{0x00,0x00, 	0x00, 0x00},	// ALT
	{' ',' ', 	0x00, 0x00},	// space
	{0x00,0x00, 	0x00, 0x00},	// caps
	{0x00,0x00, 	0x00, 0x00},	// F1
	{0x00,0x00, 	0x00, 0x00},	// F2
	{0x00,0x00, 	0x00, 0x00},	// F3
	{0x00,0x00, 	0x00, 0x00},	// F4
	{0x00,0x00, 	0x00, 0x00},	// F5
	{0x00,0x00, 	0x00, 0x00},	// F6
	{0x00,0x00, 	0x00, 0x00},	// F7
	{0x00,0x00, 	0x00, 0x00},	// F8
	{0x00,0x00, 	0x00, 0x00},	// F9
	{0x00,0x00, 	0x00, 0x00},	// F10
	{0x00,0x00, 	0x00, 0x00},	// number lock
	{0x00,0x00, 	0x00, 0x00},	// scroll
	{0x00,0x00, 	0x00, 0x00},	// home { numlook = 7 }
	{0x00,0x00, 	0x00, 0x00},	// up	{ numlook = 8 }
	{0x00,0x00, 	0x00, 0x00},	// pgup	{ numlook = 9 }
	{'-','-', 	0x00, 0x00},	// -
	{0x00,0x00, 	0x00, 0x00},	// left	{ numlook = 4 }
	{0x00,0x00, 	0x00, 0x00},	// center	{ numlook = 5 }
	{0x00,0x00, 	0x00, 0x00},	// right	{ numlook = 6 }
	{'+','*', 	0x00, 0x00},	
	{0x00,0x00, 	0x00, 0x00},	// end	{ numlook = 1 }
	{0x00,0x00, 	0x00, 0x00},	// down	{ numlook = 2 }
	{0x00,0x00, 	0x00, 0x00},	// pgdn	{ numlook = 3 }
	{0x00,0x00, 	0x00, 0x00},	// ins	{ numlook = 0 }
	{0x00,0x00, 	0x00, 0x00},	// del	{ numlook = . }
	{'/','/', 	0x00, 0x00},	// /
	{0x00,0x00, 	0x00, 0x00},	// enter
	{0x00,0x00, 	0x57, 0xD7},	// F11
	{0x00,0x00, 	0x58, 0xD8},	// F12
	{0x00,0x00, 	0x52, 0xD2},	// ins
	{0x00,0x00, 	0x53, 0xD3},	// del
	{0x00,0x00, 	0x47, 0xC7},	// home
	{0x00,0x00, 	0x4F, 0xCF},	// end
	{0x00,0x00, 	0x49, 0xC9},	// pgup
	{0x00,0x00, 	0x51, 0xD1},	// pgdn
	{0x00,0x00, 	0x4B, 0xCB},	// left
	{0x00,0x00, 	0x4D, 0xCD},	// right
	{0x00,0x00, 	0x48, 0xC8},	// up
	{0x00,0x00, 	0x50, 0xD0},	// down
	{0x00,0x00, 	0x38, 0xB8},	// right alt
	{0x00,0x00, 	0x1D, 0x9D},	// right ctrl
	{0x11,0x00, 	0x00, 0x00}};	// pause	// 99 index

void Init_kbd()
{
	int i = 0;

	for(i = 0; i < 0x54; i++ )
	{
		kbd_scancode[i].Press = i+1;
		kbd_scancode[i].Release = 0x81 + i;
	}

	Write_port_byte(0x21, 0xFD);
}

int GetScanCodeInfo(BYTE code, KeyboardInfo *pkbdInfo)
{
	int i;

	if( code > 0xD9 )	// Unknown code
		return UNKNOWN_SCANCODE;

	for( i = 0; i < 0x64; i++ )
	{
		if( kbd_scancode[i].Press == code )
		{
			*pkbdInfo = kbd_scancode[i];
			return PRESS_SCANCODE;
		}

		if(kbd_scancode[i].Release == code)
		{
			*pkbdInfo = kbd_scancode[i];
			return RELEASE_SCANCODE;
		}
	}

	return UNKNOWN_SCANCODE;
}

void Kbd_ScanCode()
{
	BYTE scancode;
	int scancode_type;
	KeyboardInfo	Info;

	scancode = GetKeyboardScanCode();

	scancode_type = GetScanCodeInfo(scancode, &Info);
		
	if(scancode_type == UNKNOWN_SCANCODE )
	{
		return;
	}

	if( scancode_type == RELEASE_SCANCODE )
	{
		switch(Info.Press)
		{
		case KEY_LSHIFT:
			IsLShift = FALSE;
		case KEY_RSHIFT:
			IsRShift = FALSE;
			break;
		case KEY_ALT:
			IsAlt = FALSE;
			break;
		case KEY_CTRL:
			IsCtrl = FALSE;
			break;
		}
		return;
	}

	if( scancode_type == PRESS_SCANCODE )
	{
		switch(Info.Press)
		{
		case KEY_LSHIFT:
			IsLShift = TRUE;
			break;
		case KEY_RSHIFT:
			IsRShift = TRUE;
			break;
		case KEY_ALT:
			IsAlt = TRUE;
			break;
		case KEY_CTRL:
			IsCtrl = TRUE;
			break;
		case KEY_CAPSLOCK:
			IsCapslock = !IsCapslock;
			break;
		case KEY_NUMBERLOCK:
			IsNumLock = !IsNumLock;
			break;
		case KEY_ENTER:
			kprint("\n");
			return;
		}

		if( IsCtrl || IsAlt )
		{
			// function key process
		}

		if( IsNumLock )
		{
			switch( Info.Press )
			{
			case 0x47:
				putc('7');
				return;
			case 0x48:
				putc('8');
				return;
			case 0x49:
				putc('9');
				return;
			case 0x4B:
				putc('4');
				return;
			case 0x4C:
				putc('5');
				return;
			case 0x4D:
				putc('6');
				return;
			case 0x4F:
				putc('1');
				return;
			case 0x50:
				putc('2');
				return;
			case 0x51:
				putc('3');
				return;
			case 0x52:
				putc('0');
				return;
			case 0x53:
				putc('.');
				return;
			}
		}

		if( IsCapslock )
		{
			if( Info.Lowercase >= 'a' && Info.Lowercase <= 'z')
			{
				if( IsLShift || IsRShift )
					putc(Info.Lowercase);
				else
					putc(Info.Uppercase);

				return;
			}
		}
		if( IsLShift || IsRShift )
		{
			if(Info.Lowercase != 0x00 )
				putc(Info.Uppercase);
			else
			{
				// function key process

				// otherwise
			}

			return;
		}

		if( Info.Lowercase != 0x00 )
			putc(Info.Lowercase);
	}
}

잘 된다.

그후 이것저것 바꿨다.

 

전번에 IDT를 구성하고 커널까지 컴파일 하고 부팅까지 해 보았다. 물론 아무런 기능도(?) 넣지 않아 눈으로 보기에는 아무 변화도 없었다. 재부팅 안하는게 어디야~~~. 우선 인터럽트 동작이 잘 동작 하는지 확인 해 보는 코드를 넣어서 내가 만든게 잘 동작하는지 확인해 보잣!!!! 아자~


int  StartKernel(int argc, char *argv[])
{
	char *hello = "Hello OS!";
	char *complete = "complete.";
	unsigned char* vidmem = (unsigned char*)0xB8000;
	unsigned char data = 0;
	int b = 5;
	int c = 0;


	printf(hello);

	InitPIC();
	Init_IDT();

	b = b/0;
	
	while(1) ;
	return 0;
}


void _declspec(naked) interrupt_handler()
{
	_asm pushad;

	printf("interrupt handler");
	_asm {
		popad
		iretd
	}		
}

인터럽트 중에 0으로 나누면 예외상황 인터럽트가 발생하는 인터럽트가 있었다. 그래서 0으로 나누면 분명히 인터럽트가 발생 할것이고 interrupt_handler함수에서 인터럽트가 발생하면 interrupt handler라는 문구를 찍도록 해보았다. 자~~ 컴파일..

헉!!!!! interrupt handler!! 무한 출력 후 재부팅 된다.

결과야 어찌되었든 인터럽트가 발생하여 화면에 출력이 되었다는 거에 만족하자~~ ㅋㅋㅋㅋㅋ 대충 했는데도 된다… ㅋ 오늘은 확인 했으니 이만 잘까??? 음… 뭐 IDT를 구현하게 된데에는 키보드 입력 받아서 출력 하는데 목적을 두었으니 키보드로 입력하면 키출력 하는데까지 하고 자야 겠다.

우선 0으로 나누는 부분을 지우고!~ 

키가 눌려지면 interrupt_handler함수가 호출 될테고 전번 포스팅 에서 했었던 것 처럼 0x60 포트를 읽으면 될것이다. 그러면 기존 코드를 붙여서..


void _declspec(naked) interrupt_handler()
{
	char keycode;

	_asm pushad;


	keycode = Read_port_byte(0x60);
	if(keycode >= 0 )
	{
		putc((char)kbd_scancode[keycode]);
	}
	_asm {
		popad
		iretd
	}		
}

짠,,… 컴파일은 잘 된다… 자 그럼!~

5..4..3..2..1 ……. 음… 안돼! 키 입력해도 아무런 변화가 없어!~~

왜???????????????????????????????????????????????????????????????????????????

IDT가 잘못 구성 되었나?? ….

소스를 봐도 뭐가 잘못 된 건지 모르겠다.. 디버깅도 안되고…ㅠㅠ 키보드 관련 검색부터 해보자..흠..

아….INIT_PIC함수에서 잘못 되었나? 그러고 보니 하드웨어 인터럽트 즉.. 8259 PIC는 하드웨어 인터럽트와 충돌이 나서 주소를 옮겨야 된다고 했다. 안 옮겨줬나?? 아닌데… 키보드 입력 되었는데… 너무 인터넷에서 긁어서 복사만 했나~~~ 흠. 그러면 다시 INIT_PIC 함수를 분석해보자 ..

void InitPIC()
{
	 
	Write_port_byte(0x20, 0x11);       
	Write_port_byte(0x21, 0x20);       
	Write_port_byte(0x21, 0x04);       
	Write_port_byte(0x21, 0x01);       
	Write_port_byte(0x21, 0xFF);         

	
	Write_port_byte(0xA0, 0x11);       
	Write_port_byte(0xA1, 0x28);       
	Write_port_byte(0xA1, 0x02);       
	Write_port_byte(0xA1, 0x01);       
	Write_port_byte(0xA1, 0xFF);        
}
      

Write_port_byte 함수야.. 뭐..첫번째 인자는 포트이고 두번째는 데이터…라는 것이고

0x20 포트에 0x11 데이터를 쓴다…. 왜?? 음.. 그냥 첫번째 PIC 아니 마스터 PIC 너를 초기화 할것이다. 라는 의미인것 같다.

Write_port_byte(0x21, 0x20) 이건 너의 인터럽트 시작 주소는 0x20 이다 라고 지정해주는 것이고.
Write_port_byte(0x21, 0x04) 요것은 음.. 마스터 PIC 3번째하고 슬레이브 PIC가 연결되어 있다라는걸 설정하는 것이란다. 3번째 인데 왜 4냐고?? PIC는 8개 비트로 구성되어 있는데~~~ 3번재 연결 핀이..

8bit 7bit 6bit 5bit 4bit 3bit 2bit 1bit
          1    

4가 된다..
Write_port_byte(0x21, 0x01) 이것은 8086 모드라는 것인데… 모르겠다.. 1로 설정 할 경우는 8086. 0인경우 80/85 모드라는데 .. 모르겠다. ㅡㅡ;;;;

Write_port_byte(0x21, 0xFF) ?????? 마스터 PIC 에서 모든 인터럽트를 막는다????? 이것 때문에 키보드가 입력 안되었던 것인가

Write_port_byte(0xA0, 0x11)  슬레이브 PIC 너를 초기화 해주겠어
Write_port_byte(0xA1, 0x28)  인터럽트 시작주소는 28 이다…
Write_port_byte(0xA1, 0x02);  넌 마스터PIC와 두번째 핀과 연결되어 있다.
Write_port_byte(0xA1, 0x01)  ?? 8086 ㅡㅡ;;;;;
Write_port_byte(0xA1, 0xFF)   모든 인터럽트를 막는다!!!!

그러니까… 저기 뻘건색으로 줄쳐진 이놈들 때문에 키보드가 동작하지 않은 것인가??? 왜???? 왜 막아야 하는건가??? 모르겠다… 지랄같은 영어.. 여튼.. 키보드는 마스터 PIC 두번째 핀에 연결되어 있다고 하니 0xFF를 0xFD로 바꿔보자… 그리고 컴파일 . 확인.

오!!!!!!!!!!!!!!

보라… Hello OS! 옆에 작은 a 으흐흐흐 동작한다… IDT 구성은 문제 없고 키보드 인터럽트도 잘 받고…??? 어라 그런데 한번만 입력된다…. 검색해보니 이러더라 EOI 를 보내라고 …. 그냥 EOI를 보내서 PIC 를 리셋 시켜야 된다고 한다.  마스터 PIC에서 인터럽트가 발생했으면 0x20 포트에다가 보내고… 슬레이브 PIC에서 발생했으면 0xA0과 , 마스터 PIC 0x20 둘다 보내야 한다고 한다. 그러면 인터럽트가 발생하면 발생한 인터럽트 IRQ를 알아야 마스터인지 슬래이브 인지 아는것 아닌가…흠… CPU가 인자로 넘겨주나??? 그런가 보다.. 넘겨주는 듯… 귀찮으니 우선은 키보드만 입력 받고… EOI를 보내라고 했으니 마스터PIC에 0x20을 보내고 다시 테스트.

흠…

잘 된다..


 

 

8042 키보드 컨트롤러

모르는건 인터넷에서 검색하다가 그것마저 귀찮으면 소스 찾아서 긁어서 사용하다 보니, 진행하다 보면 이게 뭐지? 하는 내용들이 종종 생긴다. “이것까지 내가 알아야 하나?” 하고 어물쩡 넘어갔는데 그런게 너무 반복 되다 보니  “내가 지금 뭐하고 있지?” 라는 생각이 요즘 많이 든다. 단순히 OS 만들것이면 인터넷에 널려 있는 소스들 수정해서 만들면 된다. 그냥 내가 목표로 하는 OS만 결과로 던져 내면 되는 것이다.  자… 그러면 난 지금 무엇을 하고 있는 것인가? OS를 만드는 것인가… OS를 만들기 위해서 공부를 하고 있는 것인가… 흠 그러다가 OS 만들기의 대단원의 시작인 첫 포스팅 글을 방금 다시 한번 찾아봤다.

OS를 만들기 위해 부랴부랴 인터넷 검색을 해봤다. 우와! 책도 많고 관련 정보들이 상당하다. 수준급인 OS 부터 기초 정보 또한 대단히 많다. ‘그냥 이거 가져다 수정하면서 공부해 보면 되겠네…’ 라고 생각했다가 접었다.. 왜? 내 코드가 아니고 내 구조가 아니고… 내 영혼(?)이 아니다. 그냥 하나 하나 내가 부딪치며 만들어야 내것 같다는 생각이 들어서다. 또 뭔가 고비가 있어야 내 지식이지, 단순히 복사 & 붙이기는 그냥 영혼없는 결과물 만들기일 뿐이라고 생각해서 이다.

ㅎㅎㅎ 무척 단순한 생각으로 시작했다. 남의 코드는 내것이 아니므로….^^ 영혼까지 언급했다.

복사 & 붙이기 로 결과물을 만들지 말자고 했는데 지금은 복사 & 붙이기 로 만들고 있다….. 그래… 다 공부해서 만들기로 했었지..~~~ㅎㅎㅎ

내가 왜 이런 심각한 고민을 하게 된건 , 8042 키보드 컨트롤러가 키보드 관련해서 검색해 보다가 등장해서 이다. 키보드에서 인터럽트가 발생하게 될 경우, 8259 에서 인터럽트를 CPU에 알리고 다시 0x60 포트에서 키 값을 읽으면 된다. 라는 이 부분에서 0x60 이 포트가 궁금해서 찾아보다가 찾았다. 그냥 0x60에서 키 값을 읽으면 된다 라고 하고 넘어갈까 말까 망설였었다. 왜?? 귀찮으니까~~~

ㅎㅎㅎㅎ

여하튼.. 그냥 하자!!!~~~~~~

키보드와 마우스는 시리얼 통신을 한다고 배웠고 그렇게 되어 있다고 한다. 이 키보드와 마우스는 우리가 쉽게 얘기하는 PS/2 라고 불리우는 8042 컨트롤러와 연결 되어 있고 말이다. 키보드/마우스 제어 외에도 앞선 포스팅에서 언급한 주소 확장 관련해서 A20 Enable 기능도 8042 컨트롤러가 제어한다고 한다.

위 그림은 8042와 8259PIC와 CPU 연결을 도식화 한 것이다. 키보드나 마우스 입력 시 8042는 8259 PIC 마스터나 슬레이브에 IRQ 신호를 보내고 8259는 이를 CPU에 보낸다. 또 CPU는 8042를 0x60 또는 0x64 포트를 통해 데이터를 읽던지 제어를 한다고 한다.

IO 포트

접근 구분

용도

0x60 Read/Write 데이터 읽고 쓰기
0x64 Read Status 읽기
0x64 Write 명령 요청

전번 포스팅에서 키보드에 데이터 입력이 있는지 없는지 알기 위해서 0x64 포트의 Status를 읽어서 사용한 적이 있었다.

또 키보드 관련해서 알아야 할 내용이 생겼다. Set1, Set2, Set3… 이게 뭐냐면.. IBM 에서 콤퓨타를 만들어 낼 때 마다 새로운 키보드 인터페이스를 만들었었는데 XT 키보드, AT 키보드, PS/2 키보드 등으로 구분된다고 한다. 또한 이 키보드 타입 들은 같은 문자에 따라 다른 값들을 보낸다고 한단다… 그래서 각 값들을 Set1, Set2, Set3에 맞게 찾아서 문자를 대입해야 한다고 한다. Set1은 XT , Set2는 AT, Set3은 PS/2.
내가 코드에서 사용했던 건 Set1 이다. 8042를 통해 현재 사용하고 있는 키보드의 인터페이스를 얻어 낼 수 있고 지정 할 수 있다고 한다.

음.. 8042 명령어들과 기능들도 있는데 이건 필요할 때 마다 참조 하는 것으로 하고 8042에 대해서 여기서 마쳐야 겠다.

Tistory 태그: ,,,,

저번에 Interrupt Descriptor Table에 대해서 대충(?) 알아봤다. 그래서 이번에는 구현해 보기로 했다. 막상 구현하려고 하니 뭘 해야 할지 답답… ㅎㅎㅎ 우선 저번에 간단하게 작성해 놓은 IDT 구조체 다시 한번 작성해 놓고..

typedef struct _IDTDesc
{
	unsigned short	Offset_low;
	unsigned short	Selector;
	unsigned char	Unused;
	unsigned char	Access;
	unsigned short	Offset_high;
} IDTDesc;

음… 이제… 이 구조체가 여러개 존재 한다고 하니… 배열로… 구성해서…근데 몇개??? 몇개를 해야 하지??? 음… 그냥 1바이트 255개..ㅡㅡ;;;;; 대충해 보자.. 그래….

#define MAX_IDT	255

IDTDesc IDT[MAX_IDT];

음…그리고….에.. 그러니까.. 테이블을 만들었으니까 GDT 처럼 GDTR 이라는 레지스터에 이 테이블 주소를 넣어주는 …아.. 그래 IDTR에 이 테이블 주소를 넣어줘야지!!! ㅎㅎㅎ 

그러면 IDTR 구조.

32bit 16bit            0bit
IDT의 메모리상에서 위치 IDT의 크기

뭐… 별거 없다.. GDTR 구조와 같다…그러면..

typedef struct _IDTRegistor
{
	unsigned short	Size;
	unsigned int	Address;
} IDTRegistor;

IDTRegistor	IDTR;

음… 이렇게 만들었으면 예외상황 인터럽트, 하드웨어 인터럽트, 시스템콜 들을 등록 하라고 하는데 ….. ??? 어떻게 하지?? 검색해 보자…!!!!!!…………………………………………………………

저번에 포스팅한 예외상황 인터럽트 번호 0 부터 … 채울 수 있을 때까지 채우란다.. 시스템콜들은 뭐라뭐라 하는데 그냥 건너뛰고… 예외상황 하고 하드웨어 입력….!!! 그런데 말이다…. IDT 각각의 항목이 뭔지를 알아야 채우지~~~

Offset_low(2Byte)
Offset_high(2Byte)

요 두 놈들은 인터럽트 발생 시 인터럽트가 해야 할 (ISR이라고 저번에 적었지!~~^^ 잊어버렸었다.) 작업 위치 란다. 즉, 해당 함수의 메모리 상 위치.

Selector(2Byte)

요놈은 보호모드 진입시 GDT 테이블에 적어 놓은 CODE, DATA 셀렉터를 입력하는 놈이라는 데… 난 아직도 CODE와 DATA가 어떤 의미인지 모르겠다.. 그냥, CODE 셀렉터… 음… GDT 테이블에서 0x08 가 CODE 니까.. 저긴 0x08로 입력하는 걸로.

Unused(1Byte)

요넘은 사용 안하니 패쓰하고

Access(1Byte)

요놈은 그러니까… 전번에 한번 적었던 기억이 나는데…...http://manggong.org/41 여기…

P, DPL, S, TYPE 요렇게 되어 있고. P는 1로 하라고 하니 1. DPL 특권 레벨에 대한 것이라고 하고 레벨 0, 유저는 3이라고 하는데 0. S는 그냥 0.
TYPE은 여러 종류가 있는 것 같은데 보호 모드로 진입해서 32비트 라고 하고 Trap Gate는 소프트웨어 인터럽트나 예외 인터럽트, Interrupt Gate는 하드웨어 인터럽트. 그러면 0xE 아니면 0xF 인것 같다.

자, 그러면 Access 이놈한테 줄 수 있는 값은 10001110(0x8E : 소프트웨어 인터럽트, 예외인터럽트), 또는 10001111(0x8F : 하드웨어 인터럽트)

흠.. 이제 IDT를 채워보자.

아… 그러고 보니 Offset_low, high에 입력 해 줄 인터럽트 서비스 루틴(ISR) 함수가 없다.. 음… 더미 함수 하나 만들어 보고……근데 그냥 만들면 되나??? ……….이거 뭐 하나 쉽게 진행이 안되니…
http://wiki.osdev.org/Interrupt_Service_Routines에 의하면

void _declspec(naked) interrupt_handler()
{
    _asm pushad;
 
    /* do something */
 
    _asm{
        popad
        iretd
    }
}

이런식으로 만들면 된다고 한다. 그래 그냥 저 함수 저대로 쓰자~~ ^^

자 그러면.. 입력!

void Init_IDT()
{
	int i;
	unsigned int high, low;
	
	for(i = 0; i < MAX_IDT; i++ )
	{
		IDT[i].Selector = 0x08;
		if( i < 0x14 )
			IDT[i].Access = 0x8E;		// 예외 인터럽트
	    else
			IDT[i].Access = 0x8F;		// 하드웨어

		IDT[i].Unused = 0x00;

		low = (unsigned int)&interrupt_handler;
		low = low & 0xFFFF;

		high = (unsigned int)&interrupt_handler;
		high = (high >> 16);

		IDT[i].Offset_high = (unsigned short)high;
		IDT[i].Offset_low = (unsigned short)low;
	}
}

그리고 내친김에 IDTR도…

	IDTR.Address = (unsigned int)&IDT;
	IDTR.Size  = MAX_IDT * sizeof( IDTDesc );

다 입력했다… 마지막으로 이제 어셈블리어 LIDT 로 IDTR을 로딩시켜 주면…~~~~~~~ 된다고 하는데 … 어셈은 어려워 ㅠㅠ … 대충 해보자….

void LoadIDTR(unsigned int addr)
{
	_asm
	{
		mov	eax, [addr]
		lidt	[eax]
		sti
	}
}

ㅎㅎㅎㅎ lidt에 idtr 주소 넣어 주고 sti 명령어로 인터럽트를 활성화 시켜주면 된다고 한다. 자…. 한번 해볼까??

void InitPIC();
void Init_IDT();
void interrupt_handler();
void LoadIDTR(unsigned int addr);

unsigned char Read_port_byte(unsigned short port);
void Write_port_byte(unsigned short port, unsigned char data);


typedef struct _IDTDesc
{
	unsigned short	Offset_low;
	unsigned short	Selector;
	unsigned char	Unused;
	unsigned char	Access;
	unsigned short	Offset_high;
} IDTDesc;

typedef struct _IDTRegistor
{
	unsigned short	Size;
	unsigned int	Address;
} IDTRegistor;


#define MAX_IDT		255

IDTDesc	IDT[MAX_IDT];
IDTRegistor	IDTR;

unsigned char	*Screen = (unsigned char*)0xB8000;

#define	KEY_F1		0x80
#define	KEY_F2		(KEY_F1 + 1)
#define	KEY_F3		(KEY_F2 + 1)
#define	KEY_F4		(KEY_F3 + 1)
#define	KEY_F5		(KEY_F4 + 1)
#define	KEY_F6		(KEY_F5 + 1)
#define	KEY_F7		(KEY_F6 + 1)
#define	KEY_F8		(KEY_F7 + 1)
#define	KEY_F9		(KEY_F8 + 1)
#define	KEY_F10		(KEY_F9 + 1)
#define	KEY_F11		(KEY_F10 + 1)
#define	KEY_F12		(KEY_F11 + 1)

#define	KEY_INS		0x90
#define	KEY_DEL		(KEY_INS + 1)
#define	KEY_HOME	(KEY_DEL + 1)
#define	KEY_END		(KEY_HOME + 1)
#define	KEY_PGUP	(KEY_END + 1)
#define	KEY_PGDN	(KEY_PGUP + 1)
#define	KEY_LFT		(KEY_PGDN + 1)
#define	KEY_UP		(KEY_LFT + 1)
#define	KEY_DN		(KEY_UP + 1)
#define	KEY_RT		(KEY_DN + 1)
#define	KEY_PRNT	(KEY_RT + 1)
#define	KEY_PAUSE	(KEY_PRNT + 1)
#define	KEY_LWIN	(KEY_PAUSE + 1)
#define	KEY_RWIN	(KEY_LWIN + 1)
#define	KEY_MENU	(KEY_RWIN + 1)

	static const unsigned char kbd_scancode[] =
	{
0,	0x1B,	'1',	'2',	'3',	'4',	'5',	'6',
'7',	'8',	'9',	'0',	'-',	'=',	'\b',	'\t',
'q',	'w',	'e',	'r',	't',	'y',	'u',	'i',

'o',	'p',	'[',	']',	'\n',	0,	'a',	's',
'd',	'f',	'g',	'h',	'j',	'k',	'l',	';',

'\'',	'`',	0,	'\\',	'z',	'x',	'c',	'v',
'b',	'n',	'm',	',',	'.',	'/',	0,	0,
0,	' ',	0,	KEY_F1,	KEY_F2,	KEY_F3,	KEY_F4,	KEY_F5,
KEY_F6,	KEY_F7,	KEY_F8,	KEY_F9,	KEY_F10,0,	0,	KEY_HOME,
KEY_UP,	KEY_PGUP,'-',	KEY_LFT,'5',	KEY_RT,	'+',	KEY_END,
KEY_DN,	KEY_PGDN,KEY_INS,KEY_DEL,0,	0,	0,	KEY_F11,
KEY_F12
	};

void printf( char *msg )
{

	while(*msg != '\0')
	{
		*Screen++ = *msg++;
		*Screen++ = 7;
	}
}

void putc(char Code)
{
	*Screen++ = Code;
	*Screen++ = 7;
}



int  StartKernel(int argc, char *argv[])
{
	char *hello = "Hello OS!";
	char *complete = "complete.";
	unsigned char* vidmem = (unsigned char*)0xB8000;
	unsigned char data = 0;
	int b = 5;
	int c = 0;


	printf(hello);

	InitPIC();
	Init_IDT();


	while(1) ;
	return 0;
}

unsigned char Read_port_byte(unsigned short port)
{
	unsigned char KeyCode = 0;

	__asm
	{
		mov		dx, port
		xor		eax, eax
		in		ax, dx
		lea		ebx, KeyCode
		mov		[ebx], al
	}

	return KeyCode;
}

void Write_port_byte(unsigned short port, unsigned char data)
{
	__asm
	{
        mov		dx, [ebp+8]
        mov		al, [ebp+12]
        out		dx, al
	}
}

void InitPIC()
{
	 
	Write_port_byte(0x20, 0x11);       
	Write_port_byte(0x21, 0x20);       
	Write_port_byte(0x21, 0x04);       
	Write_port_byte(0x21, 0x01);       
	Write_port_byte(0x21, 0xFF);         

	
	Write_port_byte(0xA0, 0x11);       
	Write_port_byte(0xA1, 0x28);       
	Write_port_byte(0xA1, 0x02);       
	Write_port_byte(0xA1, 0x01);       
	Write_port_byte(0xA1, 0xFF);        
}


void _declspec(naked) interrupt_handler()
{
	_asm pushad;


	_asm {
		popad
		iretd
	}		
}

void Init_IDT()
{
	int i;
	unsigned int high, low;
	
	for(i = 0; i < MAX_IDT; i++ )
	{
		IDT[i].Selector = 0x08;
		if( i < 0x14 )
			IDT[i].Access = 0x8E;		// 예외 인터럽트
	    else
			IDT[i].Access = 0x8F;		// 하드웨어

		IDT[i].Unused = 0x00;

		low = (unsigned int)&interrupt_handler;
		low = low & 0xFFFF;

		high = (unsigned int)&interrupt_handler;
		high = (high >> 16);

		IDT[i].Offset_high = (unsigned short)high;
		IDT[i].Offset_low = (unsigned short)low;
	}

	IDTR.Address = (unsigned int)&IDT;
	IDTR.Size  = MAX_IDT * sizeof( IDTDesc );

	LoadIDTR((unsigned int)&IDTR);
}

void LoadIDTR(unsigned int addr)
{
	_asm
	{
		mov	eax, [addr]
		lidt	[eax]
		sti
	}
}


예전에 키보드 입력 받았던 코드에서 PIC를 초기화 해주고…!!! (하드웨어 인터럽트 발생해야 하니깐…) 컴파일 해서 확인!!!

아무 반응 없다…

뭐… 재부팅 안된것만 해도 다행이다.

키보드 입력을 받았으면 좋겠는데… 피곤해서 오늘은 그냥 여기까지…

다음시간에는 키보드 입력 루틴까지..

 

IDT.

http://manggong.org/41 여기에서 살짝 언급 했다.

Interrupt descriptor table라고 불리는 놈으로써 Interrupt Vector Table을 구현해 놓은 놈.

이라고 정리했었다. ㅎㅎㅎ 저 글 작성할때, 감정이 다시 돌아 상기 되는듯 하다. 자…

그러면, Interrupt Vector Table 이 무엇인지를 찾아 봐야 겠지??

검색해보자..

….

리얼모드에서는 Interrupt Vector Table(IVT) 이라고 불리우고… 보호모드에서는 Interrupt descriptor table(IDT)이라고 불리우고 .. 또…음???

Interrupt Service Routine(ISR)이 있는 곳을 가르키는 테이블? 이라는데??

흠… 그러면 ISR 은 또 무엇이냐???

이것저것 많은 것들이 검색 되던데 “어떤 인터럽트 발생 시, 발생된 인터럽트가 해야 할 일” 이라고 정리하고 끝내야 겠다. 즉 인터럽트 발생되면 실행되는 애라고 보면 될 것 같다. 그러면 여기서 짚고 넘어가야 할 인터럽트는 정확히 무엇인가??


인터럽트

뭔가 있어 보이는 말이다. ㅋㅋㅋㅋ

위키백과(https://ko.wikipedia.org/wiki/%EC%9D%B8%ED%84%B0%EB%9F%BD%ED%8A%B8)에서는 다음과 같은 정의를 내렸다.

“마이크로프로세서에서 인터럽트(Interrupt:중단, 세치기)란 마이크로프로세서(CPU)가 프로그램을 실행하고 있을 때, 입출력 하드웨어 등의 장치나 또는 예외 상황이 발생하여 처리가 필요할 경우에 마이크로프로세서에게 알려 처리할 수 있도록 하는 것을 말한다.”

인터럽트는 3가지 종류가 있다고 한다.

예외상황 인터럽트, 하드웨어 인터럽트, 시스템 콜.

예외상황 인터럽트는 시스템에 심각한 에러가 생겼을 때 발생되며 예로 어떤 수를 0으로 나누라는 명령어가 실행되었다던지, 페이지 폴트(?) 일어났을 때 발생된다.

인터럽트 번호 발생상황 설명
0

0 으로 나누기 예외

0으로 나누려고 할 경우.

1

디버그

EFLG의 T플래그 설정 및 디버그

2

NMI(Non Maskable Interrupt)

마스킹 불가능한 인터럽트용으로 예약

3

int 3

int 3 명령을 만날경우

4

오버플로우 발생

INTO 명령 실행 시 EFLAGS의 OF 플래그 설정.

5

유효주소 범위 초과

피연산자가 유효주소 범위를 벗어난 경우

6

알수 없는 명령어

잘못된 CPU 명령어 만날 경우.

7

디바이스 사용불가

CR0의 TS설정 상태에서 MMX 명령어를 실행.

8

연속 예외상황 처리 불가능

예외처리 과정중 예외가 또 발생한 경우.

9

Coprocessor 에러

Coprocess 에러 발생

10

무효 TSS 에러

잘못된 TSS를 가지고 태스크전환 할시

11

세그먼트 범위 초과

메모리에 존재하지 않는 세그먼트 참조

12

스택 세그먼트 에러

스택 제한을 벗어나거나 존재하지 않는 메모리 참조

13

일반 보호 에러

보호규약을 위반한 경우

14

페이지 폴트 에러

참조한 페이지가 메모리에 존재 하지 않은 경우

15

정의되지 않음

-

16

X87 부동소수점 에러

부동소수 오버플로우나 0으로 나눈 경우

17

정렬 체크 에러

피연산자의 주소가 정렬되지 않는 경우

18

머신 체크 에러

코드와 소스가 머신과 맞지 않는 경우

19

SIMD 부동소수점 에러

SSE와 SSE2 Floating-point instruction 의해 발생

하드웨어 인터럽트는 주변장치에 뭔가 일이 생겼을 때 발생되며 예로, 키보드가 눌렸다던지 마우스가 조작이 되었을 때 발생된다.

IRQ 설명
0

타이머

1

키보드

2

8259A 슬레이브 컨트롤러

3

두번째 시리얼 포트(COM2/COM4)

4

첫번째 시리얼 포트(COM1/COM3)

5

LPT2

6

플로피 디스크

7

LPT1

8

시스템 클록 RTC

9

할당되어 있지 않음

10

할당되어 있지 않음

11

할당되어 있지 않음

12

PS/2 마우스

13

수치 Coprocessor

14

EIDE Disk 컨트롤러 1

15

EIDE Disk 컨트롤러 2

  하드웨어 인터럽트의 발생을 CPU에 알리는 놈은 8259 고 이전 포스팅에서 적었다.
리얼모드에서는 0~F 까지 (15개 입력라인을 가질 수 있다고 했다.)  IRQ0~IRQ15까지 번호가 지정되었는데 보호모드에서는 IBM 초기 아키텍쳐 설계 실수로 충돌이 난다고 한다.(CPU 인터럽트와 충돌) 그래서 이 IRQ를 옮겨야 한다고 한다.(더 깊은 설명이 있는데… 번역이 대략 난감 ㅡㅡ…) (http://manggong.org/83)
 
또한 위 IRQ와 CPU 기본 인터럽트 번호가 충돌하여 옮겨줘야 한다고 언급 했었다. 영어로 된걸 번역하면서 이해하다 보니 긴글로 오는 피로감으로 넘어갔었는데… 간략하게 이해해 보자면
8259A는 인터럽트 발생 시 IRQ번호 + 내부 Base 로 계산하여 인터럽트 번호를 전달한다고 한다. 즉 내부 Base번호를 설정함으로써 인터럽트 번호 충돌을 회피 한다는 것이다.
 
 

시스템콜은 위 두개와 달리 시스템에서 자동으로 발생되는 것은 아니고 프로그램에서 인위적으로 조작하여 발생시키는 인터럽트이다. 예로 어떤 응용 프로그램이 커널 기능을 사용하기 위하여 커널에게 요청 할때 사용한다.( 지금은 조금 모호하다…^^ )


 

위 3개의 인터럽트들을 IDT 구조에 연속하여 입력하고 이 연속된 테이블 시작 주소를 IDTR 레지스터에 등록하면 인터럽트 처리에 대한 (IVT) 테이블에 정의해 놓고 인터럽트가 발생시 해당 인터럽트 기능을 실행하게 하는게 최종 목표인듯 싶다.

자.. 그러면… IDT 구조를 다시 한번 확인해 보자.

각 항목에 대한 설명은 넘어가고.. 이걸 구조체로 만들면 다음과 같다.

struct _IDT
{
	unsigned short Offset_low;
	unsigned short Selector;
	unsigned char Unused;
	unsigned char Access;
	unsgined short Offset_high;
};

셀렉터는 커널 셀렉터를 입력해 주면 될듯 싶고 인터럽트 번호에 매칭되는 ISR 위치를 Offset에 입력해주면 인터럽트 관련하여 IDT 구성은 끝나는 것 같다.

다음에는 실제 IDT 를 구현해 보는걸로…

 

'프로그래밍 > OS 만들기' 카테고리의 다른 글

OS 만들기 #18 IDT - 3  (0) 2015.08.12
OS 만들기 #17 IDT - 2  (0) 2015.08.10
OS 만들기 #15 커널-키보드 입력  (0) 2015.02.09
OS 만들기 #14 PE 재배치  (0) 2014.03.28
OS 만들기 #13 보호모드 진입  (0) 2014.03.27

보호모드로 진입하고 PE 재배치까지 한 후 메인 함수 호출을 마지막으로 고비는 넘겼다고 생각했다.

그래~ 이제부터 내 세상이닷

…..

화면에 HELLO 라는 문구 하나 출력 뒤에 뭘해야 할지 모르겠다…ㅠㅠ

아~~~휴~~~

뭘 해야 하지???

??? 그런데 왜 키보드 에 타이핑 하면 글자가 안 써지지??

그러고 보니 이제까지 키보드 입력에 관한 건 한번도 처리 안해 봤네???

그래… 키보드 입력 처리 해보자..

자료를 찾아보니 리얼모드에서는 int 21h 로 키보드 입력을 받는다고 한다. 당연히 난 보호모드에 진입되어 있으니 BIOS 함수들은 사용못하겠지.. 그러면 어떻게 해야 하냐??

검색!!!

헉…8259 Programmable Interrupt Controller 이건 모지??

뭐 이건… 뭐만 하려고 하면 굴비 엮듯이 줄줄이 모르는 단어들이 쏟아지는지 ㅡㅡ;;;

흠…

CPU로 인터럽트를 전달하기 위한 칩이란다. Master와 Slave 구조로 되어 있고 Master가 CPU와 연결되어 있단다. Master와 Slave는 각각 8개 입력 라인을 받을 수 있는데 Master 1개 입력 라인이 Slave와 연결되어 있으므로 총 15개 입력 라인을 사용 할수 있다고 한다. CPU가 여러개일 경우 각각 CPU에 연결 할 수 없기 때문에(?) Advanced PIC(APIC)가 나왔고 CPU 자체에 내장된 Local APIC 와 메인보드에 붙어 있는 I/O APIC로 발전 되었다고 하는데… CPU 여러개일 때는 모르겠고 …ㅡㅡ;;;

여튼 키보드에서 키가 입력 되었을 때, 8259 에서는 인터럽트 신호가 발생하고 CPU에서는 하던일을 멈추고 입력된 데이터를 읽는다 라는게 그 흐름 같아 보인다. (뭐 인터럽트에 대한 일반적인….내용).

리얼모드에서는 0~F 까지 (15개 입력라인을 가질 수 있다고 했다.)  IRQ0~IRQ15까지 번호가 지정되었는데 보호모드에서는 IBM 초기 아키텍쳐 설계 실수로 충돌이 난다고 한다.(CPU 인터럽트와 충돌) 그래서 이 IRQ를 옮겨야 한다고 한다.(더 깊은 설명이 있는데… 번역이 대략 난감 ㅡㅡ…)

그럼 어떻게 옮겨???
8259 칩은 0x20 은 Master, 0xA0라는 Slave 명령 IO 포트가 있고 0x21은 master Data IO 포트, 0xA1은 Slave Data IO 포트가 있다.  명령 IO 포트에 명령을 보내고 데이터 IO 포트에 데이터를 보내면 된다고 한다. 그냥 가져다 쓰자 ㅡㅡ;;;;

예제는..

 /* Init master interrupt controller */
outb(0x11, 0x20);       /* Start init sequence */
outb(0x20, 0x21);       /* Vector base */
outb(0x04, 0x21);       
outb(0x01, 0x21);       
outb(0xFF, 0x21);         

/* Init slave interrupt controller */
outb(0x11, 0xA0);       /* Start init sequence */
outb(0x28, 0xA1);       /* Vector base */
outb(0x02, 0xA1);       
outb(0x01, 0xA1);       
outb(0xFF, 0xA1);

이렇다는데… outb 함수 2번째 인자가 IO 포트 이고 1번째 인자가 명령 또는 데이터 값인가 보다. outb 함수가 IO포트에 쓰는(?) 함수인것 같은데 어셈블리어로 구성되어 있고 out 명령어를 사용한다. 이건 또 모지???

out : IO 포트에 데이터를 쓰는 명령어..

예제좀 바꿔야 겠다. outb 함수명 보다는 다른 이름으로~^^

흠… 그냥 인라인 어셈으로 해야 하나??

void Write_port_byte(unsigned short port, unsigned char data)
{
	__asm
	{
        mov		edx, [ebp+8]
        mov		al, [ebp+12]
        out		dx, al
	}
}

void InitPIC()
{
	 /* Init master interrupt controller */
	Write_port_byte(0x20, 0x11);       /* Start init sequence */
	Write_port_byte(0x21, 0x20);       /* Vector base */
	Write_port_byte(0x21, 0x04);       
	Write_port_byte(0x21, 0x01);       
	Write_port_byte(0x21, 0xFF);         

	/* Init slave interrupt controller */
	Write_port_byte(0xA0, 0x11);       /* Start init sequence */
	Write_port_byte(0xA1, 0x28);       /* Vector base */
	Write_port_byte(0xA1, 0x02);       
	Write_port_byte(0xA1, 0x01);       
	Write_port_byte(0xA1, 0xFF);        
}

되는지 안되는지는 모르겠다… 8259 PIC 초기화(?) 는 이정도로 끝마치고 이제.. 키보드 입력을 받는 부분을 만들자…

8259 PIC 의 0x60 포트를 읽으면 키 입력값이 들어온다고 한다.

0x60 포트에서 입력된 키 값은 아스키 코드값이 아니라고 한다.. 이를 다시 아스키 코드로 변환해야 한단다.. 흠.. 아.. 그러고 보니 이제까지 0xB8000 메모리 주소에 아스키 값 써서 화면에 글자 출력 하는 방법을 썼는데 아무래도 printf 함수도 하나 만들어야 겠다. 할게 많네 …갈길이 멀다..

우선 printf 함수 부터…

unsigned char	*Screen = (unsigned char*)0xB8000;

void printf( char *msg )
{

	while(*msg != '\0')
	{
		*Screen++ = *msg++;
		*Screen++ = 7;
	}
}

void putc(char Code)
{
	*Screen++ = Code;
	*Screen++ = 7;
}

전역변수로 Screen 변수를 선언하고 이제 printf() 함수를 호출해서 문자열을 출력하고 putc() 함수를 호출 해서 문자를 호출 하는 함수를 만들었다. 나중에 printf 함수를 손좀 봐서 C Standard library를 구현해야 겠다. 안돼면 가져오는 걸~^^로 하고 지금은 임시…

키보드 스캔코드를 아스키 코드로 변환할 변환 테이블 만들고(만든다긴 보다는 그냥 인터넷 검색.)….

0x60 포트에서 키보드 값 읽어서 화면에 출력… 이걸 무한 반복..

void InitPIC();
unsigned char Read_port_byte(unsigned short port);
void Write_port_byte(unsigned short port, unsigned char data);

unsigned char	*Screen = (unsigned char*)0xB8000;

#define	KEY_F1		0x80
#define	KEY_F2		(KEY_F1 + 1)
#define	KEY_F3		(KEY_F2 + 1)
#define	KEY_F4		(KEY_F3 + 1)
#define	KEY_F5		(KEY_F4 + 1)
#define	KEY_F6		(KEY_F5 + 1)
#define	KEY_F7		(KEY_F6 + 1)
#define	KEY_F8		(KEY_F7 + 1)
#define	KEY_F9		(KEY_F8 + 1)
#define	KEY_F10		(KEY_F9 + 1)
#define	KEY_F11		(KEY_F10 + 1)
#define	KEY_F12		(KEY_F11 + 1)

#define	KEY_INS		0x90
#define	KEY_DEL		(KEY_INS + 1)
#define	KEY_HOME	(KEY_DEL + 1)
#define	KEY_END		(KEY_HOME + 1)
#define	KEY_PGUP	(KEY_END + 1)
#define	KEY_PGDN	(KEY_PGUP + 1)
#define	KEY_LFT		(KEY_PGDN + 1)
#define	KEY_UP		(KEY_LFT + 1)
#define	KEY_DN		(KEY_UP + 1)
#define	KEY_RT		(KEY_DN + 1)
#define	KEY_PRNT	(KEY_RT + 1)
#define	KEY_PAUSE	(KEY_PRNT + 1)
#define	KEY_LWIN	(KEY_PAUSE + 1)
#define	KEY_RWIN	(KEY_LWIN + 1)
#define	KEY_MENU	(KEY_RWIN + 1)

	static const unsigned char kbd_scancode[] =
	{
0,	0x1B,	'1',	'2',	'3',	'4',	'5',	'6',
'7',	'8',	'9',	'0',	'-',	'=',	'\b',	'\t',
'q',	'w',	'e',	'r',	't',	'y',	'u',	'i',

'o',	'p',	'[',	']',	'\n',	0,	'a',	's',
'd',	'f',	'g',	'h',	'j',	'k',	'l',	';',

'\'',	'`',	0,	'\\',	'z',	'x',	'c',	'v',
'b',	'n',	'm',	',',	'.',	'/',	0,	0,
0,	' ',	0,	KEY_F1,	KEY_F2,	KEY_F3,	KEY_F4,	KEY_F5,
KEY_F6,	KEY_F7,	KEY_F8,	KEY_F9,	KEY_F10,0,	0,	KEY_HOME,
KEY_UP,	KEY_PGUP,'-',	KEY_LFT,'5',	KEY_RT,	'+',	KEY_END,
KEY_DN,	KEY_PGDN,KEY_INS,KEY_DEL,0,	0,	0,	KEY_F11,
KEY_F12
	};

void printf( char *msg )
{

	while(*msg != '\0')
	{
		*Screen++ = *msg++;
		*Screen++ = 7;
	}
}

void putc(char Code)
{
	*Screen++ = Code;
	*Screen++ = 7;
}



int  StartKernel(int argc, char *argv[])
{
	char *hello = "Hello OS!";
	unsigned char* vidmem = (unsigned char*)0xB8000;

	printf(hello);

	InitPIC();

	while(1)
	{
		data = Read_port_byte(0x60);
		if( data != 0 )
		{
			putc((char)kbd_scancode[data]);
		}
	}
	return 0;
}

unsigned char Read_port_byte(unsigned short port)
{
	unsigned char KeyCode = 0;

	__asm
	{
		mov		dx, port
		lea		ebx, KeyCode
		in		ax, dx
		mov		[ebx], ax
	}

	return KeyCode;
}

void Write_port_byte(unsigned short port, unsigned char data)
{
	__asm
	{
        mov		dx, [ebp+8]
        mov		al, [ebp+12]
        out		dx, al
	}
}

void InitPIC()
{
	 /* Init master interrupt controller */
	Write_port_byte(0x20, 0x11);       /* Start init sequence */
	Write_port_byte(0x21, 0x20);       /* Vector base */
	Write_port_byte(0x21, 0x04);       
	Write_port_byte(0x21, 0x01);       
	Write_port_byte(0x21, 0xFF);         

	/* Init slave interrupt controller */
	Write_port_byte(0xA0, 0x11);       /* Start init sequence */
	Write_port_byte(0xA1, 0x28);       /* Vector base */
	Write_port_byte(0xA1, 0x02);       
	Write_port_byte(0xA1, 0x01);       
	Write_port_byte(0xA1, 0xFF);        
}

자.. 잘되나 볼까???

뭐지… 그냥 이상한 값 출력 하다가 Virtual PC 창이 닫혀 버린다… 뭐가 잘못 된거지?? 음…계속 키 값이 입력 되는 것인가?? 다시 한번 확인…

음… 값은 키값이 반복되는 모습이다… 그런데 난 키보드 안 눌렀는데?? 키보드 버퍼에 쓰레기 값이 있는건가?? 우선 0x64 에서 현재 키보드 상태값을 읽는 부분이 있다. 출력 버퍼 상태라든지 입력 버퍼 상태 등이 기록된다는데 출력버퍼(출력인지 입력인지 잘 모르겠지만, 키가 입력되었으니 CPU 에게는 출력해야 한다고 가정…)의 상태가 체크 되어 있으면 화면에 출력 하는 루틴으로 수정해 보잣!!

int  StartKernel(int argc, char *argv[])
{
	char *hello = "Hello OS!";
	char *complete = "complete.";
	unsigned char* vidmem = (unsigned char*)0xB8000;
	unsigned char data = 0;

	printf(hello);

	InitPIC();

	while(1)
	{
		while((Read_port_byte(0x64) & 1) == 0 ) ;

		data = Read_port_byte(0x60);
		if( data != 0 )
		{
			putc((char)kbd_scancode[data]);
		}
	}
	return 0;
}

으흐흐흐 잘 된다..

키 하나 입력 될 때마다 하나 하나 화면에 찍힌다…

그런데 키 하나 입력 될때 마다 공백이 생기네??? 음…

아… Key down 과 Key up 이 때문에 key down 때는 글자가 찍히고 Key up은 다른 값으로 찍히나 보다.. 맞나?? 검색…

아.. 키보드에서 a 키를 입력(down)하면 1E 가 발생하고 a 키 UP은 1E | 0x80 = 9E 가 발생한다.. 0x80 비트가 Key down/up 을 나타내는 것 같다. 그러면 저기 코드에서 0x80 비트가 체크 되지 않을 때만 화면에 출력 하는 코드로 수정… 다시 확인.

void InitPIC();
unsigned char Read_port_byte(unsigned short port);
void Write_port_byte(unsigned short port, unsigned char data);

unsigned char	*Screen = (unsigned char*)0xB8000;

#define	KEY_F1		0x80
#define	KEY_F2		(KEY_F1 + 1)
#define	KEY_F3		(KEY_F2 + 1)
#define	KEY_F4		(KEY_F3 + 1)
#define	KEY_F5		(KEY_F4 + 1)
#define	KEY_F6		(KEY_F5 + 1)
#define	KEY_F7		(KEY_F6 + 1)
#define	KEY_F8		(KEY_F7 + 1)
#define	KEY_F9		(KEY_F8 + 1)
#define	KEY_F10		(KEY_F9 + 1)
#define	KEY_F11		(KEY_F10 + 1)
#define	KEY_F12		(KEY_F11 + 1)

#define	KEY_INS		0x90
#define	KEY_DEL		(KEY_INS + 1)
#define	KEY_HOME	(KEY_DEL + 1)
#define	KEY_END		(KEY_HOME + 1)
#define	KEY_PGUP	(KEY_END + 1)
#define	KEY_PGDN	(KEY_PGUP + 1)
#define	KEY_LFT		(KEY_PGDN + 1)
#define	KEY_UP		(KEY_LFT + 1)
#define	KEY_DN		(KEY_UP + 1)
#define	KEY_RT		(KEY_DN + 1)
#define	KEY_PRNT	(KEY_RT + 1)
#define	KEY_PAUSE	(KEY_PRNT + 1)
#define	KEY_LWIN	(KEY_PAUSE + 1)
#define	KEY_RWIN	(KEY_LWIN + 1)
#define	KEY_MENU	(KEY_RWIN + 1)

	static const unsigned char kbd_scancode[] =
	{
0,	0x1B,	'1',	'2',	'3',	'4',	'5',	'6',
'7',	'8',	'9',	'0',	'-',	'=',	'\b',	'\t',
'q',	'w',	'e',	'r',	't',	'y',	'u',	'i',

'o',	'p',	'[',	']',	'\n',	0,	'a',	's',
'd',	'f',	'g',	'h',	'j',	'k',	'l',	';',

'\'',	'`',	0,	'\\',	'z',	'x',	'c',	'v',
'b',	'n',	'm',	',',	'.',	'/',	0,	0,
0,	' ',	0,	KEY_F1,	KEY_F2,	KEY_F3,	KEY_F4,	KEY_F5,
KEY_F6,	KEY_F7,	KEY_F8,	KEY_F9,	KEY_F10,0,	0,	KEY_HOME,
KEY_UP,	KEY_PGUP,'-',	KEY_LFT,'5',	KEY_RT,	'+',	KEY_END,
KEY_DN,	KEY_PGDN,KEY_INS,KEY_DEL,0,	0,	0,	KEY_F11,
KEY_F12
	};

void printf( char *msg )
{

	while(*msg != '\0')
	{
		*Screen++ = *msg++;
		*Screen++ = 7;
	}
}

void putc(char Code)
{
	*Screen++ = Code;
	*Screen++ = 7;
}



int  StartKernel(int argc, char *argv[])
{
	char *hello = "Hello OS!";
	char *complete = "complete.";
	unsigned char* vidmem = (unsigned char*)0xB8000;
	unsigned char data = 0;

	printf(hello);

	InitPIC();

	while(1)
	{
		while((Read_port_byte(0x64) & 1) == 0 ) ;

		data = Read_port_byte(0x60);

		if(data & 0x80)
			continue;
		if( data != 0 )
		{
			putc((char)kbd_scancode[data]);
		}
	}
	return 0;
}

unsigned char Read_port_byte(unsigned short port)
{
	unsigned char KeyCode = 0;

	__asm
	{
		mov		dx, port
		xor		eax, eax
		lea		ebx, KeyCode
		in		ax, dx
		mov		[ebx], ax
	}

	return KeyCode;
}

void Write_port_byte(unsigned short port, unsigned char data)
{
	__asm
	{
                            mov	dx, [ebp+8]
                            mov	al, [ebp+12]
                            out	dx, al
	}
}

void InitPIC()
{
	 /* Init master interrupt controller */
	Write_port_byte(0x20, 0x11);       /* Start init sequence */
	Write_port_byte(0x21, 0x20);       /* Vector base */
	Write_port_byte(0x21, 0x04);       
	Write_port_byte(0x21, 0x01);       
	Write_port_byte(0x21, 0xFF);         

	/* Init slave interrupt controller */
	Write_port_byte(0xA0, 0x11);       /* Start init sequence */
	Write_port_byte(0xA1, 0x28);       /* Vector base */
	Write_port_byte(0xA1, 0x02);       
	Write_port_byte(0xA1, 0x01);       
	Write_port_byte(0xA1, 0xFF);        
}

오케이… 잘된다!!!!

너무 잘되니.. 손이 부들부들… ㅎㅎㅎㅎ

음.. 다 좋은데 키 입력을 이렇게 받을 순 없다.. ㅡㅡ;;;

키입력 관련해서 검색 해 보니 전에 잠깐 언급했던 IDT(Interrupt descriptor table)을 이용해서 키 입력 처리를 하네.. 흠…. 할게 너무 많어 ㅠㅠ

이젠 IDT에 대해서 알아보고 키보드 입력 처리를 해보잣..

'프로그래밍 > OS 만들기' 카테고리의 다른 글

OS 만들기 #17 IDT - 2  (0) 2015.08.10
OS 만들기 #16 IDT - 1  (0) 2015.07.30
OS 만들기 #14 PE 재배치  (0) 2014.03.28
OS 만들기 #13 보호모드 진입  (0) 2014.03.27
OS 만들기 #12 - PE 파일 포맷  (0) 2013.09.10

 

하!~~~ 전 포스트에서 어찌어찌 해서 보호모드 까지 진입 했다. 이제 바람OS 커널을 읽어서 커널로 이동하면 되는데 Visual Studio 툴로 컴파일 하다 보니 PE 포맷이다. ㅠㅠ 아~~~메모리에 커널이미지를 재배치라는 걸 해줘야 커널로 이동 할 수 있다. 젠장 할일 정말 많네…

여튼, 보호모드 까지.. 진입 했으니 커널 이미지를 읽는 작업부터 해야 겠다.

그런데 보호모드에서는 BIOS 인터럽트를 이용 할 수 없으니 , 보호모드에서 파일 읽는 방법을 찾아야 한다.

어디 보자~~~~~~~

음… http://wiki.osdev.org/ATA_read/write_sectors#Read_in_LBA_mode 이 사이트에서 방법을 찾았다. 다만 기존의 만들어 놓은 걸 다시 고쳐서 만들어야 하니.. 그냥… 보호모드 들어가기 전에 커널 이미지를 읽자 ㅡㅡ;;;; 너~~~~~~~~~~~~~~~~~무 귀찮고… 힘들다 ….

그러면.. 기존의 부트로더에서 OS_LDR.SYS 읽는 방법을 그대로 사용…….

;; creator : cyj (mangg@manggong.org)
;; date : 2014. 3. 27
;; desc : 커널 로더.
;;        1. BARAMOS.SYS 파일을 찾아서 읽자.
;;        1. 보호모드로 진입한다.
;;        2. OK 문자 화면에 출력

[bits	16]
org 0x8000

OS_LDR_ENTRY:
	;; CS, DS, ES, & SS을 초기화 하자.
	cli
	xor eax, eax
	mov ds, ax
	mov ss, ax
	mov es, ax
	mov sp, 0xFFFF
	mov bp, sp
	sti

	mov dl,[0x0550]	; 부트 드라이브
	mov [BootDrive], dl

	lea si, [LOADING_MSG]
	call PRINT

	call FAT_INFO

	; RootDirectoryEntry 읽어 첫번째 파일을 읽자.
	mov eax, 0x9000
	mov [TEMP_MEMORY], eax

	mov eax, [RootDirectoryEntry]
	mov [READ_SECTOR], eax
	           
	mov eax, 0x10000000 ; 0x10000 위치
	mov ebx, eax
	mov [FILE_MEMORY], eax

	call READ

	mov ebx, [TEMP_MEMORY]
	add ebx, 0x200
	
	; find file name
	mov ax, [TEMP_MEMORY]
FIND_LOOP:

	cmp eax, ebx
	je NOTFOUND

	add ax, 0x20

	mov si, ax
	mov di, KERNEL_NAME
	mov cx, 11
	repe cmpsb
	jnz FIND_LOOP

	mov ax, [si+9]
	push ax
	mov ax, [si+15]
	push ax
	pop eax
	mov [CurrentCluster], eax

DATA_READ:

	lea si,[DOT]
	call PRINT

	sub eax, 2
	xor ebx, ebx
	mov bl, [BPB_SecPerClus]
	mul ebx
	mov ebx, [RootDirectoryEntry]
	add eax, ebx
	mov [READ_SECTOR], eax
	mov eax, [FILE_MEMORY]
	mov [TEMP_MEMORY], eax

	call READ

	mov eax, [BytsPerCluster]
	mov ebx, [FILE_MEMORY]
	add eax, ebx
	mov [FILE_MEMORY], eax
	xor ebx, ebx

	; FAT 리스트
	mov eax, 0x9000
	mov [TEMP_MEMORY], eax
	mov eax, [CurrentCluster]
	;xor ebx, ebx
	mov bx, [BPB_BytsPerSec]
	shr bx, 2
	xor edx, edx
	div ebx
	push edx
	add eax, [FATsector]
	mov [READ_SECTOR], eax

	call READ

	pop edx

	mov esi, 0x9000
	sal edx, 2
	add esi, edx
	mov eax, [esi]

	mov [CurrentCluster], eax
	cmp eax, [FATEoFMask]
	jz END

	jmp DATA_READ

END:
	lea si, [KERNEL_LOADING]
	call PRINT


    ; GDT 등록
    xor     eax, eax
    lgdt    [GDT_INFO]  

    cli
    mov     eax, cr0
    or      eax, 1
    mov     cr0, eax

    jmp     $+2
    nop
    nop

    jmp     0x08:Entry32

NOTFOUND:
	lea si,[FAIL_MSG]
	call PRINT
	jmp $


; 하드디스크 읽기
READ:
	; Disk Address Packet (DAP)
	xor ax,ax								;INT 13h AH=00h: Reset Disk Drive
	mov dl,BYTE [BootDrive]				;DL = Drive Number
	int 0x13
	jc READ
	mov	al, 0x10 ; 크기
	mov	[0x500], al

	mov al, [BPB_SecPerClus]	; 읽을 섹터 수
	mov	[0x502], ax
	mov eax, [TEMP_MEMORY] ; 데이터가 들어갈 주소
	mov [0x504], eax ; Segment:offset
	mov eax, [READ_SECTOR] ; 접근 할 LBA 번호
	mov	[0x508], eax

	mov ax, 0x500 ; DAP의 Segment:offset
	mov	si, ax
	mov	ax, 0
	mov ds, ax
	mov ah, 0x42 ; LBA 읽기 명령
	mov dl, [BootDrive] ; 장치

	int 0x13

	jc READ_ERROR
	ret

READ_ERROR:
	lea si,[READ_ERR]
	call PRINT

	jmp $

PRINT:
	pusha				; 모든 레지스터를 스택에 넣는다.
PRINT_LOOP:
	mov al, [si]		; al에 글자 하나 저장한다.
	inc si				; si 주소를 증가한다.
	or al, al			; or 연산. 
	jz PRINT_END		; 문자열 끝이 or 연산이 0 이라면. 종료
	mov ah,0x0E			; ah = 0xE
	int 0x10			; 인터럽트 0x10h
	jmp PRINT_LOOP		; 다음 문자 출력 루프	

PRINT_END:
	popa				;모든 레지스터 스택에서 뺀다.
	ret					;호출 한곳으로 리턴.

FAT_INFO:
	pusha

	xor ebx, ebx
	mov eax, 0x9000
	mov [TEMP_MEMORY], eax
	
	xor eax, eax
	mov eax, 0
	mov [READ_SECTOR], eax

	call READ

	xor ebx, ebx
	mov si, [TEMP_MEMORY]
	mov bl, [si+454]
	mov bl, 0x80

	mov eax, 0x9000
	mov [TEMP_MEMORY], eax
	
	mov eax, ebx
	mov [READ_SECTOR], eax

	call READ

	mov si, [TEMP_MEMORY]
	mov bx, [si]

	mov bx, [si + 11]		; BytePerSec
	mov [BPB_BytsPerSec], bx
	xor ebx, ebx
	mov bl, [si + 13]		; SecPerCluster
	mov [BPB_SecPerClus], bl

	xor ecx, ecx
	xor ebx, ebx
	xor eax, eax
	mov cl, [si + 16]    ;  FAT NUM
	mov ebx, [si + 28]	;  Hidden Sector
	mov eax, [si + 36]	;  FAT 32

	mul ecx
	mov cx, [si + 14]		; Reserved Sector

	add eax, ecx
	add eax, ebx
	mov [RootDirectoryEntry], eax

	xor eax, eax
	mov eax, ebx
	add eax, ecx
	mov [FATsector], eax

	xor eax, eax
	xor ebx, ebx
	mov bx, [si + 11]
	mov al, [si + 13]
	mul ebx
	mov [BytsPerCluster], eax

	lea si,[FAT32_OK]
	call PRINT

	popa
	ret


DOT:
	db ".", 0x0D, 0x0A,0
FAT32_OK:
	db "Loading FAT's Info...", 0x0D, 0x0A, 0
KERNEL_LOADING:
	db "Loading BARAMOS.SYS...", 0x0D, 0x0A, 0
READ_ERR:
	db "Can't Read Disk..", 0x0D, 0x0A, 0

KERNEL_NAME:
	db "BARAMOS SYS",0

LOADING_MSG			db 0x0D, 0x0A, "Searching for BARAMOS.SYS...", 0x0D, 0x0A, 0x00
FAIL_MSG			db 0x0D, 0x0A, "ERROR.... ", 0x0D, 0x0A, 0x0A, 0x00
 
BPB_BytsPerSec		dw 0
BPB_SecPerClus		db 1
BootDrive			db 0x00
RootDirectoryEntry	dd 0
FATsector			dd 0
BytsPerCluster		dd 0
FATClusterMask		dd 0
FATEoFMask			dd 0x0FFFFFFF
CurrentCluster		dd 2
TEMP_MEMORY			dd 0x9000
READ_SECTOR			dd 0
FILE_MEMORY			dd 0x10000

;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;; 보호모드 GDT
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
GDT_INFO:
			dw     GDT_TABLE_END - GDT_TABLE - 1
		    dd     GDT_TABLE

GDT_TABLE:
			dw 0x0000 ; null
			dw 0x0000
			db 0x00
			db 0x00
			db 0x00
			db 0x00
            dw 0xFFFF    ; Code
            dw 0x0000
            db 0x00
            db 0x9A
            db 0xCF
            db 0x00
            dw 0xFFFF    ; Data
            dw 0x0000
            db 0x00
            db 0x92
            db 0xCF
            db 0x00
GDT_TABLE_END:

[bits 32]
Entry32:
	xor		eax, eax
    mov     ax, 0x10
	mov		ds, ax
	mov		es, ax
	mov		ss, ax
    xor     esp, esp
    mov     esp, 0xFFFF

    mov     BYTE [0xB8000], 'O'
    mov     BYTE [0xB8002], 'K'

 hang:
	jmp hang

위와 같이 작성..

이제 BARAMOS.SYS 파일을 만들어야 하는데~~~

음..

동작 까진 필요 없는 테스트이니깐....

Visual Studio 에서 콘솔에 프로그램 임의로 하나 작성..

// testKernel.cpp : 콘솔 응용 프로그램에 대한 진입점을 정의합니다.
//

#include "stdafx.h"


int _tmain(int argc, _TCHAR* argv[])
{
	return 0;
}

Visual Studio 에서 기본 으로 생성해주는 파일 그대로 컴파일 해서 BARAMOS.SYS 로 변경!!!

자 테스트 해보잣!!!

 

os14-1

흐흐흐흐 잘된다…

자~~~~~ 이제 읽은 이미지 데이터를 이제 메모리에 재배치 하잣!!!!

메모리 0x10000위치에 커널 이미지(BARAMOS.SYS) 데이터를 섹션별로 재배치~~~

..

……

…………..

PE포맷은 ImageBase + AddressOfEntryPoint 가 실제 주소란다.. 그렇겠지..ImageBase는 기본 메모리 시작점이라고 했고, AddressOfEntryPoint가 RVA 가상 위치 오프셋 이라고 하니..

근데 … 진짜 근데…보통 실행파일은 0x400000라는데….(?)

그래서 PCVIEW 프로그램으로 내가 예제로 만든 프로그램을 분석해 봤다.

os14-2

역쉬 4M 위치다. 리얼모드 모드에서는 1M 이상은 접근 안된다고 했는데????

아~~~~ 맞다… ㅡㅡ;;; 보호 모드로 넘어가서 재배치 해야 하는거군 … 에휴!!

음. 근데 초기 메모리 레이아웃에는 0x10000 위치에 커널 이미지 올린다고 했는데 … 0x400000으로 바꿔야 하나??? 흠…

구분

위치

부트로더 0x7C00
커널 로더 0x8000
커널 임시 0x10000
커널 재배치 0x400000

자… 이제 커널 재배치를 해보잣!!!!

0x10000 커널 데이터에서 ImageBase를 우선 구해야 하니, 요넘은 IMAGE_NT_HEADER 구조체 IMAGE_OPTIONAL_HEADER32 구조체 안에 있으니… 우선 IMAGE_OPTIONAL_HEADER32 구조체 위치를 찾아야겠다.

IMAGE_DOS_HEADER의 e_lfanew 에 IMAGE_NT_HEADER위치가 기록 되어 있고, IMAGE_OPTIONAL_HEADER32 위치는 e_lfanew + 0x18 에 있네.. 여기서 다시 ImageBase 위치는 아~~~ 그냥 e_lfanew + 0x34 구만..

ImageBase = e_lfanew(IMAGE_DOS_HEADER) + 0x34

PCVIEW 프로그램으로 찾아보니 쉽네 ㅋㅋㅋㅋ

ImageBase를 찾았으니, 섹션만 ImageBase 위치에 순차적으로 복사하면 되겠군..

섹션 첫 위치는 IMAGE_OPTIONAL_HEADER32 구조체의 SizeOfHeaders에 기록되니까 ImageBase 찾는거랑 마찬가지로…

Section = e_lfanew +  0x54

아.. 그런데 섹션위치는 알았는데 얼마만큼 복사하는지 섹션 크기가 알아야 하네… 그리고 섹션이 하나가 아니고 여러개 존재 하니깐…. Section 첫번째 위치는 별 의미가 없다… 흠… 그러면 섹션들 헤더를 열고.. 섹션 크기, 섹션 실제 위치, ImageBase + 섹션 RVA를 구한후 복사해줘야 할듯… 그러자면 섹션이 몇개 있는지에 대한 정보도 필요하다..섹션갯수는 elfanew 에서 …

SectionCount = e_lfanew + 0x06

그리고 각 섹션헤더 위치는 IMAGE_OPTION_HEADER 다음 위치에 있으니깐 IMAGE_FILE_HEADER구조체 SizeOfOptionalHeader가 IMAGE_OPTION_HEADER 크기니깐 ㅡㅡ;;;

SizeOfOptionHeader = e_lfanew + 0x14 구하고,

SectionHeader = e_lfanew + SizeOfOptionHeader + IMAGE_FILE_HEADER크기

IMAGE_FILE_HEADER크기는 0x14 크기니깐..

SectionHeader = e_lfanew + SizeOfOptionHeader + 0x14

에다가 다시 Signature 4바이트 더하면…

SectionHeader = e_lfanew + SizeOfOptionHeader + 0x14 + 0x04

만큼 있다.

아고 힘들어…

이제 섹션 갯수 만큼루프 돌면서 섹션 헤더를 보고 섹션의 실제 저장된 데이터를 ImageBase + 섹션 RVA만큼 위치에다가 복사해주면 된다!!!!

그럼 코딩해 보잣!!!!

;; creator : cyj (mangg@manggong.org)
;; date : 2014. 3. 27
;; desc : 커널 로더.
;;        1. BARAMOS.SYS 파일을 찾아서 읽자.
;;        1. 보호모드로 진입한다.
;;        2. OK 문자 화면에 출력

[bits	16]
org 0x8000

OS_LDR_ENTRY:
	;; CS, DS, ES, & SS을 초기화 하자.
	cli
	xor eax, eax
	mov ds, ax
	mov ss, ax
	mov es, ax
	mov sp, 0xFFFF
	mov bp, sp
	sti

	mov dl,[0x0550]	; 부트 드라이브
	mov [BootDrive], dl

	lea si, [LOADING_MSG]
	call PRINT

	call FAT_INFO

	; RootDirectoryEntry 읽어 첫번째 파일을 읽자.
	mov eax, 0x9000
	mov [TEMP_MEMORY], eax

	mov eax, [RootDirectoryEntry]
	mov [READ_SECTOR], eax
	           
	mov eax, 0x10000000
	mov ebx, eax
	mov [FILE_MEMORY], eax

	call READ

	mov ebx, [TEMP_MEMORY]
	add ebx, 0x200
	
	; find file name
	mov ax, [TEMP_MEMORY]
FIND_LOOP:

	cmp eax, ebx
	je NOTFOUND

	add ax, 0x20

	mov si, ax
	mov di, KERNEL_NAME
	mov cx, 11
	repe cmpsb
	jnz FIND_LOOP

	mov ax, [si+9]
	push ax
	mov ax, [si+15]
	push ax
	pop eax
	mov [CurrentCluster], eax

DATA_READ:

	lea si,[DOT]
	call PRINT

	sub eax, 2
	xor ebx, ebx
	mov bl, [BPB_SecPerClus]
	mul ebx
	mov ebx, [RootDirectoryEntry]
	add eax, ebx
	mov [READ_SECTOR], eax
	mov eax, [FILE_MEMORY]
	mov [TEMP_MEMORY], eax

	call READ

	mov eax, [BytsPerCluster]
	mov ebx, [FILE_MEMORY]
	add eax, ebx
	mov [FILE_MEMORY], eax
	xor ebx, ebx

	; FAT 리스트
	mov eax, 0x9000
	mov [TEMP_MEMORY], eax
	mov eax, [CurrentCluster]
	;xor ebx, ebx
	mov bx, [BPB_BytsPerSec]
	shr bx, 2
	xor edx, edx
	div ebx
	push edx
	add eax, [FATsector]
	mov [READ_SECTOR], eax

	call READ

	pop edx

	mov esi, 0x9000
	sal edx, 2
	add esi, edx
	mov eax, [esi]

	mov [CurrentCluster], eax
	cmp eax, [FATEoFMask]
	jz END

	jmp DATA_READ

END:
	lea si, [KERNEL_LOADING]
	call PRINT


    ; GDT 등록
    xor     eax, eax
    lgdt    [GDT_INFO]  

    cli
    mov     eax, cr0
    or      eax, 1
    mov     cr0, eax

    jmp     $+2
    nop
    nop

    jmp     0x08:Entry32

NOTFOUND:
	lea si,[FAIL_MSG]
	call PRINT
	jmp $


; 하드디스크 읽기
READ:
	; Disk Address Packet (DAP)
	xor ax,ax								;INT 13h AH=00h: Reset Disk Drive
	mov dl,BYTE [BootDrive]				;DL = Drive Number
	int 0x13
	jc READ
	mov	al, 0x10 ; 크기
	mov	[0x500], al

	mov al, [BPB_SecPerClus]	; 읽을 섹터 수
	mov	[0x502], ax
	mov eax, [TEMP_MEMORY] ; 데이터가 들어갈 주소
	mov [0x504], eax ; Segment:offset
	mov eax, [READ_SECTOR] ; 접근 할 LBA 번호
	mov	[0x508], eax

	mov ax, 0x500 ; DAP의 Segment:offset
	mov	si, ax
	mov	ax, 0
	mov ds, ax
	mov ah, 0x42 ; LBA 읽기 명령
	mov dl, [BootDrive] ; 장치

	int 0x13

	jc READ_ERROR
	ret

READ_ERROR:
	lea si,[READ_ERR]
	call PRINT

	jmp $

PRINT:
	pusha				; 모든 레지스터를 스택에 넣는다.
PRINT_LOOP:
	mov al, [si]		; al에 글자 하나 저장한다.
	inc si				; si 주소를 증가한다.
	or al, al			; or 연산. 
	jz PRINT_END		; 문자열 끝이 or 연산이 0 이라면. 종료
	mov ah,0x0E			; ah = 0xE
	int 0x10			; 인터럽트 0x10h
	jmp PRINT_LOOP		; 다음 문자 출력 루프	

PRINT_END:
	popa				;모든 레지스터 스택에서 뺀다.
	ret					;호출 한곳으로 리턴.

FAT_INFO:
	pusha

	xor ebx, ebx
	mov eax, 0x9000
	mov [TEMP_MEMORY], eax
	
	xor eax, eax
	mov eax, 0
	mov [READ_SECTOR], eax

	call READ

	xor ebx, ebx
	mov si, [TEMP_MEMORY]
	mov bl, [si+454]
	mov bl, 0x80

	mov eax, 0x9000
	mov [TEMP_MEMORY], eax
	
	mov eax, ebx
	mov [READ_SECTOR], eax

	call READ

	mov si, [TEMP_MEMORY]
	mov bx, [si]

	mov bx, [si + 11]		; BytePerSec
	mov [BPB_BytsPerSec], bx
	xor ebx, ebx
	mov bl, [si + 13]		; SecPerCluster
	mov [BPB_SecPerClus], bl

	xor ecx, ecx
	xor ebx, ebx
	xor eax, eax
	mov cl, [si + 16]    ;  FAT NUM
	mov ebx, [si + 28]	;  Hidden Sector
	mov eax, [si + 36]	;  FAT 32

	mul ecx
	mov cx, [si + 14]		; Reserved Sector

	add eax, ecx
	add eax, ebx
	mov [RootDirectoryEntry], eax

	xor eax, eax
	mov eax, ebx
	add eax, ecx
	mov [FATsector], eax

	xor eax, eax
	xor ebx, ebx
	mov bx, [si + 11]
	mov al, [si + 13]
	mul ebx
	mov [BytsPerCluster], eax

	lea si,[FAT32_OK]
	call PRINT

	popa
	ret


DOT:
	db ".", 0x0D, 0x0A,0
FAT32_OK:
	db "Loading FAT's Info...", 0x0D, 0x0A, 0
KERNEL_LOADING:
	db "Loading BARAMOS.SYS...", 0x0D, 0x0A, 0
READ_ERR:
	db "Can't Read Disk..", 0x0D, 0x0A, 0

KERNEL_NAME:
	db "BARAMOS SYS",0

LOADING_MSG			db 0x0D, 0x0A, "Searching for BARAMOS.SYS...", 0x0D, 0x0A, 0x00
FAIL_MSG			db 0x0D, 0x0A, "ERROR.... ", 0x0D, 0x0A, 0x0A, 0x00
 
BPB_BytsPerSec		dw 0
BPB_SecPerClus		db 1
BootDrive			db 0x00
RootDirectoryEntry	dd 0
FATsector			dd 0
BytsPerCluster		dd 0
FATClusterMask		dd 0
FATEoFMask			dd 0x0FFFFFFF
CurrentCluster		dd 2
TEMP_MEMORY			dd 0x9000
READ_SECTOR			dd 0
FILE_MEMORY			dd 0x10000

;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;; 보호모드 GDT
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
GDT_INFO:
			dw     GDT_TABLE_END - GDT_TABLE - 1
		    dd     GDT_TABLE

GDT_TABLE:
			dw 0x0000 ; null
			dw 0x0000
			db 0x00
			db 0x00
			db 0x00
			db 0x00
            dw 0xFFFF    ; Code
            dw 0x0000
            db 0x00
            db 0x9A
            db 0xCF
            db 0x00
            dw 0xFFFF    ; Data
            dw 0x0000
            db 0x00
            db 0x92
            db 0xCF
            db 0x00
GDT_TABLE_END:

[bits 32]
Entry32:
	xor		eax, eax
    mov     ax, 0x10
	mov		ds, ax
	mov		es, ax
	mov		ss, ax
    xor     esp, esp
    mov     esp, 0xFFFF

	; 메모리 재배치 해보자
	mov		esi, 0x10000     ; 커널 이미지 저장된곳
	xor		edx, edx
	xor		eax, eax

	add		edx, 0x3C
	mov		eax, [esi+edx]
	mov		[NT_POINTER], eax ; e_lfanew 위치 구하기(IMAGE_NT_HEADER위치)
	mov		ebx, eax
	mov		edx, 0x34
	add		edx, ebx
	mov		eax, [esi+edx]
	mov		[IMAGE_BASE], eax	; IMAGE_BASE 위치 구하기
	mov		edx, 0x28
	add		edx, ebx
	mov		eax, [esi+edx]
	mov		[ENTRY_POINT], eax		; ENTRY_POINT 위치 구하기
	mov		edx, 0x06
	add		edx, ebx
	mov		ax, WORD [esi+edx]
	mov		[SECTION_COUNT], ax		; 섹션 갯수 구하기
	mov		edx, 0x14
	add		edx, ebx
	mov		ax, WORD [esi+edx]
	mov		[OPTIONHEADER_SIZE], ax		;IMAGE_OPTION_HEADER크기 구하기
	mov		edx, 0x18
	add		dx, WORD[OPTIONHEADER_SIZE]
	add		edx, ebx
	mov		eax, edx
	mov		[SECTION_HEADER], eax	;섹션 헤더 위치 구하기

	xor		ecx, ecx
	mov		cx, [SECTION_COUNT]		; 섹션 갯수 만큼 반복
SECTION_LOOP:
	mov		esi, 0x10000
	mov		ebx, [SECTION_HEADER]		; 섹션 헤더 위치

	mov		edx, 0x0C				; 섹션 RVA = 섹션 헤더 위치 + 0x0C
	add		edx, ebx
	mov		eax, [esi+edx]
	mov		[SECTION_RVA], eax
	mov		edx, 0x10				; 섹션 크기 = 섹션 헤더 위치 + 0x10
	add		edx, ebx
	mov		eax, [esi+edx]
	mov		[SECTION_SIZE], eax
	mov		edx, 0x14				; 섹션 데이터 위치 = 섹션 헤더 위치 + 14
	add		edx, ebx
	mov		eax, [esi+edx]
	mov		[SECTION_POINTER], eax

	; 복사할 원본 위치
	add		esi, [SECTION_POINTER]	; 섹션 데이터가 있는 위치 .

	mov		edi, [IMAGE_BASE]		; 복사될 메모리 위치
	add		edi, [SECTION_RVA]		; IMAGE_BASE 에서 섹션 RVA 만큼 위치

	push	ecx
	cld
	mov		ecx, [SECTION_SIZE]		; 섹션 사이즈 만큼
	repz	movsb					; 복사
	pop		ecx

	mov		eax, [SECTION_HEADER]	; 섹션 헤더 위치를 다음 섹션 헤더 위치로 변경
	add		eax, 40
	mov		[SECTION_HEADER], eax
	loop	SECTION_LOOP

    	mov     BYTE [0xB8000], 'O'
	mov     BYTE [0xB8002], 'K'


	mov		eax, [IMAGE_BASE]
	add		eax, [ENTRY_POINT]
	call	eax


 hang:
	jmp hang


IMAGE_BASE			dd	0
ENTRY_POINT			dd	0
NT_POINTER			dd	0
OPTIONHEADER_SIZE	dw	0
SECTION_COUNT		dw	0
SECTION_HEADER		dd	0
SECTION_RVA			dd	0
SECTION_SIZE		dd	0
SECTION_POINTER		dd	0

PE 메모리 재배치 하는 코드 찾다가 http://1228.tistory.com/entry/%EC%9A%B4%EC%98%81%EC%B2%B4%EC%A0%9C-%EB%A7%8C%EB%93%A4%EA%B8%B0-%EC%96%B4%EC%85%88%EB%B8%94%EB%A6%AC%EC%BD%94%EB%93%9C%EC%97%90%EC%84%9C-C%EC%BD%94%EB%93%9C%EB%A1%9C-%EC%A0%90%ED%94%84%ED%95%98%EA%B8%B0-%EB%A1%9C%EB%8D%94-%EC%9E%91%EC%84%B1 블로그 꼬꼬넷님 코드를 훔쳤다(?) … 정말 감사드린다……똭!!!

이제 테스트…!!!

헐… 또 재부팅이다… 음…

코드를 몇번을 확인해도 문제는 없어 보인다.

커널이 메모리상에 재배치 되는건 문제가 없어 보인다. 그래서 Visual Studio에서 컴파일 할때 옵션이 잘못된듯 해 보여 또 부랴부랴 검색질.

http://wiki.osdev.org/Visual_Studio

요기 이 사이트에서 하라는대로 컴파일 옵션을 수정했다.

그리고 커널 코드도 바꿨다…

int StartKernel(int argc, char* argv[])
{
    char* hello = "Hello OS!";

    unsigned char* vidmem = (unsigned char*)0xB8000;

    while( *hello != '\0' )        
    {
        *vidmem++ = *hello++;
        *vidmem++ = 7;        // 문자 기본 속성
    }   

	return 0;
}

그리고 컴파일 옵션->고급 에서 진입점을 StartKernel 입력하고 컴파일…

아직까진 간단한 코드니 에러 없이 컴파일 잘 된다!!

자 이제!! 테스트

os14-3

ㅠㅠ

된다… 와 ! 된다 ㅠㅠ

부트로더, 커널로더 까지 많은 시행 착오 및 병행 학습을 많이 했는데 막상 이렇게 기능상 구현이 된 모습을 보니 간단하다… 누가 이걸 보고 많은 지식이 포함되어 있다는걸 알겠누.. 흠..

이제 어려운 과정(?)은 다 끝난것 같다. 물론 남은 일이 더 남아 있지만, 어셈블리어 메뉴얼 뒤져 보면서 한자 한자 출력 해보면서 테스트 하는 작업은 끝난 듯….

OS의 핵심인 커널은 이것보다 험난 할 듯 보이지만.. 적어도 문법적으로 검증해야 하는 난관이 없다는데 행복함을 느낀다.

커널은 부트로더, 커널 로더 처럼 단일 기능이 아니고 필요할 때마다 학습해서 될 문제는 아닌듯 해서 커널 까지는 다시 학습이 필요 할 것 같다…

학 ………………..습.

 


너무 많은 시간을 블로그에 신경을 못썼다. 바쁘다는 핑계도 있고 어쩌면 OS를 만들다가 지쳐 버린 것인지도 모르겠다. 내일 해야지.. 하고 미뤘던게 벌써 몇달이 지나 버렸다. ㅎㅎㅎ

지난 포스트 읽고 다시 복습중….



초기 바람OS는 부트로더에서 바로 커널을 호출 하는 방법을 사용하려고 했었다. OS 이미지 파일에서 부트로더 다음 섹터를 읽는 방법으로 커널을 호출했다. 여기에서 좀 더 확장해서 파일 시스템(FAT32)를 추가해서 부트로더가 KERNEL.SYS 파일을 찾아서 로딩 하는 형태로 구현하려고 했다. 그런데 부트로더의 512바이트 한계에서 파일시스템에서 KERNEL.SYS를 찾아서 로딩 하는 구현부까지는 무리라고 생각되었다. 또 보호모드 진입을 위한 처리를 하기 위해선 부트로더와 커널간 이 처리를 해줘야 하는 무언가가 필요하다고 판단하여 OS_LDR.SYS 라는 커널 로더를 추가 하기로 했다.

부트로더->커널 로더->커널

자… 커널 로더에서 보호 모드를 진입하고 커널을 읽어 주면 된다.

말만 쉬웠다.

보호 모드 진입이 이렇게 어려울 줄은 몰랐다. 또한 커널을 Visual Studio 툴로 개발 하려고 한 난 , 컴파일이 된 생성 파일이 PE포맷 이라는 사실에 좌절하게 되었다. 그래서 부랴 부랴 보호모드와 PE 포맷을 열공 했다. (~~라고 쓰고 그냥 이론서들을 읽는 수준…)

우선, 커널 로더에서 보호 모드로 진입을 해 봐야 겠다.

그전에, 커널 로더와 커널이 메모리 상에 어디에 적재 되는지 정의부터 해야 겠다.

기존 포스트를 보니

부트로더가 0x7C00에 적재되고 FAT 관련 TEMP 데이터는 0x9000을 이용했다. 그리고 커널로더를 0x8000에 저장해 놓은 형태이다. 그러면 음.. 커널을 0x100000 에 놓으면 될까??

구분 메모리 주소
부트로더 0x7C00
커널 로더 0x8000
커널 0x10000

이렇게 대충(?) 성의 없고 계획없는 정의는 해 놓고 커널 로더를 작성하잣!!!!!!!

 

 

- 보호 모드 진입

이제 보호모드 진입하자!!~~ 보호 모드 진입은 CR0 레지스터를 이용하면 진입 할 수 있다고 한다. 흠 CR0 레지스터 넌 또 뭐하는 놈이냐!!!

CR0 레지스터 : 32비트 레지스터로 운영 모드를 변경, 제어하는 컨트롤 레지스터 (X86 프로세서에는 CR0~CR4 등의 5개의 컨트롤 레지스터가 있으며 그중 하나)

Bit 설명 엑세스
31 페이징 활성화 R/W
30 캐시 비활성화 R/W
29 NOT-WriteThrough  이라는데 모르겠음. R/W
28~19 예약  
18 Alignment Mask 몰라. R/W
17 예약  
16 Write Protect (몰라) R/W
15~6 예약  
5 Numeric Error R/W
4 Extension Type R
3 Task Switched R/W
2 Emulation R/W
1 Monitor Coprocessor R/W
0 Protection Enabled (보호 모드 활성화) R/W

요렇게 구성된다는 데 아직 다~~~ 모르겠다. 다만, 0비트 Protection Enabled 여기를 1로 바꾸면 보호 모드로 진입한다는데… 쉽네?

or CR0, 1 하면 되나???

ㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋ

CR0은 컨트롤 레지스터로 요케 하면 안되고!! 다른 레지스터를 이용해서 하라는 삽질이 필요하단다 ㅋㅋ

mov EAX, CR0 ; CR0 을 eax 레지스터로 옮기고!
or EAX, 1         ; EAX 값에다가 1을 OR 연산 시킨다.
mov CR0, EAX ; 다시 CR0으로 복사.

 

그러면 한번 해볼까???

;; creator : cyj (mangg@manggong.org)
;; date : 2014. 3. 27
;; desc : 커널 로더.
;;        1. 보호모드로 진입한다.

org 0x8000
bits 16

OS_LDR_ENTRY:

	cli
	;; CS, DS, ES, & SS을 초기화 하자.
	xor eax, eax
	mov ds, ax
	mov ss, ax
	mov es, ax
	mov sp, 0xFFFF
	mov bp, sp
	sti

	mov EAX, CR0
	or  EAX, 1
	mov CR0, EAX

자, 이걸 가상 이미지 파일에 넣고!!!

 

짜잔~~~~~~~!!!

무한 재부팅이다! ㅋㅋㅋㅋ

아~ 재미없어…..

왜 안되지???

아~~~~~~~

GDT 테이블을 우선 GDT 레지스터에 등록해야 한다고 한다.

그럼 GDT 테이블 먼저 만들어야 겠네?.. 지난 포스트(http://manggong.org/41)에서 GDT레지스터에 테이블 위치 넘기는 부분에 대해서 간단히 언급한적이 있다.

struct
{
   unsigned short GDT_SIZE;
   unsigned int GDT_ADDRESS;
} GDT_INFO

LGDT GDT_INFO

GDT_INFO는 GDT 테이블의 크기와 GDT 테이블의 주소를 가지고 있는 구조체.

그러면 GDT_INFO는 이렇게 어셈으로 코딩하고…

LGDT GDT_INFO

GDT_INFO:

GDTSIZE      dw     GDT테이블 크기
GDTADDRESS   dd     GDT_TABLE

GDT 테이블만 만들어 주면 되겠네… GDT 테이블은 GDT 디스크립터 모음이라고 한다고 하니 디스크립터 배열인듯 하다. 전 포스트에 열심히 표로 그려놓은 디스크립터….

os11-1

8바이트로 구성 된 이상한 구조체…

막상 값을 넣으려고 봤더니 뭘 넣어 줘야 할지 막막하다…

그냥 예전에 설명 해 놓은 글을 참고로

Segment Limit(0:15)라는 놈은 세그먼트 크기를 나타낸다고 하고 4GB까지 가능하다고 하니 다 0xFFFF

Base Address(15:0) 라는 놈은 세그먼트 시작 주소라고 하니 음…음…뭘 넣어주지?? 커널 로더가 0x8000에서 시작하니까 그냥 세그먼트 시작 주소를 0x8000 넣어주면 되나??? 음… 아 모르겠다.. 그냥 0x0

Base Address(23:16) 이놈도 그냥 모르겠으니 0x0

Type : 이놈… 코드 세그먼트 인지 데이터 세그먼트 인지 접근 권한 뭐 이런거 있는데 이놈은 뭘 넣어줘야 할지 모르겠다. 실행되고 읽기 지원 하는 놈을 선택했다.(잘한것 같다. 검색해보니 이렇게 하더라 ㅡㅡ ) 0xA 그러고 보니 4비트군 ㅡㅡ;;

S : 이놈 모르겠다…시스템 디스크립터면 0, 걍 디스크립터(코드,데이터) 구분자면 1이라면 넌 그냥 1

DPL : 커널과 유저 접근권한 .. 그래도 너도 0이다.

P : 유효??? 하면 1 유효하지 않으면 0  …넌…넌… 너 모르는데…너도 그냥 1 모르겠다. 그냥 1주자…

그래서 Type,S,DPL, P 요넘들이 1바이트 이니까 전체 설정한 값을 보면 0x9A가 된다.

Segment Limit(19:16) 요놈..그냥 너도 0xF

AVL: 임의의 용도?? 그럼 0

L 요건 64비트면 1 32비트면 0 이라니 난 32비트 0

D/B 이것도 난 32비트 1

G 이건 내가 아까 4G까지 Segment Limit를 쓴다고 했으니 1로 설정해야 한다.

그러면 이것들(AVL, L, D/B, G,SegmentLimit) 조합해보면 11001111 = 0xCF

Base Address(31:24) 이것도 그냥 0

휴~~~ 값 넣기 힘들다…

그럼 다시 정리 하면

Segment Limit : 0xFFFF
Base Address(0~15) : 0x0000
Base Address(15~23) : 0x00
Type,S,DPL,P : 0x9A
Segment Limit, AVL, L, D/B, G : 0xCF
Base Address(24~32) : 0x00

S값을 1로 줬기 때문에 이 디스크립터는 코드 디스크립터 이고 실행과 읽기만 가능하다.

주소는 0~4G까지이다.

코드 디스크립터 이므로 CS 세그먼트에서 참조될 꺼다…..CS 외에 DS,ES 세그먼트에서 사용할 데이터 디스크립터도 만들다…

다 똑같고..

Segment Limit : 0xFFFF
Base Address(0~15) : 0x0000
Base Address(15~23) : 0x00
Type,S,DPL,P : 0x92
Segment Limit, AVL, L, D/B, G : 0xCF
Base Address(24~32) : 0x00

Type만 데이터 디스크립터임을 나타내고 읽고/쓰기 가능으로 바꿨다.

자~~ 이제 GDT 테이블을 다 완성했닷!!!!!!!!!!!!!

그러면..

GDT_TABLE:
                  dw 0xFFFF    ; Code
                  dw 0x0000
                  db  0x00
                  db  0x9A
                  db  0xCF
                  db  0x00
                  dw 0xFFFF    ; Data
                  dw 0x0000
                  db  0x00
                  db  0x9A
                  db  0xCF
                  db  0x00
GDT_TABLE_END

이렇게 GDT_TABLE은 구성될수 있겄다. 하지만… GDT_TABLE 구조체 첫번째 인덱스는 모두 NULL로 채워라고 했다.. (이게 이제 기억이 나네 ..흠…) 그래서 0으로 채우고 전체 소스는

;; creator : cyj (mangg@manggong.org)
;; date : 2014. 3. 27
;; desc : 커널 로더.
;;        1. 보호모드로 진입한다.

org 0x8000
bits 16

OS_LDR_ENTRY:

	;; CS, DS, ES, & SS을 초기화 하자.
	xor eax, eax
	mov ds, ax
	mov ss, ax
	mov es, ax
	mov sp, 0xFFFF
	mov bp, sp
	
    ; GDT 등록
    xor     eax, eax
    lgdt    [GDT_INFO]  
    cli
    mov     eax, cr0
    or      eax, 1
    mov     cr0, eax
	
	jmp		$    ; 무한루프.
	

GDT_INFO:

GDTSIZE      dw     GDT_TABLE_END-GDT_TABLE-1
GDTADDRESS   dd     GDT_TABLE

GDT_TABLE:
			dw 0x0000 ; null
			dw 0x0000
			db 0x00
			db 0x00
			db 0x00
			db 0x00
            dw 0xFFFF    ; Code
            dw 0x0000
            db 0x00
            db 0x9A
            db 0xCF
            db 0x00
            dw 0xFFFF    ; Data
            dw 0x0000
            db 0x00
            db 0x9A
            db 0xCF
            db 0x00
GDT_TABLE_END:

잘 되나 한번 확인!!!

반응이 없다…되었는지 안되었는지 모르겠지만 재부팅은 안된다..ㅎㅎㅎ

보호모드가 성공하면 화면에 출력하는 루틴을 넣어야 겠다.

어랏 , 보호모드에서는 BIOS 인터럽트를 호출 못하네?? 이제까지 int 0x10으로 처리했는데 안된단다.. 그럼 어떻게?? 흠… 전 포스트에서 리얼모드 설명하면 1M 가 다 예약 되어 있다는 글을 쓴적이 있다. 거기에 비디오 메모리 라고 해서.. 컬러, 흑백 주소가 있다고 했는데 거기 0xB8000 컬러 텍스트 비디모 메모리 영역에다가 알파벳 쓰면 출력된다고 한다. 그래?? 쉽겠군 .. 메모리 번지에 아스키 값만 던져주면 되니..

음….

32비트 보호모드로 바꿔 주고 나서 해줘야 하는게 있단다.

CPU 파이프 라인 때문에 mov cr0, eax 다음 명령어를 의미 없는 코드로 바꿔란다.. nop 요걸로..

왜?? CPU가 명령어를 받을 때 mov cr0, eax 명령 처리 한다음 다음 명령어를 읽는게 아니고 미리 명령어를 읽어 두기 때문에.. 그래서 보통

jmp $+2
nop
nop

요렇게 쓴단다.. 뭐 별수 있나.. 남들이 하니 해야지..

그리고 이제까지 리얼 모드에서 돌아가는 코드니깐 [bits 16] 이렇게 컴파일러 한테 알려줬는데 보호모드로 들어가면 32비트 이므로 [bits 32] 라고 해줘야 한단다.. 뭐 그까짓거 하면 되지 ~~~

그리고 제일 중요한데… 주소 어드레싱이 보호모드로 가면 틀려진단다..

리얼모드 일 때는 그냥 선형 주소라서 그냥 쓰면 되는데.. 이제 세그먼트(CS,DS,ES등등) 라고 부른던 놈을 셀렉터 라고 부르고 그 셀렉터는 위에서 정의한 GDT 테이블의 디스크립터를 가리킨다고 한다. 그럼 어떻게 가리킬까 생각 해 보다 GDT 테이블이 배열이고 그 배열 인덱스 번호를???

mov ax, 0x01
mov cs, ax

요렇게 말이지… ㅎㅎ 비슷한데 0x01이 아니란다…

os11-3

요게 셀렉터 모습인데 Index가 3~15비트 부터 이다.. 그래 0x01은 RPL 0비트에 1이라고 설정하는 것 뿐이다. 그러므로 GDT 테이블의 코드 디스크립터를 가리키려면

mov ax, 0x08
mov cs, ax

라고 위와 같이 해줘야 한단다..

os13-1

그리고 주소공간에는 위 그림과 같이 세그먼테이션 작업을 거치고 페이징 작업을 거치면 물리 주소 공간에 읽고/쓴다는데.. ㅎㅎㅎ 복잡하네..

그러니까 세그먼트 셀렉터는 GDT 테이블의 디스크립터를 가르키고 디스크립터는 선형 주소 공간에서 Base Address를 결정하고 다시 여기에서 Offset 만큼 떨어진 위치를 가르킨다. 맞나?? 맞나?? 맞겠지??

페이징은 패스… 아직 모르겠다.. 필요 할때 다시 찾아 봐야 할것 같다. 페이징은 다음에 하고 보호모드 진입하고 “OK” 단어 출력해 보는걸로 오늘은 마무리 해야겠다…

지친다…

;; creator : cyj (mangg@manggong.org)
;; date : 2014. 3. 27
;; desc : 커널 로더.
;;        1. 보호모드로 진입한다.
;;        2. OK 문자 화면에 출력

[bits	16]
org 0x8000

OS_LDR_ENTRY:
	;; CS, DS, ES, & SS을 초기화 하자.
	xor eax, eax
	mov ds, ax
	mov ss, ax
	mov es, ax
	mov sp, 0xFFFF
	mov bp, sp

    ; GDT 등록
    xor     eax, eax
    lgdt    [GDT_INFO]  

    cli
    mov     eax, cr0
    or      eax, 1
    mov     cr0, eax

    jmp     $+2
    nop
    nop

    jmp     0x08:Entry32


GDT_INFO:
			dw     GDT_TABLE_END - GDT_TABLE - 1
		    dd     GDT_TABLE

GDT_TABLE:
			dw 0x0000 ; null
			dw 0x0000
			db 0x00
			db 0x00
			db 0x00
			db 0x00
            dw 0xFFFF    ; Code
            dw 0x0000
            db 0x00
            db 0x9A
            db 0xCF
            db 0x00
            dw 0xFFFF    ; Data
            dw 0x0000
            db 0x00
            db 0x92
            db 0xCF
            db 0x00
GDT_TABLE_END:

[bits 32]
Entry32:
	xor		eax, eax
    mov     ax, 0x10
	mov		ds, ax
	mov		es, ax
	mov		ss, ax
    xor     esp, esp
    mov     esp, 0xFFFF

    mov     BYTE [0xB8000], 'O'
    mov     BYTE [0xB8002], 'K'

 hang:
	jmp hang

대충 이렇게 만들어 주고…

되나???

오~~~~~~~~~~~~잘된다..

 

 

'프로그래밍 > OS 만들기' 카테고리의 다른 글

OS 만들기 #15 커널-키보드 입력  (0) 2015.02.09
OS 만들기 #14 PE 재배치  (0) 2014.03.28
OS 만들기 #12 - PE 파일 포맷  (0) 2013.09.10
OS 만들기 #11 - 리얼모드 & 보호모드  (0) 2013.08.14
OS 만들기 #10  (0) 2013.08.14

PE 파일 포맷

지금 BaramOS는 커널을 Visual studio로 컴파일 하기 때문에 파일 포맷이 PE 포맷으로 생성된다. 그.래.서 또 PE 포맷이 어떻게 생겼는지를 확인해 봐야 한다. 역시나 쉬운게 없다. OS 만들기가 이리 힘들줄이야~ 여차 하면 컴파일러도(허~~세 작렬) 만들어 볼 생각인데 아마 더 죽어 버릴 듯하다. 그냥 컴파일러는 포기!!! 이거 하나 끝내기 힘드네..

여튼. PE 포맷 요놈이 어떻게 생겼는지를 알아야 한다. 그럼 PE 파일이 뭐냐~?~~

PE(Portable Executable) 포맷은 Windows 에서 만든 포맷으로 유닉스의 COFF(Common Object file format)을 기반으로 나왔다. 다양한 운영체제에서의 이식성을 보여준다는 뜻에서 이식이 가능한 실행 형식(Portable Executable)이라는 이름을 붙였다.                           - 위키백과

이식이 가능한 실행 형식이라는데 뭘 이식 한다는 건진 모르겠다. 찾아보니 파일을 다른곳에서 실행해도 실행이 될 수 있도록 규정한 형식이란다. 예전에는 파일이 만든 곳에서만 실행 되었나??? 흠.. 모르겠다. 패스~

갑자기 실행파일 이란 무엇일까 라는 의문이 들었다. 그냥 막연하게 프로그램을 실행시킬 수 있는 파일 이라고 머리속에서 떠올랐다. 그런데 이게 어떤 원리로?? (또 생각에 꼬리를 물기 시작한다 ㅡㅡ;;;)

나름 실행파일 이란 정의를 내려봤다. CPU가 연산, 제어 할수 있는 코드들을 파일안에 모아 놓고 또 연산, 제어만 하다 보니 데이터들이 필요 했을 것이다. 이 데이터들도 파일 안에 모아 놓았을 것이고.. 그럼 실행파일은 CPU 실행 코드와 데이터 코드들을 모아 놓은 파일. 이게 실행파일 이지 않을까 하고 생각해 봤다… 그런데 왜 굳이 PE포맷으로 실행파일들을 구성해 놨을까??? 음..동적 라이브러리 때문에??? 흠.. 실행파일 포맷이 만들어진 이유는 다음에 좀더 찾아 봐야 겠다. 너무 멀리 가는것 같으니깐…

PE 파일 포맷이라고 인터넷에서 검색해 보니 많은 글들이 찾아 진다. 그리고 자세히 분석 해 놓은 곳도 많다. PE파일에 관한 자료 찾는건 어렵지 않아 보인다. (아싸~) ㅎㅎ

파일 구조는

IMAGE_DOS_HEADER

IMAGE_NT_HEADER

IMAGE_SECTION_HEADER

.TEXT SECTION

DATA SECTION

IMAGE_DOS_HEADER 도스에서 쓰던 구조체란다. 총 64바이트로 구성되어 있단다.

typedef struct _IMAGE_DOS_HEADER {     // DOS .EXE header
    WORD   e_magic;                  // Magic number
    WORD   e_cblp;                    // Bytes on last page of file
    WORD   e_cp;                       // Pages in file
    WORD   e_crlc;                     // Relocations
    WORD   e_cparhdr;               // Size of header in paragraphs
    WORD   e_minalloc;              // Minimum extra paragraphs needed
    WORD   e_maxalloc;             // Maximum extra paragraphs needed
    WORD   e_ss;                      // Initial (relative) SS value
    WORD   e_sp;                      // Initial SP value
    WORD   e_csum;                 // Checksum
    WORD   e_ip;                       // Initial IP value
    WORD   e_cs;                      // Initial (relative) CS value
    WORD   e_lfarlc;                   // File address of relocation table
    WORD   e_ovno;                   // Overlay number
    WORD   e_res[4];                 // Reserved words
    WORD   e_oemid;                 // OEM identifier (for e_oeminfo)
    WORD   e_oeminfo;              // OEM information; e_oemid specific
    WORD   e_res2[10];             // Reserved words
    LONG   e_lfanew;                // File address of new exe header
  } IMAGE_DOS_HEADER, *PIMAGE_DOS_HEADER;

이 실행파일 첫부분에 64바이트를 위와 같은 구조로 저장되어 있다.

여기서 필요한건 e_magic 과 e_lfanew 이 두 변수만 필요하단다.

e_magic 은 Dos Signature로 4D5A 기본값이 저장되어 있다. 이게 아스키 값으로 “MZ” 이라는데 만든 사람 Mark zbikowski 이니셜이라고 한다.

e_lfanew는 IMAGE_NT_HEADER 구조체 위치란다. ???  IMAGE_DOS_HEADER 64바이트 다음에 IMAGE_NT_HEADER 아닌가?? 라고 생각해서 검색해 봤더니 IMAGE_DOS_HEADER와 IMAGE_NT_HEADER 사이에 도스에서 실행되는 코드가 있다고 한다..DOS Stub 란다. 16비트 명령어로 구성되어 있어서 32비트 윈도우즈에서는 실행되지 않는단다. (윈도우즈 프로그램을 도스에서 실행시킬 때 “This is program cannot be run in DOS mode” 이렇게 화면에 출력하는 코드) e_lfanew는 파일에서 IMAGE_NT_HEADER가 있는 절대 위치를 가르킨단다.

윈도우즈 실행파일 로더는 프로그램의 첫 2바이트의 MZ 이니셜을 찾고 IMAGE_NT_HEADER로 건너 뛴단다.(물론 도스모드에서 실행하면 바로 에러 메시지 출력하고 종료…)

자 그럼 IMAGE_NT_HEADER 구조체..

이 구조체가 본격적으로 PE 구조체란다.. (아…귀찮다..)

typedef struct _IMAGE_NT_HEADERS {
    DWORD Signature;
    IMAGE_FILE_HEADER FileHeader;
    IMAGE_OPTIONAL_HEADER32 OptionalHeader;
} IMAGE_NT_HEADERS32, *PIMAGE_NT_HEADERS32

Signature는 단어 그대로 올바른 PE 포맷인지 확인하기 위한 Signature란다. 위에 IMAGE_DOS_HEADER의 첫 MZ와 마찬가지로 PE 포맷을 나타내는 PE00 (0x00004550)이 기록되어 있어야 한단다. 이게 없으면 실행한된단다.

IMAGE_FILE_HEADER는 현재 이 파일이 DLL(동적 라이브러리)인지 EXE 실행 파일인지 구분 여부와 실행될 플랫폼(CPU), 섹션등이 저장된 구조체 란다. (점점… 복잡하게 추적해야 할 구조체가 많아진다.. ㅡㅡ)

typedef struct _IMAGE_FILE_HEADER {
    WORD    Machine;
    WORD    NumberOfSections;
    DWORD   TimeDateStamp;
    DWORD   PointerToSymbolTable;
    DWORD   NumberOfSymbols;
    WORD    SizeOfOptionalHeader;
    WORD    Characteristics;
} IMAGE_FILE_HEADER, *PIMAGE_FILE_HEADER;

Machine 은 어떤 CPU 에서 실행되는지 정보. 뭐 Intel 386인지, Intel64,MIPS, ALPHA 에 관한 정보가 기록된단다.

NumberOfSections은 단어 그대로 섹션의 갯수가 기록되어 있단다. 자. 그럼 섹션은 무엇일까??? 실행파일은 적어도 코드 와 데이터 이렇게 구분된단다. 이걸 코드와 데이터로 분류해서 섹션?이라는 이름으로 저장하는걸 말한단다. 기본적으로 코드(text), 데이터(data), 리소스(.rsrs) 이렇게 이름이 된다고 한다. 이게 프로그램이 크면 섹션이 많이 존재하게 되는데 이 섹션의 갯수를 말한단다.

TimeDataStamp는 파일이 생성된 날짜와 시간이 기록된단다.

PointerToSymbolTable, NumberOfSymbols 은 유닉스 시스템에서 사용하던 것으로 지금은 사용 안한단다.

SizeOfOptionHeader요놈은 IMAGE_OPTIONAL_HEADER32구조체의 크기이다. 배열도 아니고 포인터도 아닌데 왜 크기를 명시해야 한단다. 왜? 그냥 찾아보긴 했는데 명확한 곳은 없고 64비트 일 경우에 IMAGE_OPTIONAL_HEADER32구조체가 크기가 다르단다. (뭐, 64비트 일 경우 IMAGE_OPTIONAL_HEADER64 구조체를 사용할텐데….) 그래서 크기를 명시해 놔야 한다는데 맞는건진 모르겠다.

Characteristics 파일 속성 값으로 DLL 인지 실행파일인지 등을 정의 해 놓은 속성이란다.

자… IMAGE_FILE_HEADER 했으니 IMAGE_OPTIONAL_HEADER32 해야 겠네… 이 구조체는 PE 헤더 구조체중 제일 크기가 크단다. 이 말은 알아야 할게 많다는 말이지..흠..

typedef struct _IMAGE_OPTIONAL_HEADER {
  WORD                 Magic;
  BYTE                 MajorLinkerVersion;
  BYTE                 MinorLinkerVersion;
  DWORD                SizeOfCode;
  DWORD                SizeOfInitializedData;
  DWORD                SizeOfUninitializedData;
  DWORD                AddressOfEntryPoint;
  DWORD                BaseOfCode;
  DWORD                BaseOfData;
  DWORD                ImageBase;
  DWORD                SectionAlignment;
  DWORD                FileAlignment;
  WORD                 MajorOperatingSystemVersion;
  WORD                 MinorOperatingSystemVersion;
  WORD                 MajorImageVersion;
  WORD                 MinorImageVersion;
  WORD                 MajorSubsystemVersion;
  WORD                 MinorSubsystemVersion;
  DWORD                Win32VersionValue;
  DWORD                SizeOfImage;
  DWORD                SizeOfHeaders;
  DWORD                CheckSum;
  WORD                 Subsystem;
  WORD                 DllCharacteristics;
  DWORD                SizeOfStackReserve;
  DWORD                SizeOfStackCommit;
  DWORD                SizeOfHeapReserve;
  DWORD                SizeOfHeapCommit;
  DWORD                LoaderFlags;
  DWORD                NumberOfRvaAndSizes;
  IMAGE_DATA_DIRECTORY DataDirectory[IMAGE_NUMBEROF_DIRECTORY_ENTRIES];
} IMAGE_OPTIONAL_HEADER, *PIMAGE_OPTIONAL_HEADER;

많다….….

Magic 이 구조체가 32비트인지(IMAGE_OPTIONAL_HEADER32) 64비트인지 (IMAGE_OPTIONAL_HEADER64) 구분자란다. 32비트는 10Bh, 64비트는 20Bh .

MajorLinkerVersion, MinorLinkerVersion 컴파일러 버전 정보.

sizeOfCode는 코드 영역에 전체 크기를 말한단다.

sizeOfInitializedData 초기화 된 데이터 영역 전체 크기를 말한단다.

sizeOfUnInitializedData 초기화 되지 않은 데이터 영역 전체 크기를 말한다.

AddressOfEntryPoint는 Entry Point는 프로그램의 시작 위치이다. 그 시작 위치의 RVA값이란다. RVA(Relative Virtual Address)로 상대적 가상 주소를 말한다. 상대적 가상 주소.. 단어 어렵네… 가상의 주소 인데 상대적이란다. 뭐에 대한 상대적??? 요 밑에서 언급 될 ImageBase의 상대적 가상 주소 란다. 즉 ImageBase에서 AddressOfEntryPoint값 만큼 떨어진 위치에 Entry Point가 있다는 말이다. ImageBase에서 RVA만큼 떨어진.. 그럼 Offset 과 동일한 개념이라고 보면 되는건가? 음.

BaseOfCode 또한 RVA 값이란다. 이것도 ImageBase에서 RVA 값 만큼 위치에 Code의 시작 위치가 있다는 말인가 보다.

BaseOfData 위에 코드랑 똑같이 RVA 값.

ImageBase는 프로그램이 메모리에 적재될 위치를 가르킨단다. 보통 0x400000은 실행파일, 0x10000000은 DLL이란다. 물론 다른 값으로 지정될 수 있단다. 그러니까 메모리 로딩 시작 위치에서 RVA로 각 Entry Point, Code, Data위치를 기록하나 보다. (오프셋 개념인듯…). 아. DLL은 0x10000000인데 만약 여러 DLL들이 0x10000000로 ImageBase가 기록되어 있으면(미리 하나의 DLL이 메모리에 선점되어 있으면) 다른 메모리 주소로 PE 로더가 재배치 한단다.

SectionAlignment는 메모리에서 섹션(.TEXT, .DATA)의 최소 단위 크기란다. 이 크기보다 섹션의 크기가 커지면 이 섹션 크기 배수만큼 크기여야 한단다.

FileAlignment는 PE 파일 내에서 섹션의 최소 단위 크기.

뭐 기타 Version 정보는 건너 뛰고~~~ ^^

SizeOfImage는 PE 파일을 메모리에 올렸을 때 전체 크기를 말한단다. 파일 크기가 달라 질수 있는데 이건 SectionAlignment의 영향을 받는단다. (섹션 다음에 패딩이 붙는 다든지.. 등등)

SizeOfHeaders는 PE 포맷의 모든 헤더의 크기를 말한단다. 즉 파일 시작 위치에서 SizeOfHeaders 오프셋 뒤에 첫번째 섹션이 존재한단다. 이 SizeOfHeaders의 값은 FileAlignment값의 배수가 되어야 한단다.

Checksum 파일의 변경 유무 판단할 때 사용.

SubSystem은 파일이 드라이버 파일(1)인지 GUI 프로그램인지(2), CUI 프로그램인지를 나타내는 값.

SizeOfStackReserve 프로그램에서 사용할 스택 예약 크기.

SizeOfStackCommit 프로그램에서 사용할 스택 크기.

SizeOfHeapReserve 프로그램에서 사용할 힙 예약 크기.

SizeOfHeapCommit 프로그램에서 사용할 힙 크기.

LoaderFlags 사용하지 않음.

NumberOfRvaAndSizes 밑에 IMAGE_DATA_DIRECTORY구조체 배열 DataDirectory 크기.

DataDirectory 는…음.. IMAGE_DATA_DIRECTORY 구조체의 배열이다.

typedef struct _IMAGE_DATA_DIRECTORY {
    DWORD VirtualAddress;
    DWORD Size;
} IMAGE_DATA_DIRECTORY, *PIMAGE_DATA_DIRECTORY;

IMAGE_DATA_DIRECTORY구조체에는 VirtualAddress변수와 Size가 있다. 이게 Export, Import, Resource, Exception 등등의 위치와 크기를 정의하는 것이라는데.. 별로 커널쪽에선 중요하진 않을 것 같아 패쓰~

휴~~~

IMAGE_NT_HEADER 파일 뒤에는 섹션에 관련된 IMAGE_SECTION_HEADER가 따라 온다. 뭐 섹션 테이블이라고도 불리운다고 한단다.

typedef struct _IMAGE_SECTION_HEADER {
    BYTE    Name[IMAGE_SIZEOF_SHORT_NAME];
    union {
            DWORD   PhysicalAddress;
            DWORD   VirtualSize;
    } Misc;
    DWORD   VirtualAddress;
    DWORD   SizeOfRawData;
    DWORD   PointerToRawData;
    DWORD   PointerToRelocations;
    DWORD   PointerToLinenumbers;
    WORD    NumberOfRelocations;
    WORD    NumberOfLinenumbers;
    DWORD   Characteristics;
} IMAGE_SECTION_HEADER, *PIMAGE_SECTION_HEADER;

IMAGE_SECTION_HEADER 구조체 데이터가 IMAGE_FILE_HEADER구조체의 NumberOfSections값만큼 있단다..(섹션의 갯수만큼..)

Name은 섹셕의 이름인데 NULL도 채워도 되나 보통 .text, .data, .rdata, .bss, .idata, .edata 등이 보통 관례대로 사용된단다.

VirtualSize 섹션이 메모리에 올라갈 때 섹션 크기. (SectionAlignment 배수)

VirtualAddress 섹션이 메모리에 올라 갈 메모리 위치. RVA 값이다. 즉 ImageBase + VirtualAddress .

SizeOfRawData 메모리가 아닌 실제 PE 파일내에서 섹션의 크기. (FileAlignment 배수)

PointerToRawData PE 파일내에서 섹션이 시작하는 위치(파일 오프셋).

PointerToRelocation obj 파일에서만 사용되고 실제 실행파일에서는 0 이 된단다.

PointerToLinenumbers COFF 스타일의 라인번호를 위한 파일 오프셋(??) . 모르겠다 이건.

NumberOfRelocation 요것도 0.

PointerToLinenumbers COFF 라인번호가 PE 에 첨부되었을 때만 사용.(???) 진짜 모르겠다.

Characteristics 섹션의 속성. (CODE 인지 DATA, 실행, 읽기, 쓰기) OR 연산.

     0x00000020 코드 섹션
     0x00000040 초기화 된 데이터 섹션
     0x00000080 초기화 되지 않은 데이터 섹션
     0x20000000 실행 가능한 섹션
     0x40000000 읽기 가능한 섹션
     0x80000000 쓰기 가능한 섹션.


자… 이제 필요한건 다 했다. IAT, EAT 이런것들은 아직… 필요 없다고 판단되었다. 필요 할때 다시 살펴볼 생각… IMAGE_SECTION_HEADER 다음에 실제 섹션 데이터들이 온단다. 뭐 이제 PE 포맷 구조에 대충 훑어 봤다. 휴~~~ 시간이 많이 걸렸다..(노느니라~~~)

정리가 필요하다….정리…

실행파일은 PE 포맷으로 구성되어 있다. 뭐 이런 저런 잡다한 것 다 빼고…

코드, 데이터 섹션 이런식의 섹션별로 실행 코드, 데이터를 분류해 놓았다.

이걸, IMAGE_NT_HEADER 구조체 안에 있는 IMAGE_OPTIONAL_HEADER 구조체의 ImageBase 값. 메모리에 실행파일을 로딩해 놓는다.

이 때, 각 섹션은 ImageBase + VirtualAddress(IMAGE_SECTION_HEADER 구조체 내) 의 RVA 위치에 섹션을 배치한다.

다시… 정리하면..

실행파일에서 ImageBase 값을 구하고 섹션의 갯수를 구한 다음. 각 섹션을 ImageBase + RVA 위치에 메모리에 올린다.

라고 정리가 된다. 흠…

 

'프로그래밍 > OS 만들기' 카테고리의 다른 글

OS 만들기 #14 PE 재배치  (0) 2014.03.28
OS 만들기 #13 보호모드 진입  (0) 2014.03.27
OS 만들기 #11 - 리얼모드 & 보호모드  (0) 2013.08.14
OS 만들기 #10  (0) 2013.08.14
OS 만들기 #9 - 부트로더  (0) 2013.08.14

  많은 시간을 이것저것 미리 살펴 보느라 오래 걸렸다. 처음에 너무 대책 없이 시작하다 보니 이것저것 삐걱거리고 다시 학습해야 할 내용들이 많아서 당분간 쉬었다(라고 쓰고… 그냥 귀찮아서 놀았다…) 음.. 기존 내 BaramOS(바람OS)는  부트로더 에서 커널로더를 거친 후 커널로 진입하는 방식으로 구현되어 있다. VC2010에서 커널을 만들다 보니 부트로더에서 바로 호출 할 수 없었다. 이유는 http://manggong.org/blog/64 에 간략하게 적은 내용대로 VC2010컴파일러가 32비트 컴파일러 이기 때문이다. (NASM의 obj 파일을 VC에서 링크를 할 수 없다라는게 이유. MASM으로 하면 된다는데… 것도 문제.. 흠..) 또 VC 컴파일러다 보니 PE 파일 포맷이다. (요것도 따로 배워야 한다… 메모리 재배치라나 뭐래나~~). 뭐 인터넷에 검색해 보니 부트로더에서 바로 커널 불러들이는 OS들도 있는 것 같지만..그냥 이대로 갈 생각이다.




리얼모드


부트로더, OS_LDR.SYS는 현재 16비트에서 동작한다. 요걸 달리 리얼 모드 라고 한다고 한다. 이름도 참… 리얼이 뭐냐!~~ 그럼 가상모드 도 있단 말이냐?? ㅋㅋ 헉… 검색해보니 가상모드 도 있다. 흠흠흠…. 여튼. 리얼모드는 리얼 주소 모드 또는 호환 모드 라고 하며, 80286이후의 x86호환 CPU 운영 방식이라고 위키백과 에서는 설명하고 있다. 뭔 소리냐??? 좀더 더 쉽게 풀어 달란 말이다. 흠… 검색하자… 리얼모드는 호환 모드라고 한다고 했다. 즉, 80286 이전에 CPU를 호환하기 위한 모드 라고 한다. 왜 호환 하는 거야?? 기존 만들어 놨던 소프트웨어 지원할라고??? 몰겠다. 다시 리얼모드로 돌어와서 리얼 모드라는 이놈은 1M 이상의 메모리를 접근 할 수 없단다. 왜??? 만들 당시 1M도 큰 메모리였단다.. ㅎㅎㅎ  1M이기 때문에 메모리에 액세스 할 주소는 20비트면 충분 했나 보다…(에효 좀더 크게 좀 잡아놓지…. 20비트가 뭐냐?? 계산하기 빡 세게…) 20비트로 주소를 계산했단다… 어떻게??? 세그먼트:오프셋 요렇게 말이다. 근데 세그먼트 크기는 16비트이다. 20비트에서 4비트 부족하다. 그래서 오프셋 이란 놈을 더 뒀다. 그러면 오프셋이 나머지 4비트일까??? 아니다. 요놈도 16비트다. 그럼 32비트 쓴다는거?? ㅋㅋ 그랬으면 얼마나 좋았을 까? 흠. 졸라 복잡한 계산식으로 20비트 만든단다. 졸라 어렵다. 세그먼트를 왼쪽으로 4비트 시프트 시키고 거기에다가 오프셋을 더해 준단다.  예를 들어.. 세그먼트 = 0x0700와 오프셋 0x0C00 라면 세그먼트 4비트 왼쪽 시프트해서 0x7000 만들고 여기에 0x0C00 더해 주는 방식이다.


  0 7 0 0 0
+   0 C 0 0
    7 C 0 0


이렇게 졸라 복잡한 계산식(?)으로 1M까지 메모리를 접근했다.


근데 내가 초딩 때 봤던 도스가 리얼모드에서 동작했다고 한다. 여기서 급 궁금증이 생겼다. 도스라면 640KB밖에 여유 메모리가 없었던 기억이 있다. 게임 하나 실행 할라 치면, 메모리 상주 프로그램 다 지워줘야 했다. 1M라며????


1MB안에서 다시 요놈 저놈이 쓸 메모리 공간 다 주고 640KB가 남은 거란다. 640KB도 크게 느껴졌다고 하니~^^ ㅋㅋㅋ


0x00000000 - 0x0009FFFF   RAM
  0x00000000 - 0x000003FF 리얼모드용 인터럽트 벡터
  0x00000300 - 0x000003FF BIOS DATA 영역
  0x00000400 - 0x000004FF BIOS가 사용하는 영역
  0x00000500 - 0x00007BFF 임시영역? ( 자유 영역, 도스 파라미터 영역)
  0x00007C00 - 0x00007DFF 부트 영역
  0x00008000 – 0x0009FBFF 자유영역
  0x00080000 – 0x0009FBFF 자유영역
  0x0009FC00 - 0x0009FFFF 확장 BIOS DATA영역
0x000A0000 – 0x000BFFFF   비디오 카드 접근 영역
  0x000A0000 - 0x000AFFFF 그래픽 모드 비디오 메모리
  0x000B0000 – 0x000BFFFF 흑백 테스트 모드 비디오 메모리
  0x000B8000 –0x000BC000 컬러 텍스트 모드 비디오 메모리
0x000C0000 - 0x000C7FFF   비디오 BIOS
0x000C8000 – 0x000EFFFF   각종 카드의 ROM 영역
0x000F0000 – 0x000FFFFF   메인보드 BIOS

(출처 : http://wiki.osdev.org/Memory_Map_(x86))


요기 위 처럼 1M를 이넘 저넘 예약 되어 있다. 휴~ 리얼모드에서 플밍 하기 참~ 힘들었겠다는 생각이 단다. 뭐~ 힘들게 만든 프로그램 컴퓨터에서 실행시키기도 힘들었다. 무슨 설정을 그리 해야 했는지..




보호모드


80286 맹글면서 메모리 버스를 24bit로 늘렸단다. 뭔 말이냐면 메모리를 24bit 만큼 쓸 수 있다는 것이다. 16M인데?? (참~~~ 많기도 하다..ㅋ ) ㅎㅎ 80386에서는 요게 32bit로 늘렸단다. 그래 드뎌 4GB 메모리가 참조 가능해진다. 뭐, 요즘 요것도 적은 편이지만… 64비트가 나온판에 ㅋㅋㅋ 여튼. 보호모드에서는 세그먼트 레지스터를 세그먼트 셀렉터(selector)라는 이름으로 바꾸었단다. 왜 바꾸냐?? 머리 아프게 시리.. 세그먼트 레지스터는 물리 주소 자체를 가르키는 역활을 했으나 보호모드에서는 세그먼트 셀렉터 이놈은 주소 공간을 가르키는 게 아니고 디스크립터 테이블의 디스크립터 이라는 놈 자체를 가르킨다고 한다. 그럼 디스크립터 테이블 이놈이 무엇이냐?? 8바이트로 구성된 코드 세그먼트 디스크립터, 데이터 세그먼트 디스크립터 등등의 세그먼트 디스크립터를 모아 놓은게 디스크립터 테이블이란다. 이 중 한놈을 세그먼트 셀렉터 라는 놈이 가르킨단다. 딱 보니, 디스크립터 구조체 배열의 인덱스 번호를 세그먼트 셀렉터가 가르키는 꼴이다. 흠..디스크립터 구조는 다음과 같다.

 

겁나 복잡하게 생겼다. 저 Base Address 주소 가르키는 구조좀 봐라 … 아주 조각을 내놨다. 얼씨구 Segment Limit도 조각 냈네.. 요거 설계 하는 사람이 머리에 스팀 좀 받았겠다… 그러니 요거 배우는 나는 더 스팀 받을 수 밖에…ㅋㅋㅋㅋ 여튼 우습다. 흠.. 각 설명은 요렇다는 데..

  • Base Address : 세그먼트의 시작 주소를 가르킨다. 32비트 크기이므로 0~4GB까지 가능하다.
  • Segment Limit : 세그먼트의 크기를 나타낸다. 20비트의 크기로 0~1MB까지 가능하다. G 비트가 1로 설정되면 4KB를 여기에다 곱해준단다. 그럴 경우 0~4GB까지 설정 가능하단다.
  • Type : 4비트 크기로 세그먼트의 타입을 지정한단다. 코드 또는 데이터 로 말이다. 근데 여기에 읽기 쓰기 권한을 비트로 준단다. (엠병)
    0bit 1bit 2bit 3bit Type 설명
    0 0 0 0 0 데이터 데이터 세그먼트 읽기 전용
    1 0 0 0 1 데이터 데이터 세그먼트 읽고/ 접근 여부 (접근 여부 : 디스크립터가 참조 될 때 1로 설정함)
    2 0 0 1 0 데이터 데이터 세그먼트 읽고/쓰기
    3 0 0 1 1 데이터 데이터 세그먼트 읽고/쓰기/접근 여부
    4 0 1 0 0 데이터 데이터 세그먼트 읽기 전용/역방향 (역방향: 스택처럼 상위 주소에서 하위 주소로 자라남)
    5 0 1 0 1 데이터 데이터 세그먼트 읽기 전용/역방향/접근여부
    6 0 1 1 0 데이터 데이터 세그먼트 읽고/쓰기/역방향
    7 0 1 1 1 데이터 데이터 세그먼트 읽고/쓰기/역방향/접근 여부
    8 1 0 0 0 코드 코드 세그먼트 실행 전용
    9 1 0 0 1 코드 코드 세그먼트 실행전용/접근 여부
    A 1 0 1 0 코드 코드 세그먼트 실행/읽기
    B 1 0 1 1 코드 코드 세그먼트 실행/읽기/접근여부
    C 1 1 0 0 코드 코드 세그먼트 실행 전용/접근가능 (접근가능: (권한에 관계없이 모두 접근 가능)
    D 1 1 0 1 코드 코드 세그먼트 실행 전용/접근가능/접근 여부
    E 1 1 1 0 코드 코드 세그먼트 실행/읽기/접근 가능
    F 1 1 1 1 코드 코드 세그먼트 실행/읽기/접근 가능/접근여부
  • S : 디스크립터 타입을 가르키신단다. 0 일때는 시스템 디스크립터, 1일 때는 걍 디스크립터(코드, 데이터)
  • DPL : 2비트로 세그먼트 접근 권한(? 요건 또 뭐냐..) 을 주신단다. 주로 0은 커널, 3은 유저(?) 접근 가능 하단다.
  • P : 페이징과 관련 있단다.
  • AVL : 임의 용도란다.
  • L : 64비트 코드 세그먼트인지, 32비트인지를 의미한단다. 1: 64, 0:32비트 ..
  • D/B : 16비트용인지 32비트용인지를 의미한단다. 1: 32비트, 0: 16비트
  • G : Segment Limit에 4Kb를 곱할지 안 곱할지.


빡시다… 겁나 복잡(?) 하다. 뭐가 이리 복잡하게 해 놓은 건지 원… 여튼, 세그먼트 셀렉터는 이놈들 중 하나의 인덱스 번호를 가지고 있고 해당 디스크립터가 가리키는 메모리에 접근 한단다. 뭐, 메모리 조각조각 내서 사용하는 페이징은 사용하지 않으면 실제 물리 주소고 여기에 페이징을 사용한다면 그냥 가상 주소라고 한단다. 뭐 페이징은 아직까지 사용 할 생각 없으니, 물리 주소라고 봐도 무관하겠다.


흠 근데 이런 디스크립터 테이블이 3종류나 있단다. 아놔!~~~~ 보호모드 겁나 짜증나는 놈이네…


GLOBAL DESCRIPTOR TABLE 이라는 GDT 라는 놈하고

LOCAL DESCRIPTOR TABLE 이라는 LDT 라는 놈하고

INTERRUPT DESCRIPTOR TABLE 이라는 IDT 테이블


 요렇게 3개 있단다. 그러면 왜 3개씩이나 있는 이유가 뭐냐??? 물리주소를 가르키는 얘라며~!!!!!!!!!!!!!!! 이런게 3개 있을 필요가 있냐고!!!!~~~휴~~ 진정해야지…


GDT : 전역 디스크립터 테이블이라고 하며 커널에서 사용하기 위해서 구성된다고 한다.
LDT : 지역 디스크립터 테이블이라고 하며 보통 어플리케이션 에서 사용하기 위해 구성된다고 한다.
IDT :  각 인터럽트를 사용하기 위해 구성된다고 한다.


GDT와 LDT 디스크립터 구성은 동일 한단다. (그래~~~)  위에 표기한 디스크립터 구조다.

IDT 구조는 다음과 같다.


  • 세그먼트 셀렉터(2바이트) : GDT 던 LDT 던 이 놈들이 가지고 있는 디스크립터 인덱스로 설정한다.
  • 오프셋(4바이트) : 디스크립터에 설정된 Base Address에서 + 오프셋 한 값.
  • P(1비트) : 디스크립터가 물리 메모리 영역인지 아닌지 여부를 나타내는 것 같은데 1로 설정하란다. 0은 사용안하거나 Paging 처리 할때.
  • DPL(2비트) : 특권 레벨이라는데 디스크립터의 DPL 처럼 접근권한. 즉 0은 커널, 3은 유저. 요것도 0.
  • S(1비트) : 요건 모르겠다. 그냥 0 .
  • Type(4비트) : Gate 타입을 입력.
    0101 0x5 5 80386 32bit Task Gate
    0110 0x6 6 80286 16bit Interrupt Gate
    0111 0x7 7 80286 16bit Trap Gate
    1110 0xE 14 80386 32bit Interrupt Gate
    1111 0xF 15 80386 32bit Trap Gate

    Interrupt Gate : 하드웨어 인터럽트를 위해 만들어 진 넘.
    Trap Gate : 소프트웨어 인터럽트나 Exception 처리하기 위해 만들어 진 넘
    Task Gate : Task Switching을 위해 만들어 진 넘.

IDT는 Interrupt vector table을 구현해 놓은 놈인데
- 소트트웨어 인터럽트
- 하드웨어 인터럽트
- 프로세서 Exception

의해 사용 된단다.


정리하믄 IDT는 GDT나 LDT에 있는 디스크립터가 가리키는 메모리 세그먼트에서 offset 만큼 위치의 함수를 호출하나 보다.

아흐… 겁나 머리 아프다. 뭐 이리 알아야 할게 많아~~~!!!!


흠…위에서 IDT에서 언급한 특권 레벨, 디스크립터의 접근 권한 요거에 대해서 더 살펴 봤더니 0,1,2,3 요렇게 권한등급이 있는데 0은 코널 수준이고 3은 유저(어플리케이션)에서 접근 할 수 있는 없는지를 나타내는 거란다. 보통 0과 3만 쓴다고 한다. 현재 불리우는 놈의 권한 기록은 세그먼트 셀렉터에 있다고 한다. 엥?? CS, DS, ES, FS 에???

세그먼트 셀렉터의 구조다. 보호모드로 바뀌면서 저렇게 구조를 나눠서 사용하나 보다. Index는 디스크립터 테이블에서 인덱스 값 이고 TI  요놈은 GDT(0) 테이블을 참조 할 것인가 LDT(1) 테이블을 참조 할 것인가 구분자이고 RPL 요놈이 접근하는 특권 레벨 을 가지고 있는 놈이란다. 요넘이 3인데 권한이 0인 놈을 접근 하면 CPU는 에러를 뱉어 낸다는군.

자!!~~~ 이제 또 뭘 알아야 하냐???




GDTR, LDTR, IDTR


흠… 힘들다..귀찮고…

보호 모드 진입을 하기 위해선, GDT는 이미 구성되어 있어야 한다고 한다… 메모리에 그냥 만들어 놓으면 되나??? 아니다. GDT를 읽는 레지스터가 있다고 한다. 이름하야 GDTR 이 레지스터에 GDT 테이블을 메모리 위치를 넘겨 주면 된다고 한다. 그럼 그냥 mov GDTR, [메모리 위치] 냐… 것도 아니다. lgdt 라는 어셈 명령어를 써서 넘겨야 한다. 또한 lgdt 명령어에 테이블 메모리 위치가 아니고 GDT의 총크기와 메모리 위치를 넘겨야 한단다.

struct
{
    unsigned short GDT_Size;
    unsigned int GDT_Address;
} GDT_INFO

lgdt GDT_INFO

요런 방식으로 명령을 주면 GDTR 레지스터에 GDT가 들어간다고 한다. 흠… 그래 그리고 ? 또… GDT 구성에 있어서 첫번째 인덱스는 NULL 이여야 한다고 한다. NULL??? 아무값도 없이 0으로 채워주면 된다고 한다. 사용되지 않는다는데 뭘…흠… 왜?? 모르겠다. 그냥 그래 라고 하니 그래야지. 하지만 또 성실하게 검색 질  해봤더니 “초기화 과정에서 사용되지 않은 레지스터를 통해 메모리에 접근하려는 시도를 막기 위함이다.” 라고 하네…뭔 소리인지..흠.


LDT를 LDTR에 입력하려면, 우선 GDT에 LDT에 관한 디스크립터를 만들어야 한다고 한다. 왜??? LDT는 전역이 아니고 프로세스에 대응하는 테이블이기 때문에 GDT와 같은 속성이 아니란다. (뭐… 맥락은….) 그래서 LDT 위치를 GDT에 우선 등록해주고 GDT의 인덱스값을 LDTR에 입력해 주면 지가 알아서 세그먼트 위치, 크기, 속성까지 로딩한다고 한다. (편하네~~~)


mov ax, GDT내의 LDT인덱스값
lldt ax


IDTR에 IDT를 넣는 방법은 GDTR과 유사하다.

struct
{

   unsgined short IDT_SIZE; (256*8-1)
   unsigned int IDT_ADDRESS;
}


256에다가 8(IDT 디스크립터 크기)을 곱하고 빼기 1을 해준단다.
근데 256개의 디스크립터를 모두 생성해 줘야 하는지 모르겠다. 그러니까 256이 아닌 10개만 등록하면 안 되는지 말이다. 흠…뭐 해봐야 알겠지.




A20 GATE


여기까지 세그먼트 셀렉터로 시작해서 디스크립터 테이블, GDTR,LDTR,IDTR까지 꼬리를 물고 배워왔다. 뭐~~ 더 뭐가 필요한지 모르겠지만.. 완전 지친다. 왜 이리 처리해 줘야 하는 게 많은지..

아… 인터넷에서 보호 모드 검색하다 보니 A20 GATE 라는 놈이 등장했다.

앞에서 80286에서 20비트 에서 24비트로 메모리 버스가 늘었다고 했다. 16MB까지…. 헌데 리얼모드에서는 20비트 밖에 못쓴다. 세그먼트:오프셋 이 방식 때문에 말이다. 기존 8086/8088 요넘들은 0xFFFF:000F 까지 메모리를 액세스 할 수 없었다. 왜?? 20비트 넘어가면 이상한 주소가 참조해 버리니 원하는 위치를 벗어나 버리기 때문이다. 암만 0xFFFF:1234 주소로 입력한들 .. 0x1224 주소로 접근한다. 메모리 접근은 20비트니 20비트 이상은 다 없어지는 거지. 근데 80286은 24비트다. 뭔말이냐 하믄  0xFFFF:1234를 주면 0x101224로 접근이 되서 메모리를 읽고 쓸 수 있다는 말이다. 호~~~ 그럼 16M까지 접근 가능하겠네?? 라고 생각했다가 세그먼트:오프셋 주소 지정 방식을 생각해 보니, 웃음만 나왔다. ㅋㅋㅋ  0xFFFF:FFFF = 0x10FFEF 까지만 가능하다. ㅋㅋㅋㅋㅋ 메모리 버스를 24비트로 늘려도 쓸 수 없다 ㅋㅋㅋㅋ 기존 0xFFFFF에서 64kb 정도만 더 쓸 수 있다. ㅋㅋ 아놔 것도 많다면 많은 거지만.. 왜케 웃기지 ㅋㅋㅋㅋ

여튼 0x10FFEF까지 주소의 64kb 메모리라도 더 쓸라면 A20 GATE 요놈을 이용해야 한다. 요놈이 Keyboard 컨트롤러에 달려 있는 요놈을 ON 시켜야 쓸 수 있단다. 왜 키보드 컨트롤러 일까? 라고 의문이 들었지만 오늘 많은 검색질로 인해서 지쳤다….여튼 요놈을 ON 시켜주면 쓸 수 있다고 한다.. 어떻게??? 메모리 버스 21비트에 A20과 AND 연산기와 묶여 있단다. 그래서 A20 GATE에 ON(1)이 되어 있어야지만 21비트에 접근 가능하단다. 왜 굳이 A20 GATE를 달아서 AND 연산 시키는 괴상망측한 짓이 진행되었는지는…..모르겠다. 여튼, 요넘 때문에 보호모드에서 메모리 접근 할 때 문제가 생긴다. 왜??? A20 GATE가 ON이 안되어 있으면 21비트는 항상 0 이기 때문에…. 그래서 보호 모드 넘어 갈 때는 항상 A20 GATE를 켜고 진입해야 한다고 한다.

'프로그래밍 > OS 만들기' 카테고리의 다른 글

OS 만들기 #13 보호모드 진입  (0) 2014.03.27
OS 만들기 #12 - PE 파일 포맷  (0) 2013.09.10
OS 만들기 #10  (0) 2013.08.14
OS 만들기 #9 - 부트로더  (0) 2013.08.14
OS 만들기 #8 - 부트로더  (0) 2013.08.14

+ Recent posts