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

들어가기

리얼 모드에서 보호 모드로 전환하고, 메세지를 출력해 보호 모드임을 확인해보자!!

본론

  • 간략하게 표시된 리얼모드에서 보호모드 전환 과정.

4.1 세그먼트 디스크립터 생성

  • 세그먼트 디스크립터는 세그먼테이션 기법에서 세그먼트의 정보를 나타내는 자료구조.
  • 세그먼트 디스크립터는 크게 코드 세그먼트 디스크립터와 데이터 세그먼트 디스크립터로 나뉨.
    • 코드 세그먼트 디스크립터, 실행 가능한 코드가 포함된 세그먼트 정보로 CS 세그먼트 셀렉터 사용.
    • 데이터 세그먼트 디스크립터, 데이터가 포함된 세그먼트에 대한 정보로 CS 제외한 나머지 세그먼트 셀렉터 사용.
    • 세그먼트 레지스터 명칭은 보호 모드에서 세그먼트 셀렉터라는 이름으로 명칭

  • 보호 모드의 세그먼트 디스크립터는 8 바이트로 다양한 필드가 존재.
  • 복잡한 필드로 어떻게 설정할지는 목표로 하는 프로젝트를 먼저 정리하고 설정.
    • 커널 코드와 데이터용 디스크립터 각 1개.
    • 모든 디스크립터는 0 ~ 4GB까지 접근.
    • 코드와 데이터 사용할 기본 오퍼랜드 크기는 32비트.
    • 보호 기능은 사용하지 않으며, 프로세서의 명령을 사용하는데 제약 없음.

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

  • 코드와 데이터 세그먼트를 설정하려면 S 필드와 타입 필드를 조합.
    • S 필드는 세그먼트 디스크립터를 설정함으로 값을 1로 설정.
    • 타입 필드는 4 비트의 필드를 이용해서 설정.
  • 프로젝트를 진행하면서 기본 세그먼트 타입만 사용하고, 코드 세그먼트는 실행/읽기, 데이터 세그먼트는 읽기/쓰기 타입으로 설정. (코드 : 0x0A, 데이터 : 0x02)

4.1.2 세그먼트의 영역 설정

  • 이번 프로젝트의 커널 세그먼트 디스크립터는 4GB 전체 영역에 접근.
    • 따라서 커널 세그먼트 디스크립터의 기준 주소를 0으로 설정.
  • 크기 필드는 총 20 비트, 2^20(1MB)는 4GB까지 표현 불가능.
  • G 필드의 값을 1로 설정해 4GB로 확장.

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

  • 보호 모드는 32 비트로 동작하므로 기본 오퍼랜드의 크기도 32 비트로 설정.
  • D/B 필드의 값을 1로 설정.
  • 보호 모드는 권한을 따로 구분하지 않으므로 0으로 설정.

4.1.4 기타 필드 설정

  • 디스크립터의 유요함을 나타내는 P 필드를 1로 설정.
  • AVL 필드는 이번 프로젝트에서는 사용하지 않을 예정으로 0으로 설정.

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

 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]
  • 코드와 표를 비교하면서 이해.

4.2 GDT 생성

  • GDT는 연속된 디스크립터의 집합이므로 연속된 어셈블리어 코드로 나타내면 그 자체가 GDT.
  • 하지만 한가지 제약은 NULL 디스크립터를 가장 앞부분에 추가.

  • GDT는 프로세서에 GDT의 시작 주소와 크기 정보를 로딩해야 함.
    • 따라서 이것을 저장하는 자료구조가 필요.
  • GDT 정보를 저장하는 자료구조의 기준 주소는 데이터 세그먼트의 기준 주소와 관계 없이 어드레스 0을 기준으로 하는 선형 주소.
  • GDT의 실제 주소를 변환 해야함.
    • 부트 로더의 의해 0x10000에 로딩되어 실행 되고 있으므로 자료구조를 생성할때 GDT 오프셋 + 0x10000.
; GDTR 자료구조 정의
GDTR:
 dw GDTEND - GDT - 1         ; 아래 위치하는 GDT 테이블의 전체 크기
 dd ( GDT - $$ + 0x10000 )   ; 아래에 위치하는 GDT 테이블의 시작 어드레스
                             ; 실제 GDT가 있는 선형 주소 계산을 위해
                             ; 현재 섹션 내의 GDT 오프셋에 세그먼트 기준 주소인 0x10000을 더함

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

    ; ~~ 생략 ~~

    ; 보호 모드 커널용 데이터 세그먼트 디스크립터
    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:
  • 즉, GDT 정보를 저장하는 자료구조의 기준 주소는 GDT가 섹션(SECTION)에서 어느 정도 떨어져 있는지 저장.
    • 따라서 GDT - $$으로 GDT 오프셋을 구함.

