1. 개념 요약
부팅(boot)은 전원을 켠 뒤 펌웨어(BIOS/UEFI)가 부트로더를 실행하고 부트로더가 커널을 메모리에 올린 뒤 커널 진입점으로 제어를 넘기는 과정이다.
go-os에서는 QEMU 위에서 GRUB과 Multiboot2를 사용해 커널을 부팅한다. GRUB은 커널 파일을 읽고 메모리에 올린 뒤 32비트 보호 모드 상태에서 커널 진입점으로 점프한다. 이 시점부터 직접 작성한 커널 코드가 실행된다.
커널이 GRUB에 인식되고 의도한 위치에서 실행되려면 커널의 진입점, 로드 주소, CPU 실행 모드, Multiboot2 헤더 설정이 맞아야 한다. 이 값들이 맞지 않으면 커널 코드가 맞게 작성되어 있어도 GRUB이 커널을 인식하지 못하거나 의도한 위치에서 실행되지 않을 수 있다.
2. 왜 필요한가
커널이 실행되려면 먼저 디스크나 ISO 이미지 안에 있는 커널 파일을 찾아야 한다. 그다음 커널을 메모리에 올리고 CPU가 실행할 진입점으로 점프해야 한다. 이 과정에서는 파일시스템 해석, 메모리 배치, CPU 모드 전환, 부팅 정보 전달 같은 작업이 필요하다.
이 작업을 모두 직접 작성하면 커널 본문을 시작하기 전에 부트로더부터 만들어야 한다. go-os의 초기 단계에서는 부트로더 자체를 만드는 것이 목표가 아니므로 GRUB을 사용한다. GRUB을 사용하면 파일시스템 처리와 커널 로드, Multiboot2 형식의 핸드오프를 맡길 수 있다.
이 문서에서 GRUB이 맡는 범위는 다음과 같다.
| 항목 | 설명 |
|---|---|
| 커널 파일 탐색 | ISO 이미지 안에서 kernel.elf를 찾는다. |
| 커널 형식 확인 | Multiboot2 헤더를 보고 부팅 가능한 커널인지 확인한다. |
| 메모리 로드 | 커널을 메모리에 올린다. |
| 부팅 정보 전달 | 메모리맵 같은 부팅 정보를 커널에 전달한다. |
| 커널 진입 | 커널의 진입점으로 점프한다. |
go-os에서는 GRUB이 커널로 제어를 넘긴 이후부터 직접 작성한 코드의 범위로 본다. 64비트 Long mode 전환, Go 코드 연결, interrupt 설정은 GRUB 이후의 커널 초기화 단계에서 처리한다.
3. 등장 배경
x86 계열 CPU는 전원을 켠 직후 바로 64비트 코드를 실행하지 않는다. 하위호환성을 유지해야 하기 때문에 초기 실행 상태는 과거 x86 환경과 맞춰져 있다. 그래서 커널은 바로 64비트 코드로 시작할 수 없고 단계적으로 실행 환경을 준비해야 한다.
부팅 초기에 실제로 누가 어떤 단계를 처리하는지는 펌웨어와 부트로더 구성에 따라 달라진다. BIOS 기반 부팅에서는 펌웨어가 디스크의 초기 부트 코드를 읽어 실행하고 그 이후 부트로더가 커널을 로드한다. UEFI 기반 부팅에서는 펌웨어가 더 많은 기능을 제공하고 UEFI 애플리케이션 형태의 부트로더를 실행할 수 있다.
go-os의 현재 단계에서는 QEMU에서 GRUB과 Multiboot2를 사용하는 흐름을 기준으로 한다. 따라서 직접 작성하는 커널 코드는 GRUB이 넘겨준 32비트 보호 모드 상태에서 시작한다고 본다. 16비트 초기 부팅 코드는 직접 작성하지 않는다.
4. 필요한 전제 조건
커널이 GRUB에 인식되고 의도한 위치에서 실행되려면 다음 조건이 맞아야 한다.
| 전제 조건 | 설명 |
|---|---|
| Multiboot2 헤더 | 커널 파일 앞쪽에 Multiboot2 헤더가 있어야 한다. GRUB은 이 헤더를 보고 커널을 Multiboot2 커널로 인식한다. |
| 8바이트 정렬 | Multiboot2 헤더는 8바이트 정렬 조건을 만족해야 한다. |
| 앞 32KB 안의 헤더 | GRUB이 찾을 수 있도록 Multiboot2 헤더가 커널 파일 앞 32KB 안에 있어야 한다. |
| 올바른 매직넘버 | Multiboot2 매직넘버 0xE85250D6이 정확히 들어가야 한다. |
| 올바른 checksum | magic, architecture, header length, checksum을 32비트로 더했을 때 0이 되도록 맞춰야 한다. |
grub.cfg의 multiboot2 지시 | GRUB 설정에서 해당 커널을 Multiboot2 방식으로 부팅하라는 지시가 있어야 한다. |
| 커널 로드 주소 | linker script에서 커널이 로드될 주소를 명시해야 한다. |
| 32비트 핸드오프 수용 | GRUB이 32비트 보호 모드로 넘기므로 _start는 32비트 코드로 시작해야 한다. |
이 조건 중 하나라도 맞지 않으면 GRUB이 커널을 인식하지 못하거나 커널 진입점으로 제어를 넘기지 못할 수 있다. 예를 들어 Multiboot2 매직넘버가 틀리면 GRUB은 커널을 Multiboot2 커널로 인식할 수 없다. 헤더 위치가 너무 뒤에 있거나 정렬이 맞지 않는 경우에도 GRUB이 헤더를 찾지 못할 수 있다.
5. 핵심 구성 요소
부팅 흐름을 이해하려면 펌웨어, 부트로더, 커널, 커널 파일 포맷, CPU 실행 모드, 진입점, 화면 출력 방식을 나누어 보아야 한다.
5.1 펌웨어
펌웨어는 전원을 켠 뒤 CPU가 처음 실행하는 코드다. BIOS와 UEFI가 여기에 해당한다.
BIOS(Basic Input/Output System)는 오래된 방식의 펌웨어다. UEFI(Unified Extensible Firmware Interface)는 BIOS 이후에 널리 쓰이는 펌웨어 인터페이스다. 두 방식은 부팅 절차와 제공하는 기능이 다르지만 이 문서에서는 펌웨어가 부트로더를 실행하는 주체라는 점만 기준으로 본다.
5.2 부트로더
부트로더는 커널을 메모리에 올리고 커널 진입점으로 제어를 넘기는 프로그램이다. go-os에서는 GRUB(GRand Unified Bootloader)을 사용한다.
GRUB은 여러 운영체제와 커널 형식을 다룰 수 있는 범용 부트로더다. 그래서 커널 파일 안에 Multiboot2 헤더가 있어도 grub.cfg에서 multiboot2 지시를 함께 적어야 한다. 헤더는 커널이 자신을 Multiboot2 커널로 알리는 정보이고 grub.cfg는 GRUB에게 그 파일을 Multiboot2 방식으로 부팅하라는 지시다.
5.3 커널
커널은 부트로더가 메모리에 올린 뒤 실행하는 코드다. go-os의 Phase 0에서는 순수 어셈블리로 작성한 최소 커널이 여기에 해당한다.
이 단계의 커널은 아직 운영체제 기능을 제공하지 않는다. GRUB이 진입점으로 점프했는지 확인하고 VGA 텍스트 메모리에 값을 써서 화면에 OK를 표시하는 정도만 처리한다.
5.4 CPU 실행 모드
CPU 실행 모드는 같은 바이트를 어떤 규칙으로 해석할지 정하는 실행 상태다. 16비트 리얼 모드, 32비트 보호 모드, 64비트 Long mode가 대표적이다.
| 모드 | 설명 | go-os에서의 위치 |
|---|---|---|
| Real mode | 초기 x86 호환 실행 모드 | 직접 작성하지 않음 |
| Protected mode | 32비트 보호 모드 | GRUB이 커널로 넘겨주는 상태 |
| Long mode | 64비트 실행 모드 | Phase 1 이후 커널이 직접 전환해야 함 |
GRUB은 Multiboot2 방식으로 커널을 실행할 때 32비트 보호 모드 상태에서 제어를 넘긴다. 따라서 커널의 첫 진입점은 32비트 코드여야 한다. 64비트 코드를 실행하려면 커널이 직접 Long mode로 전환해야 한다.
5.5 명령 포인터
CPU는 메모리에 있는 기계어 명령을 순서대로 읽고 실행한다. 이때 다음에 실행할 명령의 주소를 가리키는 레지스터가 명령 포인터다.
| 모드 | 명령 포인터 |
|---|---|
| 16비트 | IP |
| 32비트 | EIP |
| 64비트 | RIP |
GRUB이 커널로 점프한다는 것은 명령 포인터가 커널의 진입점 주소를 가리키도록 바뀐다는 뜻이다. 그 이후 CPU는 커널 코드의 명령을 읽고 실행한다.
5.6 ELF
ELF(Executable and Linkable Format)는 실행 파일과 오브젝트 파일에 사용되는 구조화된 파일 포맷이다. 커널 ELF에는 기계어 코드뿐만 아니라 섹션 정보, 로드 주소, 진입점 같은 메타데이터가 함께 들어간다.
GRUB은 ELF 정보를 읽고 커널의 각 섹션을 메모리에 올린 뒤 ELF에 기록된 진입점으로 점프할 수 있다. 따라서 커널 파일은 단순한 기계어 덩어리가 아니라 GRUB과 linker가 해석할 수 있는 구조를 가진 파일이어야 한다.
5.7 VGA 텍스트 메모리
VGA 텍스트 모드에서는 0xB8000 주소에 값을 쓰면 화면에 문자가 표시된다. 이 주소는 일반 RAM처럼 보이지만 실제로는 화면 출력 장치와 연결된 메모리 영역이다.
텍스트 모드 화면은 보통 80칸 × 25줄로 다룬다. 한 칸은 2바이트로 구성된다.
| 바이트 | 의미 |
|---|---|
| 첫 번째 바이트 | 표시할 문자 |
| 두 번째 바이트 | 글자 색과 배경색 |
예를 들어 0xB8000에 문자 O와 색상 값을 쓰면 화면 좌상단에 O가 표시된다.
6. 동작 흐름
부팅 흐름은 펌웨어, 부트로더, 커널 순서로 제어가 이동하는 과정이다.
flowchart TD
A["전원 ON"]
B["펌웨어
BIOS 또는 UEFI"]
C["부트로더
GRUB"]
D["커널
직접 작성한 코드"]
A --> B
B -->|"부트로더를 메모리에 올리고 실행"| C
C -->|"커널을 메모리에 올리고 진입점으로 점프"| D
이 구조에는 두 가지 중요한 흐름이 있다. 첫 번째는 실행 주체가 펌웨어에서 GRUB으로 넘어가는 흐름이고 두 번째는 GRUB에서 커널로 넘어가는 흐름이다. go-os에서 직접 작성한 코드는 두 번째 흐름이 끝난 뒤부터 실행된다.
소스 코드가 커널 파일이 되는 흐름은 다음과 같다.
flowchart LR
A["boot.asm"]
B["boot.o"]
C["kernel.elf"]
D["ISO 이미지"]
E["QEMU 부팅"]
A -->|"NASM"| B
B -->|"ld + linker.ld"| C
C -->|"GRUB ISO 구성"| D
D -->|"QEMU 실행"| E
이 흐름에서는 NASM이 어셈블리 소스를 오브젝트 파일로 만들고 linker가 오브젝트 파일을 ELF 커널로 묶는다. 그다음 GRUB이 읽을 수 있는 ISO 이미지를 만들고 QEMU에서 부팅한다.
CPU 내부의 명령 실행 흐름은 다음처럼 볼 수 있다.
flowchart TD
F["Fetch
명령 포인터가 가리키는 주소에서 명령을 읽음"]
D["Decode
명령을 해석함"]
E["Execute
명령을 실행함"]
R["Update
다음 명령 주소로 이동함"]
F --> D
D --> E
E --> R
R --> F
기본 실행 흐름은 순차 실행이다. jmp, call, ret, interrupt 같은 제어 흐름 변경이 발생하면 명령 포인터가 다른 주소를 가리키게 된다. GRUB이 커널로 제어를 넘기는 것도 커널 진입점으로 실행 위치를 바꾸는 동작이다.
7. 상태 변화
부팅 과정에서 CPU 실행 상태와 제어 주체는 단계적으로 바뀐다.
| 단계 | 제어 주체 | CPU 상태 | 설명 |
|---|---|---|---|
| 전원 인가 직후 | 펌웨어 | 초기 x86 호환 상태 | CPU가 펌웨어 코드를 실행한다. |
| 부트로더 실행 | GRUB | 부트로더가 준비한 상태 | GRUB이 커널 파일을 찾고 로드한다. |
| 커널 진입 | 커널 _start | 32비트 보호 모드 | GRUB이 커널 진입점으로 점프한다. |
| Long mode 전환 후 | 커널 | 64비트 Long mode | Phase 1에서 커널이 직접 전환해야 하는 상태다. |
Multiboot2 방식에서는 GRUB이 커널로 제어를 넘길 때 일부 정보를 레지스터로 전달한다.
| 레지스터 | 값 | 설명 |
|---|---|---|
EAX | 0x36D76289 | Multiboot2 부트로더가 넘겨준 진입임을 확인하는 매직값 |
EBX | 부트 정보 구조체 주소 | 메모리맵 같은 부팅 정보가 들어 있는 구조체의 주소 |
정보 본문은 메모리에 있고 EBX는 그 위치를 가리킨다. 커널은 이 값을 사용해 GRUB이 넘겨준 부팅 정보를 읽을 수 있다.
8. 코드나 설정에서 나타나는 형태
부팅 개념은 주로 boot.asm, linker.ld, grub.cfg에서 확인할 수 있다.
8.1 boot.asm
다음 코드는 Multiboot2 헤더와 커널 진입점, VGA 출력의 최소 예시다.
section .multiboot
align 8
header_start:
dd 0xE85250D6
dd 0
dd header_end - header_start
dd 0x100000000 - (0xE85250D6 + 0 + (header_end - header_start))
dw 0
dw 0
dd 8
header_end:
global _start
section .text
bits 32
_start:
mov word [0xB8000], 0x0F4F
mov word [0xB8002], 0x0F4B
.hang:
hlt
jmp .hang
section .multiboot에는 GRUB이 읽을 Multiboot2 헤더가 들어간다. 이 영역은 CPU가 실행할 코드가 아니라 GRUB이 커널 형식을 확인하기 위해 읽는 데이터다.
bits 32는 NASM에게 이후 코드를 32비트 명령 기준으로 어셈블하라고 지시한다. GRUB이 32비트 보호 모드로 제어를 넘기므로 _start의 첫 코드는 32비트 기준으로 작성해야 한다.
mov word [0xB8000], 0x0F4F는 VGA 텍스트 메모리에 문자와 색상 값을 쓰는 명령이다. 리틀엔디언 때문에 메모리에는 4F 0F 순서로 들어가고 0x4F는 문자 O, 0x0F는 흰색 속성으로 해석된다.
8.2 linker.ld
다음 설정은 커널의 진입점과 섹션 배치를 정한다.
ENTRY(_start)
SECTIONS
{
. = 1M;
.multiboot : {
*(.multiboot)
}
.text : {
*(.text)
}
.rodata : {
*(.rodata)
}
.data : {
*(.data)
}
.bss : {
*(.bss)
}
}
ENTRY(_start)는 linker에게 커널 진입점을 _start로 사용하라고 지시한다. GRUB은 ELF 메타데이터를 보고 이 진입점으로 점프한다.
. = 1M은 이후 섹션을 1MB 지점부터 배치하라는 설정이다. 커널을 낮은 메모리 영역에 두지 않고 1MB 이후에 배치하기 위한 설정이다.
.multiboot를 앞쪽에 둔 이유는 GRUB이 Multiboot2 헤더를 커널 파일 앞쪽에서 찾아야 하기 때문이다. 이 섹션이 너무 뒤로 밀리면 GRUB이 헤더를 찾지 못할 수 있다.
8.3 grub.cfg
다음 설정은 GRUB에게 kernel.elf를 Multiboot2 방식으로 부팅하라고 지시한다.
menuentry "go-os" {
multiboot2 /boot/kernel.elf
boot
}
multiboot2 /boot/kernel.elf에는 /boot/kernel.elf 파일을 Multiboot2 커널로 다루라는 내용이 담겨 있다. 커널 파일 안에 Multiboot2 헤더가 있더라도 GRUB 설정에서 이 지시가 필요하다.
8.4 NASM 지시어와 CPU 명령어
어셈블리 파일 안에는 CPU가 실행하는 명령어와 NASM이 해석하는 지시어가 함께 들어간다.
| 종류 | 예시 | 설명 |
|---|---|---|
| CPU 명령어 | mov, jmp, hlt | CPU가 실제로 실행하는 명령 |
| NASM 지시어 | section, bits, global, dd, dw | 어셈블러에게 코드 배치나 데이터 생성을 지시하는 문법 |
mov, jmp, hlt는 실행 시점에 CPU가 처리한다. section, bits, dd, dw는 빌드 시점에 NASM이 처리한다. 이 둘을 구분해야 Multiboot2 헤더처럼 실행되는 코드가 아닌 데이터를 올바르게 이해할 수 있다.
9. 자주 헷갈리는 지점
9.1 GRUB이 64비트로 넘겨준다고 볼 수 없다
GRUB이 커널을 실행한다고 해서 바로 64비트 코드가 실행되는 것은 아니다. Multiboot2 방식의 커널 진입점은 32비트 보호 모드 기준으로 작성해야 한다. 64비트 Long mode 전환은 커널이 직접 처리해야 한다.
그래서 Phase 0에서는 bits 32로 시작하고 VGA 메모리에 값을 쓰는 것까지만 확인한다. Long mode 전환은 다음 단계에서 GDT, 페이지 테이블, CR0, CR3, CR4, EFER 설정과 함께 다룬다.
9.2 Multiboot2 헤더와 grub.cfg는 역할이 다르다
Multiboot2 헤더는 커널 파일 안에 들어 있는 자기 식별 정보다. 반면 grub.cfg의 multiboot2 지시는 GRUB에게 해당 파일을 어떤 방식으로 부팅할지 알려 주는 설정이다.
커널 파일에 헤더가 있어도 grub.cfg에 multiboot2 지시가 없으면 GRUB이 그 파일을 Multiboot2 방식으로 부팅하지 않을 수 있다. 두 정보는 서로 대체 관계가 아니라 함께 필요한 정보다.
9.3 ELF는 플랫 바이너리와 다르다
ELF도 넓은 의미에서는 바이너리 파일이라고 부를 수 있다. 하지만 구조 없는 기계어 덩어리인 플랫 바이너리와는 다르다.
ELF에는 섹션, 심볼, 진입점, 로드 주소 같은 메타데이터가 들어간다. GRUB과 linker는 이 정보를 사용해 커널을 올바른 위치에 배치하고 진입점으로 제어를 넘긴다.
9.4 라벨 주소는 최종적으로 linker가 정한다
NASM은 어셈블리 파일을 오브젝트 파일로 만들면서 섹션 안의 상대 위치를 정한다. 최종 로드 주소는 linker가 linker.ld를 기준으로 정한다.
따라서 _start 같은 라벨의 최종 주소는 NASM만으로 결정되지 않는다. linker.ld에서 어떤 베이스 주소를 주는지에 따라 최종 주소가 달라진다.
9.5 레지스터에 명령이 들어 있는 것은 아니다
기계어 명령은 메모리에 있다. CPU의 명령 포인터가 다음에 실행할 명령의 메모리 주소를 가리킨다.
EAX, EBX 같은 일반 레지스터는 계산에 쓸 값이나 주소를 담는다. Multiboot2 진입 시점의 EAX와 EBX도 명령이 아니라 GRUB이 커널에 전달하는 값이다.
10. 확인 방법
부팅 흐름은 다음 항목으로 확인할 수 있다.
| 확인 대상 | 확인 방법 | 확인되는 내용 |
|---|---|---|
| Multiboot2 헤더 | grub-file --is-x86-multiboot2 kernel.elf | GRUB이 커널을 Multiboot2 커널로 인식할 수 있는지 확인한다. |
| ELF 진입점 | readelf -h kernel.elf | 진입점 주소가 의도한 값인지 확인한다. |
| 섹션 배치 | readelf -S kernel.elf | .multiboot, .text 섹션이 의도한 순서로 배치되었는지 확인한다. |
| QEMU 부팅 | QEMU 화면 확인 | 커널 코드가 실제로 실행되어 VGA 메모리에 값을 썼는지 확인한다. |
| 화면 출력 | 좌상단 OK 확인 | _start 이후 코드가 실행되었는지 확인한다. |
QEMU 화면에 OK가 표시되면 GRUB이 커널을 인식했고 커널을 메모리에 올린 뒤 진입점으로 제어를 넘겼으며 커널 코드가 VGA 텍스트 메모리에 값을 썼다는 것까지 확인할 수 있다.
다만 이 결과만으로 Long mode 전환이나 Go 코드 실행이 확인되는 것은 아니다. Phase 0에서 확인한 범위는 Multiboot2 인식, 32비트 커널 진입, VGA 출력까지다.
11. 다음에 볼 개념
부팅 흐름을 이해했다면 다음에는 Long mode 전환을 봐야 한다. GRUB은 커널을 32비트 보호 모드 상태로 실행하므로 64비트 코드를 실행하려면 커널이 직접 Long mode로 전환해야 한다.
Long mode 전환을 이해하려면 GDT, 페이지 테이블, CR0, CR3, CR4, EFER 설정을 함께 봐야 한다. Long mode 전환 뒤에는 interrupt 처리를 위해 IDT와 handler 등록 흐름으로 이어진다.
부록 A. 참고 레퍼런스
| 주제 | 출처 |
|---|---|
Multiboot2 헤더, checksum, 진입 상태, EAX, EBX | Multiboot2 Specification version 2.0 (GNU) |
| Multiboot 개요와 커널 진입 상태 | Multiboot - OSDev Wiki |
| Go 기반 OS 구현 참고 | dmarro89/go-dav-os |
부록 B. 다시 확인할 질문
| 질문 | 답 요약 | 관련 절 |
|---|---|---|
| BIOS와 UEFI는 무엇인가? | 전원을 켠 뒤 부트로더를 실행하는 펌웨어 방식이다. BIOS는 오래된 방식이고 UEFI는 이후에 널리 쓰이는 펌웨어 인터페이스다. | §5.1 |
| 왜 바로 64비트로 시작하지 않는가? | x86 계열은 하위호환성을 유지해야 하므로 초기 실행 상태가 과거 x86 환경과 맞춰져 있다. | §3 |
| GRUB은 무엇을 하는가? | 커널 파일을 찾고 메모리에 올린 뒤 커널 진입점으로 제어를 넘긴다. | §5.2 |
| 커널은 언제부터 직접 작성한 코드인가? | GRUB이 커널 진입점으로 점프한 뒤부터 직접 작성한 코드가 실행된다. | §1 |
| ELF는 플랫 바이너리인가? | 아니다. ELF는 섹션, 진입점, 로드 주소 같은 메타데이터를 가진 구조화된 파일 포맷이다. | §9.3 |
grub.cfg의 multiboot2 지시는 왜 필요한가? | GRUB에게 해당 커널을 Multiboot2 방식으로 부팅하라고 알려 주는 설정이기 때문이다. | §9.2 |
| 라벨의 최종 주소는 누가 정하는가? | NASM이 상대 위치를 만들고 linker가 linker.ld를 기준으로 최종 주소를 정한다. | §9.4 |