Part11. 커널 로더(Kernel Loader) 설명

원문 :  http://kkamagui.springnote.com/pages/347834

 

들어가기 전에...

0.시작하면서...

 커널 로더(Kernel Loader)는 프레임워크 소스 중에서 가장 CPU Architecture에 의존적이고 복잡한 부분이다. 커널 로더를 이해하려면 Intel Architecture에 대한 이해가 필요한데, 일일이 설명하기에 양이 너무 많다. 따라서 부트 로더와 마찬가지로 기능적인 부분으로 나누어 간략히 설명하겠다.

 커널 로더의 소스는 01Boot 폴더에 01Kloader 에 있다. 소스를 같이 참고하도록 하자.

 커널 로더에서 수행하는 순서는 아래와 같다.

  1. 세그먼트 레지스터의 재설정 
  2. 16bit 모드에서 32bit 모드로 전환 
  3. 커널 코드를 1M 주소 영역으로 이동 
  4. 커널 실행

 이제 각각의 순서에 대해서 살펴보자.

 

1.세그먼트 레지스터의 재설정

 부트 로더에서 커널 로더로 실행이 이행되면 세그먼트 레지스터 설정을 따로 할 필요가 없다. 왜냐하면 부트 로더에서 이미 다 설정해서 넘겨주기 때문이다. 하지만 프레임워크의 부트 로더가 커널 로더를 실행하라는 법이 없으므로 다시 세그먼트를 초기화 해준다.

  1.          ;   일단 세그먼트의 초기화
            mov     ax, cs
            mov     ds, ax
            mov     es, ax

 

2.16bit 모드에서 32bit 모드(Protected Mode)로 전환

 32bit 모드는 보호 모드 또는 Protected Mode라고 불리며 일반 16bit 모드와는 달리 여러가지 권한 설정과 영역 보호를 하드웨어적으로 수행할 수 있는 모드이다. 모든 기재를 사용하면 세세한 권한 설정 및 체크가 가능하지만  프레임워크에서는 이를 간단히 설정하여 사용한다.

 앞서 16bit와 32bit 모드간의 차이에 대해서는 Part5. Intel Architecture에 대한 소개에서 설명했으므로 참고하도록 하고, 지금은 전환에 대한 내용만 소개하겠다. 32bit 전환" href="http://kkamagui.springnote.com/pages/363853">참고. Intel i386 CPU의 16bit->32bit 전환 문서도 같이 참고하면 도움이 될 것이다.

 

