Sqix

64bit 멀티코어 OS 제작하기 [4] - 1 : OS 이미지 로딩을 위한 부트로더 기능 추가 (1) 본문

MINT64 OS

64bit 멀티코어 OS 제작하기 [4] - 1 : OS 이미지 로딩을 위한 부트로더 기능 추가 (1)

Sqix_ow 2018. 5. 16. 16:13

이번 글은 OS를 로딩하기 위한 과정에 대해 다룹니다.


뭐 일단 부트로더를 만들고 원하는 메세지를 나오도록 출력을 하긴 했습니다만, 가장 중요한 역할인 OS 이미지를 읽어서 메모리로 복사하도록 하는 코드를 작성하지 않았습니다. 더군다나 이걸 로딩해서 쓰려면 아무래도 32비트 혹은 64비트 커널 개발을 하고 OS 이미지를 만들어야 하는데, 이건 나중에 하고 가상 이미지를 만들어 보는 것으로 진행하겠습니다.


OS를 로딩하기 위해서는 BIOS 서비스를 이용해야 합니다. 따라서, 먼저 BIOS에 대해서 알아보겠습니다. BIOS는 우리에게 Interrupt라는 방식을 이용해서 서비스를 제공합니다. 기능이 담긴 함수 주소를 Interrupt Vector Table(IVT)에 담아서 우리가 Software Interrupt를 호출하도록 하고, 이에 맞는 인터럽트가 발생하면 이를 핸들링하는 Interrupt Handler를 검색하여 서비스를 제공합니다.



