참고. Intel i386 CPU의 16bit->32bit 전환

 

들어가기 전에...

 

1.전환방법

 이 글은 INTEL Architecture Manual Volume3, Software Developer's Manual에 나와있는 9.9 Mode Switching 섹션을 정리한 문서이다. 모드 스위칭(Mode Switching)을 하기위한 코드는 그리 길지 않으나, 그 코드를 이해하기 위해서는 여러가지 기반 지식이 필요하다.

 

 Intel Manual에 따르면 모드 스위칭을 하는 과정은 아래의 순서로 수행하면 된다.

  1. CLI 명령을 사용하여 인터럽트 불가 설정(모드 스위칭시에 인터럽트가 발생하는 것을 막기위함)
  2. GDT Table에 현재 16bit Mode의 Code/Data 영역을 그대로 맵핑한 Descriptor를 생성하고 LGDT 명령을 이용해서 생성한 GDT Table을 GDTR 레지스터에 로드
  3. MOV CR0 명령을 이용해서 PE Flag 설정
  4. MOV CR0 명령 다음에 바로 jmp나 call 명령을 통한 Code Selector 스위칭 및 기타 Selector 설정
  5. Local Descriptor Table(LDT)를 사용할려면 LLDT 명령으로 LDT Table을 LDTR레지스터에 로드
  6. LTR 명령을 수행하여 Task 로드
  7. 여기까지는 16bit Real Mode의 주소를 그대로 맵핑한 Code 및 Data Descriptor를 사용하고 있었으니 새로 32bit용 Descriptor를  생성하여 전체 Selector 다시 맵핑(4번과 거의 동일한 명령 순서 사용)
  8. 인터럽터에 대한 핸들링 테이블(Interrupt Descriptor Table)을 생성한 뒤, LIDT 명령을 이용하여 IDTR 레지스터에 로드
  9. STI 명령을 사용하여 인터럽트 가능으로 설정

 사실 32bit에서 다시 16bit Mode로 돌아가는 과정도 소개되어있으나, 사용할 일이 없기때문에 생략하고 궁금한 사람은 Intel 메뉴얼을 참조하면 된다.

 Intel Manual의 9.10.2를  보면 STARTUP.asm 코드가 나와있다. 해당 코드에 대한 자세한 설명이 나와있으므로 메뉴얼에 나와있으므로 내가 만든 커널 로더의 코드를 이용해서 해당 부분의 설명을 할까 한다. 코드가 없는 사람은 21 OS 프레임워크 소스 릴리즈에 가서 프레임워크를 다운받아 01Boot/01KLoader 폴더에 파일을 참조하도록 하자.

 

 Part11. 커널 로더(Kernel Loader) 설명 문서와 내용이 상당히 중복되는데, 한번 더 복습한다는 생각으로 서로 참고하면서 보면 될것 같다.

  1. [org 0x00]    <= 코드의 Base 주소를 0x00으로 맞추라는 어셈블리어 디렉티브
    [bits 16]     <= 아래의 코드를 16bit 기계어로 생성하라는 어셈블리어 디렉티브
        start:
            ;   일단 세그먼트의 초기화     <= 세그먼트를 다 Code 세그먼트와 일치시킴
            mov     ax, cs
            mov     ds, ax
            mov     es, ax

 위의 두가지 어셈블리어 디렉티브는 위에 설명된 주석을 참고하자. 다른 디렉티브는 nasm 메뉴얼을 참조하면 다양한 것을 알 수 있다.

 

  1.         ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
            ;   GDT를 설정한다.
            xor     ebx, ebx
            xor     eax, eax
  2.         mov     bx, ds         <= 현재 DS의 위치 BASE로 하는 세그먼트를 설정
            shl     ebx, 4
            mov     eax, ebx
            mov     word [codeDesc + 2], ax
            mov     word [dataDesc + 2], ax
            shr     eax, 16
            mov     byte [codeDesc + 4], al
            mov     byte [dataDesc + 4], al
            mov     byte [codeDesc + 7], ah
            mov     byte [dataDesc + 7], ah
            
            ; gdtr 레지스터 설정
            mov     eax, ebx
            add     eax, gdt
  3.         mov     [gdtr + 2], eax    <= GDT Table의 값을 설정

  여기서 몇가지 눈여겨 보아야 할 것이 있다. 디스크립터의 형식을 보면 Base Address + 크기 정보로 간략히 표시할 수 있는데, 위의 코드에서 보면 Base Address를 현재 16bit 세그먼트의 값을 4만큼 좌로 쉬프트하여 그 값을 Base로 설정하는 것을 알 수 있다. 세그먼트 레지스터의 값을 4만큼 좌로 쉬프트하는 이유는 세그먼트 레지스터의 주소가 실제 주소로 맵핑될때 아래와 같은 공식으로 계산되기 때문이다.