2.1 디스크립터(Descriptor) 설정 

 전환을 위해 가정 먼저 해야될 준비는 32bit 모드에서 사용할 코드/데이터/스택 등등의 영역(세그먼트)에 대한 정보를 기술하는 디스크립터(Descriptor)를 생성하는 일이다.

 세그먼트를 세세하게 나누면 유저 모드용/커널 모드용 영역으로 크게 구분하고 다시 각 영역을 코드/데이터/스택 등등의 영역으로 각각 분할하여 처리할 수 있다. 하지만 프레임워크에서는 0 ~ 0xFFFFFFFF 크기의 세그먼트를 잡아서 코드/데이터에 할당하여 이를 간단하게 정의하였다. 이러한 모드를 일반적으로 Flat 모드라고 하는데, 논리주소가 물리주소에 1:1로 대응되고 메모리 관가 편리하므로 간단한 커널에서는 많이 쓰인다.

 

 자 그럼 실제로 디스크립터를 설정하는 부분에 대해서 살펴보자.

  1. gdt:
        ;null describtor
            dw 0
            dw 0
            db 0
            db 0
            db 0
            db 0
        ;새로 옮겨진 커널의 CodeOffset
        kernelCodeOffset equ $ - gdt
            dw 0xffff
            dw 0x0000
            db 0x0000
            db 0x9a
            db 0xcf
            db 0
        ;새로 옮겨진 커널의 DataOffset
        kernelDataOffset equ $ - gdt
            dw 0xffff
            dw 0x0000
            db 0x0000
            db 0x92
            db 0xcf
            db 0
        ;videoMemory
        videoOffset equ $ - gdt
        videoDesc:
            dw 0xffff
            dw 0x8000
            db 0x0b
            db 0x92
            db 0xcf
            db 0
        ;linear모드 데이터
        linearDataOffset equ $ - gdt
        linearDataDesc:
            dw 0xffff
            dw 0
            db 0
            db 0x92
            db 0xcf
            db 0
        ;code describtor
        codeOffset equ $ - gdt
        codeDesc:
            dw 0xffff
            dw 0
            db 0
            db 0x9a
            db 0xcf
            db 0
        ;data 영역의 descriptor
        dataOffset equ $ - gdt
        dataDesc:
            dw 0xffff
            dw 0
            db 0
            db 0x92
            db 0xcf
            db 0
    gdt_end:

 주석이 친절히 달려있으니 해당 디스크립터가 어떤 디스크립터인지 쉽게 파악할 수 있을 것이다 . 위에서 주의해서 볼 것은 디스크립터의 첫번째가 NULL 디스크립터로 전부 0으로 채워 할당한 부분이다. 이 부분은 CPU에 의해 예약된 영역이므로 다른 값으로 초기화 하거나 하면 곤란하다(영영 커널이 부팅되는 모습을 볼 수 없을지도 모른다).

 조금 전에 프레임워크에서 코드와 데이터 2개만 할당해 준다고 했는데, 왜 디스크립터가 6개나 되는 걸까? 주로 사용하는 영역은 코드와 데이터 디스크립터이고 나머지는 부수적으로 사용하는 디스크립터라서 그렇다. 필요에 의해 만든 것이므로 크게 신경쓰지 않아도 된다. 실제 동작을 위해서는 32bit 코드/데이터 디스크립터, 그리고 NULL 디스크립터만 있어도 된다.

 

 각 필드에 대한 의미나 플래그 값들은 Intel Architecture의 Volume 3을 참조하도록 하고 일단 디스크립터 형태에 대해서 잠깐 보고 넘어가자.

디스크립터.PNG

<Segment Descriptor>

 

 디스크립터는 위와같이 8byte로 이루어져있으며 각 속성(코드/데이터)에 맞게 플래그를 설정해주면 된다. 실제로 이 작업은 굉장히 까다로운 작업이며 약간 실수(??)하면 재부팅을 연발한다. 벗어나는 방법은 잘 설정된 코드를 참조하거나 끊임없는 수정을 통해 적절한 값을 찾는 방법말고는 없다.... ㅡ_ㅜ... 위에 디스크립터 구조와 부트 로더 코드를 보면서 비교하면 설정된 값의 의미를 알 수 있으므로 넘어간다.

 디스크립터의 설정이 끝나고나면 기대하던 16bit -> 32bit 전환의 단계가 남아있다.  전환하는 방법은 아주 간단하며 CR0 레지스터에 플래그 하나를 바꿔주는 것으로 전환은 끝이난다. 하지만 그 전에 전환 준비가 좀 까다롭다.

 

2.2 Global Descriptor Table Register(GDTR) 설정

 시스템 전체적인 디스크립터의 테이블을 Global Descriptor Table(GDT)이라고 하는데, 단 하나만 존재하는 영역으로써 코드/데이터/스택 등의 세그먼트에 대한 디스크립터나 태스크에 대한 디스크립터 등등을 저장하는 역할을 한다.

 위에서 디스크립터를 연속적으로 할당하여 값을 설정해 주었는데, 이 영역이 바로 GDT가 되는 것이다. GDT가 따로 존재하는 것이 아니고 연속된 메모리 위치에 디스크립터를 생성하면 그것이 GDT가 된다. 이 GDT 영역을 16bit->32bit 전환 전에 GDT Register에 등록해 줘야 하는데 GDTR은 아래와 같은 구조를 가지는 구조체의 주소를 가진다.

 디스크립터2.PNG

<GDTR 구조체>

 

