본문 바로가기

MINT64 OS 개발

3. OS이미지 메모리에 복사

1. 1024 섹터 크기의 이미지를 메모리로 복사하는 코드

(1) C언어

#include <stdio.h>

int main(){
    int iTotalSectorCount = 1024;
    int iSectorNumber = 2;
    int iHeadMumber = 0;
    int iTrackNumber = 0;
    
    // 실제 이미지를 복사할 address(물리주소)
    char* pcTargetAddress = (char*) 0x1000;

    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)? 0x01:0x00; 과 같은 의미
        iHeadNumber = iHeadNumber ^ 0x01;
        iSectorNumber = 1;

        if(iHeadNumber != 0){
            continue;
        }

        iTrackNumber = iTrackNumber + 1;
    }

}

void HandleDiskError(){
    printf("Disk Error-!!");
    while( 1 );
}

(2) asm

TOTALSECTORCONT:    dw 1024 ;부트로더를 제외한 MINT64 OS 이미지의 크기. 최대 1152 섹터(0x90000byte)까지 가능
SECTORNUMBER:       db 0x02 ;OS 이미지가 시작하는 섹터 번호를 저장하는 영역
HEADNUMBER:         db 0x00 ;OS 이미지가 시작하는 헤드 번호를 저장하는 영역
TRACKNUMBER:        db 0x00 ;OS 이미지가 시작하는 트랙 번호를 저장하는 영역

    ;디스크의 내용을 메모리로 복사할 어드레스(ES:BX)를 0x10000으로 설정
    mov si, 0x1000      ;OS 이미지를 복사할 어드레스(0x10000)를 세그먼트 레지스터 값으로 변환
    mov es, si          ;ES 세그먼트 레지스터에 값 설정
    mov bx, 0x0000      ;BX 레지스터에 0x0000을 설정하여 복사할 어드레스를 0x1000:0000(0x10000)으로 최종 설정
    mob 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          ;에러가 발생했다면 HANDELEDISKERROR로 이동

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

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

    ;마지막 섹터까지 읽었으면(섹터 번호가 19일 경우) 헤드를 토글 (0->1, 1->0)하고, 섹터 번호를 1로 설정
    xor byte [HEADNUMBER], 0x1      ;헤드 번호를 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 증가
READEND:

HANDELEDISKERROR:       ;에러 처리하는 코드
    ```생략```

2. 스택 초기화와 함수 구현

(1) 스택 사용할 영역

  • 0x010000(64KB) 주소부터 OS이미지가 로딩되므로 0x01000이하로 지정함
  • 본 실습에서는 0x0000:0000 ~ 0x0000:FFFF 영역으로 지정함
    • 스택 세그먼트 레지스터(SS)의 값은 0x0000로 설정하였으며, SP와 BP를 0xFFFF로 설정하여 스택 영역의 크기는 세그먼트의 최대 크기로 지정하였음

(2) 스택 초기화 코드

  • 부트 로더 앞부분에 추가될 코드
;stack을 0x0000:0000~0x0000:FFFF 영역에 64KB 크기로 생성
mov ax, 0x0000  ;스택 세그먼트의 시작 어드레스(0x0000)를 세그먼트 레지스터 값으로 변환
mov ss, ax      ;SS 세그먼트 레지스터에 설정
mov sp, 0xFFFE  ;SP 레지스터의 어드레스를 0xFFFE로 설정
mov bp, 0xFFFE  ;BP 레지스터의 어드레스를 0xFFFE로 설정

