Ⅰ. 개요
- 리얼 모드에서 보호모드로 전환하려면, 크게 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 컨트롤 레지스터에 위에서 저장한 플래그를 설정하여 보호모드로 전환
- FPU 내장 여부에 관련된 필드 설정
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
- (디스크 이미지 생성부분만 수정)
- 1.Kernel32 디렉터리의 makefile 수정
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 개발' 카테고리의 다른 글
8장. A20 게이트를 활성화하여 1MB이상 영역에 접근 (0) | 2023.07.20 |
---|---|
7장. C언어로 커널 작성 (0) | 2023.07.20 |
5. OS이미지 메모리에 복사 (0) | 2023.05.06 |
4. 32비트 보호 모드로 전환 (0) | 2023.04.13 |
3. OS이미지 메모리에 복사 (0) | 2023.04.10 |