$bash

0x05_플로피 디스크에서 OS이미지 로딩

0x0500 : 기술, 분석 문서/0x0503 : OS 원리와 구조

BIOS서비스와 소프트웨어 인터럽트

BIOS 에선 키보드/마우스같은 주변기기 에 대한 제어하는 기능을 제공한다. 16비트의 OS를 구현한다고 하면 BIOS만 활용해서 OS를 개발 할 수 있다.


BIOS는 기존의 라이브러리 파일과 달리 자신의 기능을 외부적으로 특별하게 제공한다. 함수의 어드레스를 인터럽트 벡터 테이블(Interrupt Vector Table)에 넣어두고, 소프트웨어 인터럽트(SWI, Software Interrupt)를 호출하는 방법을 사용한다. 인터럽트 벡터 테이블은 메모리 어드레스 0에 있는 테이블로 특정 번호의 인터럽트가 발생했을때 인터럽트를 처리하는 함수검색에 사용한다. 인터럽트가 발생했을 때 처리하는 함수 어드레스가 저장되어있으며, 각 항목은 크기가 4바이트이다. 또한 인터럽트는 최대 256개 까지 설정할 수 잇으므로 리얼 모드의 인터럽트 벡터 크기는 최대 1024(256 * 4)바이트가 된다.


여기서 BIOS가 제공하는 플로피 디스크 서비스를 이용하려면 0x13 인터럽트(Disk I/O Service)를 발생해야 된다. SWI는 CPU에 가상으로 특정 인터럽트가 발생했다고 알리는 명령어로 int 0x13 형태로 사용한다. 또한 함수의 어드레스를 인터럽트 벡터 테이블에 넣어뒀다면 int 명령으로 언제든지 해당 함수로 이동할 수 있다.


결론만 말하자면 BIOS 서비스는 SWI로 호출을 할 수 있지만 결국 독자적으로 사용은 못한다. 결국 관련된 파라미터를 넘겨 줘야 되는데 그때 사용되는게 레지스터이다.


// 관련된 이미지가 없다... 흐앙


OS 이미지 로딩 구현

Bashsi OS에서 이미지는 크게 부트로더, 보호 모드 커널, IA-32e 모드 커널로 구성되어 있다. 각 부분은 섹터 단위로 정렬해서 하나의 부팅 이미지 파일로 합치며 디스크의 두번째 섹터부터 읽어서 특정 메모리 어드레스에 순서대로 복사하면 이미지 로딩은 끝이다. 고로 OS 이미지 를 0x10000(64Kbyte)에 로딩해서 실행한다. 


// OS 이미지를 반드시 0x10000 위치에 로딩해야 실행되는것은 아니다. 부트로더 이후(0x07C00)에 연속해서 복사해도 OS실행에 문제는 없다.


플로피 디스크의 첫 번째 섹터는 부트로더로 BIOS가 메모리에 로딩한다. 따라서 플로피 디스크의 두 번째 섹터부터 OS 이미지 크기만큼 읽어서 메모리에 복사하면 된다. 플로피 디스크의 섹터는 '섹터 > 헤드 > 트랙' 의 순서로 배열되어 있으므로 이 순서만 지킨다면 큰 문제 없이 로딩할 수 있다.


또한 이걸 이제 ASM화 시켜보자.


// 한없이 작은 맥북에어...


어셈블리어 소스 코드와 디스크 리셋 기능만 부트로더에 추가하면 로딩할 준비가 끝난다. 그런데 기능은 구현했지만 화면에 출력하는 코드가 없어서 진행 상황이나 완료 유무를 확인하기 어렵다. 이번에는 화면에 진행 상태를 출력하도록 코드를 추가해보자..


앞에서 환영 메시지를 출력하는 코드를 구현했다. 하지만 함수 형태로 구현하지 않아서 원하는 곳에서 호출할 수 없었다. 무네즌 코드 구조뿐 만 아니라 함수 호출에 필요한 핵심 자료구조또한 빠져있다는것이다. 이를 보완하여 함수 호출이 가능한 구조로 만들어 보자.


스택 초기화 와 함수 구현

x86 프로세서에서 함수를 사용하려면 스택이 꼭 필요하다...


x86 프로세서에서는 함수를 호출한 코드의 다음 어드레스, 즉 되돌아갈 어드레스(Return Address)를 저장하는 용도로 스택을 사용한다. 함수를 호출하면 프로세서가 자동으로 되돌아올 어드레스를 스택에 저장하며, 호출된 함수에서 되돌아감(RET)을 요청하면 자동으로 스택에서 어드레스를 꺼내 호출한 다음 어드레스로 이동하는 것이다. 스택은 복귀 어드레스를 저장하는 역할뿐만 아니라 함수의 파라미터를 저장하는 역할도 겸한다. 호출하는 쪽 과 호출되는 쪽 은 정해진 규칙에 따랄 파라미터를 스택에 저장함으로서 협엄할 수 있다.


