본문 바로가기

MINT64 OS 개발

4. 32비트 보호 모드로 전환

Ⅰ. 개요

  • 리얼 모드에서 보호모드로 전환하려면, 크게 6단계를 거쳐야 한다.
    • 상위 2단계는 보호 모드 전환에 필요한 자료구조를 생성하는 단계이다.
    • 나머지 4단계는 생성된 자료구조를 프로세서에 설정하는 단계이다.
    • 세그먼트 디스크립터 생성 → GDT 정보 생성 → 프로세서에 GDT 정보 설정 → CR0 컨트롤 레지스터 설정 → jmp 명령으로 CS 세그먼트 셀렉터 변경과 보호 모드로 전환 → 각종 세그먼트 셀렉터 및 스택 초기화 (32bit 보호 모드) → 보호 모드 커널 실행
  • 보호 모드에서 반드시 생성해야 하는 자료구조는 세그먼트 디스크립터와 GDT이다.

Ⅱ. 세그먼트 디스크립터 생성

1. 개요

  • 세그먼테이션 기법에서 세그먼트의 정보를 나타내는 자료구조를 의미한다.
    • 세그먼트: 메모리 공간을 임의의 크기로 나눈 영역을 의미한다.
    • 세그먼트를 복잡하게 구성할수록 세그먼트 디스크립트의 수도 증가한다.
    • 보호 모드의 세그먼트 디스크립터는 8byte로, 다양한 필드가 존재한다.

2. 개발 시, 설정하고자 하는 세그먼트

  • 커널 코드와 데이터용 세그먼트 디스크립터 각 1개씩 생성한다.
  • 커널 코드와 데이터용 세그먼트는 0~4GB까지 모든 영역에 접근할 수 있어야 한다.
  • 보호 모드용 코드와 데이터에 사용할 기본 오퍼랜드 크기는 32bit여야 한다.
  • 보호 기능은 사용하지 않으며, 프로세서의 명령을 사용하는데 제약이 없어야 하므로 최상위 권한(0)이어야 한다.

3. 코드 세그먼트 디스크립터와 데이터 세그먼트 디스크립터 타입 설정

  • 코드 & 데이터 세그먼트를 설정하려면, S필드와 타입 필드를 조합해야 한다.
  • S필드
    • 코드 세그먼트와 데이터 세그먼트는 세그먼트 디스크립터이므로, S필드의 값을 1로 설정한다.
  • 세그먼트 타입
    • 4bit 크기의 타입 필드를 이용해서 설정한다.
    • 해당 실습에서는 기본적인 세그먼트 타입만 사용하고, 코드 세그먼트는 실행/읽기 타입으로 설정한다.
    • 코드 세그먼트 타입: 0x0A (Execute/Read)
    • 데이터 세그먼트 타입: 0x02 (Read/Write)

4. 세그먼트의 영역 설정

  • MINT64 OS의 커널 세그먼트 디스크립터는 4GB 전체 영역에 접근할 수 있어야 한다.
    • 커널용 세그먼트 디스크립터의 기준 주소는 0으로 설정한다.
  • 세그먼트의 크기
    • 크기 필드만으로는 4GB영역을 표현할 수 없으므로 20bit의 크기를 4GB로 확장할 것이 필요한데, 이 때 사용되는 것이 “G 필드”이다.
    • G필드의 값을 1로 설정하면, 크기 필드에 4KB 곱한 것이 실제 세그먼트의 크기가 된다.
    • 1MB에 4KB를 곱하면, 4GB가 되므로 크기 필드와 G필드를 사용하면 메모리 전체 영역을 세그먼트 영역으로 설정할 수 있다.

5. 기본 오퍼랜드 크기와 권한 설정

(1) 기본 오퍼랜드 크기

  • 보호 모드는 32bit에서 동작하므로, 기본 오퍼랜드의 크기도 32bit로 설정한다.
  • 기본 오퍼랜드의 크기는 D/B 필드가 담당하며, 1로 설정하면 32bit 크기로 설정할 수 있다.
    • IA-32e 모드의 64bit 서브 모드 or 32bit 호환 모드를 설정하는 L필드도 기본 오퍼랜드 크기를 지정할 수 있다.
    • 본 실습에서는 32bit 보호모드용이므로 L비트는 0으로 설정한다.

(2) 권한 필드

  • 보호 모드의 주요 특징 중, 하나인 보호 기능에 핵심 역할을 한다.
  • 프로세서는 디스크립터의 권한 필드에 설정된 값과 세그먼트 셀렉터의 권한을 비교하여 접근 가능한지 판단한다.
  • MINT64 OS의 보호 모드는 권한을 따로 구분하지 않고, 권한 필드를 모두 최상위 권한(0)으로 설정한다.