Real Address = Segment Register * 24 + Register Value

 즉 세그먼트 레지스터의 값에 16을 곱하고 이 값을 Base로 사용한다는 것이다.

 

 위 코드에서 GDT 값에 현재 세그먼트의 값을 설정하는 이유는 위에 어셈블리어 디렉티브 명령인 ORG 명령과도 관계가 있다. 일단 ORG 명령의 영향에 대해서 알아 보자.

  1. OFFSET    CODE SEQUENCE
  2. 0x0000    A:    DW 0x00  <= 여기서 DW는 Word 크기만큼 자리를 할당 하는 어셈블리어 디렉티브
  3. 0x0002    MOV   AX, A

  위의 간단한 코드에서 AX 레지스터에 들어가는 값은 과연 얼마일까? 0x0000 일까? 정답은 ORG 명령에 따라서 코드의 시작이 달라지므로 LABEL A의 위치 또한 ORG 명령에 따라 달라진다. 만약 [ORG 0x00] 이라면 저 코드의 AX 레지스터에는 0x0000 이 들어갈 것이다. 하지만 만약 [ORG 0x10] 이라면? AX 레지스터에는 0x0010 이 들어간다는 말씀 @0@)/~ 간단히 생각해서 ORG에 선언된 값 만큼 더해진다고 생각하면 된다.

 부트로더의 코드 시작이 0x0000 이므로 코드내에 LABEL들은 다 0x0000 기반으로  되어있다. 실제 실행 시에  이 코드의 위치는 0x7E00 에 위치에 있고 세그먼트 레지스터를 0x07E0으로 설정했기때문에 자연스레 레이블의 BASE에 0x7E00이 더해지는 효과가 되어 정상적인 동작이 가능하다.

 

 그럼 만약 [ORG 0x7E00]으로 설정해서 코드를 생성하고 실행할 수 는 없을까? 물론 가능하다. 세그먼트 레지스터의 값을 0x0000으로 설정해서 base가 0이 되도록 하고 GDT의 Code/Data의 Base를 0x0000으로 설정하면 된다. 하지만 여기서 주의해야 할 것은 앞부분에 16bit 코드의 부분이다. 16bit 코드의 경우 접근할 수 있는 코드의 최대 범위는 (Segment Register * 24  + 레지스터의 값)과 같은데 실제 레지스터가 16bit이기 때문에 범위는 0x0000 ~ 0xFFFF 까지이다.

 세그먼트 레지스터의 Base를 0x0000으로 설정한 상태이기 때문에 실제로 접근할 수 있는 영역은 0x0000 ~ 0xFFFF까지 밖에 안된다. 이미 커널 로더가 로딩된 위치가 0x7E00 이므로 여유공간은 0x81FF 정도라는 것에만 유의하면 된다. 커널 로더의 코드가 그리 크지 않다면 별 문제가 되지 않는다는 말씀~ >ㅁ<)/~

 

 GDT Table의 값까지 설정했으므로 GDT Table의 값을 로드하고 CR0를 변경해서 32bit로 변경하는 코드 부분을 살펴보자.

  1.         ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
            ; 인터럽터 불가 설정
            cli
           
            lgdt    [gdtr]     <= GDT Table을 로드
  2.         ;   보호모드를 설정하고
            ;   인덱스 2 비트가 Floating Point 비트다.
            ;   Floating Point계산 유닛을 내부 유닛을 사용하도록
            ;   설정한다 그렇지 않으면 7번 인터럽터가 발생한다.
            mov     eax, cr0
            or      eax, 0x80000000      <= 페이징 플래그를 끄는 부분
            xor     eax, 0x80000000
            or      al, 1                <= 32bit 플래그를 설정하는 부분
            or      al, 0x04
            xor     al, 0x04
            mov     cr0, eax             <= 실제로 변경된 플래그를 적용하는 부분
  3.         jmp     codeOffset:code_32   <= 32bit 모드로 점프~

 우측에 달린 설명을 읽어보면 각 역할을 알 수 있다. 마지막에 jmp 명령에서 Segment:Offset 형태의 fat jmp를 수행하고 나면 32bit 코드가 된다. 계속해서 마무리 부분을 살펴보자.

  1. ;   여기에 진입하면 이미 보호모드다. 이제 리얼모드와는
    ;   상관없고 커널이 시작되면 돌아갈수도 없다.
    [bits 32]
        code_32:
            ;   레지스터를 초기화 하고
            mov     ax, dataOffset    <= 32bit 코드, 레지스터 마무리
            mov     ds, bx
            mov     es, bx
            mov     ss, bx
            mov     gs, bx
            xor     esp, esp
            mov     esp, 0x8ffff
            mov     ebp, 0x8ffff

  위의 코드가 32bit 코드의 시작 부분이고 모드 전환 부분의 마무리 부분이다. 이 아래부터는 각 기능 구현을 위한 32bit 코드들이 나열되고 수행된다.

 

  원래 GDT 설정 부분을 좀 더 자세히 다룰 생각이었으나, 역시 INTEL 메뉴얼을 그대로 옮겨적는 수준 밖에 되지 않을 것 같아서 코드로 대체했다. 사실 코드를 보면 앞서 설명했듯 그리 어렵지 않다. 하지만 이 코드를 제대로 쓰기위해서는 세그먼트에 대한 이해와 실제 코드가 컴파일 되고 링크되었을 때, 레이블과 변수들의 위치가 어떻게 되는지에 대한 이해, 그리고 GDT에 대한 정확한 이해가 필요하다. 경험한 사람은 알겠지만, 위 항목중에 하나라도 어긋나거나 잘못 이해하게되면 블루스크린의 공포보다 더 무섭다는 블랙스크린의 공포를 뼈져리게 느끼게 된다(글고 헤어나지 못해 GG를 치는 상황까지.. ㅡ,.ㅡ;;;;).

 

 어쨋든 32bit 모드로 스위칭 했으니 이제 32bit 모드로 계속 전진하도록 하자 @0@)/~ 화이팅~!!!!

 

 

이 글은 스프링노트에서 작성되었습니다.

이 글은 스프링노트에서 작성되었습니다.

+ Recent posts