→ 보호 모드로 전환
보호 모드로 전환하려면 GDTR 레지스터 설정, CR0 컨트롤 레지스터 설정, jmp 명령 수행 이 3단계만 수행하면 됩니다.
프로세서의 레지스터에 값을 설정하는 작업은 대부분 과정이 한두 줄의 어셈블리어 코드로 끝나므로 코드를 이해하는 데 큰 어려움 없고, 간단합니다.
→ 프로세서에 GDT 정보 설정
프로세서에 GDT 정보를 설정하려면 lgdt 명령어를 사용합니다.
lgdt 명령어는 2byte 크기와 4byte 기준 주소로 된 GDT 정보 자료구조를 오퍼랜드로 받습니다.
아래와 같이 코드 한 줄로 GDT 정보를 프로세서에 로딩할 수 있습니다.
참고)
보호 모드로 전환하여 인터럽트에 관련된 설정이 완료되기 전에 인터럽트가 발생하면 프로세서는 인터럽트 처리 함수(인터럽트 핸들러)를 찾을 수 없기 때문에 예기치 못한 문제가 발생할 수 있으며, 최악의 상황에는 리부팅될 수도 있으므로 보호 모드로 전환하는 과정과 전환 후 인터럽트 설정을 완료하기 전까지는 인터럽트가 발생하지 않게 하는 것이 좋습니다.
인터럽트를 발생할 수 없게 설정하려면 cli 명령어를 사용하고
인터럽트를 발생할 수 있게 설정하려면 sti 명령어를 사용합니다.
sti 명령은 IA-32e 모드로 전환하고 난 뒤, 인터럽트 처리가 모두 끝나고 나서 실행합니다.
→ CR0 컨트롤 레지스터 설정
CR0 컨트롤 레지스터에는 보호 모드 전환에 관련된 필드 외에 캐시(Cache), 페이징(Paging), 실수 연산 장치(FPU)등과 관련된 필드가 포함되어 있습니다.
CR0 컨트롤 레지스터는 아래와 같은 형식으로 구성되며, 각 필드에 대한 내용은 다음과 같습니다.
FS64 OS에서 보호 모드는 거쳐 가는 임시 모드에 불과하므로 세그먼테이션 기능 외에는 사용하지 않습니다.
페이징, 캐시, 메모리 정렬 검사, 쓰기 금지 기능 등 모두 사용하지 않음으로 설정하고, FPU 역시 쓰지 않기 때문에 임시 값으로 설정합니다.
다른 필드들은 해당 필드에 값을 설정하는 것만으로 관련 기능을 제어할 수 있지만, FPU에 관련된 필드(EM, ET, MP, TS, NE)는 서로 연관되어 있기 때문에 잘 설정해 주어야 합니다.
x86 프로세서에는 FPU가 내장되어 있으므로 EM 필드를 0으로 설정해서 FPU 명령을 소프트웨어로 에뮬레이션하지 않게 하고, ET 필드를 1로 설정합니다.
지금은 임시로 초기화를 수행한 것이기 때문에 FPU를 사용하면 정상적으로 작동하지 않으므로 MP 필드와 TS 필드와 NE 필드를 1로 설정하여 FPU 명령이 실행되었을 때 예외가 발생하게 설정합니다.
보호 모드에서는 예외에 대해 처리를 하지 않으므로 가능하다면 실수 연산을 하지 않는 것이 좋습니다.
위의 CR0 컨트롤 레지스터의 구조와 보호 모드 전환을 위한 설정 값 사진의 아래에 표시된 값은 위에서 설명한 내용을 바탕으로 CR0 컨트롤 레지스터의 필드와 의미 사진의 내용을 참고하여 설정한 CR0 컨트롤 레지스터의 실제 값입니다.
이 값을 CR0 컨트롤 레지스터에 설정하는 것이 전부이며, 설정하는 코드는 다음과 같습니다.
특이한 부분으로는 CR0 컨트롤 레지스터가 32bit 크기이므로 32bit 크기의 범용 레지스터인 EAX를 사용했습니다.
CR0 컨트롤 레지스터도 설정했으니, 이제 jmp 명령으로 CS 세그먼트 레지스터를 갱신하고 32bit 코드로 변경하는 것만 남았습니다.
이 과정도 몇 줄의 어셈블리어 코드로 처리합니다.
참고)
외부 메모리의 내용과 캐시의 내용을 일치시키는 동기화 정책은 크게 Write-through 방식과 Write-back 방식으로 구분됩니다.
이 두 방식의 가장 큰 차이점은 캐시의 내용을 외부 메모리로 언제 쓰는가 입니다.
▶ Write-through 방식
● 메모리에 쓰기가 수행될 때마다 캐시의 내용과 외부 메모리의 내용을 모두 갱신합니다.
▶ Write-back 방식
● 쓴 내용을 캐시에만 갱신하고 외부 메모리에 쓰는 작업은 최대한 뒤로 미룹니다.
● 캐시는 작고 한정적인 공간이어서 새로운 데이터를 캐시에 넣으려면 현재 캐시에 있는 데이터를 버려야 하기 때문에 캐시의 내용을 버릴 때 캐시의 내용을 외부 메모리에 갱신함으로써 외부 메모리에 접근하는 횟수를 줄이는 것입니다.
속도 면에서는 당연히 Write-back 방식이 좋지만 메모리를 통해 I/O를 수행하는 메모리 맵 I/O 방식을 사용하는 디바이스에는 문제가 발생할 수 있습니다.
메모리 맵 I/O 방식은 메모리 주소에 읽고 쓰는 데이터를 디바이스로 송수신하는 방식인데, Write-back 방식을 사용하면 외부 메모리에 업데이트가 바로 되지 않아서 문제가 생깁니다.
FS64 OS는 IA-32e 모드에 진입해서 모든 초기화 작업이 끝나고 나서 캐시를 활성화합니다.
캐시에 대한 더 자세한 내용은 인텔 매뉴얼 "Volume 3A: System Programming Guide, Part 1"의 10.Memory Cache Control을 참고하면 됩니다.
→ 보호 모드로 전환과 세그먼트 셀렉터 초기화
이제 보호 모드로 전환하기 위한 모든 준비가 끝났으므로 32bit 코드를 준비한 후, 한 줄의 어셈블리어 코드로 CS 세그먼트 셀렉터(세그먼트 레지스터)의 값을 바꾸면 됩니다.
어셈블리어로 16bit나 32bit 코드를 생성하려면 BITS 명령을 사용합니다.
[BITS 16] 혹은 [BITS 32]와 같이 사용하면, 이후에 위치하는 코드는 모두 16bit나 32bit 코드로 생성됩니다.
CS 세그먼트 셀렉터를 교체하려면 jmp 명령과 세그먼트 레지스터 접두사를 사용해야 합니다.
리얼 모드의 세그먼트 레지스터는 세그먼트의 시작 주소(기준 주소)를 저장했지만, 보호 모드의 세그먼트는 리얼 모드와 달리 다양한 정보를 포함하고 있기 때문에 세그먼트 정보는 디스크립터에 저장하고 세그먼트 셀렉터(레지스터)는 그 디스크립터를 지시하는 용도로 사용합니다.
보호 모드에서 GDT 내의 디스크립터를 지시하고 싶을 때는 마찬가지로 세그먼트 셀렉터에 주소를 설정하면 됩니다만, 세그먼트의 기준 주소 대신 GDT 내의 디스크립터의 주소를 사용하는데, 이는 GDT의 시작 주소로부터 떨어진 거리(Offset)를 의미합니다.
예를 들어 NULL 디스크립터 다음에 있는 커널 코드 세그먼트 디스크립터를 사용하고 싶으면, 디스크립터의 크기가 8byte이므로 0x08을 세그먼트 셀렉터에 넣으면 됩니다.
또는 세 번째에 위치한 커널 데이터 디스크립터를 사용하고 싶다면 0x10(=16)와 같이 사용하면 됩니다.
다음은 커널 코드 세그먼트를 사용하여 보호 모드로 전환하고 나서 나머지 세그먼트 셀렉터를 커널 데이터 세그먼트 디스크립터로 초기화하는 코드입니다.
지금까지의 내용을 하나의 파일로 만들어 FS64 OS에 추가하면 리얼 모드에서 보호 모드로 전환할 수 있습니다.
실행 화면만 보면 이전과 다른 것이 아무것도 없는 것처럼 보이기 때문에 화면에 32bit 보호 모드로 변경되었다는 것을 표시하고, 16bit 코드를 32bit 코드로 바꾸는 요령도 익힐 겸 PRINTSTRING 함수를 보호 모드용으로 변환합니다.
→ 보호 모드용 PRINTSTRING 함수
리얼 모드용 함수를 보호 모드로 변환하려면 스택의 크기가 2byte에서 4byte로 증가하며, 범용 레지스터의 크기가 32bit로 커졌다는 것 정도만 알면 됩니다.
아래의 사진은 보호 모드용 PRINTSTRING 함수인 PRINTMESSAGE 함수입니다.
리얼 모드 PRINTSTRING 함수와 비교해보는 것도 좋습니다.
기존의 리얼 모드 PRINTSTRING 함수와의 차이점은 연산에 사용되는 범용 레지스터가 대부분 32bit 범용 레지스터로 수정되었다는 것과 스택의 크기가 4byte로 변경되었기에 파라미터를 오프셋이 4의 배수로 바뀐 것 정도이고
또 다른 점으로는 리얼 모드에서는 레지스터의 한계로 64KB 범위의 주소만 접근 가능했으므로 화면 표시를 위해 별도의 세그먼트가 필요했지만
보호 모드에서는 32bit offset을 사용할 수 있으므로 4GB 전 영역에 접근할 수 있습니다.
따라서 리얼 모드에서 사용했던 ES 세그먼트 레지스터를 제거하고 직접 비디오 메모리에 접근해서 데이터를 쓰도록 수정했습니다.
함수를 호출하여 사용하는 부분은 리얼 모드와 보호 모드가 거의 같습니다.
→ 보호 모드용 커널 이미지 빌드와 가상 OS 이미지 교체
이제 이전의 내용을 조합하여 하나의 파일로 만들고 가상 OS 이미지와 교체하여 정상적으로 실행되는지 확인합니다.
→ 커널 엔트리 포인트 파일 생성
01.Kernel32 디렉터리의 Source 디렉터리 밑에 EntryPoint.s 파일을 추가하고 아래와 같이 입력합니다.
EntryPoint.s 파일은 보호 모드 커널의 가장 앞부분에 위치하는 코드로 보호 모드 전환과 초기화를 수행하여 이후에 위치하는 코드를 위한 환경을 제공합니다.
참고)
엔트리 포인트(Entry Point)는 외부에서 해당 모듈을 실행할 때 실행을 시작하는 지점을 의미합니다.
이전에 작성한 코드는 부트 로더(외부)에서 보호 모드 커널로 진입하는 부분이므로 보호 모드 커널의 엔트리 포인트라고 할 수 있습니다.
→ Makefile 수정과 가상 OS 이미지 파일 교체
보호 모드 커널이 가상 OS 이미지 파일을 대체하기 때문에 가상 OS에 관련된 VirtualOS.asm 파일과 VirtualOS.bin 파일은 삭제해도 됩니다.
새로운 파일이 추가되었으므로 makefile을 수정합니다.
먼저 01.Kernel32 디렉터리에 있는 makefile을 수정합니다.
기존의 코드는 VirtualOS.asm 파일을 빌드하여 가상 OS 이미지를 생성했지만, 이제는 가상 OS 이미지 대신 실제 커널 이미지를 생성해야 하기 때문에 엔트리 포인트 파일을 빌드할 수 있게 아래와 같이 수정합니다.
위의 makefile에서 " $< "라고 표시된 새로운 기호는 매크로이며, Dependency(:의 오른쪽)의 첫 번째 파일을 의미합니다.
따라서 'Source/EntryPoint.s'로 치환되며, 이 엔트리 파일은 빌드되어 Kernel32.bin 파일로 생성됩니다.
그다음 최상위 디렉터리에 있는 makefile을 수정합니다.
아래의 사진과 같이 다른 부분은 같으므로 그대로 두고 디스크 이미지를 생성하는 부분을 수정합니다.
위의 코드에서 " $^ "라는 새로운 기호도 역시 매크로이며, " $< "의 역할과 비슷하지만, Dependency(:의 오른쪽)에 나열된 전체 파일을 의미합니다.
따라서 '00.BootLoader/BootLoader.bin 01.Kernel32/Kernel32.bin'로 치환되며, 이 두 가지 파일을 합쳐서 디스크 이미지를 생성합니다.
→ OS 이미지 통합과 QEMU 실행
마지막으로 부트 로더의 코드를 수정해주어야 합니다.
부트 로더에 OS 이미지의 크기가 1024로 설정되어 있기 때문에 지금 상태에서 QEMU를 실행하면 정지한 상태로 아무런 반응이 없을 것입니다.
보호 모드 커널 이미지의 크기는 512byte(1 섹터)밖에 안되기 때문에 부트 로더가 한 섹터를 로딩한 후 나머지 1023 섹터를 읽으려다 정지합니다.
그렇기 때문에 부트 로더 BootLoader.asm 파일의 TOTALSECTORCOUNT의 값을 1024에서 1로 바꾸면 정상적으로 실행될 것입니다.
이제 파일을 저장하고 자신이 사용하는 IDE(ex. eclipse)에서 빌드하거나
우분투의 터미널에서 자신의 OS 프로젝트 디렉터리(ex. FS64 OS)에 들어가서 make 명령을 수행하여 빌드한 뒤
QEMU를 실행하면 아래와 같이 보호 모드 전환이 완료됩니다.
'시작하지 말았어야 했던 것 > 64비트 멀티코어 OS' 카테고리의 다른 글
64비트 멀티코어 OS[6] - 2. C 소스 파일 추가와 보호 모드 엔트리 포인트 통합 (0) | 2021.03.12 |
---|---|
64비트 멀티코어 OS[6] - 1. 실행 가능한 C 코드 커널 생성 방법 (3) | 2021.03.11 |
64비트 멀티코어 OS[5] - 1. 세그먼트 디스크립터 생성과 GDT 정보 생성 (0) | 2021.03.08 |
64비트 멀티코어 OS[4] - 3. 테스트를 위한 가상 OS 이미지 생성 (0) | 2021.03.05 |
64비트 멀티코어 OS[4] - 2. 보호 모드에서 사용되는 세 가지 함수 호출 규약과 최종 부트 로더 코드 (0) | 2021.03.03 |