- 512바이트 부트 섹터에서 시작해 x86_64 CPU를 16비트 real mode에서 64비트 long mode로 올리는 최소 부트로더 흐름을 단계별로 구성함
- BIOS 인터럽트로 출력부터 확인한 뒤
nasm,ld,objcopy, QEMU를 연결해 부트 이미지가 실제로 실행되는지 검증함 - 부트 섹터의 용량 한계 때문에 stage 1/stage 2로 나누고, protected mode 진입 전에 BIOS
int 0x13으로 다음 코드를 디스크에서 읽어옴 - 32비트 protected mode부터는 BIOS 루틴을 쓸 수 없어 GDT, flat segmentation, VGA 버퍼 직접 출력 같은 하드웨어 초기화가 필요함
- 64비트 long mode 진입은 페이지 테이블, PAE,
EFER.LME,cr0.PG, 64비트용 GDT를 모두 맞춰야 하며, 이후 freestanding C 코드를 커널처럼 호출할 수 있음
BIOS에서 시작되는 16비트 환경
- x86 CPU는 reset 이후 real mode에 있으며, 기본 피연산자 크기는 16비트임
- real mode는 segmentation으로 20비트 주소 공간을 만들고, 최대 1MB 메모리를 다룰 수 있음
- BIOS가 처음 실행하는 코드는 디스크의 부트 섹터에 들어 있음
- BIOS는 첫 섹터가
0xaa55매직 넘버로 끝나는 디스크를 찾음 - 해당 섹터를 메모리 주소
0x7c00에 로드함
- BIOS는 첫 섹터가
- BIOS가 넘겨주는 공간은 512바이트뿐이어서, 이 코드는 나머지 부트로더를 불러오는 bootstrap 역할에 집중함
- BIOS 루틴은 real mode에 머무는 동안만 사용할 수 있음
준비물과 빌드 환경
- 따라 하려면 Intel 64 and IA-32 Architectures Software Developer’s Manual, assembler, QEMU가 필요함
- 예제 assembler는 nasm이며, x86 assembly와 nasm 문법을 알고 있다는 전제로 진행됨
- x86_64 CPU가 없어도 QEMU에서 x86 CPU를 에뮬레이션해 실행할 수 있음
- 32비트 모드까지의 기초 자료로 Writing a Simple Operating System — from Scratch를 참고함
부트 섹터 만들고 실행 확인하기
- 첫 부트 섹터는 BIOS 루틴으로
"Hello, world!"를 출력한 뒤hlt와 루프로 멈춤 - 문자열 출력에는 BIOS video services 호출인
int 0x10과ah = 0x0e를 사용함 Makefile은 nasm으로 object를 만들고, linker script로 링크한 뒤objcopy -O binary로 raw 부트 이미지를 생성함make boot는 QEMU로 이미지를 실행함qemu-system-x86_64 -no-reboot -drive file=$<,format=raw,index=0,media=disk
- linker script는 부트 섹터가
0x7c00기준으로 배치되도록 설정함MEMORY의boot_sectororigin은0x7c00, length는512.bootsign섹션은0x7c00 + 510위치에0x55,0xaa를 추가함
- 부트 섹터 assembly 안에서 offset과 magic number를 직접 처리할 수도 있지만, 여기서는 linker script가 그 역할을 맡음
stage 1이 stage 2를 디스크에서 읽어오기
- stage 1은 BIOS가 로드하는 부트 섹터 코드이고, 목표는 stage 2를 메모리로 불러오는 것임
- stage 2는 16비트 real mode에서 32비트 protected mode로 넘어가는 코드를 담음
- protected mode에 들어간 뒤에는 BIOS 루틴을 사용할 수 없으므로, 디스크 섹터 읽기는 전환 전에 끝내야 함
- 디스크 접근에는 BIOS disk services
int 0x13을 사용함ah = 0x42는 BIOS extended read 기능dl = 0x80은 드라이브 번호- disk address packet에는 읽을 섹터 수, 대상 주소, 시작 섹터가 들어감
- 예제는
READ_SECTORS_NUM equ 64로 64개 섹터를 읽음- 부트 섹터는 sector 0이므로 stage 2는 sector 1부터 읽음
- 대상 주소는
BOOT_LOAD_ADDR + SECTOR_SIZE, 즉0x7c00 + 512
- 코드에는 요청한 것보다 적은 섹터가 읽히는 경우를 받아들이는 임시 처리가 남아 있음
- stage 2는 처음에 real mode용
print_string을 복사해"Hello from stage 2"를 출력하고, stage 1에서 점프가 제대로 이어졌는지 확인함
32비트 protected mode로 전환하기
- protected mode로 들어가려면 먼저 Global Descriptor Table(GDT) 을 정의해야 함
- protected mode에서는 기본적으로 segmentation이 메모리 보호에 쓰임
- 64비트 long mode에서는 paging이 필요하지만, 그 전 단계인 protected mode 진입에는 segmentation 설정이 먼저 필요함
- 예제 GDT는 Intel manual의 flat model을 따름
- code segment와 data segment를 둠
- 두 segment는 전체 linear address space에 매핑됨
- long mode로 가기 위한 중간 단계라 가장 단순한 모델을 사용함
- GDT는 메모리에 놓인 연속 구조체임
- 시작에는 invalid translation을 잡기 위한 null descriptor가 있음
- 그 뒤에 code segment descriptor와 data segment descriptor가 이어짐
- 전환은 다음 순서로 진행됨
cli로 interrupt를 비활성화함lgdt [gdt32_pseudo_descriptor]로 GDTR에 GDT 주소와 길이를 로드함cr0.PE, 즉cr0의 bit 0을 설정해 protected mode를 켬- far jump로 instruction pipeline을 비우고
cs를 새 code segment로 갱신함
- protected mode 진입 후 기존 segment 값은 더 이상 의미가 없으므로
ds,ss,es,fs,gs를 새 data segment selector로 설정함 - 모든 설정 뒤 interrupt를 다시 켜려면 추가 작업이 필요함
BIOS 없이 화면에 출력하기
- protected mode에서는 BIOS 루틴을 더 이상 호출할 수 없음
- 문자열 출력은 VGA buffer에 직접 쓰는 방식으로 바뀜
print_string32는0xb8000주소에 문자와 색상 바이트를 기록함- 색상 값은
0xf - 각 문자 셀은 2바이트를 사용함
- 색상 값은
- 이 출력 함수는 매우 단순해서 메시지는 항상 화면 왼쪽 위에 표시됨
long mode 진입을 위한 페이지 테이블
- Intel 문서의 IA-32e mode는 AMD64 manual의 long mode에 해당함
- long mode로 전환하려면 CPU가 protected mode에 있어야 하고, paging도 활성화되어야 함
- paging 개념은 Introduction to Paging과 OSTEP를 참고함
- PAE가 활성화된 long mode에서는 4단계 페이지 테이블을 사용함
build_page_table은 지정된 주소에서 4단계 페이지 테이블을 생성함- page 크기는
0x1000 - 각 page table 크기는
0x1000 - entry 수는
512 - 먼저 네 개의 table을 0으로 초기화해 모든 entry를 not present 상태로 둠
- PML4 → PDP → PD → page table의 첫 entry들을 연결함
- 가장 낮은 page table layer에서 512개 entry를 설정함
- page 크기는
64비트용 GDT와 long mode 전환 순서
- paging이 virtual address space와 권한 관리를 맡지만, long mode에서도 GDT는 필요함
- 64비트용 GDT도 flat model을 따르며, protected mode용 GDT와 거의 같음
- 차이는 long mode 관련 bit 설정에 있음
- code segment의 64-bit code segment flag를 설정함
- 이 flag가 설정되면 default operation size bit는 0이어야 함
- long mode 전환은 다음 흐름으로 이어짐
0x1000주소에 4단계 page table을 생성함cr3에 PML4 table 주소를 넣음cr4의 bit 5를 설정해 PAE를 활성화함0xc0000080MSR을 읽고EFER.LME, 즉 bit 8을 설정함cr0의 bit 31인PGflag를 설정해 paging을 활성화함- 64비트용 GDT를
lgdt로 로드함 - 64비트 code segment로 far jump해 64비트 모드에 들어감
- paging을 켠 직후에는 IA-32e compatibility mode 상태이며, 64비트 segment flag가 설정된 GDT로 점프하면서 64비트 모드로 전환됨
- 성공 확인 메시지는 VGA buffer를 통해 화면 왼쪽 위에 표시됨
freestanding C 코드 호출하기
- 64비트 long mode까지 진입하면 freestanding C 코드를 호출할 수 있음
kernel.c는 VGA buffer0xb8000을 지우고"Hello from C"를 출력함- assembly 쪽
start_long_mode는 64비트용 문자열 출력 뒤extern _start_kernel을 선언하고_start_kernel을 호출함 - linker script는 메모리 영역을 세 부분으로 나눔
boot_sector:0x7c00, length512stage2:0x7e00, length512kernel:0x8000, length0x10000
.text,.data,.rodata,.bss섹션은 kernel 영역에 배치됨Makefile은 assembly와 C를 모두 빌드하도록 바뀜- C compiler는
gcc - 주요 CFLAGS는
-std=c99 -ffreestanding -m64 -mno-red-zone -fno-builtin -nostdinc -Wall -Wextra
- C compiler는
- 전체 예제 코드는 다운로드 링크로 제공됨