ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • Long mode
    프로젝트/운영체제 만들기 2023. 6. 20. 18:56

    글의 참고

    - https://en.wikipedia.org/wiki/X86-64

    - https://os.phil-opp.com/entering-longmode/

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


    글의 전제

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

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


    글의 내용

    - 사전 작업

    : 롱 모드 진입을 위해서는 당연히 머신이 64비트를 지원해야 한다. 즉, QEMU에서 64비트를 지원하는 머신(`qemu-system-x86_64`)을 사용해야 한다. 

     

    - 진입 절차

    : 진입 자체는 쉽다. 레지스터의 비트 몇 개만 건들면 되지만, 문제는 페이징이다. 일단 스펙을 봐야한다.

    :  IA-32e 모드 진입 전, 운영 체제는 반드시 페이징 활성화와 함께 보호 모드 상태에 있어야 한다. 그리고 PAE와 함께 4-레벨 페이지 구조를 사용해야 한다. 운영체제는 반드시 아래의 절차를 따라야 한다.

    1" 보호 모드로 시작해야 한다. 그리고 `MOV CR0` 명령어를 통해 페이징을 비활성화(CR0.PG = 0) 한다.
    2" CR4.PAE를 활성화한다.
    3" PML4 베이스 어드레스를 CR3에 로드한다.
    4" IA32_EFER.LME 비트를 활성화한다. IA32_EFER은 64비트에 추가된 컨트롤 레지스터다. 참고로, LME는 `Long Mode Enable` 의 약자다.
    5. 페이징을 다시 활성화한다(CR0.PG = 1). 이때 IA32_EFER.LMA 비트가 프로세서에 의해 자동으로 SET 된다(LMA는 Long Mode Active의 약자다). 그리고 페이징을 활성화하는 코드는 반드시 아이텐티티 맵핑이 된 영역에 있어야 한다.

    : 스펙에서는 그 아래 주의 사항으로 몇 가지 당부 말씀을 써놓았다.

     

    " 페이징 테이블은 반드시 4GB안에 있어야 한다. 위의 절차를 보면, CR3에 PML4 베이스 어드레스가 들어가는 시점은 아직 보호 모드 일 때다. 즉, 하위 32비트만 유지된다는 것이다. 이 시점에는 32비트만 의미가 있으니 4GB 안으로 페이지 테이블이 들어와 주어야 한다.

    " IA-32e 모드에서 `I32_EFER.LME, CR0.PG, CR4.PAE`의 값을 절대 함부로 바꾸면 안된다. #GP 를 보고 싶다면 건드려도 된다.

     

    : IA-32e 모드가 되더라도 여전히 시스템 디스크립터 테이블 레지스터들은 보호 모드 디스크립터들을 참조하고 있다. 32비트가 아닌 64비트 디스크립터들을 참조할 수 있도록 변경해야 한다(9.8.5.1 참고).

     

    : IA-32e 모드 전환 시에 절대 인터럽트 혹은 익셉션이 발생해서는 안된다. 그러므로, 모드 전환시에는 비활성화 해야한다. 우리가 잘 아는 `CLI` 명령어를 이용하면 된다. 그런데, NMI 까지도 비활성을 해줘야 한다. 위에서 외부 디바이스를 사용해서 NMI를 비활성화 해야한다고 했는데, NMI는 프로세서에 다이렉트로 연결되어 있다. 외부 디바이스(PIC, APIC)가 컨트롤 할 수 있는 핀이 아니다. 프로세서에서 직접 제어해야 한다. 모드 전환 완료 후, 역시나 시스템 디스크립터 레지스터들은 레거시 모드의 디스크립터들에 대한 참조를 유지하고 있으므로, 새로운 64비트 디스크립터를 참조하도록 설정해줘야 한다(9.8.5.2 참고).

     

    : 정리하면 모드가 변경되었으니, 당연히 시스템 자료 구조(GDTR, LDTR, IDTR, TR)도 변경해줘야 한다는 것이다. 

     

     

    - 롱 모드 여부

     

    : 롱 모드를 지원하는지 여부는 CPUID의 `확장 기능(0x8000_0001)`을 사용하면 된다. 그런나, 현재 대부분의 x86 CPU는 CPUID 명령어가 기본적으로 탑재되어 있겠지만, 이 명령어는 조금 늦게 추가된 명령어이기 때문에 오래된 CPU에서는 지원하지 않을 가능성도 있다. 그래서 CPUID 명령어를 지원하는지 부터 알아봐야 한다. 

     

    : CPUID 명령어가 지원된다면, 그 다음은 해당 CPU에서 CPUID `확장 기능`을 지원하는지 여부와 지원한다면 몇 개까지 지원하는지 알아봐야 한다. 롱 모드의 지원여부는 위에서 말한 것처럼 CPUID 확장 기능 0x8000_0001을 통해서 확인이 가능하다. 주의점은 리턴값이 EAX가 아닌, EDX와 ECX로 들어온다는 것이다.

    출처 - https://en.wikipedia.org/wiki/CPUID#EAX=80000001h:_Extended_Processor_Info_and_Feature_Bits

     

     

    - Canonical form addresses

    : `Canonical form addresses(CFA)` 란 AMD가 x86_64 표준을 만들면서, 가상 주소를 64비트까지 확장하는 것은 여러 효율성면에서 좋지 않다고 생각했다. 그래서 일단은 전체 주소중에 64비트 전체가 아닌 48비트만을 가상 주소로 사용하기로 했다. 거기에 더해, 최상위 16비트는 반드시 47번째 비트와 동일한 값을 가져야 한다는 규칙을 만들었다(`Sign-extended`). 그 규칙을 `CFA` 라고 한다. 만약, x86_64에서 가상 주소를 사용하는데 이 요건을 충족하지만, #GP 를 만나게 될 것이다.

     

    물리 주소와 가상 주소의 비트 수의 차이는 이 글을 참고하자. 

    출력 - https://en.wikipedia.org/wiki/X86-64

    https://en.wikipedia.org/wiki/X86-64

    : CFA는 규칙이 있다. `부호 확장`이라고 해서 가상 주소를 작성할 때, 마지막 16개 비트를 48번째 비트와 동일한 값을 가지도록 해야 한다. 이렇게 함으로써, 해당 영역은 사용되지 않는 영역임을 알리게 된다. 예를 들어보자.

    " FFFF8000 00001234 - 이 값은 유효한 값이다. 왜냐면, 47비트와 48 ~ 63번째 비트의 값이 모두 동일하기 때문이다.
    " 12348000 00001234 - 이 값은 유효하지 않은 값이다.
    " 00000000 00001234 - 이 값은 유효한 값이다. 왜냐면, 47비트와 48 ~ 63번째 비트의 값이 모두 동일하기 때문이다.

    : 앞에 상위 16비트를 빼고 보면 하위 48비트(XXXX7FFF_FFFFFFFF)와 상위 48비트(XXXX8000_00000000)는 연결된다. 왜 상위 48비트의 16비트는 1로 부호 확장을 하고, 하위 48비트에는 0으로 부호 확장을 했을까? 커널이 상주하는 영역이 계속 바뀔 여지가 있기 때문이다.

    : 최대 사용가능한 주소 영역이 48비트, 52비트, 56비트로 증가하는 메모리 구조를 보여준다. 위에서 `X`로 표시된 부분은 모두 `0`이라고 생각하자. 만약, 사용 불가능한 상위 MSB들을 모두 0으로 할 경우, 커널이 상주하는 영역이 계속 바뀌게 되는 것을 볼 수 있다. 계속 바뀌면 안좋을걸까? 현대의 커널은 가상 주소에서 메모리의 가장 뒷쪽 영역을 할당받게 된다. 커널이 가상 주소를 물리 주소로 매핑하는 방식은 특정 오프셋을 기준으로 계산을 한다. 예를 들어, 맨 앞 케이스의 경우 `물리 주소 = 가상 주소 - 0x0000_8FFF_8000_0000`가 될 것이다. 두 번째 케이스는 `물리 주소 = 가상 주소 - 0x0008_FFFF_8000_0000`일 것이다. 즉, 할당가능한 주소 영역이 증가할수로 오프셋 코드가 바뀌게 된다. 그런데 만약, `CFA` 방식으로 주소를 할당한다면 어떻게 될까?  

     

    : 사용 가능한 주소가 증가하더라도, 메모리 뒤쪽에 커널이 할당된 것을 볼 수 있다. 즉, 미래를 고려한 설계라고 보는 것이 맞다.

     

     

    : 위에 그림에서 보는 것처럼 실제 사용되는 영역은 녹색으로 표시된 하위 128TB와 상위 128TB가 된다. 그리고 중요한 건, 아래의 주소들은 물리 주소가 아닌 선형/가상 주소라는 것이다.

    " 00000000 00000000 - 00007FFF FFFFFFFF (하위 절반)
    " 00008000 00000000 - FFFF7FFF FFFFFFFF (유효하지 않음 영역)
    " FFFF8000 00000000 - FFFFFFFF FFFFFFFF(상위 절반)

     

    : 이 구조가 이상해 보일 수 있다. 만약, 64비트에서 상위 16비트를 무시하고 48비트만 사용하는 전략이었다면 어땠을까? 예전에 80286이 처음 나왔을 때는,  즉, 32비트가 완전히 처음 도입되던 시기에 CPU는 32비트인데, 여전히 주소 라인이 24비트 대역폭이었다. 그래서 상위 8비트는 무시해버렸다. 그런데 시간이 지나면서 남은 상위 8비트를 다른 용도로 사용하는 애플리케이션들이 등장하기 시작했다. 그리고 또 시간이 흘러서, 완전한 32비트 시스템이 도입되면서 그 애플리케이션들은 32비트 컴퓨터에서 동작하지 못하게 됬다(A20을 생각해보자). 만약, 64비트에서 상위 16비트를 벤더사 및 OEM에서 자기들이 원하는 대로 사용하게 두면, 나중에 64비트를 모두 사용할 때, 문제가 발생할 수 있다.

     

    : 만약, 자신의 시스템에 RAM 사이즈가 16GB 라면, 상위 128TB는 사용할 일이 없으 것이다. 왜냐면, 하위 128TB 만으로도 충부나기 때문이다. 현대의 운영체제들이 적용하는 상위 절반 전략을 사용하기 좋은 구조처럼 보인다. 그러나, 일반 컴퓨터는 여전히 8GB 램을 사용하고 있어서, 위의 구조를 사용하기는 아직 멀었다고 볼 수 있다.

     

    - 64비트 주소 매핑

    : 64비트가 도래해도 여전히 RAM은 개인용 컴퓨터에서 4GB ~ 8GB 정도이다. 높아도 16GB 정도가 최대다. 근데, 논리 주소 48비트가 물리 주소 52비트에 어떻게 매핑이 될까?

     

    - 페이징

    : x86_64 아키텍처의 64비트 주소 지정은 기본적으로 PAE가 활성화 된다는 것을 전제한다. 그래서 페이지 사이즈가 4KB, 2MB가 되고 롱 모드에서 지원하는 1GB 까지 해서 총 3개의 페이지 사이즈가 선택이 가능하다.

     

    : 32비트 x86 아키텍처에서는 PAE 모드를 기반으로 3-레벨 페이지 테이블을 사용했지만, 롱 모드에서는 4-레벨 페이지가 기본이다. PAE를 활성화하면 생겼던 `Page Directory Pointer Table`의 엔트리 개수가 32비트에서는 4개 였다면 롱 모드에서는 512개로 확장된다. 그리고 롱 모드에서는 `Page-Map Level 4 (PML4)` 가 하나 더 추가 되었다. PML4는 48비트로 구성된 엔트리를 512개를 갖는다.

     

    - 64비트 주소 지정의 한계

    : 가상 메모리에서 사용하는 페이지 사이즈가 너무 작은 것이 문제다. 64비트는 굉장히 큰 양이다. 예를 들어, 가상 메모리로 4KB 를 사용하면 2^52개를 사용할 수가 있다. 이 많은 양의 페이지들이 프로세스 컨텍스트 스위칭 시마다 이전 TLB 캐시들이 FLUSH 됬다가 새로운 프로세스의 페이지들이 TBL에 다시 캐시되면 어마어마한 오버헤드가 된다. 일반적으로, 이런 오버헤드를 줄이는 방법은 간단하게 페이지 크기를 늘려볼 수 있다. 즉, 4KB를 4MB로 늘릴 수 있다. 그렇면, 페이지 개수가 2^42개가 된다. 문제는 현재 대부분의 운영 체제가 기본적으로 페이지 사이즈를 4KB로 되어있어서 4MB로 바꿀 경우 호환이 안된다는 것이 문제다.

     

     

    - 48비트 가상 주소

    : 64비트가 되면서 페이지 테이블 엔트리 사이즈들이 4바이트에서 8바이트로 증가. 그러나, 테이블 사이즈는 그대로 유지됨. 그 결과, 페이지 테이블 엔트리 최대 개수 1024에서 512로 줄어듬. 그러므로, 각 테이블당 9비트만 할당해도 됨. 테이블을 늘리게 되면, 메타 데이터 사이즈가 증가할 수 있지만, 페이지 사이즈를 다양하게 가져갈 수 있음. 컨텍스트 시 발생하는 TBL 및 캐시 오버헤드와 운영체제를 제외한 애플리케이션에서 실제 요청하는 메모리 사이즈가 1GB는 절대 되지 않는 다는 점을 고려해서 32비트 대비 16비트 정도만 추가해서 페이지 사이즈의 유연성을 제공하면서, 나중을 대비해서 비트를 남겨놓음.

     

    - 52비트 가상 주소

    : 32비트 + PAE = 36비트, 가상 주소는 16비트가 증가. 물리 주소도 그에 맞춰 16비트 증가 = 52비트.

     

    - 48비트 가상 주소 == 52비트 물리 주소

    : 32비트에서 PAE를 활성화하면 36비트 물리 주소에 접근이 가능했다. 즉, 4GB에서 64GB에 접근이 가능해졌다. 롱 모드에서도 여전히 4KB를 사용한다. 48비트 라는 숫자는 `32 + 4 + 12 = 36(페이지 넘버) + 12(페이지 오프셋)` 으로 볼 수 있다.

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

    [x86] 멀티 부트  (0) 2023.06.27
    BOCHS  (0) 2023.06.24
    [리눅스] - printf & printk  (0) 2023.06.19
    참고 자료  (0) 2023.06.14
    재배치  (0) 2023.06.12
Designed by Tistory.