64비트 멀티코어 OS 만들기 - BIOS, SW 인터럽트, OS 이미지 로딩 구현

170927

지난 포스트에 이어서 이제 기본 부트 로더에 OS 이미지를 로딩하는 기능을 추가하자.


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

OS 이미지의 빌드가 끝나면 MINT64 OS의 최종 이미지는 플로피 디스크용으로 생성된다. 이는 부트 로더가 플로피 디스크에서 OS 이미지를 읽어서 메모리로 복사해야 한다는 것을 말한다. 플로피 디스크를 제어하는 방법은 직접 플로피 디스크 컨트롤러에 접근하는 방법과 BIOS 서비스를 이용하는 방법이 있지만, 여기서는 BIOS의 기능을 사용하겠다.

BIOS 서비스를 이용하면 OS 이미지를 로딩하는 기능을 구현할 수 있다. 한 번 가상 OS 이미지를 만들고 로딩해서 테스트까지 진행해보자!

BIOS는 일반적으로 많이 사용하는 라이브러리와 달리 함수의 주소를 인터럽트 벡터 테이블(Interrupt Vector Table)에 넣어 두고, 소프트웨어 인터럽트(SWI, Software Interrupt)를 호출하는 방법을 사용하여 자신의 기능을 외부 인터페이스로 제공한다.

인터럽트 벡터 테이블은 메모리 주소 0에 있는 테이블로, 주로 특정 번호의 인터럽트가 발생했을 때 처리하는 함수 검색에 사용된다.

그래서 우리가 임의로 인터럽트를 발생시킬 수 있어야 BIOS가 소프트웨어 인터럽트 명령을 사용할 수 있다. BIOS의 기능을 사용할 때는 파라미터와 결과값을 레지스터(AX, BX, CX, DX 레지스터와 ES 세그먼트 레지스터)를 통해 넘겨 받으며, 이는 다른 서비스에도 마찬가지다.

  • 리셋과 섹터 읽기에 사용하는 레지스터


OS 이미지 로딩 기능 구현

MINT64 OS의 이미지는 크게 부트 로더, 보호 모드 커널, IA-32e 모드 커널로 구성되며, 각 부분은 섹터 단위로 정렬해서 하나의 부팅 이미지 파일로 합쳐진다. 즉, 플로피 디스크의 첫 번째 섹터는 부트 로더로 BIOS가 메모리에 로딩한다는 말이다. 그래서, 디스크의 두 번째 섹터부터 OS 이미지 크기 만큼을 읽어 특정 메모리 주소에 순서대로 복사하면 이미지 로딩이 끝난다. 참고로, 플로피 디스크의 섹터는 섹터 -> 헤드 -> 트랙 순서로 배열되어 있다. 아래는 섹터 배열 순서를 고려하여 작성한 C언어 소스 코드다.

섹터 번호를 순서대로 증가시켜 읽다가 마지막 섹터에서 헤드와 트랙 번호를 증가시키는 것이 이 코드의 핵심!

BIOS의 섹터 읽기 기능은 최대 128섹터까지다. 사실, 한 트랙에 있는 섹터의 배수 단위(18섹터)로 읽으면 코드가 간결하지만, 여기서는 한 트랙씩 읽는 방법으로 작성했다.

int main(int argc, char* argv[])
{
    int iTotalSectorCount = 1024;
    int iSectorNumber = 2;
    int iHeadNumber = 0;
    int iTrackNumber = 0;
    // 실제 OS 이미지를 복사할 물리적 주소
    char* pcTargetAddress = (char*) 0x10000;

    while(1)
    {
        // 전체 섹터 수를 하나씩 감소시키면서 0이 될 때까지 섹터를 복사
        if(iTotalSectorCount == 0)
        {
            break;
        }
        iTotalSectorCount = iTotalSectorCount - 1;

        // 1섹터를 읽어들여서 메모리 주소에 복사
        // BIOSReadOneSector: BIOS의 섹터 읽기 기능을 호출하는 임의의 함수
        if(BIOSReadOneSector(iSectorNumber, iHeadNumber, iTrackNumber, pcTargetAddress) == ERROR)
        {
            HandleDiskError();
        }
        // 1섹터는 512(0x200)바이트이므로 복사한 수만큼 주소 증가
        pcTargetAddress = pcTargetAddress + 0x200;

        // 섹터 -> 헤드 -> 트랙 순으로 번호 증가
        iSectorNumber = iSectorNumber + 1;
        if (iSectorNumber < 19)
        {
            continue;
        }
        // 헤드의 번호는 0과 1이 반복되므로 이를 편리하게 처리하기위해 XOR 연산을 사용
        // iHeadNumber = (iHeadNumber == 0x00) ? 0x00 : 0x01; 과 같은 의미
        iHeadNumber = iHeadNumber ^ 0x01;
        iSectorNumber = 1;

        if (iHeadNumber != 0)
        {
            continue;
        }

        iTrackNumber = iTrackNumber + 1;
    }
    return 0;
}

