IBM 호환 규격 PC, 흔히들 x86이라고 말하는 시스템은 BIOS를 통해 POST 과정을 거친 후,

각 부트 장치들의 첫번째 섹터를 읽어가며 부트코드를 탐색하기 시작한다.

2023년 현재로서는 UEFI를 쓰는 것이 현명하겠다마는,

그래도 MBR부터 시작하는 것이 공부에는 도움이 될것이므로 UEFI 지원은 나중에 추가해보도록 하는 것이다.

 

Master Boot Record

부트코드는 위에서도 언급했던 장치의 첫번째 섹터를 읽어 가져온다.

CHS(Cylinder-Head-Sector) 주소 지정 방식으로는 (0, 0, 1)일 것이고, LBA (Logical Block Addressing) 방식으로는 0 인 것이다. 

해당 영역은 Master Boot Record (MBR) 이라고도 불리는 영역이며, 512 바이트 가량의 크기로 디스크 내 파티션 테이블을 같이 담고 있다.

 

그림 1. MBR 구조

 

그림 1에서 보듯 MBR 방식으로 파티션을 관리하는 디스크는 파티션을 최대 4개까지 가질 수 밖에 없고,

이 이상으로 파티션을 활용하고자 하는 경우는 확장 파티션으로 나누어 사용할 수 밖에 없게 된다.

확장 파티션에 대한 내용은 논외이므로 여기서는 넘어간다.

 

맨 처음 512바이트에서 중요한 부분은 끝부분에 위치한 0x55, 0xAA 인데,

시스템에서 디스크를 읽어온 뒤 마지막으로 도착한 2바이트가 0x55, 0xAA가 아니라면 MBR로 인식하지 않고 다음 장치로 넘어가려고 할 것이다.

그렇게 계속 장치를 탐색하다가 더 이상 장치를 찾을 수 없을때 다음과 같은 오류 메세지를 출력하고 장치가 새로 삽입되기까지 대기한다.

 

Reboot and select proper boot device

or Insert Boot Media in selected boot device

 

파티션에 대한 고려가 없다면 파티션 테이블의 나머지 64바이트를 전부 활용해서 510바이트의 영역을 전부 부트코드로 활용해 볼 수 있다.

하지만 이번에 만드는 운영체제는 최대한 현대 컴퓨팅 환경의 표준을 존중하는 방향으로 진행할 것이므로 얌전히 446바이트만 활용하도록 하자.

 

그림 2. 파티션 테이블 엔트리 구조

 

파티션 테이블 엔트리는 각 16바이트 크기를 가지며, 해당 파티션이 어떤 특성과 타입을 가지는 지, 또 디스크 내에서 어느 위치에서 시작하고 종료하는지에 대한 정보를 담고 있다.

그러므로 부트코드에서 해당 영역을 읽은 다음 해당 파티션의 정보를 읽어 계속 부트 과정을 진행해볼 수 있을 것이다.

제일 첫번째 Status의 경우 0x00 혹은 0x80(Active)인데, 0x80일 경우에 해당 파티션의 시작 주소를 구하고 VBR 부트코드를 로드해오도록 한다.  

 

Volume Boot Record

각 파티션은 첫번째 섹터에 MBR과 유사하게, Volume Boot Record(VBR) 을 가지고 있다. VBR은 파티션이 취하고 있는 파일 시스템에 따라 그 구조가 다르며, 일부 Microsoft 계열 파티션의 경우에는 BPB(BIOS Parameter Block) 이라는 구조를 가지기도 한다.

초장부터 기성 파일시스템을 따라가면 구현하다가 지쳐버릴 것이 뻔하기 때문에, 독자 구조의 파일 시스템을 도입하기로 한다.

 