함수 호출을 위해 가장 먼저 해야할 일은 스택 생성이다. x86프로세서는 스택관련 레지스터가 세가지 있다. 스택 세그먼트 레지스터(SS)와 스택 포인터 레지스터(SP), 그리고 베이스 포인터 레지스터(BP) 가 있다. 스택 세그먼트 레지스터는 스택 영역으로 사용할 세그먼트의 기준 주소를 지정한다. 스택 포인터 레지스터는 데이터를 삽입하고 제거하는 상위을 지정한다. 마지막으로 베이스 포인터 레지스터는 스택의 기준 주소를 임시로 지정할 때 사용한다. 16비트 모드는 세그먼테이션 방식으로 어드레스를 변환하므로 스택 세그먼트 레지스터를 사용해서 최대 64KB를 스택 영역으로 지정할 수 있다. 스택 세그먼트 레지스터에 0x0000을 설정한다면 사용가능한 영역은 0x00000~0x0FFFF까지가 되며, 스택 세그먼트 레지스터에 0x1000을 설정한다면 사용 가능한 영역은 0x010000~0x01FFFF까지가 된다. 스택 세그먼트 레지스터로 스택 세그먼트의 범위는 지정할 수 있지만, 실제 스택의 크기는 지정할 수 없다.


따라서 스택의 실제 크기는 스택 포인터 레지스터와 베이스 포인터 레지스터의 조깃값으로 지정한다. x86프로세서의 스택은 아래와 같이 데이터가 삽입될때마다 스택의 상위(TOP)를 나타내는 스택 포인터 레지스터가 낮은 어드레스(0x00에 가까운 주소)로 이동한다. 따라서 두 레지스터의 초깃값을 어떻게 설정하는가에 따라서 스택의 크기가 결정된다.



스택으로 사용할 영역을 결정해야하는데, 0x010000(64KB) 어드레스부터는 OS 이미지가 로딩되므로 0x010000이하, 즉 0x0000:0000~0x0000:FFFF 영역을 사용하겠다. 따라서 스택 세그먼트 레지스터(SS)의 값은 0x0000으로 설정하겠다. 또한 스택은 넉넉한 것이 좋으므로 스택 포인터 레지스터와 베이스포인터 레지스터를 0xFFFE로 설정하여, 스택 영역의 크기를 세그먼트의 최대크기로 지정하겠다. 아래 소스가 부트로더 앞부분에 추가될 스택 초기화 코드이다.



이제 스택 설정이 끝이 났다. 이제는 메세지를 출력하는 함수를 구현해 보자. 메세지를 출력하는 함수는 대부분 같다. 함수에서 사용하는 레지스터를 저장하고, 복구하는 코드와 넘겨받는 파라미터를 스택에서 꺼내는 코드를 추가하면 된다. x86 프로세서는 스택 작업을 처리하는 두 가지 명령 push, pop을 지원하며, 각 명령은 스택에 데이터를 넣고 꺼낸다.



구현할 C언어 코드 :: PrintMessage(iX, iY, pcString);


위 코드를 보면 함수 호출을 끝낸 뒤 스텍 포인터 레지스터(SP)에 6을 더한다. 이는 함수 파라미터로 스택에 삽입된 값을 제거하기 위함이다. 16비트 모드에서는 스택에 2바이트(WORD) 크기로 삽입/제거되고 삽입은 스택 포인터 레지스터(SP)를 아래로 이동시킨다. 따라서 파라미터 3개가 삽입되면 삽입되기 전의 위치에서 -6(2바이트 * 3)만큼 이동할 것이다. 함수 수행이 끝난후, 스택을 다시 원래대로 복원하려면 감소한 만큼 더해주면 되므로 6을 더하는 것이다. 


호출하는 코드는 작성이 완료 되었으니, 이번엔 호출받는 코드를 작성하자. 스택의 특정 위치를 기준으로 오프셋을 이용해 접근하면 파라미터를 찾게 되는데 여기서 문제가 한 가지 있다. 스택의 상위(TOP)을 의미하는 스택 포인터 레지스터(SP)는 스택 관련 명령(push, pop)에 따라 계속 변한다는 것이다. 스택에 삽입된 파라미터에 접근하려면 시시가각 변하는 스택 포인터 레지스터(SP)대신 스택에 고정된 갑승ㄹ 가리키는 레지스터를 사용하는거싱 편리하다. 이러한 역할을 하는 거싱 베이트 포인트 레지스터(BP)이며, 호출된 함수는 베이스 포인터 레지스터(BP) + 오프셋으로 파라미터에 접근하게 된다.