// 발생한 에러를 처리하는 함수
int HandleDiskError()
{
    printf("DISK Error~!!");
    while (1);
}

위 소스 코드를 어셈블리어 소스 코드로 다시 작성하면 아래와 같다. C언어의 코드 블록을 그대로 어셈블리어로 변환했다.

  • 1024섹터 크기의 OS 이미지를 메모리로 복사하는 소스 코드(어셈블리어)
TOTALSECTORCOUNT:   dw  1024    ; 부트 로더를 제외한 MINT64 OS 이미지의 크기
                                ; 최대 1152섹터(0x90000바이트)까지 가능
SECTORNUMBER:       db  0x02    ; OS 이미지가 시작하는 섹터 번호를 저장하는 영역
HEADNUMBER:         db  0x00    ; OS 이미지가 시작하는 헤드 번호를 저장하는 영역
TRACKNUMBER:        db  0x00    ; OS 이미지가 시작하는 트랙 번호를 저장하는 영역

    ; 디스크의 내용을 메모리로 복사할 주소(ES:BX)를 0x1000으로 설정
    mov si, 0x1000      ; OS 이미지를 복사할 주소(0x10000)를 세그먼트 레지스터 값으로 변환
    mov es, si          ; ES 세그먼트 레지스터에 값 설정
    mov bx, 0x0000      ; BX 레지스터에 0x0000을 설정하여 복사할 주소를 0x1000:0000(0x10000)으로 최종 설정

    mov di, word [ TOTALSECTORCOUNT ]   ; 복사할 OS 이미지의 섹터 수를 DI 레지스터에 설정

READDATA:               ; 디스크를 읽는 코드의 시작
    ; 모든 섹터를 다 읽었는지 확인
    cmp di, 0           ; 복사할 OS의 이미지의 섹터 수를 0과 비교
    je READEND          ; 복사할 섹터 수가 0이라면 다 복사했으므로 READEND로 이동
    sub di, 0x01        ; 복사할 섹터 수를 1 감소

    ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
    ; BIOS Read Function 호출
    ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
    mov ah, 0x02                    ; BIOS 서비스 번호 2(Read Sector)
    mov al, 0x1                     ; 읽을 섹터 수는 1
    mov ch, byte [ TRACKNUMBER ]    ; 읽을 트랙 번호 설정
    mov cl, byte [ SECTORNUMBER ]   ; 읽을 섹터 번호 설정
    mov dh, byte [ HEADNUMBER ]     ; 읽을 헤드 번호 설정
    mov dl, 0x00                    ; 읽을 드라이브 번호(0=Floppy) 설정
    int 0x13                        ; 인터럽트 서비스 수행
    jc HANDLEDISKERROR              ; 에러가 발생했다면 HANDLEDISKERROR로 이동

    ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
    ; 복사할 주소와 트랙, 헤드 섹터 주소 계산
    ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
    add si, 0x0020      ; 512(0x200)바이트 만큼 읽었으므로 이를 세그먼트 레지스터 값으로 변환
    mov es, si          ; ES 세그먼트 레지스터에 더해서 주소를 한 섹터만큼 증가

    ; 한 섹터를 읽었으므로 섹터 번호를 증가시키고 마지막 섹터(18)까지 읽었는지 판단
    ; 마지막 섹터가 아니면 섹터 읽기로 이동해서 다시 섹터 읽기 수행
    mov al, byte [ SECTORNUMBER ]   ; 섹터 번호를 AL레지스터에 설정
    add al, 0x01                    ; 섹터 번호를 1 증가
    mov byte [ SECTORNUMBER ], al   ; 증가시킨 섹터 번호를 SECTORNUMBER에 다시 설정
    cmp al, 19                      ; 증가시킨 섹터 번호를 19와 비교
    jl READDATA                     ; 섹터 번호가 19미만이라면 READDATA로 이동

    ; 마지막 섹터까지 읽었으면(섹터 번호가 19이면) 헤드를 토글(0->1, 1->0)하고, 섹터 번호를 1로 설정
    xor byte [ HEADNUMBER ], 0x01   ; 헤드 번호를 0x01과 XOR하여 토글(0->1, 1->0)
    mov byte [ SECTORNUMBER ], 0x01 ; 섹터 번호를 다시 1로 설정

    ; 만약 헤드가 1->0으로 바뀌었으면 양쪽 헤드를 모두 읽은 것이므로 아래로 이동하여 트랙 번호를 1 증가
    cmp byte [ HEADNUMBER ], 0x00   ; 헤드 번호를 0x00과 비교
    jne READDATA                    ; 헤드 번호가 0이 아니면 READDATA로 이동

    ; 트랙을 1 증가시킨 후, 다시 섹터 읽기로 이동
    add byte [ TRACKNUMBER ] , 0x01 ; 트랙 번호를 1 증가
    jmp READDATA                    ; READDATA로 이동