PFS (Patche's File System)

라고 그냥 즉석에서 지었다. 일단 당장 써먹을 구조는 이렇게 간단하게 생겨먹었다.

 

그림 3. Provisional PFS

 

VBR 부터는 파일 시스템의 구조를 따라가므로 앞 부트섹터 부분에서 미련없이 510바이트 전부 사용하기로 했다.

MBR에서 PFS의 VBR로 점프가 이루어지면 VBR의 부트코드는 CPU의 보호모드를 활성화 하고, 0x10000에 Loader를 불러와 실행하도록 할 생각이다.

Loader부터는 어셈블리가 아닌 C로 빌드할 것이고 그러면 실행 크기 또한 달라져야 할테니, 크기를 매직 넘버 바로 뒤에 위치시켰다.

 

그럼 이제 구현을 해보자.

 

x86-64 어셈블리 개발환경 구축

어셈블리 프로그래밍을 위해 NASM을 다운로드했다. 공짜이기고 하고 사용하기도 편리하다.

귀찮은 사람은 그림 4에서 보듯  installer를 사용하고, 그렇지 않은 경우에는 zip을 받아서 PATH 설정 후 실행하도록 한다.

그림 4. 다운로드 페이지

그림 5. installer로 설치 후 추가된 NASM

 

installer로 설치했을 경우 그림 5와 같이 nasm-shell 이라는 이름으로 간단하게 PATH를 잡아주는 바로가기가 같이 설치된다.

 

 

MBR Bootcode

시스템이 부트코드를 읽기 시작하면, 0x07C0 위치에 로드한다. 이후 VBR로 점프하기 위해  + 0x1BE부터 파티션 테이블을 읽고, Bootable인지 확인한다.

만일 0x80 (Active)가 아니라면 16바이트 뒤의 엔트리를 읽으면 될 것이다.

본격적으로 파일시스템을 활용하기까지는 바이너리 이미지로만 작업을 진행할 것이므로, 그림 6과 같이 구조를 짜 둔다.

 

그림 6: Full Depiction of Provisional PFS Image

 

VBR을 위한 MBR Relocation

VBR이 0x7C00에 위치할 것이므로, MBR을 512바이트 밑에 위치시켜야 한다.

rep movsw 명령어를 사용하면 cx 레지스터에 지정한만큼 si에서 di로 복사해준다.

이 과정에서 어떠한 인터럽트도 장애요소가 될 뿐이므로, 불필요한 버그를 방지하기 위해 인터럽트 또한 같이 제거(cli)해준다.

 

코드1: MBR Relocation, Commit cb6482b

 

코드2: 세그먼트 설정 후 MBR main 프로시저 실행, Commit cb6482b

 

MBR main 프로시저 실행이 성공적으로 되었다면, CS는 0x07C0으로, IP는 0x0000으로 변경되어 있을 것이다.

이제부터 본격적으로 부팅 과정을 개시할 것이므로 인터럽트는 다시 복원(sti)시켜준다.

 

코드3: MBR main 프로시저, Commit cb6482b

 

IBM 호환 규격 PC는 dl 레지스터에 MBR을 읽어낸 디스크의 ID값을 저장한다.

하지만 레지스터의 갯수는 한정되어 있으므로, 나중에 쓸 때를 위해 메모리에 저장해둔다. (Line 46)

이후 VBR 로드까지 다음과 같은 과정을 거치게 된다.

  1. 파티션 테이블 (0x7A00 +1BE ~ +1FE) 를 순회하면서 Active Partition이 있는지 확인한다.
  2. Active Partition에서 시작 LBA 주소를 획득한다.
  3. 해당 LBA 주소에서 1개 섹터를 읽어내어 그 내용을 0x7C00에 저장한다.
  4. 0x07C0:0000 으로 점프한다.

그림 6에 따라 PFS 파티션의 첫번째 섹터는 VBR이므로 위 과정을 거친다면 파티션에 저장되어 있던 VBR 부트코드가 실행될 것이다.

 

Active Partition 탐색

그림 2를 다시 살펴보자.

그림 2. 파티션 테이블 엔트리 구조 (Review)

 

파티션 테이블은 부트코드가 끝나는 지점부터, 매직넘버 0x55, 0xAA가 있는 지점까지 각 16바이트씩 4개의 엔트리를 가진다.

각 엔트리는 첫번째 바이트에 Status라는 값을 저장하는데 이 값이 0x00일 경우 부팅 불가능(비활성) 파티션, 0x80일 경우 부팅 가능 파티션으로 간주할 수 있다.

물론 부트코드를 어떻게 짜냐에 따라 활성여부 관계없이 부팅을 진행할 수 있겠으나 일단 스펙이 그러하니 준수하는 것이 좋다.

 

코드4: Active Partition 탐색 및 Start LBA 추출, Commit cb6482b

 

코드 4는 위에서 설명한 내용을 구현한 것으로, 부트코드 이후에 위치한 파티션 테이블 엔트리 __ENTRY1 부터 시작해서 순회하다가, Active Partition을 만나면 해당 파티션의 LBA 값을 읽어 저장하도록 하였다.

만일 현재 파티션이 Active가 아니라면 16바이트만큼 오프셋을 추가하는 식으로 다음 테이블로 넘어갈 수 있다.

하지만 마지막 엔트리까지 Inactive라면 에러(func_chk_part.end.err)로 점프하여 더 이상의 부팅 시퀀스를 중단한다. 실제로 Provisional PFS Image의 __ENTRY1에 지정된 0x80을 0x00으로 변경 후 부트하면 그림 7과 같이 실행된다.

 

그림 7: Active Partition 탐색 실패

 

 

디스크 읽기 및 VBR 로드

위 과정을 전부 정상적으로 거친다면 첫째로는 읽을 디스크의 ID를 확보할 수 있었을 것이고, 둘째로는 디스크를 읽기 시작할 시점에 대한 정보를 확보할 수 있었을 것이다.

이들 정보는 레지스터가 아닌 메모리에 저장했는데, 그 구조는 코드 5와 같다.

 

코드 5: 변수 저장 영역, Commit cb6482b

 

변수 _VAR_VBR_DAP는 앞으로 디스크를 읽기 위해 사용할 구조체로, Disk Address Packet이라고 불리는 자료구조이다.

요즘 세상에 CHS 주소지정 방식은 굉장한 구식이므로 Enhanced BIOS에 구현된 LBA 방식 디스크 읽기를 구현해보자.

 

코드 6: VBR 부트코드 로드, Commit cb6482b

 

우선 INT 13h 에서 Extended Disk Sector Read 서비스 호출을 위해 ah 레지스터에 0x42를 설정해주고, 512바이트만을 가져올 것이므로 al에는 0x01을 설정해준다.

그 후 DS:SI에 DAP 구조체의 주소를 설정해준다. 여기서 DS는 이미 MBR 시작 시점에서 세팅되었으므로 si 레지스터만 지정해준다. 그리고 dl에 저장해 두었던 디스크의 ID를 설정해준다.

DAP 구조의 버퍼 주소는 0x7C00으로 설정한다. 이렇게 하면 디스크에서 읽혀나온 VBR 부트 코드가 해당 주소에 저장된다.

 

코드 7: VBR 실행, Commit cb6482b

 

만약 부트코드 실행이 실패했다면 시스템이 재부팅되겠지만, 성공했다면 곧바로 0x07C0:0000 으로 점프하여 VBR을 실행하게 될 것이다.

VBR에서 아무것도 하지 않고 무한 루프를 하도록 jmp $ 만 돌게끔 한다음 테스트 해보자.

 

그림 8: VBR 로드 및 실행

그림 9: 그림 8 이후 IP 및 CS 레지스터 상황

 

이로써 리얼모드 MBR은 준비되었다. VBR 로 넘어간 이후에는 보호모드까지 진입한 후 로더까지 진행해보자.