GDT의 크기와 시작주소를 가지는 간단한 구조체이다. 이 구조체에 값을 설정하고 32bit 모드로 전환했을 때 사용할 디스크립터에 값을 설정하는 코드는 아래와 같다.

  1.         ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
            ;   GDT를 설정한다.
            xor     ebx, ebx
            xor     eax, eax
            mov     bx, ds
            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 레지스터 설정
            ;lea    eax, [gdt+ebx]
            ;lea 명령은 아래의 2줄과 같다.
            mov     eax, ebx
            add     eax, gdt
  2.         mov     [gdtr + 2], eax
  3. ...... 생략 ......
  4. gdtr:
            dw gdt_end - gdt - 1
            dd gdt

 설정된 gdtr의 값을 GDTR 레지스터에 설정하는 코드는 뒤에 16bit->32bit 모드 전환에서 나온다.

 

2.3 16bit->32bit 모드 전환

 마지막 작업은 인터럽터를 불가로 설정하고 실제 32bit 모드로 변환해 주는 것이다. 인터럽터를 불가로 설정하는 이유는 32bit 모드에서 인터럽트 처리를 위한 기반 작업(Interrupt Descriptor Table 설정과 같은 것들..)이 되어있지 않기 때문이다. 실제로 이 작업은 커널이 완전히 로딩된 후 설정하는 작업이기 때문에 커널 실행 전까지는 인터럽트 불가 상태로 설정해 놓는다.

 실제 모드를 변경하는 코드는 아래와 같다.

  1.         ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
            ; 인터럽터 불가 설정
            cli
           
            lgdt    [gdtr]   <= 실제로 이부분이 GDT를 로딩하는 부분이다.
            ;   보호모드를 설정하고
            ;   인덱스 2 비트가 Floating Point 비트다.
            ;   Floating Point계산 유닛을 내부 유닛을 사용하도록
            ;   설정한다 그렇지 않으면 7번 인터럽터가 발생한다.
            mov     eax, cr0
  2.         or      eax, 0x80000000
            xor     eax, 0x80000000
            or      al, 1
            or      al, 0x04
            xor     al, 0x04
            mov     cr0, eax
  3.         jmp     codeOffset:code_32
  4. ;   여기에 진입하면 이미 보호모드다. 이제 리얼모드와는
    ;   상관없고 커널이 시작되면 돌아갈수도 없다.
    [bits 32]
        code_32:
            ;   레지스터를 초기화 하고
            mov     ax, dataOffset
            mov     ds, ax
            mov     es, ax
            mov     ss, ax
            mov     gs, ax
            xor     esp, esp
            mov     esp, 0x8ffff
            mov     ebp, 0x8ffff

 위에서 점프 명령을 이용하는 이유는 코드 영역을 설정하는 CS 레지스터 같은 경우는 일반적인 mov 명령으로 값을 바꿀 수 없기 때문이다. 따라서 far jump 명령을 이용해서 CS의 값을 커널 코드 영역을 가리키는 코드 세그먼트의 값으로 바꾸어 준다.

 32bit 영역으로 이동한 후에는 DS, ES, SS, GS, FS와 같은 데이터 관련 레지스터를 모두 커널 데이터 디스크립터로 변경하여 이후 데이터 접근 시에 문제가 발생하지 않도록 한다. 마지막 작업으로 스택을 설정함으로써 32bit 모드로 진입을 끝낸다.

 

3.커널 코드 이동