READEND:

HANDLEDISKERROR:    ; 에러를 처리하는 코드
; 생략
  jmp   $

위 어셈블리어 소스 코드와 디스크 리셋(초기화) 기능만 부트 로더에 추가하면 로딩할 준비가 끝난다. 하지만, 화면에 출력하는 부분이 없어서 진행 상황이나 완료 유무를 확인하기 어려우므로 여기 화면에 진행 상태를 출력하도록 코드를 추가하자. 지난 시간에 했던 메시지 출력과 달리, 함수 호출이 가능한 구조로 만들어 보자.


스택 초기화와 함수 구현

x86 프로세서에서 함수를 사용하려면 스택(Stack)이 꼭 필요하다. 스택은 데이터를 삽입하는 포인트와 제거하는 포인트가 같아서, 마지막에 들어간 데이터가 가장 먼저 나오는 형태의 자료구조(LIFO, Last-in, First-out)이다. x86 프로세서에서는 함수를 호출(Call)한 코드의 다음 되돌아갈(Ret) 주소, 즉 복귀 주소(Return Address)를 저장하는 용도로 스택을 사용한다. 또한, 호출하는 쪽(Caller)과 호출되는 쪽(Callee)이 정해진 규칙에 따라 함수의 파라미터를 저장하여 협업도 가능하다.

x86 프로세서에는 스택 세그먼트 레지스터(SS, Stack Segment), 스택 포인터 레지스터(SP, Stack Pointer)와 베이스 포인터 레지스터(BP, Base Pointer)가 있다. 스택 세그먼트 레지스터는 스택 영역으로 사용할 세그먼트의 기준 주소를 지정하고, 스택 포인터 레지스터는 데이터를 삽입하고 제거하는 상위(Top)를 지정하며, 베이스 포인터 레지스터는 스택의 기준 주소를 ‘임시‘로 지정할 때 사용한다.

16비트 모드에서는 세그먼테이션 방식으로 주소를 변환하므로, 스택 세그먼트 레지스터에 최대 64KB(0x10000)까지 스택 영역으로 지정할 수 있다. 예를 들어, 0x0000을 설정하면 0x00000~0x0FFFF까지 사용 가능하고, 0x1000을 설정하면 0x010000~0x01FFFF까지 사용 가능하다. 하지만 이는 실제 스택의 크기를 지정하는 건 아니다. 데이터가 삽입될 때마다 스택 포인터 레지스터가 낮은 주소(0x00에 가까운 주소)로 이동하므로, 스택 포인터 레지스터와 베이스 포인터 레지스터의 초깃값이 스택의 크기를 결정한다.

자, 위 내용을 참고해서 부트 로더에 스택을 생성한다. 0x010000(64KB) 주소부터 OS 이미지가 로딩되므로, 스택 영역의 크기를 최대 크기로 지정(영역 크기: 0x0000:0000~0x0000:FFFF, SS: 0x0000, SP: 0xFFFE, BP: 0xFFFE)하여 만들어 보자. 아래는 부트 로더 앞부분에 추가될 스택 초기화 코드다.

; 스택을 0x0000:0000~0x0000:FFFF 영역에 64KB 크기로 생성
mov ax, 0x0000  ; 스택 세그먼트 시작 주소(0x0000)를 세그먼트 레지스터 값으로 변환
mov ss, ax      ; SS 세그먼트 레지스터에 설정
mov sp, 0xFFFE  ; SP 레지스터의 주소를 0xFFFE로 설정
mov bp, 0xFFFE  ; BP 레지스터의 주소를 0xFFFE로 설정

스택 설정은 끝났고, 이제 메시지 출력 함수를 수정하자. 메시지를 출력하는 핵심 코드는 함수에서 사용하는 레지스터를 저장하고, 복구하는 코드와 넘겨 받은 파라미터를 스택에서 꺼내는 코드 정도다. 여기서 사용할 PUSH 명령은 각각 SP 레지스터가 가리키는 주소에 데이터를 저장하고 SP 레지스터를 감소시키며, POP 명령은 반대로 SP 레지스터를 증가시킨다.

화면에 문자열을 출력하려면 X 좌표, Y 좌표, 문자열의 주소가 필요하다. 이 세 가지를 함수 파라미터로 정의하고 스택에 삽입하는 순서를 정하면 되는데, C언어 연계를 고려해야 중복 작업을 피할 수 있다. C언어의 호출 규약(cdecl 방식)을 따르면, 파라미터의 역순(오른쪽에서 왼쪽)으로 삽입하여 스택에서 꺼낸 순서가 파라미터 순서와 같다. 아래 코드를 확인하자.