(3) 메시지 출력 함수

  • 화면에서 원하는 위치에 문자열을 출력하려면 X좌표, Y좌표, 출력할 문자열 어드레스가 필요함
    • 필요한 3가지를 함수 파라미터로 정의하고 스택에 삽입하는 순서를 지정해야함
    • C언어 경우, 파라미터의 역순(오른쪽 → 왼쪽)으로 삽입하고 스택에서 꺼낸 순서가 파라미터 순서와 같게 함
    • Example
      • C언어
      PrintMessage( iX, iY, pcString );
      
      • 어셈블리어
      push word [pcString] ;문자열 어드레스를 스택에 삽입
      push word [iY]       ;화면의 Y좌표를 스택에 삽입
      push word [iX]       ;화면의 X좌표를 스택에 삽입
      call PRINTMESSAGE    ;PRINTMESSAGE 함수 호출
      add sp, 6            ;2byte*3하여, 6만큼 sp 증가
      
      • 어셈블리 함수의 일반적인 형식
      push bp              ;베이스 포인터 레지스터(BP)를 스택에 삽입
      mov bp, sp           ;베이스 포인터 레지스터(BP)에 스택 포인터 레지스터(SP)의 값 설정
                           ;베이스 포인터 레지스터(BP)를 통해 파라미터에 접근할 목적
      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(iY, 화면 Y좌표)
      mov cx, word  [bp+8] ;파라미터 3(pcString, 출력할 문자열의 어드레스)
      
      ```생략```
      
      pop dx      ;함수에서 사용이 끝난 DX 레지스터부터 ES레지스터까지 스택에 삽입된 값을 이용해서 복원
      pop cx      ;스택은 가장 마지막에 들어간 데이터가 가장 먼저 나오는 자료구조이므로
      pop ax      ;삽입의 역순으로 제거해야함
      pop di
      pop si
      pop es
      pop bp      ;베이스 포인터 레지스터(BP) 복원
      ret         ;함수를 호출한 다음 코드의 위치로 복원
      
      
  • 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레지스터의 하위 1byte를 의미
    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         ;함수를 호출한 다음 코드의 위치로 복원

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

  • 이전까지는 리얼모드인 16비트 모드 기준이었음. 보호모드는 32bit 이므로 레지스터&스택 크기가 리얼 모드의 2배임
  • 함수 규약은 함수를 호출할 때, 파라미터와 복귀 어드레스 등을 지정하는 규칙을 의미함
  • 보호 모드에서 사용하는 대표적 호출 규약: stdcall, cdecl, fastcall
    • stdcall: 파라미터를 스택에 저장하며, 호출된 쪽에서 스택을 정리함
    • cdecl: 파라미터를 스택에 저장하지만, 함수를 호출한 쪽에서 스택을 정리함
    • fastcall: 일부 파라미터를 레지스터에 저장하는 것을 제외하면, stdcall방식과 같음

4. 최종 부트 로더 소스코드_BootLoader.asm

  • OS이미지가 정상적으로 로딩되었다면, 0x1000위치로 이동해서 보호모드 커널 코드를 실행하도록 진행되었음
  • 현재 로딩할 커널 이미지가 없으므로 이후 과정에서 구현해야함
    • 부트 로더를 제외한 나머지 섹터 크기를 1024로 설정하였으므로 부트 로더를 포함해서 최소한 1025개 이상의 섹터로 구성된 OS이미지가 필요함 (but, 부트 로더 테스트 단계로 가상 OS이미지로 대체할 예정)
[ORG 0x00]  ; 코드의 시작 어드레스를 0x00으로 설정
[BITS 16]   ; 이하의 코드는 16비트 코드로 설정

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

jmp 0x07C0:START

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

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

        ;stack을 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

.SCREENCLEARLOOP:
	mov byte [es:si], 0
	mov byte [es:si+1], 0x20

	add si, 2
	cmp si, 80*25*2

	jl .SCREENCLEARLOOP

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

;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;  OS 이미지를 로딩한다는 메시지 출력
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
        push IMAGELOADINGMESSAGE
        push 1          ;화면 Y좌표(1)을 스택에 삽입
        push 0          ;화면 X좌표(0)을 스택에 삽입
        call PRINTMESSAGE
        add sp, 6

        ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
        ;  디스크에서 OS이미지를 로딩
        ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
        ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
        ;  디스크를 읽기 전에 먼저 리셋
        ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
