ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • [x86] Unreal mode
    프로젝트/운영체제 만들기 2023. 6. 28. 18:57

    글의 참고

    - https://en.wikipedia.org/wiki/Unreal_mode

    - https://wiki.osdev.org/Unreal_Mode

    - https://riptutorial.com/x86/example/19574/unreal-mode


    글의 전제

    - 밑줄로 작성된 글은 강조 표시를 의미한다.

    - 그림 출처는 항시 그림 아래에 표시했다.


    글의 내용

    - 언리얼 모드를 사용하는 이유

    : 리얼 모드에서 1MB이상에 보호 모드 커널을 로드하고 싶은 경우 사용한다. 이렇게 생각할 수 도 있다. 보호 모드를 `부트 로더 + 커널`로 나누고, 부트 로더를 1MB 아래에 로드하고, 32비트 부트 로더에서 32비트 커널을 로드하면 안될까? 당연히 그게 가장 좋다고 생각된다. 그러나, 드라이버 드라이버 코드를 어셈블리 언어로 짜야 한다는 고통이 있다. 즉, 보호 모드로 진입하면 더 이상 BIOS에 도움을 받을 수 없어서 본인이 직접 드라이버를 모두 구현해야 한다.

     

    : 1MB 이상에 접근하면서, BIOS를 사용할 수는 없을까? 이럴 때, `언리얼 모드`를 사용한다. 언리얼 모드로 진입하는 방법은 간단하다.

    1" 보호 모드 용 플랫 기반의 코드 및 데이터 세그먼트 디스크립터를 만든다.
    2" 보호 모드로 일단 진입한다(이 때, 데이터 세그먼트만 4GB 오프셋을 가져도 된다면, 굳이 FAR JUMP로 CS를 바꿀 필요없다. 보호 모드 진입은 CR0 설정으로 충분하다).
    3" DS에 보호 모드 용 데이터 세그먼트 디스크립터를 GDT에 로드한다.
    4" 다시 리얼 모드로 돌아온다.

    : 프로세서는 리얼 모드로 돌아오고 나서도 계속해서 보호 모드에서 만들어놨던 세그먼트 디스크립터를 사용하게 된다. 그렇면 이제부터 리얼 모드에서도 4GB까지 접근이 가능하게 된다. 주의할 점은 리얼 모드로 돌아왔으므로, 세그먼트 주소 지정 방식을 다시 사용해야 한다. 언리얼 모드와 리얼 모드의 주소 지정의 차이는 오프셋의 차이다. 

     

    1" 리얼 모드 - [세그먼트 주소 << 4 : 16비트 오프셋]
    2" 언리얼 모드 - [세그먼트 주소 << 4 : 32비트 오프셋]

    : 참고로, 위의 주소 지정이 적용되는 것은 `데이터 세그먼트` 뿐이다. 그래서 EIP는 결국 32비트 오프셋을 사용하지는 못한다. 언리얼 모드에서 이 모드를 빅 언리얼 모드 라고 한다. 코드까지 32비트 영역까지 넓히고 싶다면, Hugh Unreal Mode 로 진입해야 한다. 이 원리가 뭘까? 보호 모드로 진입 후, CS 및 DS 세그먼트 셀렉터에 각각 코드 세그먼트 인덱스, 데이터 세그먼트 인덱스를 할당하고 특정 세그먼트에 접근하면, 세그먼트 레지스터의 `Invisible Part`에 세그먼트 셀렉터에 대응하는 세그먼트 디스크럽트의 정보들이 `캐쉬`된다. 재미있는 건, 이 상태에서 그냥 리얼 모드로 복귀하더라도 세그먼트 레지스터에 캐쉬된 내용들은 사라지지 않는다는 것이다. 그래서, 세그먼트 레지스터를 변경하지 않는 한, 보호 모드에서 접근 가능했던 1MB 이상의 영역에 접근이 가능해진다.

     

    - 언리얼 모드 진입

    : 언리얼 모드에서 중요한 부분은 보호 모드를 진입할 때, CS를 바꾸면서 FAR JUMP를 하는게 아니라 CR0만 바꿔서 보호 모드로 진입한다는 것이다. 그리고 보호 모드 진입 후 데이터 세그머트만 교체하고 리얼 모드로 복귀한다. 복귀하고 나서 반드시 DS는 다시 이전 값으로 원복해야 한다.

    .gdt_load:                 ; GDT address must be linear address.
        xor eax, eax
        mov ax, ds
        shl eax, 4             ; Left shift segment address in 4 times
        add eax, _gdt_tbl      ; Added the offset of GDT address
        mov [_gdtr+2], eax
        mov eax, _gdtr
        sub eax, _gdt_tbl
        mov [_gdtr], ax
        lgdt [_gdtr]           ; Load GDT Table to CPU

    _pre_unreal:    
        mov eax, cr0           
        or al, 1
        mov cr0, eax           ; Enable protected mode

    _do_unreal:
        mov ax, GDT_DATA        
        mov ds, ax              ; Load 32bit data segment discriptor

        and al, 0xFE        
        mov cr0, eax            ; Disable protected mode 

    _unreal_mode:             ; here is already unreal mode
        xor ax, ax 
        mov ds, ax
        mov es, ax
        
        ; for test code, now we can write the value above 1MB
        mov ecx, 0x100008   
        mov dword [ds:ecx], 0x99aabbcc                                                                             

        jmp $

        ; below codes for protected mode
        push word [total_read_sec]
        jmp PMODE_ENTRY_POINT   ; boot loader entry point of protected mode

    : 먼저 GDT를 로드 해야한다. 그리고, 보호 모드를 활성화한다. 그리고 32비트 데이터 세그먼트 디스크립터를 등록한다. 그리고 보호 모드를 비활성화한다. 보호 모드를 비활성화하면 자동으로 리얼 모드가 된다. 이제 32비트 오프셋이 가능하므로, 세그먼트 레지스터 베이스 어드레스를 모두 0으로 세팅(플랫 모델)하고, 오프셋만으로 이용해서 코딩하면 된다. 이제부터는 32비트 레지스터인 접두사 `E` 레지스터를 맘껏 사용할 수 있다. 언리얼 모드에서 CPU가 제약이 있는 것은 코드 세그먼트이지 데이터 세그먼트가 아니므로, 데이터는 32비트까지 자유롭게 작성 및 수정이 가능하다.

     

     

    - 언리얼 모드 1MB 이상 

    : 언리얼 모드에서 모든 BIOS 인터럽트 서비스가 그런건 아니지만, 몇몇 서비스는 DS 및 ES의 제한 크기를 다시 0xFFFF로 돌린다. 그래서 #GP가 발생하게 된다. 예를 들어, 언리얼 모드에서 1MB 이상의 주소에 커널을 로드하고 싶다고 치자. 그래서, BIOS INT 13h F02h 서비스 이용하려고 했는데, 호출하는 순간 에러가 발생할 수 있다. BIOS가 세그먼트 디스크립터를 다시 원복한 것이다. 그래서 언리얼 모드에서는 BIOS 서비스를 제공받기가 쉽지 않을 것 같다.

     

    : 쉽지 않다고 했지, 이것이 아예 불가능한 것은 아니다. 첫 링크에 걸린 글을 보면, `Brendan` 이 제시하는 방법이 좋아 보인다.

    1" #GP에 BIOS Interrupt handler를 설정한다. 
    2" 마스터 PIC의 5번 인터럽트에 대해 인터럽트 핸들러를 등록한다. 마스터 PIC 5번 인터럽트는 CPU 인터럽트 0x0D(#GP)에 연결된다.

    : 위의 내용을 통해서 BIOS가 다시 세그먼트 디스크립터를 다시 되돌리더라도, 다시 언리얼 모드로 진입하라고 한다. 그런데, 내가 #GP는 핸들러를 등록했는데 후킹이 걸리지가 않는다. PIC IRQ#5인 것인지 확인이 필요하다. 그리고 매번 `#GP`나 `PIC#IRQ5`가 발생하면 언리얼 모드로 진입해야 하는 번거로움이 있는 것 같다. 

     

    : `Michael Petch` 가 스택오버플로우에서 제시한 방법도 있다. 기존에 내도 생각했던 방식이지만, 전혀 효율적이지 못하다고 생각했다. 대신 새로운 명령어를 알게됬다. 이 방식은 BIOS INT 13h AH=02h를 통해서 직접 1MB 위쪽에 쓰는게 아니라, 일단 1MB 안쪽에 쓰고나서 해당 내용을 직접 `MOV` 관련 명령어를 통해서 1MB 위쪽에 쓰는 방법이다. 언리얼 모드에서는 데이터를 1MB에 직접 쓰는것은 DS 및 ES를 건들지 않기 때문에 문제가 되지 않는다. 그러나, 사실 이 방법도 좋아 보이지는 않는다. 마땅한 해결책이 보이지 않는 상황이다.

     

    : 그런데 위의 방법은 QEMU에서는 테스트가 불가능하다. 왜냐면, QEMU에서는 리얼 모드라는 것을 몰라서, 세그먼트 제한이 없다. 그냥 한계를 넘어도 막써버린다. BOCHS에서 확인이 필요하다.

     

    : 다른 대안을 한 번 더 찾아봤다. `BIOS INT 15h AH=0x87` 서비스가 가장 현실적인 방법이라고 얘기하는 것 같다. 

     

    : DS 세그먼트 디스크립터만 해주면 안된다... ES, FS, GS, 즉, 4GB로 사용할 모든 디스크립터들은 전부 GDT 셋업을 하고 언리얼로 와야한다...

     

    : `언리얼 모드`에서 뭘 해도 1MB 이상에 로드하는 것은 쉽지 않다. 디바이스 드라이버를 구현하지 않고 간단하게 BIOS 서비스를 이용해서 디스크 정보를 읽어오려 했지만 쉽지 않은 것 같다.  BIOS EDD만이 현실적으로 가능하다고 하는데, 그냥 1MB안에 한 번 복사하고 리얼 모드로 들어가서 1MB에 쓰는 것이 빠르다고 생각이 든다.

     

    - 에러 사항

    : 아래에서 왼쪽 코드는 오류가 있고, 오른쪽은 오류가 없다. 혹시나, 언리얼에서 4GB 메모리에 접근이 가능해서 `[bits 32]`를 생각할 수 있지만, 큰일난다. 아래서 `[bits 32]`는 어셈블러가 해당 지시어 아래 코드부터는 32비트로 인지하겠다는 뜻이다. 그래서 어셈블러는 해당 코드부터는 재배치 정보를 전부 32비트를 기준으로 할당한다. 근데, CPU는 다시 리얼 모드로 돌아와서 해당 내용을 16비트 모드로 실행하게 된다. 즉, CPU는 32비트 내용을 이해하지 못한다. 

    [bits 16]
    ...
    ...

    _pre_unreal:    
        mov eax, cr0
        or al, 1
        mov cr0, eax    

    _do_unreal:
        mov ax, GDT_DATA        
        mov ds, ax              ; Load 32bit Date Segment Discriptor

        and al, 0xFE        
        mov cr0, eax            ; Disable Protected Mode 

    [bits 32]
    _unreal_mode:
        xor ax, ax 
        mov ds, ax
        mov es, ax
        
        mov ecx, 0x100008
        mov dword [ds:ecx], 0x99aabbcc

        jmp $

        ; below codes for protected mode                                                                           
        push word [total_read_sec]
        jmp PMODE_ENTRY_POINT   ; boot loader entry point of protected mode

    ...
    ...
    [bits 16]
    ...
    ...

    _pre_unreal:    
        mov eax, cr0
        or al, 1
        mov cr0, eax    

    _do_unreal:
        mov ax, GDT_DATA        
        mov ds, ax              ; Load 32bit Date Segment Discriptor

        and al, 0xFE        
        mov cr0, eax            ; Disable Protected Mode 

    _unreal_mode:
        xor ax, ax 
        mov ds, ax
        mov es, ax
        
        mov ecx, 0x100008
        mov dword [ds:ecx], 0x99aabbcc

        jmp $

        ; below codes for protected mode                                                                           
        push word [total_read_sec]
        jmp PMODE_ENTRY_POINT   ; boot loader entry point of protected mode

    ...
    ...

    : 2023.07.10을 기준으로 내가 작성한 언리얼 모드 진입 코드는 실제로 언리얼 모드를 진입하지 못하는 것 같다. 언리얼 모드에 정상적으로 진입할 경우, 아래의 코드로 VGA 텍스트 버퍼에 특정 글자를 쓸 수 있어야 한다. 아래 코드를 보면, 세그먼트 레지스터의 값을 0으로 초기화하고, 오직 오프셋을 통해서만 메모리에 접근한다. 언리얼 모드에 진입할 경우, 화면에 `MDP`를 띄울 것이다.

     

    xor ax, ax
    mov ds, ax
    mov es, ax
    mov fs, ax
    mov gs, ax
    
    mov edi, 0xb8000
    mov word [edi],   0x57<<8 | 'M';
    mov word [edi+2], 0x57<<8 | 'D';
    mov word [edi+4], 0x57<<8 | 'P';

    '프로젝트 > 운영체제 만들기' 카테고리의 다른 글

    [xv6] Page tables 상세 분석 1  (0) 2023.07.12
    TSS  (0) 2023.07.06
    [x86] 멀티 부트  (0) 2023.06.27
    BOCHS  (0) 2023.06.24
    Long mode  (0) 2023.06.20
Designed by Tistory.