// C언어의 함수 호출 코드
PrintMessage(iX, iY, pcString);
push word [ pcString ]  ; 문자열의 주소를 스택에 삽입
push word [ iY ]        ; 화면의 Y 좌표를 스택에 삽입
push word [ iX ]        ; 화면의 X 좌표를 스택에 삽입
call PRINTMESSAGE       ; PRINTMESSAGE 함수 호출
add sp, 6               ; 스택에 삽입된 함수 파라미터 3개(2바이트 * 3)를 제거

마지막에 SP 레지스터에 6을 더하는 이유는 함수 파라미터로 스택에 삽입된 값을 함수 수행이 끝난 후, 다시 원래대로 복원하기 위해서다.

16비트 모드에서는 스택에 2바이트(WORD) 크기로 삽입/제거되며, 삽입은 스택 포인터 레지스터를 아래로 이동시킨다.

위 코드를 보면, 호출되는 함수는 파라미터가 순서대로 삽입된다. 이럴 때는 스택에 고정된 값을 가리키는 레지스터를 사용하기 편리하므로, 베이스 포인터 레지스터 + Offset으로 파라미터에 접근할 수 있다. 호출된 함수에서 작업을 마치고 호출한 함수로 돌아갈 때 호출되기 전후의 레지스터 상태가 같아야 하므로, 이를 위해 호출되는 함수에서는 자신이 사용하는 레지스터의 값을 미리 스택에 저장한다. 이 특징 때문에 대부분 어셈블리어 함수는 아래와 같은 형태로 정형화되어 있다.

push bp     ; 베이스 포인터 레지스터(BP)를 스택에 삽입
mov bp, sp  ; 베이스 포인터 레지스터(BP)에 스택 포인터 레지스터(SP)의 값을 설정
            ; 베이스 포인터 레지스터(BP)를 이용해서 파라미터에 접근할 목적
            ; 호출된 직후의 SP 레지스터 값을 저장하여 BP 레지스터와 고정된 Offset으로 파라미터에 접근하게 함
push es     ; ES 세그먼트 레지스터부터 DX 레지스터까지 스택에 삽입
push si     ; 함수에서 임시로 사용하는 레지스터로, 함수의 마지막 부분에서 스택에 삽입된 값을 꺼내 원래 값으로 복원
push di
push ax
push cx
push dx

; 생략
;
mov ax, word [ bp + 4 ]     ; 파라미터 1(iX, 화면 X 좌표)
mov bx, word [ bp + 6 ]     ; 파라미터 2(iX, 화면 Y 좌표)
mov cx, word [ bp + 8 ]     ; 파라미터 3(pcString, 출력할 문자열의 주소)

; 생략

pop dx      ; 함수에서 사용이 끝난 DX 레지스터부터 ES 레지스터까지를 스택에 삽입된 값을 이용해서 복원
pop cs      ; 스택은 가장 마지막에 들어간 데이터가 가장 먼저 나오는 자료구조이므로 삽입의 역순으로 제거
pop ax
pop di
pop si
pop es

pop bp      ; 베이스 포인터 레지스터(BP) 복원
ret         ; 함수를 호출한 다음 코드의 위치로 복귀

함수 호출을 위해 Call 명령을 수행하면 자동으로 복귀 주소를 스택에 저장하고, 스택 포인터 레지스터의 값에 2를 빼서 복귀 주소를 가리키게 한다. 그리고, 호출된 함수는 파라미터 사용을 위해 베이스 포인터 레지스터의 값을 스택에 저장 후, 스택 포인터 레지스터의 값으로 바꾼다. 그 결과, 함수 파라미터가 베이스 포인터 레지스터를 기준으로 일정한 값만큼 증가하는 주소에 위치하고 있어서 베이스 포인터 레지스터 기준, 4바이트를 더한 주소에서 2바이트씩 증가하여 스택에 접근하는 것이다.

지금까지의 내용을 바탕으로 PRINTMESSAGE 함수의 소스 코드를 수정하면 아래와 같다.

