본문 바로가기

정글캠프-WIL/서브아이템

Pintos - 어떻게 부팅해서 운영체제가 되는가? [1]

Pintos는 어떻게 부팅해서 운영체제가 되는가?

start.S에서 main(), 그리고 thread_exit() 까지의 흐름 정리 [1]

 

Pintos를 보면서 가장 인상 깊었던 지점은 main()이 시작점이 아니라는 점이다.
보통 프로그램에서 익숙하게 생각하는 main()은 운영체제 입장에서 이미 CPU와 메모리, 실행 모드가 어느정도 준비된 뒤에 호출되는 함수다.

 

start.S -> main() -> thread_exit() 흐름을 따라가며, Pintos가 어떻게 "실제로 동작하는 운영체제"가 되는지 정리

 

main()보다 먼저 실행되는 start.S

start.S는 커널이 C 코드로 진입하기 전에 CPU를 준비시키는 역할을 맡는다.

이 시점의 핵심은 아직 "운영체제 기능"을 하는 것이 아니라, "운영체제가 실행될 수 있는 환경을 만드는 것"

 

1. 32비트 부트스트랩에서 시작

bootstrap은 먼저 CPU가 long mode를 지원하는지 확인하고, 이어 CR4에 PAE 비트를 켠다.
여기서 중요한 점은 x86-64로 올라가기 전에 CPU가 바로 64비트처럼 동작하는 것이 아니라, 필요한 제어 레지스터와 모드를 하나씩 켜 줘야 한다는 점이다.

즉, Pintos는 처음부터 64비트 커널이 아니라, 32비트 부트 환경에서 시작해서 64비트 커널 실행 환경으로 점프하는 구조를 가진다.
CR4(Control Register)
x86 계열 CPU의 제어 레지스트 중 하나
운영체제가 CPU의 동작 방식을 바꿀 때 사용하는 레지스터이며, 페이징 보조 기능, 보호 기능, 확장 모드 같은 옵션들을 비트 단위로 켜고 끄는 역할을 함
PAE(Physical Address Extension)
원래 32비트 환경에서는 한 번에 다룰 수 있는 물리 메모리 주소 공간에 한계가 있었는데, PAE를 켜면 CPU가 더 확장된 형태의 페이지 테이블 구조를 사용할 수 있게 됨.
Pintos의 start.S에서 CR4에 PAE 비트를 켜는 이유는 단순히 메모리를 더 많이 쓰기 위해서만은 아니고 x86-64의 long mode로 진입하려면 CPU가 PAE 기반 페이징 구조를 사용할 준비가 되어 있어야 하기 때문이다.
즉, 이 설정은 선택적인 최적화가 아니라, 이후 64비트 페이지 테이블과 long mode를 사용하기 위한 필수 준비물이다.

2. 임시 페이지 테이블을 만든다.

그 다음 start.S는 boot_pml4e, boot_pdpt, boot_pde를 이용해 임시 페이지 테이블을 만든다.
그 작업은 아직 정식 메모리 관리가 시작되기 전이므로, 커널이 최소한의 주소 변환만 가능하도록 하는 부팅용 매핑이다.

이 단계가 필요한 이유는 단순
64비트 long mode와 현대적인 커널 실행은 페이징 없이는 제대로 올라갈 수 없기 때문
즉, C 코드로 진입하기 전 CPU가 "가상 주소를 해석할 수 있는 상태"가 되어 있어야 함

 

boot_pml4e
최상위 페이지 테이블인 PML4의 엔트리들을 담는 영역

boot_pdpt
그 아래 단계인 PDPT(Page Directory Pointer Table)

boot_pde
실제로 큰 단위의 메모리 영역을 가리키는 페이지 디렉터리 엔트리들

즉, CPU는 가상 주소를 해석할 때
PML4 -> PDPT -> PD -> 실제 물리 메모리
순서로 내려가며 주소를 찾음

부팅 시점에는 아직 정식 커널 메모리 관리가 완성되지 않았기 때문에, Pintos는 이 'boot_*' 테이블들을 이용해 커널이 최소한 실행될 수 있을 정도의 주소 매핑만 먼저 구성
쉽게 말하면, 이들은 "정식 메모리 관리자"가 준비되기 전까지 잠깐 쓰는 "부팅용 주소 변환 지도" 
특히 start.S 에서는 이 테이블들을 0으로 초기화하고, 엔트리를 채운 뒤,
그 시작 주소를 CR3에 넣어 CPU가 실제로 이 페이지 테이블을 사용하게 만듬
즉, 단순한 배열이 아니라 "CPU가 직접 참조하는 주소 변환 구조체" 인 셈

3. CR3, EFER, CR0를 설정해 long mode로 진입

