-
[xv6] entry프로젝트/운영체제 만들기 2023. 8. 1. 20:08
글의 참고
- https://github.com/jserv/xv6-x86_64/tree/64bit
글의 전제
- 밑줄로 작성된 글은 강조 표시를 의미한다.
- 그림 출처는 항시 그림 아래에 표시했다.
글의 내용
: `mboot_entry`는 `xv6` 64비트 모드에 진입 시, 가장 먼저 시작되는 엔트리 포인트다. `mboot_entry`에 진입하면 제일 먼저하는 작업은 64비트 페이징 작업을 진행한다. 아래의 내용을 이해하기 위해서는 `페이징`과 `Higher Half Kernel`에 대한 내용을 알고 있어야 한다.
: `mboot_entry`의 처음 시작은 물리 주소 0x1000 ~ 0x6000까지 0으로 초기화한다.
// entry64.S ... mboot_entry: # zero 4 pages for our bootstrap page tables xor %eax, %eax mov $0x1000, %edi mov $0x5000, %ecx rep stosb # P4ML[0] -> 0x2000 (PDPT-A) mov $(0x2000 | 3), %eax mov %eax, 0x1000 # P4ML[511] -> 0x3000 (PDPT-B) mov $(0x3000 | 3), %eax mov %eax, 0x1FF8 # PDPT-A[0] -> 0x4000 (PD) mov $(0x4000 | 3), %eax mov %eax, 0x2000 # PDPT-B[510] -> 0x4000 (PD) mov $(0x4000 | 3), %eax mov %eax, 0x3FF0 # PD[0..511] -> 0..1022MB mov $0x83, %eax mov $0x4000, %ebx mov $512, %ecx ptbl_loop: mov %eax, (%ebx) add $0x200000, %eax add $0x8, %ebx dec %ecx jnz ptbl_loop ...
: 64비트 `xv6` 초기 페이징은 아래와 같은 구조를 뛴다. 이 구조를 알기 위해서는 64비트 `xv6` 커널이 가상 주소 `0xFFFF_FFFF_8000_0000`에 로드된다는 것을 전제로 해야한다. 왜, 저 주소일까? 호환성을 위해서다. 0xFFFF_FFFF_8000_0000은 메모리의 끝지점을 의미한다. 이 영역에 위치하게 되면, 실제 할당 가능한 물리 주소가 64비트로 확장되더라도, 코드가 바뀔 위험이 사라진다.
: 그렇면, 끝지점이 아닌, 맨 앞 지점은 안되나? 예를 들어, `0x0000_0000_0000_0000`도 물리 주소가 확장되더라도 영향을 받지 않는 지점이지 않나?
: `xv6`는 커널을 가상 주소 `0xFFFF_FFFF_8000_0000`에 로딩할 것이므로, `0xFFFF_FFFF_8000_0000 ~ 0xFFFF_FFFF_FFFF_FFFF` 영역에 가상 주소를 할당해야 한다. 즉, 뒤쪽 2GB만 사용한다는 것이다. x86_64에서 PML4의 엔트리 하나 당 512GB를 관리할 수 있다. 그리고, PDTP 엔트리 하나 당, 1GB를 관리할 수 있다.
: `xv6`는 0x1000에 PML4 테이블을 하나 할당한다. 그리고, 제일 처음과 제일 마지막 엔트리를 `유효하다`고 설정한다. 왜? 페이징이 처음활성화 되는 시점에 현재 실행되는 주소가 1MB 이하일 것이기 때문이다. 그래서 제일 앞에 PML4 엔트리를 `아이텐티티 매핑`해야 한다. `x86_64`는 가상 주소 `0xFFFF_FFFF_8000_0000`가 입력되면, 먼저 상위 16비트는 버린다. 그러면, `FFFF_8000_0000`가 된다. 먼저, CR3에 들어있는 PML4 테이블을 가져온다. 그리고, 여기서 상위 9비트가 PML4 엔트리를 찾는데 사용된다. 즉, `0b111111111`가 PML4 테이블 엔트리 인덱스 번호가 된다. 그래서, 가상 주소 `0xFFFF_FFFF_8000_0000 ~ 0xFFFF_FFFF_FFFF_FFFF` 에 접근하기 위해서는 511번째 PML4 엔트리를 참조하면 된다(`mov %eax, 0x1FF8`).
0xFFFF_8000_0000 = 0b1111_1111_1111_1111_1000_0000_0000_0000
: 여기서 끝이 아니다. 위 그림에서 볼 수 있다시피, PDPT 엔트리도 찾아야 한다. 510번째 PDPT 엔트리를 참조하면 된다(`mov %eax, 0x3FF0`). 이 과정은 `PDPT-B`의 과정이다. `PDPT-A`는 왜 사용할까? 페이징이 활성화된 직후는, 아이텐티티 매핑으로 `#PF`가 발생하지 않도록 해야 한다. 그것 때문에, `PDPT-A` 를 임시로 할당한다. 64비트 상위 주소로 점프하기 전에는 `PDPT-A` 를 사용한다. 점프 이후에는 `PDPT-B`를 사용한다.
: 그리고 최종적으로 PD 테이블을 할당한다. 모든 PD 엔트리 플래그를 `0x83`으로 설정하고 있다. 여기서 중요한 점은 `0x80` 이다. 7번째 비트는 PSE로 사이즈를 의미한다. 이 값이 1이면, PD 엔트리 하나 당 2MB를 차지하게 된다. 아래에서 `eax`는 `페이지 번호 + 플래그`가 설정된다. 그리고 PD 테이블 엔트리 인덱스를 `ebx`를 통해 할당한다. `ecx`는 총 512개의 PD 엔트리를 만들기 위해 사용된다.
# PD[0..511] -> 0..1022MB mov $0x83, %eax mov $0x4000, %ebx mov $512, %ecx ptbl_loop: mov %eax, (%ebx) add $0x200000, %eax add $0x8, %ebx dec %ecx jnz ptbl_loop ...
: 아래는 각 `eax`, `ebx`, `ecx`가 루프를 돌 때마다 어떻게 증가하는지를 보여준다.
eax ebx ecx 0 0x83 0x4000 512 1 0x200083 0x4008 511 2 0x400083 0x4010 510 3 0x600083 0x4018 509 ... ... ... ... 511 0x3FE083 0x4FF8 1 : 이렇게 64비트 페이지 테이블을 만드는 과정은 끝났다. 이제 실제 페이지 테이브를 등록하고 64비트 모드로 전환하는 코드를 보자.
: `xv6`는 멀티 프로세서 기반으로 동작하는 운영체제다. `xv6`는 BSP와 AP가 시작하는 부트 코드가 다르다. 예를 들어, BSP는 32비트 부트 소스가 `entry.S` 파일인데, AP는 `entryother.S` 파일이다. 왜 나눴을까? BSP는 부트 시점에 시스템 관련 자료 구조 뿐만 아니라 하드웨어들을 모두 초기화한다. 그런데, 이 과정을 AP가 또 하는 것은 문제를 일으킬 수 있고, 또 시간 낭비인 부분도 있기 때문이다. 그래서, `xv6`는 64비트 모드에 진입하면, BSP면 ebx를 `0`으로 AP면 `1`로 설정해서 서로 다른 루틴을 타도록 한다.
// entry64.S ... # Clear ebx for initial processor boot. # When secondary processors boot, they'll call through # entry32mp (from entryother), but with a nonzero ebx. # We'll reuse these bootstrap pagetables and GDT. xor %ebx, %ebx .global entry32mp entry32mp: # CR3 -> 0x1000 (P4ML) mov $0x1000, %eax mov %eax, %cr3 lgdt (gdtr64 - mboot_header + mboot_load_addr) # Enable PAE - CR4.PAE=1 mov %cr4, %eax bts $5, %eax mov %eax, %cr4 # enable long mode - EFER.LME=1 mov $0xc0000080, %ecx rdmsr bts $8, %eax wrmsr # enable paging mov %cr0, %eax bts $31, %eax mov %eax, %cr0 # shift to 64bit segment ljmp $8,$(entry64low - mboot_header + mboot_load_addr) ...
: 그 다음 `CR3` 레지스터에 0x1000을 넣는 것을 볼 수 있다. 이 의미는 위에서 작성한 `PML4`를 `xv6` 64비트 커널의 페이지 테이블로 사용하겠다는 의미다. 그리고, 64비트 모드로 진입하기 위한 과저들을(PAE, LME, PAGING) 거친다. 이 내용은 이 글을 참고하자.
: 마지막에 `ljmp $8,$(entry64low - mboot_header + mboot_load_addr)` 의미가 뭘까? `ljmp`는 ASM 문법으로 `Long jump`를 의미한다. 첫 번째 오퍼랜드는 CS 레지스터의 값으로 쓰이고, 두 번째 오퍼랜드는 점프할 주소가 된다. 근데, 왜 `ljmp` 명령어의 첫 번째 오퍼랜드로 `8`이 왔을까? 보호 모드로 진입하면, 세그먼트 레지스터라는 개념은 `HIDDEN` 된다. 예를 들어, 16비트에서는 `mov $8, %ds` 코드를 적용하면, DS 데이터 세그먼트(`%ds`)에 8이 실제로 대입됬다. 그러나, 보호 모드로 진입해서 `mov $8, %ds` 의미는 DS 데이터 세그먼트가 아닌, DS 세그먼트 셀렉터에 8을 넣게되는 것이다. 즉, `GDT에 있는 1번째 세그먼트 디스크립터의 Base를 DS 세그먼트 레지스터에 할당해줘`라는 뜻이 된다. 그래서 `gdt64_begin`의 8바이트 위치에 64비트용 CS 세그먼트 디스크립터가 있는 것을 볼 수 있다.
// entry64.S ... # shift to 64bit segment ljmp $8,$(entry64low - mboot_header + mboot_load_addr) .align 16 gdtr64: .word gdt64_end - gdt64_begin - 1; .quad gdt64_begin - mboot_header + mboot_load_addr .align 16 gdt64_begin: .long 0x00000000 # 0: null desc .long 0x00000000 .long 0x00000000 # 1: Code, R/X, Nonconforming .long 0x00209800 .long 0x00000000 # 2: Data, R/W, Expand Down .long 0x00009000 gdt64_end: .align 16 .code64 entry64low: movq $entry64high, %rax jmp *%rax .global _start _start: entry64high: # ensure data segment registers are sane xor %rax, %rax ...
: 왜 `entry64low - mboot_header + mboot_load_addr`로 점프할까? `FAR JUMP`의 두 번째 오퍼랜드는 `절대 주소`가 온다. 그리고 `물리 주소`여야 한다. 참고로, `x86`에서 절대 주소는 세그먼트 레지스터에 영향을 받지 않는 주소를 의미한다. 아래의 내용을 보자.
ffffffff80100000 T mboot_header
0000000000100000 A mboot_load_addr
ffffffff801000e0 t entry64low: 빌드를 통해 나온 값들을 위와 같다. 결국, `0x1000e0`이 `entry64low`의 램에 로딩되는 물리 주소가 된다.
: 왜 `entry64log`를 거치서, `entry64high`로 점프하는걸까? 한 번에 `entry64high`로 점프하면 안될까? `ljmp $8, $(entry64low - mboot_header + mboot_load_addr)` 명령어를 실행하는 시점에 CPU가 64비트로 동작을 한다. 그런데, 문제는 `어셈블러`에 있다. 어셈블러는 `.code32`를 보면, 그 아래 코드를 32비트 기준으로 빌드한다. 32비트를 초과하는 값들은 모두 0으로 처리한다. 예를 들어, `.code32` 선언된 이후에, `mov %0xF00000000, %rax` 같은 명령어를 사용하면 `%rax`에는 0이 들어간다. 왜냐면, 어셈블러가 32비트 기준으로 값들을 모두 조정하기 때문이다. 그리고, 심볼들의 재비치 주소도 32비트를 기준으로 할당한다.
: 그런데, `entry64high` 심볼의 주소가 몇일까?
'프로젝트 > 운영체제 만들기' 카테고리의 다른 글
부트 로더 (0) 2023.08.03 [빌드] Makefile (0) 2023.08.03 [xv6] mkfs (0) 2023.07.30 [xv6] Boot-loader (0) 2023.07.29 [xv6] - fork (0) 2023.07.27