; 메시지를 출력하는 함수
;   PARAM: x 좌표, y 좌표, 문자열
PRINTMESSAGE:
    push bp     ; 베이스 포인터 레지스터(BP)를 스택에 삽입
    mov bp, sp  ; 베이스 포인터 레지스터(BP)에 스택 포인터 레지스터(SP)의 값을 설정
                ; 베이스 포인터 레지스터(BP)를 이용해서 파라미터에 접근할 목적

    push es     ; ES 세그먼트 레지스터부터 DX 레지스터까지 스택에 삽입
    push si     ; 함수에서 임시로 사용하는 레지스터로, 함수의 마지막 부분에서 스택에 삽입된 값을 꺼내 원래 값으로 복원
    push di
    push ax
    push cx
    push dx

    ; ES 세그먼트 레지스터에 비디오 모드 주소 설정
    mov ax, 0xB800          ; 비디오 메모리 시작 주소(0x0B8000)를 세그먼트 레지스터 값으로 변환
    mov es, ax              ; ES 세그먼트 레지스터에 설정

    ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
    ; X, Y 좌표로 비디오 메모리의 주소를 계산함
    ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
    ; Y 좌표를 이용해서 먼저 라인 주소를 구함
    mov ax, word [ bp + 6 ] ; 파라미터 2(화면 좌표 Y)를 AX 레지스터에 설정
    mov si, 160             ; 한 라인의 바이트 수(2 * 80 컬럼)를 SI 레지스터에 설정
    mul si                  ; AX 레지스터와 SI 레지스터를 곱하여 화면 Y 주소 계산
    mov di, ax              ; 계산된 화면 Y 주소를 DI 레지스터에 설정

    ; X 좌표를 이용해서 2를 곱한 후 최종 주소를 구함
    mov ax, word [ bp + 4 ] ; 파라미터 1(화면 좌표 X)를 AX 레지스터에 설정
    mov si, 2               ; 한 문자를 나타내는 바이트 수(2)를 SI 레지스터에 설정
    mul si                  ; AX 레지스터와 SI 레지스터를 곱하여 화면 X 주소를 계산
    add di, ax              ; 화면 Y 주소와 계산된 X 주소를 더해서 실제 비디오 메모리 주소를 계산

    ; 출력할 문자열의 주소
    mov si, word [ bp + 8 ] ; 파라미터 3(출력할 문자열의 주소)

.MESSAGELOOP:               ; 메시지를 출력하는 루프
    mov cl, byte [ si ]     ; SI 레지스터가 가리키는 문자열 위치에서 한 문자를 CL 레지스터에 복사
                            ; CL 레지스터는 CX 레지스터의 하위 1바이트를 의미
                            ; 문자열은 1바이트면 충분하므로 CX 레지스터의 하위 1바이트만 사용

    cmp cl, 0               ; 복사된 문자와 0을 비교
    je  .MESSAGEEND         ; 복사한 문자의 값이 0이면 문자열이 종료되었음을 의미하므로 .MESSAGEEND로 이동하여 문자 출력 종료

    mov byte [ es: di ], cl ; 0이 아니라면 메모리 주소 0xB800:di에 문자를 출력

    add si, 1               ; SI 레지스터에 1을 더하여 다음 문자열로 이동
    add di, 2               ; DI 레지스터에 2를 더하여 비디오 메모리의 다음 문자 위치로 이동
                            ; 비디오 메모리는 (문자, 속성)의 쌍으로 구성되므로 문자만 출력하려면 2를 더해야 함

    jmp .MESSAGELOOP        ; 메시지 출력 루프로 이동하여 다음 문자를 출력

.MESSAGEEND:
    pop dx      ; 함수에서 사용이 끝난 DX 레지스터부터 ES 레지스터까지를 스택에 삽입된 값을 이용해서 복원
    pop cx      ; 스택은 가장 마지막에 들어간 데이터가 가장 먼저 나오는 자료구조이므로 삽입의 역순으로 제거
    pop ax
    pop di
    pop si
    pop es

    pop bp      ; 베이스 포인터 레지스터(BP) 복원
    ret         ; 함수를 호출한 다음 코드의 위치로 복귀


최종 부트 로더 소스 코드

아래는 지금까지 내용을 모두 적용한 부트 로더의 전체 소스 코드다. OS 이미지가 정상적으로 로딩되었다면, 0x10000 위치로 이동해서 보호 모드 커널 코드를 실행하도록 했다.

  • MINT64 OS의 최종 부트 로더 소스 코드(BootLoader.asm)
[ORG 0x00]          ; 코드의 시작 주소를 0x00으로 설정
[BITS 16]           ; 이하의 코드는 16비트 코드로 설정

SECTION .text       ; text 섹션(세그먼트) 정의

jmp 0x07C0:START    ; CS 세그먼트 레지스터에 0x07C0을 복사하면서, START 레이블로 이동

;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
; MINT 64 OS에 관련된 환경 설정 값
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
TOTALSECTORCOUNT:   dw  1024    ; 부트 로더를 제외한 MINT64 OS 이미지의 크기
                                ; 최대 1152섹터(0x90000바이트)까지 가능


;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
; 코드 영역
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
START:
    mov ax, 0x07C0  ; 부트 로더의 시작 주소(0x07C0)를 세그먼트 레지스터 값으로 변환
    mov ds, ax      ; DS 세그먼트 레지스터에 설정
    mov ax, 0xB800  ; 비디오 메모리의 시작 주소(0xB800)를 세그먼트 레지스터 값으로 변환
    mov es, ax      ; ES 세그먼트 레지스터에 설정

    ; 스택을 0x0000:0000~0x0000:FFFF 영역에 64KB 크기로 생성
    mov ax, 0x0000  ; 스택 세그먼트 시작 주소(0x0000)를 세그먼트 레지스터 값으로 변환
    mov ss, ax      ; SS 세그먼트 레지스터에 설정
    mov sp, 0xFFFE  ; SP 레지스터의 주소를 0xFFFE로 설정
    mov bp, 0xFFFE  ; BP 레지스터의 주소를 0xFFFE로 설정


    ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
    ; 화면을 모두 지우고 속성값을 녹색으로 설정
    ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
    mov     si, 0                   ; SI 레지스터문자열 원본 인덱스 레지스터)를 초기화

