본문 바로가기

MINT64 OS 개발

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

Ⅰ. 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:       ;에러 처리하는 코드
    ```생략```

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

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가지 함수 호출 규약

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

Ⅳ. 최종 부트 로더 소스코드_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를 써서 섹터로 표기함
  • 결과

Ⅴ. 테스트를 위한 가상 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 개발' 카테고리의 다른 글

7장. C언어로 커널 작성  (0) 2023.07.20
6. 32비트 보호 모드로 전환  (0) 2023.05.06
4. 32비트 보호 모드로 전환  (0) 2023.04.13
3. OS이미지 메모리에 복사  (0) 2023.04.10
2. 간단한 부트로더 제작  (0) 2023.04.10