호출되는 함수에서는 자신이 사용하는 레지스터의 값을 미리 스택에 저장해두고, 수행이 끝나면 이를 복원하여 호출한 이후의 코드 수행에 영향을 미치지 않아야 한다. 



이를 바탕으로 메세지 출력 함수를 수정해보자.



보호모드에서 사용되는 세 가지 함수 호출 규약

이번엔 보호 모드에서 주로 사용하는 함수 호출 규약 세 가지를 잠깐 살펴보자. 호출 규약(Calling Convention)은 함수를 호출할 때 파라미터와 복귀 어드레스 등을 지정하는 규칙이다. 보호 모드에서 사용하는 대표적인 호출 규약에는 stdcall, cdecl, fastcall이 있다. stdcall 방식은 파라미터를 스택에 저장하며, 호출된 쪽에서 스택을 정리한다. cdecl 방식식도 역시 파라미터를 스택에 저장하지만, 함수를 호출한 짜고스택을 정리한다. fastcall 방식은 일부 파라미터를 레지스터에 저장하는 것을 제외하면 stdcall 방식과 같다.



stdcall(Standard call)은 파라미터를 스택에 넣을때 오른쪽에서 왼쪽 순서로 집어 넣는다. 그리고 함수의 반환값은 EAX 레지스터(32비트 AX레지스터)를 사용하며 스택에서 파라미터를 제거하는 작업을 호출된 함수가 처리하게 한다.



cdecl(C-Declare CAll)은 stdcall과 동일하게 파라미터의 오른쪽에서 왼쪽 순서로 스택에 집어넣는다. 함수의 반환값 역시 AX 레지스터를 사용한다. 단 한가지 차이점은 스택에서 파라미터를 제거하는 작업을 호출한 함수가 대신 처리한다는 점이다.


그럼 이제 9번 줄에서 ret 12 > ret 으로 수정되고 20번 줄 다음에 add esp, 12 가 추가 되어 작성 된다.


fastcall은 컴파일러마다 구현하는 방식이 조금씩 다르다. 윈도우에서 많이 쓰이는 마이크로소프트사의 컴파일러를 기준으로 설명하면, 처음 2개의 파라미터를 ECX, EDX 레지스터에 삽입하는 점을 제외하고는 stdcall과 같다.



IA-32e모드의 호출규약은 fastcall을 확장한 방식이며, 보다 많은 레지스터르 파라미터 전달용으로 사용한다. IA-32e모드로 전환되면 기존 레지스터에 8개의 범용 레지스터가 추가된다. IA-32e모드의 호출규약은 기존 레지스터와 추가된 레지스터를 포마하여 파라미터를 최대 6개까지 전달할 수 있도록 설계되었기 때문에, 파라미터 개수만 제한하여 사용하다면 스택관련 작업을 줄일 수 있다.

// 다음에는 소스코드 올리는 스크립트 써서 해야겠다....;;;

테스트를 위한 가상 OS 이미지 생성

가상 OS 이미지는 여기서만 사용하고 후에 실제 OS 이미지로 대체하므로 세세한 부분까지 구현하지 않아도 무관하다. 부트로더 코드를 기반으로 기능을 간소화하여 OS가 실행되었음을 표시하는 기능만 넣겠다. 자신의 섹터 번호를 화면 위치에 대응시켜서 0~9까지 번호를 출력한다면 화면에 출력된 문자의 위치와 수를 확인하여 정상여부를 판단할 수 있다.

먼저 kernel32 에서 가상 OS 소스파일로 사용할 VirtualOS.asm을 만든다. 부트 로더 코드와 동일한 방식으로 레지스터를 초기화 한 다음 화면 2번째 라인의 가장 왼쪽 위치에 0을 출력한다. SECTORCOUNT라는 메로리 어드레스는 정상적으로 처리된 섹터의 수를 기록하고, 이를 사용해 화면에 출력될 자표를 계산할 용도로 추가했다. 지금은 한 섹터 크기의 코드이므로 이 값이 중요한 역할을 하지는 않는다.

이제는 1섹터 크기의 가상 OS 코드를 확장해서 1024 섹터로 만들어 보겠다. 1024섹터의 가상 OS 이미지를 만든 방법은 의외로 간단하다. 1024 섹터중에 마니작 섹터를 제외한 1023섹터의 코드를 화면에 자신을 출력하는 코드 및 다음 섹터의 어드레스로 이동하는 코드를 반복하면 된다. 그리고 마지막 섹터 하나는 더이상 섹터가 없으므로 위 예제와 같이 자신을 출력하고 무한 루프를 수행하도록 하면 끝이다.

// 추가로 아래 마지막으로 반복문 종료로 %endrep 을 쓰자.


이후..makefile을 수정을 하여 QEMU를 생성하면 된다... 끝