.SCREENCLEARLOOP:                   ; 화면을 지우는 루프
    mov byte [ es: si ], 0          ; 비디오 메모리의 문자가 위치하는 주소에 0을 복사하여 문자를 삭제
    mov byte [ es: si + 1 ], 0x0A   ; 비디오 메모리의 속성이 위치하는 주소에 0x0A를 복사
    add si, 2                       ; 문자와 속성을 설정했으므로 다음 위치로 이동

    cmp si, 80 * 25 * 2             ; 화면 전체 크기는 80문자 * 25라인
                                    ; 출력한 문자의 수를 의미하는 SI 레지스터와 비교
    jl  .SCREENCLEARLOOP            ; SI 레지스터가 80 * 25 * 2보다 작다면 아직 지우지 못한 영역이 있으므로 .SCREENCLEARLOOP 레이블로 이동
                                    ; jl = Jump if less


    ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
    ; 화면 상단에 시작 메시지 출력
    ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
    push MESSAGE1                   ; 출력할 메시지의 주소를 스택에 삽입
    push 0                          ; 화면 Y 좌표(0)을 스택에 삽입
    push 0                          ; 화면 X 좌표(0)을 스택에 삽입
    call PRINTMESSAGE               ; PRINTMESSAGE 함수 호출
    add sp, 6                       ; 삽입한 파라미터 제거


    ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
    ; OS 이미지를 로딩한다는 메시지 출력
    ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
    push IMAGELOADINGMESSAGE        ; 출력할 메시지의 주소를 스택에 삽입
    push 1                          ; 화면 Y 좌표(1)을 스택에 삽입
    push 0                          ; 화면 X 좌표(0)을 스택에 삽입
    call PRINTMESSAGE               ; PRINTMESSAGE 함수 호출
    add sp, 6                       ; 삽입한 파라미터 제거


    ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
    ; 디스크에서 OS 이미지를 로딩
    ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;


    ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
    ; 디스크를 읽기 전에 먼저 리셋
    ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
RESETDISK:                          ; 디스크를 리셋하는 코드의 시작
    ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
    ; BIOS Reset Function 호출
    ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
    ; 서비스 번호 0, 드라이브 번호(0=Floppy)
    mov ax, 0
    mov dl, 0
    int 0x13
    ; 에러가 발생하면 에러 처리로 이동
    jc  HANDLEDISKERROR


    ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
    ; 디스크에서 섹터를 읽음
    ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
    ; 디스크의 내용을 메모리로 복사할 주소(ES:BX)를 0x10000으로 설정
    mov si, 0x1000                  ; OS 이미지를 복사할 주소(0x10000)를 세그먼트 레지스터 값으로 변환
    mov es, si                      ; ES 세그먼트 레지스터에 값 설정
    mov bx, 0x0000                  ; BX 레지스터에 0x0000을 설정하여 복사할 주소를 0x1000:0000(0x10000)으로 최종 설정

    mov di, word [ TOTALSECTORCOUNT ] ; 복사할 OS 이미지의 섹터 수를 DI 레지스터에 설정