4.3 보호 모드로 전환

  • 보호모드로 넘어가려면 간단하게 GDTR 설정, CR0 컨트롤 레지스터 설정, jmp 명령 수행 3단계만 수행하면 가능.

4.3.1 프로세서와 GDT 정보 설정

lgdt (GDTR)
  • 프로세서에 GDT 정보 생성을 위함 lgdt 명령어 사용.
  • lgdt 명령으로 2 바이트의 크기와 4 바이트의 기준 주소로 된 GDT 정보 자료구조를 오퍼랜드로 받음.

4.3.2 CR0 컨트롤 레지스터 설정

  • CR0 컨트롤 레지스터에는 보호 모드 전환 필드 외에 다양한 필드 포함.
  • 하지만 이번 보호 모드에서의 CR0는 세그먼테이션 기능 외에는 사용하지 않을 예정.

4.3.3 보호 모드로 전환과 세그먼트 셀렉터 초기화

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

[BITS 32]           ; 이하의 코드는 32비트 코드로 설정
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로 설정
  • 32비트 코드를 준비한 후, 어셈블리어 코드로 CS 세그먼트 셀렉터의 값을 바꿈.
  • CS 세그먼트 셀렉터 교체를 위한 jmp 명령과 세그먼트 레지스터 접두사를 사용.
    • 리얼 모드의 세그먼트 레지스터는 세그먼트의 시작 주소를 저장.
    • 보호 모드의 세그먼트 디스크립터는 다양한 정보를 포함하고 있으므로 세그먼트 셀렉터(레지스터)는 세그먼트의 시작 주소가 아닌 해당 디스크립터의 주소를 저장.
    • 디스크립터의 주소는 보호 모드로 변환하면서 GDT 내의 오프셋으로 접근.(즉, 보호 모드로 변환하면서 세그먼트 셀렉터의 기준 값이 GDT의 시작 주소가 되는 것)

4.3.4 보호 모드용 PRINTSTRING 함수

; 메시지를 출력하는 함수
;   PARAM: x좌표, y좌표, 문자열
PRINTMESSAGE:
    push ebp    ; 베이스 포인터 레지스터(EBP)를 스택에 삽입
    mov ebp, esp; 베이스 포인터 레지스터(EBP)에 스택 포인터 레지스터(ESP)의 값을 설정
                ; 베이스 포인터 레지스터(EBP)를 이용해서 파라미터에 접근할 목적
                ; 호출된 직후의 ESP 레지스터 값을 저장하여 BP 레지스터와 고정된 오프셋으로 파라미터에 접근하게함
    push esi    ; ES 세그먼트 레지스터부터 DX 레지스터까지 스택에 삽입
    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 레지스터는 ECX 레지스터의 하위 1바이트를 의미
                            ; 문자열은 1바이트면 충분하므로 ECX 레지스터의 하위 1바이트만 사용

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

    mov byte [ edi + 0x0B8000 ], cl  ; 0이 아니라면 메모리 어드레스 0xB8000:EDI에 문자를 출력

    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     ; 베이트 포인터 레지스터(EBP) 복원
    ret         ; 함수를 호출한 다음 코드의 위치로 복귀
  • 리얼 모드용 함수를 보호 모드로 변환 하는 것은 스택의 크기가 2 바이트에서 4 바이트로 증가하며, 범용 레지스터의 크기가 4 바이트라는 정도만 알면 쉽게 교체.
  • 3. 플로피 디스크에서 OS 이미지를 로딩하자에서의 PRINTSTRING와 비교.
    • 범용 레지스터가 수정되고 스택의 크기 변경으로 인한 오프셋 변경.
    • 보호 모드는 주소 접근 범위가 4GB로 넓어져 ES 세그먼트가 사라짐.

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

4.4.1 커널 엔트리 포인트 파일 생성

EntryPoint.s 코드 보기

  • EntryPoint.s는 외부(부트 로더)에서 보호 모드 커널로 진입하는 부분.

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

all: Kernel32.bin

Kernel32.bin: Source/EntryPoint.s
    nasm -o Kernel32.bin $<

clean:
    rm -f Kernel32.bin
  • $<의 사용으로 Dependency 첫번째 파일을 의미.

makefile 소스 보기

-$^, Dependency 전체 파일을 의미.

4.4.3 OS 이미지 통합 및 QEMU 실행

  • 부트로더의 섹터 크기 조절 후 실행하면 정상작동.

마치며

생각 안나면 무조건 복습이 답이다.

Share