RESETDISK:                      ;디스크를 리셋하는 코드의 시작
        ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
        ;  BIOS Reset Function 호출
        ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
        ;서비스 번호 0, 드라이브 번호(0=Floppy)
        mov ax, 0
        mov dl, 0                   ;읽을 드라이브 번호(0==Floppy) 설정
        int 0x13                    ;인터럽트 서비스 수행
        jc HANDLEDISKERROR          ;에러가 발생했다면 HANDELEDISKERROR로 이동

        ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
        ;  디스크에서 섹터를 읽음
        ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
        ;디스크의 내용을 메모리로 복사할 어드레스(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          ;에러가 발생했다면 HANDELEDISKERROR로 이동

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

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

        ;마지막 섹터까지 읽었으면(섹터 번호가 19일 경우) 헤드를 토글 (0->1, 1->0)하고, 섹터 번호를 1로 설정
        xor byte [HEADNUMBER], 0x1      ;헤드 번호를 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
        add sp, 6

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

;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;  함수 코드 영역
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
HANDLEDISKERROR:                       ;에러 처리하는 코드
        push DISKERRORMESSAGE   
        push 1                          ;화면 Y좌표(1)를 스택에 삽입
        push 20                         ;화면 X좌표(20)를 스택에 삽입
        call PRINTMESSAGE

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

;메시지 출력하는 함수  (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레지스터의 하위 1byte를 의미
    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

db 0x55
db 0xAA ; 1byte를 선언하고 값은 0xAA
        ; 어드레스 511, 512에 0x55, 0xAA를 써서 섹터로 표기함
  • 결과

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

  • 테스트를 위해, 잠깐 사용하므로 세세한 부분까지 구현하지 않아도 됨
    • 따라서, 부트 로더 코드를 기반으로 기능을 간소화하여 OS가 실행되었음을 표시하는 기능만 생성함
  • 자신의 섹터 번호를 화면 위치에 대응시켜 0~9까지 번호를 출력한다면 화면에 출력된 문자의 위치와 수를 확인하여 정상여부를 판단할 수 있음
  • 가상 OS이미지는 거의 같은 코드를 반복하는 구조이므로, 첫번째 섹터에 삽입할 코드를 구현하고 이를 반복하여 나머지 섹터도 생성하였음
  • 본 코드는 “01.Kernel32 디렉터리에 VirtualOS.asm”파일로 생성하였음
    • VirtualOS.asm파일은 임의로 지정한 가상 OS 소스 파일임
    • 해당 코드는 부트로더 코드와 동일한 방식으로 레지스터 초기화를 수행 후, 화면 2번째 라인의 가장 왼쪽 위치에 0을 출력하도록 하였음
    • SECTORCOUNT라는 메모리 어드레스는 정상적으로 처리된 섹터의 수를 기록하고 이를 이용하여 화면에 출력될 좌표를 계산할 용도록 추가하였음

(1) 1섹터 크기의 가상 OS 소스코드(VirtualOS.asm)

[ORG 0x00]          ;코드의 시작 어드레스는 0x00으로 설정
[BITS 16]           ;이하의 코드는 16비트 코드로 설정

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

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

SECTORCOUNT:    dw 0x0000   ;현재 실행 중인 섹터 번호를 저장

START:
    mov ax, cs      ;CS 세그먼트 레지스터의 값을 AX레지스터에 설정
    mov ds, ax      ;AX 레지스터의 값을 DS 세그먼트 레지스터에 저장
    mov ax, 0xB800  ;비디오 메모리 어드레스인 0xB8000을 세그먼트 레지스터 값으로 변환
    mov es, ax

    mov ax, 2                       ;한 문자를 나타내는 바이트 수(2)를 AX레지스터에 설정
    mul word [SECTORCOUNT]          ;AX레지스터와 섹터 수를 곱함
    mov si, ax                      ;곱한 결과를 si레지스터에 설정
    mov byte [es:si + (160*2)], '0' ;계산한 결과를 비디오 메모리에 오프셋으로 삼아 세번째 라인부터 화면에 0을 출력
    add word [SECTORCOUNT], 1       ;섹터 수를 1 증가

    jmp $                           ;현재 위치에서 무한루프 수행
    
    times 512 - ($-$$)  db 0x00     ;512 - ($-$$): 현재부터 어드레스 512까지
                                    ;현재 위치에서 어드레스 512까지 0x00으로 채움

(2) 1024섹터를 만드는 방법

  • 1024섹터 중 마지막 섹터를 제외한 1023섹터의 코드는 화면에 자신을 출력하는 코드 및 다음 섹터의 어드레스로 이동하는 코드를 반복하면 됨
  • 마지막 섹터의 경우, 더 이상 섹터가 없으므로 자신을 출력하고 무한 루프를 수행하도록 만들면 됨
  • NASM 어셈블러는 반복되는 구문을 위해, %rep라는 전처리문을 제공함
    • times와 동일하게 반복 횟수와 반복할 대상을 입력하여 사용할 수 있음
    • %endrep사이의 코드를 반복횟수만큼 확장해줌
    • 이 외에도 변수할당, 대입문(%assign), 조건문(%if-%elif-%else-%endif)도 지원함
    • NASM어셈블러 문서 웹페이지: http://www.nasm.us/doc
  • Code
    • TOTALSECTORCOUNT라는 상수를 1024로 정의하여, 1024 섹터의 os이미지가 생성되도록 구현하였음 (equ명령은 상수를 정의하는 명령어임)
    • times (512 - ($-$$) %512) db 0x00
      • 각 섹터의 상위에 화면 출력 코드를 삽입하고, 512byte단위로 정렬하기 위해 남은 영역을 0으로 채움
    [ORG 0x00]                      ;코드의 시작 어드레스는 0x00으로 설정
    [BITS 16]                       ;이하의 코드는 16비트 코드로 설정
    
    SECTION .text                   ;text 섹션(세그먼트)을 정의
    
    jmp 0x1000:START                ;CS 세그먼트 레지스터에 0x1000을 복사하면서, START레이블로 이동
    
    SECTORCOUNT:    dw 0x0000       ;현재 실행 중인 섹터 번호를 저장
    TOTALSECTORCOUNT    equ 1024    ;가상OS의 총 섹터 수 (최대 1152섹터인 0x90000byte까지 가능)
    
    START:
        mov ax, cs      ;CS 세그먼트 레지스터의 값을 AX레지스터에 설정
        mov ds, ax      ;AX 레지스터의 값을 DS 세그먼트 레지스터에 저장
        mov ax, 0xB800  ;비디오 메모리 어드레스인 0xB8000을 세그먼트 레지스터 값으로 변환
        mov es, ax
    
        %assign i   0                   ;i라는 변수를 지정하고 0으로 초기화
        %rep TOTALSECTORCOUNT           ;TOTALSECTORCOUNT에 저장된 값만큼 아래 코드를 반복
            %assign i   i+1             ;i를 1증가
            
            ;현재 실행 중인 코드가 포함된 섹터의 위치를 화면 좌표로 변환
            mov ax, 2                       ;한 문자를 나타내는 바이트 수(2)를 AX레지스터에 설정
            mul word [SECTORCOUNT]          ;AX레지스터와 섹터 수를 곱함
            mov si, ax                      ;곱한 결과를 si레지스터에 설정
            mov byte [es:si + (160*2)], '0' + (i % 10) ;계산한 결과를 비디오 메모리에 오프셋으로 삼아 세번째 라인부터 화면에 0을 출력
            add word [SECTORCOUNT], 1       ;섹터 수를 1 증가
    
            %if i == TOTALSECTORCOUNT       ;마지막 섹터이면 현재 위치에서 무한 루프 수행
                jmp $                       ;현재 위치에서 무한루프 수행
            %else
                jmp (0x1000 + i * 0x20): 0x0000 ;다음 섹터 오프셋으로 이동 
            %endif                          ;if문 끝
    
            times (512 - ($-$$) %512) db 0x00   ;512 - ($-$$): 현재부터 어드레스 512까지
                                            ;현재 위치에서 어드레스 512까지 0x00으로 채움
        %endrep                             ;반복문의 끝
    

(3) 이미지 생성을 위해, Makefile 생성

  • 이전에 만든 소스코드를 통해, 이미지 파일 생성
  • 1.Kernel32 디렉토리에 makefile 생성
all: VirtualOS.bin

VirtualOS.bin: VirtualOS.asm
	nasm -o VirtualOS.bin VirtualOS.asm

clean:
	rm -f VirtualOS.bin

(4) OS이미지 통합

  • 가상 OS이미지까지 만들었으므로, 파일들을 하나로 합쳐서 부팅 이미지를 만들어줌
  • 부팅 이미지를 만드는 작업이므로, 최상위 디렉토리에 존재하는 makefile을 수정
  • 추가 내용
    • 먼저, 1.Kernel32 디렉터리에서 make를 수행하여, 가상의 OS이미지 빌드 진행
    • 두번째로, 부트로더와 가상 OS이미지를 하나의 파일로 합치도록 수정
    • all 레이블에서 부트 로더 → 가상 OS이미지 → 부팅 이미지 순서로 진행하도록 작성
  • makefile
all: BootLoader Kernel32 Disk.img

BootLoader:
	@echo
	@echo =================== Build Boot Loader ===================
	@echo

	make -C 0.BootLoader

	@echo
	@echo ==================== Build Complete ====================
	@echo

Kernel32:
	@echo
	@echo =================== Build 32bit Kernel ===================
	@echo

	make -C 1.Kernel32

	@echo
	@echo ==================== Build Complete ====================
	@echo

Disk.img: BootLoader Kernel32
	@echo
	@echo =================== Disk Image Build Start ===================
	@echo

	cat 0.BootLoader/BootLoader.bin 1.Kernel32/VirtualOS.bin > Disk.img

	@echo
	@echo ==================== Build Complete ====================
	@echo

clean:
	make -C 0.BootLoader clean
	make -C 1.Kernel32 clean
	rm -f Disk.img

(5) QEMU 실행

  • 에러 발생

  • 해결방안
    • Floppy Disk가 1.44MB FD에서 2.88MB FD로 변경되면서, 트랙당 섹터 개수가 36개로 변경되었음. 따라서, 18개로 산정한 부트로더에서 에러가 발생하는 것임
    • 0.BootLoader 디렉토리의 BootLoader.asm 코드 수정하여 해결할 수 있음 : 19(18+1) 비교하는 것을 37 (36+1)로 변경하면 에러 해결할 수 있음
    • 참고: http://jsandroidapp.cafe24.com/xe/development/12035

  • 정상적인 결과 모습

'MINT64 OS 개발' 카테고리의 다른 글

6. 32비트 보호 모드로 전환  (0) 2023.05.06
5. OS이미지 메모리에 복사  (0) 2023.05.06
4. 32비트 보호 모드로 전환  (0) 2023.04.13
2. 간단한 부트로더 제작  (0) 2023.04.10
1. 개요 & 환경 구축  (0) 2023.04.10