READDATA:                           ; 디스크를 읽는 코드의 시작
    ; 모든 섹터를 다 읽었는지 확인
    cmp di, 0                       ; 복사할 OS 이미지의 섹터 수를 0과 비교
    je READEND                      ; 복사할 섹터 수가 0이라면 다 복사했으므로 READEND로 이동
    sub di, 0x1                     ; 복사할 섹터 수를 1 감소


    ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
    ; BIOS Read Function 호출
    ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
    mov ah, 0x02                    ; BIOS 서비스 번호 2(Read Sector)
    mov al, 0x1                     ; 읽을 섹터 수는 1
    mov ch, byte [ TRACKNUMBER ]    ; 읽을 트랙 번호 설정
    mov cl, byte [ SECTORNUMBER ]   ; 읽을 섹터 번호 설정
    mov dh, byte [ HEADNUMBER ]     ; 읽을 헤드 번호 설정
    mov dl, 0x00                    ; 읽을 드라이브 번호(0=Floppy) 설정
    int 0x13                        ; 인터럽트 서비스 수행
    jc HANDLEDISKERROR              ; 에러가 발생했다면 HANDLEDISKERROR로 이동


    ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
    ; 복사할 주소와 트랙, 헤드 섹터 주소 계산
    ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
    add si, 0x0020      ; 512(0x200)바이트 만큼 읽었으므로 이를 세그먼트 레지스터 값으로 변환
    mov es, si          ; ES 세그먼트 레지스터에 더해서 주소를 한 섹터만큼 증가

    ; 한 섹터를 읽었으므로 섹터 번호를 증가시키고 마지막 섹터(18)까지 읽었는지 판단
    ; 마지막 섹터가 아니면 섹터 읽기로 이동해서 다시 섹터 읽기 수행
    mov al, byte [ SECTORNUMBER ]   ; 섹터 번호를 AL 레지스터에 설정
    add al, 0x01                    ; 섹터 번호를 1 증가
    mov byte [ SECTORNUMBER ], al   ; 증가시킨 섹터 번호를 SECTORNUMBER에 다시 설정
    cmp al, 19                      ; 증가시킨 섹터 번호를 19와 비교
    jl READDATA                     ; 섹터 번호가 19 미만이라면 READDATA로 이동

    ; 마지막 섹터까지 읽었으면(섹터 번호가 19이면) 헤드를 토글(0->1, 1->0)하고, 섹터 번호를 1로 설정
    xor byte [ HEADNUMBER ], 0x01   ; 헤드 번호를 0x01과 XOR하여 토글(0->1, 1->0)
    mov byte [ SECTORNUMBER ], 0x01 ; 섹터 번호를 다시 1로 설정

    ; 만약 헤드가 1->0으로 바뀌었으면 양쪽 헤드를 모두 읽은 것이므로 아래로 이동하여 트랙 번호를 1 증가
    cmp byte [ HEADNUMBER ], 0x00   ; 헤드 번호를 0x00과 비교
    jne READDATA                    ; 헤드 번호가 0이 아니면 READDATA로 이동

    ; 트랙을 1 증가시킨 후, 다시 섹터 읽기로 이동
    add byte [ TRACKNUMBER ] , 0x01 ; 트랙 번호를 1 증가
    jmp READDATA                    ; READDATA로 이동
READEND:


    ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
    ; OS 이미지가 완료되었다는 메시지를 출력
    ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
    push LOADINGCOMPLETEMESSAGE     ; 출력할 메시지의 주소를 스택에 삽입
    push 1                          ; 화면 Y 좌표(1)를 스택에 삽입
    push 20                         ; 화면 X 좌표(20)을 스택에 삽입
    call PRINTMESSAGE               ; PRINTMESSAGE 함수 호출
    add sp, 6                       ; 삽입한 파라미터 제거

    ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
    ; 로딩한 가상 OS 이미지 실행
    ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
    jmp 0x1000:0x0000


;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
; 함수 코드 영역
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
; 디스크 에러를 처리하는 함수
HANDLEDISKERROR:
    push DISKERRORMESSAGE   ; 에러 문자열의 주소를 스택에 삽입
    push 1                  ; 화면 Y 좌표(1)를 스택에 삽입
    push 20                 ; 화면 X 좌표(20)을 스택에 삽입
    call PRINTMESSAGE       ; PRINTMESSAGE 함수 호출
    add sp, 6               ; 삽입한 파라미터 제거

    jmp $                   ; 현재 위치에서 무한 루프 수행

; 메시지를 출력하는 함수
;   PARAM: x 좌표, y 좌표, 문자열
PRINTMESSAGE:
    push bp     ; 베이스 포인터 레지스터(BP)를 스택에 삽입
    mov bp, sp  ; 베이스 포인터 레지스터(BP)에 스택 포인터 레지스터(SP)의 값을 설정
                ; 베이스 포인터 레지스터(BP)를 이용해서 파라미터에 접근할 목적
                ; 호출된 직후의 SP 레지스터 값을 저장하여 BP 레지스터와 고정된 Offset으로 파라미터에 접근하게 함
    push es     ; ES 세그먼트 레지스터부터 DX 레지스터까지 스택에 삽입
    push si     ; 함수에서 임시로 사용하는 레지스터로 함수의 마지막 부분에서 스택에 삽입된 값을 꺼내 원래 값으로 복원
    push di
    push ax
    push cx
    push dx

    ; ES 세그먼트 레지스터에 비디오 모드 주소 설정
    mov ax, 0xB800          ; 비디오 메모리 시작 주소(0xB8000)를 세그먼트 레지스터 값으로 변환
    mov es, ax              ; ES 세그먼트 레지스터에 설정


    ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
    ; X, Y 좌표로 비디오 메모리의 주소를 계산
    ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
    ; Y 좌표를 이용해서 먼저 라인 주소를 구함
    mov ax, word [ bp + 6 ] ; 파라미터 2(화면 좌표 Y)를 AX 레지스터에 설정
    mov si, 160             ; 한 라인의 바이트 수(2 * 80 컬럼)를 SI 레지스터에 설정
    mul si                  ; AX 레지스터와 SI 레지스터를 곱하여 화면 Y 주소 계산
    mov di, ax              ; 계산된 화면 Y 주소를 DI 레지스터에 설정

    ; X 좌표를 이용해서 2를 곱한 후 최종 주소를 구함
    mov ax, word [ bp + 4 ] ; 파라미터 1(화면 좌표 X)를 AX 레지스터에 설정
    mov si, 2               ; 한 문자를 나타내는 바이트 수(2)를 SI 레지스터에 설정
    mul si                  ; AX 레지스터와 SI 레지스터를 곱하여 화면 X 주소를 계산
    add di, ax              ; 화면 Y 주소와 계산된 X 주소를 더해서 실제 비디오 메모리 주소를 계산

    ; 출력할 문자열의 주소
    mov si, word [ bp + 8 ] ; 파라미터 3(출력할 문자열의 주소)