3.1 A20 Line Enable

 이제 남은 작업은 커널 코드를 1M 주소로 이동해서 재배치 하는 작업이다. 굳이 옮기지 않아도 되지만 옮긴 이유는 1M 이하 영역은 BIOS의 코드도 포함되어있고 DMA 관련 데이터 송/수신 시 사용할 영역을 확보한다는 의미도 있다(구버전의 DMA의 경우에는 1M 이상의 메모리에 접근이 불가능 했다).

 커널 코드 이동은 단순이 커널 존재하는 메모리를 1M 영역으로 복사하는 것을 의미하는 것은 아니다. IBM 호환 PC의 경우 16bit 모드일때 Address Line을 다 사용하지 않기 때문에 Address Line을 확장하도록 설정해 줘야 한다. 즉 확장하지 않으면 1M 이상의 메모리로 접근을 해도 이것이 다시 1M 안쪽의 메모리로 Wrapping되어서 접근된다.

 

 그럼 1M 이상의 메모리로 접근하기위해서는 어떻게 해야 할까? A20 Line을 활성화 하면 된다!!. A20이 무엇인지에 대해서는 http://www.resultspk.net/create_os/os-faq-memory.html#what_is_a20를 참고하도록 하고 1M 이상의 메모리로 접근하기위해 Off 상태로 되어있는 Address Line을 활성화 한다는 정도만 알아놓자.

 

 아래는 A20을 활성화하는 코드이다.

  1. ;   A20 영역을 Enable 시킨다.
    ;   wrapping된 line을 Enable한다.
    enable_A20:
        call    a20wait
        mov     al,0xAD
        out     0x64,al
  2.     call    a20wait
        mov     al,0xD0
        out     0x64,al
  3.     call    a20wait2
        in      al,0x60
        push    eax
  4.     call    a20wait
        mov     al,0xD1
        out     0x64,al
  5.     call    a20wait
        pop     eax
        or      al,2
        out     0x60,al
  6.     call    a20wait
        mov     al,0xAE
        out     0x64,al
  7.     call    a20wait
        ret
  8. a20wait:
    .l0:    mov     ecx,65536
    .l1:    in      al,0x64
        test    al,2
        jz      .l2
        loop    .l1
        jmp     .l0
    .l2:    ret

  9. a20wait2:
    .l0:    mov     ecx,65536
    .l1:    in      al,0x64
        test    al,1
        jnz     .l2
        loop    .l1
        jmp     .l0
    .l2:    ret

 

3.2 커널 이동

 이제 커널을 1M 영역으로 복사하면 끝이다. 코드가 좀 복잡하긴 한데, 주된 흐름은 이동 시작 위치와 목적 위치를 설정해주고 부트 로더에 들어있던 커널 이미지 크기만큼 1M 이상의 영역으로 복사하는 것이다.

  1.         ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
            ;   뒷 메모리에 있는 커널을 읽어 들여서 점프 한다.
            ;   일단 movsd 명령이 si 와 di의 값을 하나씩 증가시키게 한다.
            ;   df = 0 이면 증가 1 이면 감소
            cld
            ;   그리고 커널을 1M 메모리 영역으로 올린다.
            ;   커널이 위치하는 세그먼트 정보는 0x07c0:0x01fa에 있다.
            mov     ax, linearDataOffset
            mov     es, ax
            xor     eax, eax
            xor     ebx, ebx
            mov     ax, word [es:0x7dfa]
            sub     ax, 1
            mov     bx, 0x0200
            mul     ebx
            ;   커널 시작 섹터에서 0x0200을 곱하고 0xfe00을 더하면
            ;   커널이 시작되는 주소가 나온다.
            ;   거기로 옮기면 된다
            add     eax, 0xfe00
            mov     esi, eax
            ;   그리고 0x07c0:0x01fc에 커널의 섹터수가 들어있으므로
            ;   그것을 읽고 movsd의 명령으로 4byte씩 전송하므로 512/4 = 128
            ;   을 곱하면 movsd의 명령을 실행할 횟수가 나온다.
            xor     eax, eax
            xor     ebx, ebx
            mov     ax, word[es:0x7dfc]
            mov     bx, 0x80
            mul     ebx
            mov     ecx, eax
            ;   그리고 마지막으로 옮길 부분의 Offset을 구하면 1Mbyte의 위치이므로
            ;   0x100000 의 위치로 옮긴다.
            mov     eax, 0x100000
            mov     edi, eax
            ;   마지막으로 세그먼트를 설정한다. 둘다 linear로 설정한다.
            mov     ax, linearDataOffset
            mov     ds, ax
            rep     movsd

 위의 코드가 끝이 나면 1M 위치에 커널이 복사가 완료된 것이다. 이제 1M로 Jump하면 커널이 실행된다.

 