6. 기타 필드 설정

  • 생성한 세그먼트 디스크립터는 보호 모드로 전환하는 과정에서 사용하므로, 유효한 디스크립터라는 것을 알려야 한다.
    • P필드를 1로 설정하면, 해당 디스크립터를 사용할 수 있다.
    • AVL 필드는 임의로 사용할 수 있는 필드로, MINT64 OS에서는 별도의 값을 쓰지 않으므로 0으로 설정한다.

7. 세그먼트 디스크립터 생성 코드

  • 앞의 내용을 바탕으로, 어셈블리어를 사용하여 코드 세그먼트 디스크립터와 데이터 세그먼트 디스크립터를 생성한다.
CODEDESCRIPTOR:
	dw 0xFFFF	; Limit [15:0]
	dw 0x0000	; Base [15:0]
	db 0x00		; Base [23:16]
	db 0x9A		; P=1, DPL=0, Code Segment, Execute/Read
	db 0xCF		; G=1, D=1, L=0, Limit[19:16]
	db 0x00		; Base [31:24]

DATADESCRIPTOR:
	dw 0xFFFF	; Limit [15:0]
	dw 0x0000	; Base [15:0]
	db 0x00		; Base [23:16]
	db 0x92		; P=1, DPL=0, Data Segment, Read/Write
	db 0xCF		; G=1, D=1, L=0, Limit[19:16]
	db 0x00		; Base [31:24]
  • 보호 모드는 현대 OS가 제공하는 4GB 주소 공간, 멀티태스킹, 페이징, 메모리 보호 등의 기능을 하드웨어적으로 지원한다.