.MESSAGELOOP:               ; 메시지를 출력하는 루프
    mov cl, byte [ si ]     ; SI 레지스터가 가리키는 문자열 위치에서 한 문자를 CL 레지스터에 복사
                            ; CL 레지스터는 CX 레지스터의 하위 1바이트를 의미
                            ; 문자열은 1바이트면 충분하므로 CX 레지스터의 하위 1바이트만 사용

    cmp cl, 0               ; 복사된 문자와 0을 비교
    je  .MESSAGEEND         ; 복사한 문자의 값이 0이면 문자열이 종료되었음을 의미하므로 .MESSAGEEND로 이동하여 문자 출력 종료

    mov byte [ es: di ], cl ; 0이 아니라면 메모리 주소 0xB800:di에 문자를 출력

    add si, 1               ; SI 레지스터에 1을 더하여 다음 문자열로 이동
    add di, 2               ; DI 레지스터에 2를 더하여 비디오 메모리의 다음 문자 위치로 이동
                            ; 비디오 메모리는 (문자, 속성)의 쌍으로 구성되므로 문자만 출력하려면 2를 더해야 함

    jmp .MESSAGELOOP        ; 메시지 출력 루프로 이동하여 다음 문자를 출력

.MESSAGEEND:
    pop dx      ; 함수에서 사용이 끝난 DX 레지스터부터 ES 레지스터까지를 스택에 삽입된 값을 이용해서 복원
    pop cx      ; 스택은 가장 마지막에 들어간 데이터가 가장 먼저 나오는 자료구조이므로 삽입의 역순으로 제거
    pop ax
    pop di
    pop si
    pop es

    pop bp      ; 베이스 포인터 레지스터(BP) 복원
    ret         ; 함수를 호출한 다음 코드의 위치로 복귀


;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
; 데이터 영역
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
; 부트 로더 시작 메시지
MESSAGE1:   db 'MINT64 OS Boot Loader Start~!!', 0  ; 출력할 메시지 정의
                                                    ; 마지막은 0으로 설정하여 .MESSAGELOOP에서 문자열이 종료되었음을 알 수 있도록 함
DISKERRORMESSAGE:       db  'DISK Error~!!', 0
IMAGELOADINGMESSAGE:    db  'OS Image Loading...', 0
LOADINGCOMPLETEMESSAGE: db  'Complete~!!', 0

; 디스크 읽기 관련된 변수들
SECTORNUMBER:       db  0x02    ; OS 이미지가 시작하는 섹터 번호를 저장하는 영역
HEADNUMBER:         db  0x00    ; OS 이미지가 시작하는 헤드 번호를 저장하는 영역
TRACKNUMBER:        db  0x00    ; OS 이미지가 시작하는 트랙 번호를 저장하는 영역

times 510 - ( $ - $$ )  db  0x00    ; $: 현재 라인의 주소
                                    ; $$: 현재 섹션(.text)의 시작 주소
                                    ; $ - $$: 현재 섹션을 기준으로 하는 Offset
                                    ; 510 - ( $ - $$ ): 현재부터 주소 510까지
                                    ; db 0x00: 1바이트를 선언하고 값은 0x00
                                    ; time: 반복 수행
                                    ; 현재 위치에서 주소 510까지 0x00으로 채움

db 0x55             ; 1바이트를 선언하고 값은 0x55
db 0xAA             ; 1바이트를 선언하고 값은 0xAA
                    ; 주소 511, 512에 0x55, 0xAA를 써서 부트 섹터로 표기함

위 소스 코드를 빌드해서 이미지를 QEMU로 실행하면 아래와 같은 화면이 나온다.

CLion을 이용해서 빌드했다.

qemu-system-x86_64 -L . -m 64 -fda {MINT64 디렉토리 내 Disk.img 위치} -localtime -M pc

다음 시간에는 x86 프로세서의 보호 모드에서 사용되는 세 가지 함수 호출 규약에 대해 알아보겠다.

Comments