4.커널 실행

 커널의 실행은 허무할 정도로 간단하다. 아래의 한줄로 끝이난다.

  1.         jmp     kernelCodeOffset:0x100000

 그 이후는 커널이 실행되고 프레임워크 초기화를 수행하는 등등의 작업을 한다. 커널 코드가 이동된 곳은 1M(0x100000)의 위치다. 다시말해 커널 코드를 컴파일해서 링크했을 때 1M의 위치에서 실행가능하도록 해야한다.

 어떻게 1M의 위치에서 실행가능한 코드를 만들어낼까? 답은 링크 옵션(Link Option)에 있다. 프레임워크 소스 파일에서 커널을 생성하는 makefile을 열어보면 알겠지만, ld의 옵션중에 코드를 생성할때 특정 위치에서 실행가능하도록 링크하는 옵션이 있다.

-Ttext 0x100000

 위의 옵션을 사용하면 접근하는 변수, 상수들의 주소가 1M 위치를 Base로 해서 생성되기 때문에 1M의 위치에서 실행가능한 것이다. 만약 커널을 2M 영역으로 이동할 필요가 있다면 위의 부분을 0x200000 수정하고 부트 로더에서 커널을 이동하는 위치를 1M -> 2M로 수정해 주면 2M의 위치에서 커널을 실행할 수 있다.

 

5.물리 메모리 크기 확인

 실제 머신에 사용가능한 물리 메모리의 크기를 알면 커널 동작 시에 좀 더 능동적으로 작업을 수행할 수 있다. 물리 메모리의 크기를 아는 방법은 BIOS 함수을 이용해서 구하는 방법이 있으며, 직접 수작업으로 확인하는 방법이 있다.

 수작업으로 확인하는 방법은 메모리를 1M씩 증가시키면서 해당 주소에 1byte 값을 쓰고 다시 읽어서 쓴 값이 정상적인가를 확인하는 방법이다. 만약 물리 메모리가 사용가능하다면 쓰고 읽었을 때 정상적인 값이 나오지만, 사용이 불가능하다면 틀린 값이 나올 것이다. 실제로 Bellona2 OS(http://www.bellona2.com)가 이러한 방식을 택하고 있으며, 프레임워크에서도 직접 체크하는 방식을 따르고 있다.

 

 아래는 직접 체크하는 코드이다.

  1. ;   1M 이후 영역부터 루프를 돌면서 그 결과값을 저장한다.
    ;   결과값을 저장하는것은 0x7c00 즉 부트 섹터 시작 점에 적는다.
    ;   값은 Double Word로 한다.
    checkMemoryAmount :
            push        ebp
            push        eax
            push        ecx
            push        ebx
            push        gs
            push        es
  2.         mov     ecx, 0x00
            mov     ebx, 0x000000                  
            mov     ax, linearDataOffset
            mov     gs, ax
            mov     ax, videoOffset
            mov     es, ax
  3.     continue :
            add     ebx, 0x100000
            inc     ecx
            ;   메모리 체크 진행상황을 본다.
            ;mov        byte[es:ecx], '.'
            ;   일단 메모리에 무각기로 쓰고
            mov     byte[gs:ebx], 0x03
            ;   다시 메모리에 있는 내용을 비교한다.
            mov     al, byte[gs:ebx]
            cmp     al, 0x03
            je      continue
           
            mov     ebx, 0x7c00
            mov     dword[gs:ebx], ecx
  4.         pop     es
            pop     gs
            pop     ebx
            pop     ecx
            pop     eax
            pop     ebp
            ret

 

6.마치면서...

 이로서 부트 로더와 커널 로더의 설명이 끝이났다. 원래는 CPU 의존적인 부분들이라 설명을 하지 않고 넘어가려 했으나, 그대로 넘어가는 것이 마음에 걸려서 약간 설명을 한다는 것이.... ㅡ_ㅡ;;;;

 역시나 딱딱한 이야기들로 가득찼는데... ㅡ_ㅜ... 다음부터는 프레임워크를 이용하여 커널을 작성하면서 해당 파트에 대한 설명을 하겠다(쓰면서도 지루해 죽을 껏 같았다는.. ㅡ_ㅡ).

 

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

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

+ Recent posts