임시 페이지 테이블을 준비한 뒤에는 CR3에 페이지 테이블의 시작 주소를 넣고, EFER의 LME 비트를 껴서 long mode 진입 조건을 맞춤.
이후 CR0의 paging 비트까지 켜면서 CPU는 64비트 주소 공간을 사용할 준비를 끝냄.
CR0(Control Register)
CPU의 가장 기본적인 동작 모드를 제어하는 레지스터
PE (Protection Enable) : 보호 모드 사용 여부를 킴
PG (Paging) : 페이징 기능을 킴
WP (Write Protect) : 커널도 읽기 전용 페이지에 함부로 쓰지 못하게 하는 보호와 관련 있음

CR1(Control Register)
사용하지 않는 예약 레지스터
보통 x86 설명에서 CR0, CR2, CR3, CR4가 주로 등장하고, CR1은 비어 있다고 보면 됨

CR2(Control Register)
페이지 폴트가 발생했을 때, 어떤 주소 접근이 문제였는지 저장하는 레지스터
예를 들어 어떤 코드가 잘못된 가상 주소를 읽거나 썼다면, CPU는 page fault 예외를 발생시키고, 그 문제의 주소를 CR2에 넣어줌
그래서 운영체제는 page fault handler에서 CR2를 읽어 어느 주소 때문에 fault가 났는가?를 확인

CR3(Control Register)
현재 CPU가 사용할 페이지 테이블의 시작 주소를 담고 있는 레지스터
x86-64에서는 보통 최상위 페이지 테이블인 PML4의 물리 주소가 들어감
즉, CPU는 주소 변환이 필요할 때 CR3를 보고 "이번에는 이 페이지 테이블을 기준으로 가상 주소를 해석해야겠구나" 라고 판단 (들어가는 것은 물리 주소)
start.S에서 boot_pml4e를 만든 다음 CR3에 넣는다는 말은, CPU에게 이제부터 이 페이지 테이블을 보고 주소 변환을 해라 라고 알려주는 것과 같음
또한 운영체제가 프로세스를 바꿀 때 CR3를 다른 값으로 바꾸면, CPU는 완전히 다른 주소 공간을 바라보게 됨
즉, CR3는 현재 주소 공간의 기준점

CR4(Control Register)
CR4는 CR0보다 더 확장된 기능들을 켜는 레지스터
PAE (Physical Address Extension) : 확장된 페이지 테이블 구조를 사용할 수 있게 함
PSE (Page Size Extension) : 페이지 테이블에서 4kb 대신 4mb의 대형 페이지를 사용할 수 있게 함
PGE (Page Global Extension) : TLB flush에도 유지되는 전역 페이지를 활성화하는 기능
EFER (Extended Feature Enable Register)
CPU의 확장 기능을 켜는 MSR(Model Specific Register) 중 하나
LME (Long Mode Enable) 비트
이제 64비트 long mode 진입을 허용하는 스위치

4. GDT를 로드하고 64비트 코드로 점프

`lgdt`로 64비트용 GDT를 로드한 뒤, `lret`으로 코드 세그먼트를 바꾸며 `entry_64`로 넘어감

CPU가 "이제부터는 64비트 코드 문맥으로 실행 하겠다"는 사실을 명확히 전환하는 시점

lgdt : (Load Global Descriptor Table Register)
CPU의 `GDTR` 레지스터에 `GDT(Global Descriptor Table)의 위치와 크기를 로드하는 명령어
GDT는 세그먼트 정보를 담고 있는 표
현대 x86-64에서는 예전처럼 세그먼트를 적극적으로 쓰진 않지만, CPU가 코드 세그먼트와 데이터 세그먼트의 기본 정보를 알기 위해서는 여전히 필요
Pintos에서는 long mode로 넘어가기 전 64비트용 GDT를 준비하고 `lgdt`로 CPU에 등록

lret : far return 계열 명령어
일반적인 ret이 단순히 현재 함수에서 복귀 주소만 꺼내 오는 거라면, lret은 스택에서 주소뿐 아니라 코드 세그먼트(CS)까지 함께 꺼내 실행 흐름을 바꿈

 

5. 커널 스택을 잡고 main()을 호출

`entry_64`에 도착하면 `rsp`를 커널 스택 위치로 맞춘 뒤, 비로소 `main()`을 호출

여기서 부터 CPU를 준비하는 코드가 아니라 실제 운영체제를 초기화 하는 코드 init.c로 넘어간다.

rsp (register stack pointer)
스택 포인터 레지스터
현재 스택의 맨 위(top)를 가리키는 주소가 들어 있음
x86-64에서 스택은 보통 "높은 주소에서 낮은 주소 방향으로 삽입"

- start.S에서 main(), 그리고 thread_exit() 까지의 흐름 정리 [2]

https://gyumingomin.tistory.com/86

 

Pintos - 어떻게 부팅해서 운영체제가 되는가? [2]

https://gyumingomin.tistory.com/85 Pintos - 어떻게 부팅해서 운영체제가 되는가? [1]Pintos는 어떻게 부팅해서 운영체제가 되는가?start.S에서 main(), 그리고 thread_exit() 까지의 흐름 정리 [1] Pintos를 보면서 가장

gyumingomin.tistory.com