(출처 : https://en.wikipedia.org/wiki/INT_13H)


IVT 표에서 보다시피, int 0x13h BIOS Call이 Disk I/O Service를 나타냅니다. 따라서, 이를 발생시켜 우리는 OS 이미지를 로드할 수 있습니다. 하지만, 이 또한 함수인지라 파라미터를 넘겨줘야 합니다. 따라서 우리는 레지스터를 이용하여 우리가 넘겨주고자 하는 값을 주고, 또 받아옵니다. 


레지스터 별 역할은 다음과 같습니다.


RESET


Input    -    AH    : 기능의 번호 입력. 리셋 기능 사용 시 0으로 설정

   DL    : 드라이브 번호 입력(0x00 : 플로피 디스크, 0x80 : 첫 번쨰 하드디스크, 0x81 : 두 번째 하드디스크)


Output -    AH    : 기능 수행 수 드라이브 상태 값 리턴. 0x00은 성공이고 다른 값이면 에러 발생.

CF bit  : 성공시 FLAGS의 CF 비트 0으로 설정. 그렇지 않다면 1로 설정.


Sector 읽기


Input    -    AH    : 기능 번호. 섹터 읽기 기능 사용 시 2로 설정

   AL     : 읽을 섹터의 수 (1~128)

  CH     : 트랙 혹은 실린더의 번호 (0~1023). CL의 상위 2비트도 사용하므로, 총 10비트.

  CL      : 읽기 시작할 섹터 번호 (1~18).

  DH     : 읽기 시작할 헤드 번호 (0~15).

  DL     : 드라이브 번호 입력(0x00 : 플로피 디스크, 0x80 : 첫 번째 하드디스크, 0x81 : 두 번째 하드디스크)

ES:BX  : 읽은 섹터를 저장할 메모리 주소.


Output  -   AH    : 수행 후 드라이브 상태

   AL    : 읽은 섹터 수

CF bit  : 성공 시 FLAGS의 CF 비트 0으로 설정. 그렇지 않다면 1로 설정.



이제 이미지를 로딩해 볼 것입니다. 이를 위해서는 이미지가 어떻게 구성되어 있는지에 대해 알아야 할 것입니다. 이미지는 크게 부트로더, 보호 모드(32bit) 커널, IA-32e 커널(64비트)로 구성되어 있습니다. 각 부분은 섹터 단위로 정렬되어 하나의 부팅 이미지 파일로 합쳐져 있습니다. 기본적으로 이미지는 부트로더 이후(0x7C00)에 복사해야 하며, 여기서는 0x7C00 ~ 0x10000 사이의 공간을 특정 용도로 추후에 사용할 것이기에 0x10000 이후로 로드하도록 할 것입니다. 0x10000 이후에 순차적으로 로드하도록 하겠습니다.


우선 대상 주소를 지정해야 할 것입니다(0x10000). OS 이미지의 크기는 1024(부트 로더 제외)byte이고, 디스크 섹터, 헤드, 트랙을 저장하는 변수를 지정해 줍니다. 


TOTALSECTORCOUNT :    dw    1024

SECTORNUMBER :             db    0x02

HEADNUMBER :                 db    0x00

TRACKNUMBER :               db    0x00


그 후, 초기 디스크 주소를 0x10000으로 설정해야 할 것입니다. 또한, 복사할 섹터의 수들을 설정해야 합니다. es 세그먼트 레지스터의 값에 0x1000을 지정하고, bx 레지스터에 0x0000을 지정하여 총 0x1000:0000(==0x10000)으로 설정하여 줍니다. 


mov    si,    0x1000

mov    es,    s

mov    bx,    0x0000

mov    di,    word [TOTALSECTORCOUNT ]


변수들에 대한 세팅을 완료하였으니, 이제 복사하는 코드를 작성하여 봅시다(Function : READDATA). 먼저 섹터를 모두 로드하였는지에 대한 검사 루틴을 수행합니다.


cmp    di,    0

je    READEND

sub    di,    0x1


만약 로드할 것이 남아있다면, 호출을 해야 하겠죠. 우리는 BIOS에서 제공하는 서비스를 이용할 것입니다. 02번 서비스는 섹터를 읽어오는 것이므로, 02번 서비스를 이용하도록 하고 섹터를 읽어오는 함수의 파라미터(섹터 수, 트랙 번호, 섹터 번호, 헤드 번호, 드라이브 번호(플로피 디스크 == 0))를 순서대로 지정해주고 인터럽트를 걸어 (int 0x13) 서비스를 수행하도록 합니다. 또한, 에러 핸들링을 위해 만약 에러가 발생한다면 에러 핸들링을 하는 부분으로 넘어갑니다.


mov    ah,    0x02

mov    al,    0x1

mov    ch,    byte    [ TRACKNUMBER ]

mov    cl,    byte    [ SECTORNUMBER ]

mov    dh,    byte    [ HEADNUMBER ]

mov    dl,    0x00    ;                                                Drive Number : Floppy disk(0x00)

int    0x13

jc    HANDLEDISKERROR


섹터를 가져왔다면, 이제 어디에 복사할지 지정해야 합니다. 일반적으로 플로피 디스크의 섹터의 크기는 512바이트(신형 하드 디스크는 최대 4096바이트인 경우도 있습니다.)로 구성되어 있으니, 512바이트씩 읽어온다고 가정하고 진행합니다. 위에서 AL 레지스터가 섹터의 개수에 관련된 레지스터라고 하였습니다. 또한, 플로피 디스크의 섹터의 개수는 총 18개로 구성되어 있습니다. 따라서 18개의 섹터를 읽을 때 마다 헤드를 토글(0 -> 1 || 1 -> 0)하여 주고 (디스크를 뒤집어 반대편의 데이터를 읽는 역할을 합니다) 다시 읽습니다. 만약 토글 및 데이터 로드를 완료하면 트랙 넘버를 1 증가시킵니다. 플로피 디스크에서 트랙은 기본적으로 80개(0~79까지의 넘버링)까지 있습니다.


; 읽어온 데이터를 세그먼트 레지스터에 반환하고 어드레스 위치를 한 섹터만큼 증가시켜 준다.

add    si,    0x0020

mov    es,    si


; 토글 이전(헤드 == 0)의 디스크의 면을 읽어오는 루프 코드.

mov    al,    byte    [ SECTORNUMBER ]

add    al,    0x01

mov    byte    [ SECTORNUMBER ],    al

cmp    al,    19

jl    READDATA


; 헤드 토글을 시키는(헤드 == 1) 코드.

xor    byte    [ HEADNUMBER ], 0x01

mov    byte    [ SECTORNUMBER ], 0x01

cmp    byte    [ HEADNUMBER ], 0x00

jne    READDATA


; 트랙을 1 증가시키고 다시 섹터 읽기

add    byte    [ TRACKNUMBER ], 0x01

jmp READDATA


===========================

READEND :


HANDLEDISKERROR:


jmp $

===========================


이렇게 어셈블리 코드를 작성할 수 있습니다. 하지만, 우리는 이를 함수 형식으로 작성하지 않았기에, 호출을 할 수 없으므로 이를 함수로 바꾸어 보도록 합시다. 함수를 작성하기 위해서는 기본적으로 스택이 필요합니다. 이는 선입후출 구조가 필요해서인데, 복귀 어드레스를 가장 먼저 넣어 놓고, 함수에 필요한 파라미터들을 그 후에 스택에 쌓아 놓으면 필요한 파라미터들을 이용하고 마지막으로 복귀 주소를 통해 다시 main부로 복귀하기 편하기 때문입니다. 



어셈블리 언어를 이용하여 스택을 만들기 위해서는 스택 관련 레지스터를 이용하여야 합니다. 이전에 언급하였던 SS(Stack Segment Register), SP(Stack Point Register), BP(Base Point Register), 이 세 개의 레지스터들이 바로 그것입니다. SS는 스택으로 이용할 최상위 어드레스, BP는 스택으로 사용할 최하위 어드레스, SP는 현재 스택 안에서의 어드레스를 나타냅니다. 리얼 모드(16bit)에서는 최대 64KB(0x10000, 세그먼테이션 기법)의 스택 크기를 가지게 됩니다.


이제 스택을 만들어 봅시다. 0x010000 이후에는 OS 이미지가 로드되어야 하는 공간이기 때문에, 이 이하의 공간, 즉 0x0000:0000 ~ 0x0000:FFFF에 스택을 생성하여 이용할 것입니다. 즉, 스택의 최상위 어드레스로(SS Register) 0x0000,  최하위 어드레스로(BP Register) 0xFFFF를 설정합니다. '


mov    ax,    0x0000

mov    ss,    ax

mov    sp,    0xFFFE    

mov    bp,    0xFFFE


※ 여기서 스택 주소를 FFFE로 하는 이유는, FFFF로 지정하여도 아무 문제는 없지만, x86 Architecture에서는 address를 짝수로 지정하면 Code / Data Loading에 걸리는 지연시간이 짧아져서 더욱 빠른 실행을 할 수 있도록 이렇게 조정한 것으로 추정됩니다. (출처 : https://goo.gl/gFnz6A : Stack Overflow)


스택을 설정하였으니, 기존 부트로더 코드를 전부 함수화합시다. 이미 작성된 코드들의 핵심 기능 부분은 같습니다. 그저 함수에서 사용하는 레지스터를 스택에 넣고 다시 복구하는 코드(push / pop)와 넘겨받는 인자를 스택에서 꺼내는 코드(pop) 정도면 됩니다. push 명령은 스택에 데이터를 넣는 코드로, Stack Pointer가 감소되고, pop 명령은 최상위의 데이터를 꺼내 가져오는 코드로, Stack Pointer가 증가합니다. 만약 많은 데이터를 넣고자 하면, 주소공간 자체에 접근하여 주소공간들을 채우고, Stack Pointer를 바꾸어도 됩니다.


Stack을 이용할 것이기 때문에, 이에 맞춰서 변수를 지정해야 할 것입니다. x, y좌표와 문자열의 어드레스를 담는 변수를 지정하고, 이를 PRINTMESSAGE의 파라미터로써 사용할 것입니다. 마지막에는 사용한 스택을 제거할 것인데, 이는 SP에 6을 더해 주면 됩니다.


push    word    [ pcString ]

push    word    [    iY        ]

push    word    [    iX        ]

call    PRINTMESSAGE

add    sp,    6


스택에서 호출된 함수의 파라미터에 접근하기 위해서는 베이스 포인터 + 오프셋을 이용합니다. 예를 들어, 범용 레지스터(ax, bx, cx, ...)에 파라미터를 넣고자 하면, 

mov    ax,    word    [    bp + 4    ]

와 같이 bp에 오프셋을 더한 값의 포인터를 이용한다는 것입니다.


또한, 함수를 호출한다는 것은 프로그램 실행 흐름을 따라 실행하던 도중 다른 곳으로 넘어갔다 다시 돌아온다는 것이기에, 모든 레지스터 상태가 보존된 상태로 다시 기존 실행 흐름으로 돌아와야 합니다. 따라서, 레지스터의 값 역시 미리 스택에 저장하고 함수의 에필로그가 나오기 전에 모든 레지스터들을 복원해 주어야 할 것입니다.


이제 다음 글에서 함수 호출 규약 및 스택 프레임에 대해서 다루고, 부트 로더의 소스 코드를 함수 호출 규약에 맞게 수정하고, 이미지를 로드하는 소스 코드를 추가하여 부트 로더를 완성하여 보도록 하겠습니다.




Comments