Ⅲ. GDT 정보 생성

  • GDT(Global Descriptor Table)자체는 연속된 디스크립터의 집합이다.
    • 즉, MINT64 OS에서는 사용하는 코드 세그먼트 디스크립터와 데이터 세그먼트 디스크립터를 연속된 어셈블리어 코드로 나타내면 그 전체 영역이 GDT이다.
    • 다만, 가장 앞부분에 NULL Descriptor를 추가해야 한다.
    • (Null Descriptor는 프로세서에 의해, 예약된 디스크립터로 모든 필드가 0으로 초기화된 디스크립터이며, 일반적으로 참조되지 않는다.
  • GDT는 디스크립터의 집합으로, 프로세서에 GDT의 시작 어드레스와 크기 정보를 로딩해야 한다.
    • GDT 정보를 저장하는 자료구조의 기준 주소는 32bit의 크기이다.
    • 데이터 세그먼트 기준 주소와 관계없이 어드레스 0을 기준으로 하는 선형 주소이다.
    • (따라서, GDT 시작 어드레스를 실제 메모리 공간상의 어드레스로 변환해야 한다.)
    • GDT 선형 주소는 현재 실행되는 세그먼트 기준 주소를 알고 있으므로, 현재 세그먼트 시작을 기준으로 GDT 오프셋을 구하고 세그먼트 기준 주소를 더해주면 구할 수 있다.
;GDTR 자료구조 정의
GDTR:
	dw GDTEND - GDT - 1		; 아래에 위치한 GDT 테이블의 전체 크기 
	dd (GDT - $$ + 0x10000)	; 아래에 위치한 GDT 테이블의 시작 어드레스

;GDT 테이블 정의
GDT:
	; 널 디스크립터. 반드시 0으로 초기화해야함
	NULLDescriptor:
		dw 0x0000
		dw 0x0000
		db 0x00
		db 0x00
		db 0x00
		db 0x00

; 보호 모드 코드 세그먼트 디스크립터
CODEDESCRIPTOR:
	dw 0xFFFF	; Limit [15:0]
	dw 0x0000	; Base [15:0]
	db 0x00		; Base [23:16]
	db 0x9A		; P=1, DPL=0, Code Segment, Execute/Read
	db 0xCF		; G=1, D=1, L=0, Limit[19:16]
	db 0x00		; Base [31:24]

; 보호 모드 커널용 데이터 세그먼트 디스크립터
DATADESCRIPTOR:
	dw 0xFFFF	; Limit [15:0]
	dw 0x0000	; Base [15:0]
	db 0x00		; Base [23:16]
	db 0x92		; P=1, DPL=0, Data Segment, Read/Write
	db 0xCF		; G=1, D=1, L=0, Limit[19:16]
	db 0x00		; Base [31:24]

Ⅲ. 보호 모드 전환

  • 보호 모드로 전환하려면, GDTR 레지스터 설정 → CR0 컨트롤 레지스터 설정 → jmp 명령 수행 등의 3단계만 수행하면 된다.

1. 프로세서에 GDT 정보 설정

  • 프로세서에 GDT 정보를 설정하려면, lgdt 명령어를 사용한다.
    • lgdt 명령어로 2byte크기와 4byte 기준 주소로 된 GDT 정보 자료구조를 오퍼랜드로 받는다.
  • GDT 정보를 프로세서에 로딩할 수 있는 코드
  • lgdt [ GDTR ] ;GDTR 자료구조를 프로세서에 설정하여, GDT 테이블을 로드

2. CR0 컨트롤 레지스터 설정

  • CR0 컨트롤 레지스터에는 보호 모드 전환에 관련된 필드 외 캐시(cache), 페이징(paging), 실수 연산 장치(FPU) 등과 관련된 필드가 포함되어 있다.
  • MINT64 OS에서 보호모드는 거쳐 가는 임시 모드이므로, 세그먼테이션 기능 외에 사용하지 않아도 된다.
    • 페이징, 캐시, 메모리 정렬 검사, 쓰기 금지 기능은 모두 사용하지 않음으로 설정하면 된다.
    • but, FPU에 관련된 필드인 EM, ET, MP, TS, NE는 서로 연관되어 있으므로 FPU관련 필드를 설정해야 한다.
  • FPU 설정
    • FPU 내장 여부에 관련된 필드 설정
      • x86 프로세서에는 FPU가 내장되어 있으므로 EM필드를 0으로 설정해서 FPU 명령을 소프트웨어로 에뮬레이션하지 않게 하고 ET필드를 1로 설정한다.
      • 현재에는 임시로 초기화 한 것이기 때문에, FPU가 정상적으로 작동하지 않는다. 따라서, MP필드, TS필드, NE필드를 1로 설정하여 FPU명령이 실행될 때 예외가 발생하도록 설정해야 한다.
      • 보호 모드에서는 예외에 대한 처리를 하지 않으므로 가능하면, 실수 연산을 하지 앙ㄶ아야 한다.
    • CR0 컨트롤 레지스터 설정하는 코드
    • mov eax, 0x4000003B ;PG=0, CD=1, NW=0, AM=0, WP=0, NE=1, ET=1, TS=1, EM=0, MP=1, PE=1 mov cr0, eax ;CR0 컨트롤 레지스터에 위에서 저장한 플래그를 설정하여 보호모드로 전환

3. 보호 모드로 전환과 세그먼트 섹렉터 초기화

  • 32bit 코드를 준비한 후, 한줄의 어셈블리어 코드로 CS 세그먼트 섹렉터(=레지스터) 값을 바꾸는 것이다.
    • 어셈블리어로 16bit나 32bit 코드를 생성하려면 BITS명령어를 사용한다.
    • 16bit (리얼 모드): [BITS 16]
    • 32bit (보호 모드): [BITS 32]
  • CS 세그먼트 섹렉터를 교체하려면 jmp명령과 세그먼트 레지스터 접두사를 사용해야 한다.
    • 리얼모드의 세그먼트 레지스터 : 세그먼트의 시작 어드레스(기준 주소)를 저장하는 레지스터이다.
    • 보호모드의 세그먼트 셀렉터
      • 리얼모드와 달리 다양한 정보를 포함하고 있으므로, 세그먼트 정보는 디스크립터에 저장하고 세그먼트 셀렉터는 그 디스크립터를 지시하는 용도로 사용한다.
      • 세그먼트 셀렉터에 어드레스를 설정하여, 디스크립터를 지시할 수 있다.
      • (단, 세그먼트 기준 주소 대신 GDT 내의 디스크립터의 어드레스를 사용하며 이는 GDT의 시작 어드레스로부터 떨어진 거리를 의미한다.)
  • 커널 코드 세그먼트를 사용하여, 보호 모드로 전환하고 나서 나머지 세그먼트 셀렉터를 커널 데이터 세그먼트 디스크립터로 초기화하는 코드
; 커널 코드 세그먼트를 0x00을 기준으로 하는 것으로 교체하고 EIP의 값을 0x00을 기준으로 재설정
; CS 세그먼트 셀렉터: EIP
jmp dword 0x08: ( PROTECTEDMODE - $$ + 0x10000 )
; 커널 코드 세그먼트가 0x00을 기준으로 하는 반면, 실제 코드는 0x10000을 기준으로 실행되므로, 오프셋에 0x10000를 더해서 세그먼트 교체 후에도 같은 선형 주소를 가리키게 함

[BITS 32]  ; 이하의 코드는 32bit 코드로 설정
PROTECTEDMODE:
			mov ax, 0x10      ;보호 모드 커널용 데이터 세그먼트 디스크립터를 AX 레지스터에 저장
      mov ds, ax        ;DS 세그먼트 셀렉터에 설정
      mov es, ax        ;ES 세그먼트 셀렉터에 설정
      mov fs, ax        ;FS 세그먼트 셀렉터에 설정
      mov gs, ax        ;GS 세그먼트 셀렉터에 설정

			; 스택을 0x00000000 ~ 0x0000FFFF 영역에 64KB 크기로 생성
			mov ss, ax        ;SS 세그먼트 셀렉터에 설정
      mov esp, 0xFFFE   ;ESP 레지스터의 어드레스를 0xFFFE로 설정
      mov ebp, 0xFFFE   ;EBP 레지스터의 어드레스를 0xFFFE로 설정

4. 보호 모드용 PRINTSTRING 함수

  • 리얼 모드에서 보호 모드로 변환 시, 스택의 크기를 2byte에서 4byte로 증가하며 범용 레지스터의 크기가 32bit 커진 정도로 바꿀 수 있다.
  • 32bit 기준으로 작성된 소스코드와 16bit 소스코드의 차이점
    • 범용 레지스터가 대부분 32bit 범용 레지스터로 수정되었다.
    • 스택의 크기가 4byte로 변경되어, 파라미터를 오프셋이 4의 배수로 변경한 것이다.
    • 리얼 모드에서 비디오 메모리 어드레스 지정을 위해, 사용하던 ES 세그먼트 레지스터가 사라지고 직접 비디오 메모리에 접근해서 데이터를 쓰도록 수정되었다.
    • (리얼 모드 경우, 레지스터의 한계로 64KB 범위의 어드레스만 접근 가능했으므로 화면 표시를 위해 별도의 세그먼트가 필요했다.)
;메시지 출력하는 함수  (PARAM: x좌표, y좌표, 문자열)
PRINTMESSAGE:
    push ebp            ;베이스 포인터 레지스터(BP)를 스택에 삽입
    mov ebp, esp        ;베이스 포인터 레지스터(BP)에 스택 포인터 레지스터(SP)의 값 설정
    push esi            ;함수에서 임시로 사용하는 레지스터로 함수의 마지막 부분에서    
    push edi            ;스택에 삽입된 값을 꺼내 원래 값으로 복원
    push eax
    push ecx
    push edx

    ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
    ; X, Y의 좌표로 비디오 메모리의 어드레스를 계산함
    ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
    ; Y좌표를 이용해서 라인 어드레스를 먼저 구함
    mov eax, dword [ebp+12]  ;파라미터 2(화면 Y좌표)를 EAX레지스터에 설정
    mov esi, 160             ;한 라인의 바이트 수(2*80 컬럼)을 ESI레지스터에 설정
    mul esi                  ;EAX레지스터와 ESI레지스터를 곱하여, 화면 Y어드레스 계산
    mov edi, eax             ;계산된 화면 Y어드레스를 EDI레지스터에 설정

    ;X좌표를 이용해서 2를 곱한 후, 최종 어드레스 구함
    mov eax, dword [ebp+8]  ;파라미터 1(화면 X좌표)를 EAX레지스터에 설정
    mov esi, 2              ;한 문자를 나타내는 바이트수(2)를 ESI레지스터에 설정
    mul esi                 ;EAX레지스터와 ESI레지스터를 곱하여, 화면 X어드레스를 계산
    add edi, eax            ;화면 y어드레스와 계산된 X 어드레스를 더해서 실제 비디오 메모리 어드레스 계산

    ; 출력한 문자열의 어드레스
    mov esi, dword [ebp+16]  ;파라미터 3(출력한 문자열의 어드레스)

.MESSAGELOOP:            ;메시지를 출력하는 루프
    mov cl, byte [esi]   ;ESI레지스터가 가르키는 문자열 위치에 한 문자를 CL레지스터에 복사.
                         ;CL레지스터는 CX레지스터의 하위 1byte를 의미
                         ;문자열은 1byte면 충분하므로 ECX레지스터의 하위 1byte만 사용
    cmp cl, 0            ;복사된 문자와 0을 비교
    je .MESSAGEEND       ;복사한 문자의 값이 0이면 문자열이 종료되었음을 의미하며, .MESSAGEEND로 이동하여 문자 출력 종료
    mov byte [edi + 0xB8000], cl ;0이 아니라면 비디오 메모리 어드레스 0xB800:di에 문자를 출력

    add esi, 1            ;ESI레지스터에 1을 더하여, 다음 문자열로 이동
    add edi, 2            ;EDI레지스터에 2을 더하여, 비디오 메모리의 다음 문자 위치로 이동
                          ;비디오 메모리는 (문자, 속성)의 쌍으로 구성되므로 문자만 출력하려면 2를 더해야함
    jmp .MESSAGELOOP      ;메시지 출력 루프로 이동하여 다음 문자 출력

.MESSAGEEND:
    pop edx      ;함수에서 사용이 끝난 EDX 레지스터부터 EBP레지스터까지 스택에 삽입된 값을 이용해서 복원
    pop ecx      ;스택은 가장 마지막에 들어간 데이터가 가장 먼저 나오는 자료구조이므로
    pop eax      ;삽입의 역순으로 제거해야함
    pop edi
    pop esi
    pop ebp      ;베이스 포인터 레지스터(BP) 복원
    ret          ;함수를 호출한 다음 코드의 위치로 복원

Ⅸ. 보호 모드용 커널 이미지 빌드와 가상 OS 이미지 교체

1. 커널 엔트리 포인트 파일 생성

1.Kernel32 디렉토리의 Source 디렉토리 밑에 EntryPoint.s 파일 추가하고 code는 아래와 같다.

  • 해당 파일은 보호 모드 커널의 가장 앞부분에 위치하는 코드로, 보호 모드 전환과 초기화를 수행하여 이후에 위치하는 코드를 위한 환경을 제공한다.
  • entry point: 외부에서 해당 모듈을 실행할 때, 실행을 시작하는 지점이다.
[ORG 0x00]  ; 코드의 시작 어드레스를 0x00으로 설정
[BITS 16]   ; 이하의 코드는 16비트 코드로 설정

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

;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;  코드 영역
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
START:
	mov ax, 0x1000      ; 보호 모드 엔트리 포인트의 시작 어드레스를 세그먼트 레지스터 값으로 변환
	mov ds, ax          ; DS 세그먼트 레지스터에 설정	
    mov es, ax          ; ES 세그먼트 레지스터에 설정

    cli                 ; 인터럽트가 발생하지 못하도록 설정
    lgdt [ GDTR ]       ; GDTR 자료구조를 프로세서에 설정하여, GDT 테이블을 로드
    
    ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
    ;  보호 모드로 진입
    ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
    mov eax, 0x4000003B    ;PG=0, CD=1, NW=0, AM=0, WP=0, NE=1, ET=1, TS=1, EM=0, MP=1, PE=1
    mov cr0, eax           ;CR0 컨트롤 레지스터에 위에서 저장한 플래그를 설정하여 보호모드로 전환

    ; 커널 코드 세그먼트를 0x00을 기준으로 하는 것으로 교체하고 EIP의 값을 0x00을 기준으로 재설정
    ; CS 세그먼트 셀렉터: EIP
    jmp dword 0x08: ( PROTECTEDMODE - $$ + 0x10000 )
    ; 커널 코드 세그먼트가 0x00을 기준으로 하는 반면, 실제 코드는 0x10000을 기준으로 실행되므로, 오프셋에 0x10000를 더해서 세그먼트 교체 후에도 같은 선형 주소를 가리키게 함

;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;  보호 모드로 진입
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
[BITS 32]  ; 이하의 코드는 32bit 코드로 설정
PROTECTEDMODE:
	mov ax, 0x10      ;보호 모드 커널용 데이터 세그먼트 디스크립터를 AX 레지스터에 저장
    mov ds, ax        ;DS 세그먼트 셀렉터에 설정
    mov es, ax        ;ES 세그먼트 셀렉터에 설정
    mov fs, ax        ;FS 세그먼트 셀렉터에 설정
    mov gs, ax        ;GS 세그먼트 셀렉터에 설정

    ; 스택을 0x00000000 ~ 0x0000FFFF 영역에 64KB 크기로 생성
    mov ss, ax        ;SS 세그먼트 셀렉터에 설정
    mov esp, 0xFFFE   ;ESP 레지스터의 어드레스를 0xFFFE로 설정
    mov ebp, 0xFFFE   ;EBP 레지스터의 어드레스를 0xFFFE로 설정

    ; 화면에 보호 모드로 전환되었다는 메시지를 찍는다.
    push ( SWITCHSUCCESSMESSAGE - $$ + 0x10000 )    ;출력할 메시지의 어드레스를 스택에 삽입
    push 2                                          ;화면 Y 좌표(2)를 스택에 삽입
    push 0                                          ;화면 X 좌표(0)를 스택에 삽입
    call PRINTMESSAGE                               ;PRINTMESSAGE 함수 호출
    add esp, 12                                     ;삽입한 파라미터 제거

    jump $                                          ;현재 위치에서 무한 루프

;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;  함수 코드 영역
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;   
;메시지 출력하는 함수  (PARAM: x좌표, y좌표, 문자열)
PRINTMESSAGE:
    push ebp            ;베이스 포인터 레지스터(BP)를 스택에 삽입
    mov ebp, esp        ;베이스 포인터 레지스터(BP)에 스택 포인터 레지스터(SP)의 값 설정
    push esi            ;함수에서 임시로 사용하는 레지스터로 함수의 마지막 부분에서    
    push edi            ;스택에 삽입된 값을 꺼내 원래 값으로 복원
    push eax
    push ecx
    push edx

    ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
    ; X, Y의 좌표로 비디오 메모리의 어드레스를 계산함
    ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
    ; Y좌표를 이용해서 라인 어드레스를 먼저 구함
    mov eax, dword [ebp+12]  ;파라미터 2(화면 Y좌표)를 EAX레지스터에 설정
    mov esi, 160             ;한 라인의 바이트 수(2*80 컬럼)을 ESI레지스터에 설정
    mul esi                  ;EAX레지스터와 ESI레지스터를 곱하여, 화면 Y어드레스 계산
    mov edi, eax             ;계산된 화면 Y어드레스를 EDI레지스터에 설정

    ;X좌표를 이용해서 2를 곱한 후, 최종 어드레스 구함
    mov eax, dword [ebp+8]  ;파라미터 1(화면 X좌표)를 EAX레지스터에 설정
    mov esi, 2              ;한 문자를 나타내는 바이트수(2)를 ESI레지스터에 설정
    mul esi                 ;EAX레지스터와 ESI레지스터를 곱하여, 화면 X어드레스를 계산
    add edi, eax            ;화면 y어드레스와 계산된 X 어드레스를 더해서 실제 비디오 메모리 어드레스 계산

    ; 출력한 문자열의 어드레스
    mov esi, dword [ebp+16]  ;파라미터 3(출력한 문자열의 어드레스)

.MESSAGELOOP:            ;메시지를 출력하는 루프
    mov cl, byte [esi]   ;ESI레지스터가 가르키는 문자열 위치에 한 문자를 CL레지스터에 복사.
                         ;CL레지스터는 CX레지스터의 하위 1byte를 의미
                         ;문자열은 1byte면 충분하므로 ECX레지스터의 하위 1byte만 사용
    cmp cl, 0            ;복사된 문자와 0을 비교
    je .MESSAGEEND       ;복사한 문자의 값이 0이면 문자열이 종료되었음을 의미하며, .MESSAGEEND로 이동하여 문자 출력 종료
    mov byte [edi + 0xB8000], cl ;0이 아니라면 비디오 메모리 어드레스 0xB800:di에 문자를 출력

    add esi, 1            ;ESI레지스터에 1을 더하여, 다음 문자열로 이동
    add edi, 2            ;EDI레지스터에 2을 더하여, 비디오 메모리의 다음 문자 위치로 이동
                          ;비디오 메모리는 (문자, 속성)의 쌍으로 구성되므로 문자만 출력하려면 2를 더해야함
    jmp .MESSAGELOOP      ;메시지 출력 루프로 이동하여 다음 문자 출력

.MESSAGEEND:
    pop edx      ;함수에서 사용이 끝난 EDX 레지스터부터 EBP레지스터까지 스택에 삽입된 값을 이용해서 복원
    pop ecx      ;스택은 가장 마지막에 들어간 데이터가 가장 먼저 나오는 자료구조이므로
    pop eax      ;삽입의 역순으로 제거해야함
    pop edi
    pop esi
    pop ebp      ;베이스 포인터 레지스터(BP) 복원
    ret          ;함수를 호출한 다음 코드의 위치로 복원

;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;  데이터 영역
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; 
; 아래 데이터들을 8byte에 맞춰 정렬하기 위해 추가
align 8, db 0

;GDTR의 끝을 8byte로 정렬하기 위해 추가
dw 0x10000

;GDTR 자료구조 정의
GDTR:
	dw GDTEND - GDT - 1		; 아래에 위치한 GDT 테이블의 전체 크기 
	dd (GDT - $$ + 0x10000)	; 아래에 위치한 GDT 테이블의 시작 어드레스

;GDT 테이블 정의
GDT:
	; 널 디스크립터. 반드시 0으로 초기화해야함
	NULLDescriptor:
		dw 0x0000
		dw 0x0000
		db 0x00
		db 0x00
		db 0x00
		db 0x00

; 보호 모드 코드 세그먼트 디스크립터
CODEDESCRIPTOR:
	dw 0xFFFF	; Limit [15:0]
	dw 0x0000	; Base [15:0]
	db 0x00		; Base [23:16]
	db 0x9A		; P=1, DPL=0, Code Segment, Execute/Read
	db 0xCF		; G=1, D=1, L=0, Limit[19:16]
	db 0x00		; Base [31:24]

; 보호 모드 커널용 데이터 세그먼트 디스크립터
DATADESCRIPTOR:
	dw 0xFFFF	; Limit [15:0]
	dw 0x0000	; Base [15:0]
	db 0x00		; Base [23:16]
	db 0x92		; P=1, DPL=0, Data Segment, Read/Write
	db 0xCF		; G=1, D=1, L=0, Limit[19:16]
	db 0x00		; Base [31:24]
GDTEND:

;보호모드로 전환되었다는 메시지
SWITCHSUCCESSMESSAGE: db 'Switch To Protected Mode Success~!!', 0

times 512 - ($ - $$) db 0x00    ;512byte를 맞추기 위해, 남은 부분을 0으로 채움

 

 

2. makefile 수정과 가상 OS 이미지 파일 교체

  • 새로운 파일이 추가되었으므로, makefile을 수정함
    • 1.Kernel32 디렉터리의 makefile 수정
      • $< : Dependency(:의 오른쪽)의 첫번째 파일을 의미하는 매크로이다. 즉, $<는 /Source/EnrtyPoint.s로 치환되어 실행된다.
      all: Kernel32.bin
      
      Kernel32.bin: Source/EntryPoint.s
      	nasm -o Kernel32.bin $<
      
      clean:
      	rm -f Kernel32.bin
      
    • 최상위 디렉터리의 makefile 수정
      • $^ : Dependency(:의 오른쪽)에 나열된 전체 파일을 의미하는 매크로이다.
      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: 0.BootLoader/BootLoader.bin 1.Kernel32/Kernel32.bin
      	@echo
      	@echo =================== Disk Image Build Start ===================
      	@echo
      
      	cat $^> Disk.img
      
      	@echo
      	@echo ==================== All Build Complete ====================
      	@echo
      
      clean:
      	make -C 0.BootLoader clean
      	make -C 1.Kernel32 clean
      	rm -f Disk.img
      
    • (디스크 이미지 생성부분만 수정)

3. OS 이미지 통합과 QEMU 실행

  • QEMU실행 시, 현재는 정지상태로 아무런 반응이 없을 것이다.
    • 부트로더에 os이미지 크기가 1024로 설정되어 있기 때문이다.
    • 빌드한 보호 모드 커널 이미지의 크기는 512byte(1섹터)밖에 되지 않으므로 부트 로더가 한 섹터를 로딩하고 나머지 1023섹터를 읽으려다 정지된 것이다.
  • 정상적인 실행을 하려면, BootLoader.asm의 TOTALSECTORCOUNT 값을 수정해줘야 한다.
  • TOTALSECTORCOUNT: dw 1
  • 총 소스 코드
[ORG 0x00]  ; 코드의 시작 어드레스를 0x00으로 설정
[BITS 16]   ; 이하의 코드는 16비트 코드로 설정

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

;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;  코드 영역
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
START:
	mov ax, 0x1000      ; 보호 모드 엔트리 포인트의 시작 어드레스를 세그먼트 레지스터 값으로 변환
	mov ds, ax          ; DS 세그먼트 레지스터에 설정	
    mov es, ax          ; ES 세그먼트 레지스터에 설정

    cli                 ; 인터럽트가 발생하지 못하도록 설정
    lgdt [ GDTR ]       ; GDTR 자료구조를 프로세서에 설정하여, GDT 테이블을 로드
    
    ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
    ;  보호 모드로 진입
    ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
    mov eax, 0x4000003B    ;PG=0, CD=1, NW=0, AM=0, WP=0, NE=1, ET=1, TS=1, EM=0, MP=1, PE=1
    mov cr0, eax           ;CR0 컨트롤 레지스터에 위에서 저장한 플래그를 설정하여 보호모드로 전환

    ; 커널 코드 세그먼트를 0x00을 기준으로 하는 것으로 교체하고 EIP의 값을 0x00을 기준으로 재설정
    ; CS 세그먼트 셀렉터: EIP
    jmp dword 0x08: ( PROTECTEDMODE - $$ + 0x10000 )
    ; 커널 코드 세그먼트가 0x00을 기준으로 하는 반면, 실제 코드는 0x10000을 기준으로 실행되므로, 오프셋에 0x10000를 더해서 세그먼트 교체 후에도 같은 선형 주소를 가리키게 함


;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;  보호 모드로 진입
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
[BITS 32]  ; 이하의 코드는 32bit 코드로 설정
PROTECTEDMODE:
	mov ax, 0x10      ;보호 모드 커널용 데이터 세그먼트 디스크립터를 AX 레지스터에 저장
    mov ds, ax        ;DS 세그먼트 셀렉터에 설정
    mov es, ax        ;ES 세그먼트 셀렉터에 설정
    mov fs, ax        ;FS 세그먼트 셀렉터에 설정
    mov gs, ax        ;GS 세그먼트 셀렉터에 설정

     ; 스택을 0x00000000~0x0000FFFF 영역에 64KB 크기로 생성
    mov ss, ax          ; SS 세그먼트 셀렉터에 설정
    mov esp, 0xFFFE     ; ESP 레지스터의 어드레스를 0xFFFE로 설정
    mov ebp, 0xFFFE     ; EBP 레지스터의 어드레스를 0xFFFE로 설정
    
    ; 화면에 보호 모드로 전환되었다는 메시지를 찍는다.
    push ( SWITCHSUCCESSMESSAGE - $$ + 0x10000 )    ; 출력할 메시지의 어드레스르 스택에 삽입
    push 2                                          ; 화면 Y 좌표(2)를 스택에 삽입
    push 0                                          ; 화면 X 좌표(0)를 스택에 삽입
    call PRINTMESSAGE                               ; PRINTMESSAGE 함수 호출
    add esp, 12                                     ; 삽입한 파라미터 제거

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

;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;  함수 코드 영역
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;   
;메시지 출력하는 함수  (PARAM: x좌표, y좌표, 문자열)
PRINTMESSAGE:
    push ebp            ;베이스 포인터 레지스터(BP)를 스택에 삽입
    mov ebp, esp        ;베이스 포인터 레지스터(BP)에 스택 포인터 레지스터(SP)의 값 설정
    push esi            ;함수에서 임시로 사용하는 레지스터로 함수의 마지막 부분에서    
    push edi            ;스택에 삽입된 값을 꺼내 원래 값으로 복원
    push eax
    push ecx
    push edx

    ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
    ; X, Y의 좌표로 비디오 메모리의 어드레스를 계산함
    ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
    ; Y좌표를 이용해서 라인 어드레스를 먼저 구함
    mov eax, dword [ebp+12]  ;파라미터 2(화면 Y좌표)를 EAX레지스터에 설정
    mov esi, 160             ;한 라인의 바이트 수(2*80 컬럼)을 ESI레지스터에 설정
    mul esi                  ;EAX레지스터와 ESI레지스터를 곱하여, 화면 Y어드레스 계산
    mov edi, eax             ;계산된 화면 Y어드레스를 EDI레지스터에 설정

    ;X좌표를 이용해서 2를 곱한 후, 최종 어드레스 구함
    mov eax, dword [ebp+8]  ;파라미터 1(화면 X좌표)를 EAX레지스터에 설정
    mov esi, 2              ;한 문자를 나타내는 바이트수(2)를 ESI레지스터에 설정
    mul esi                 ;EAX레지스터와 ESI레지스터를 곱하여, 화면 X어드레스를 계산
    add edi, eax            ;화면 y어드레스와 계산된 X 어드레스를 더해서 실제 비디오 메모리 어드레스 계산

    ; 출력한 문자열의 어드레스
    mov esi, dword [ebp+16]  ;파라미터 3(출력한 문자열의 어드레스)

.MESSAGELOOP:            ;메시지를 출력하는 루프
    mov cl, byte [esi]   ;ESI레지스터가 가르키는 문자열 위치에 한 문자를 CL레지스터에 복사.
                         ;CL레지스터는 CX레지스터의 하위 1byte를 의미
                         ;문자열은 1byte면 충분하므로 ECX레지스터의 하위 1byte만 사용
    cmp cl, 0            ;복사된 문자와 0을 비교
    je .MESSAGEEND       ;복사한 문자의 값이 0이면 문자열이 종료되었음을 의미하며, .MESSAGEEND로 이동하여 문자 출력 종료
    mov byte [edi + 0xB8000], cl ;0이 아니라면 비디오 메모리 어드레스 0xB800:di에 문자를 출력

    add esi, 1            ;ESI레지스터에 1을 더하여, 다음 문자열로 이동
    add edi, 2            ;EDI레지스터에 2을 더하여, 비디오 메모리의 다음 문자 위치로 이동
                          ;비디오 메모리는 (문자, 속성)의 쌍으로 구성되므로 문자만 출력하려면 2를 더해야함
    jmp .MESSAGELOOP      ;메시지 출력 루프로 이동하여 다음 문자 출력

.MESSAGEEND:
    pop edx      ;함수에서 사용이 끝난 EDX 레지스터부터 EBP레지스터까지 스택에 삽입된 값을 이용해서 복원
    pop ecx      ;스택은 가장 마지막에 들어간 데이터가 가장 먼저 나오는 자료구조이므로
    pop eax      ;삽입의 역순으로 제거해야함
    pop edi
    pop esi
    pop ebp      ;베이스 포인터 레지스터(BP) 복원
    ret          ;함수를 호출한 다음 코드의 위치로 복원

;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;  데이터 영역
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; 
; 아래 데이터들을 8byte에 맞춰 정렬하기 위해 추가
align 8, db 0

;GDTR의 끝을 8byte로 정렬하기 위해 추가
dw 0x10000

;GDTR 자료구조 정의
GDTR:
	dw GDTEND - GDT - 1		; 아래에 위치한 GDT 테이블의 전체 크기 
	dd (GDT - $$ + 0x10000)	; 아래에 위치한 GDT 테이블의 시작 어드레스

;GDT 테이블 정의
GDT:
	; 널 디스크립터. 반드시 0으로 초기화해야함
	NULLDescriptor:
		dw 0x0000
		dw 0x0000
		db 0x00
		db 0x00
		db 0x00
		db 0x00

; 보호 모드 코드 세그먼트 디스크립터
CODEDESCRIPTOR:
	dw 0xFFFF	; Limit [15:0]
	dw 0x0000	; Base [15:0]
	db 0x00		; Base [23:16]
	db 0x9A		; P=1, DPL=0, Code Segment, Execute/Read
	db 0xCF		; G=1, D=1, L=0, Limit[19:16]
	db 0x00		; Base [31:24]

; 보호 모드 커널용 데이터 세그먼트 디스크립터
DATADESCRIPTOR:
	dw 0xFFFF	; Limit [15:0]
	dw 0x0000	; Base [15:0]
	db 0x00		; Base [23:16]
	db 0x92		; P=1, DPL=0, Data Segment, Read/Write
	db 0xCF		; G=1, D=1, L=0, Limit[19:16]
	db 0x00		; Base [31:24]
GDTEND:

;보호모드로 전환되었다는 메시지
SWITCHSUCCESSMESSAGE: db 'Switch To Protected Mode Success~!!', 0

times 512 - ($ - $$) db 0x00    ;512byte를 맞추기 위해, 남은 부분을 0으로 채움

 

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

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