부팅 과정 심화 (Boot Process)
BIOS/UEFI 펌웨어에서 init 프로세스까지 — 리눅스 커널의 전체 부팅 흐름을 커널 소스 코드 수준에서 분석합니다.
부팅 과정 개요
리눅스 시스템의 부팅은 하드웨어 초기화부터 사용자 공간 진입까지 여러 단계를 거칩니다. 각 단계는 이전 단계가 설정한 환경 위에서 동작하며, 문제 발생 시 정확한 단계를 파악하는 것이 디버깅의 핵심입니다.
| 단계 | 실행 주체 | 주요 소스 경로 |
|---|---|---|
| 1. 펌웨어 (BIOS/UEFI) | 하드웨어/펌웨어 | - |
| 2. 부트로더 (GRUB2) | GRUB/systemd-boot | - |
| 3. 리얼 모드 커널 | arch/x86/boot/ | header.S, main.c |
| 4. 보호 모드 전환 | arch/x86/boot/ | pm.c, pmjump.S |
| 5. 커널 압축 해제 | arch/x86/boot/compressed/ | head_64.S, misc.c |
| 6. 아키텍처 초기화 | arch/x86/kernel/ | head_64.S, head64.c |
| 7. start_kernel() | init/ | main.c |
| 8. rest_init() → init | init/ + kernel/ | main.c |
부팅 흐름 다이어그램
펌웨어 단계: BIOS vs UEFI
Legacy BIOS 부팅
전통적인 BIOS(Basic Input/Output System)는 IBM PC(1981년) 이래 x86 시스템의 표준 펌웨어입니다. ROM(또는 NOR Flash)에 저장되며, 전원 인가 시 CPU의 리셋 벡터(0xFFFFFFF0, 16바이트 아래)에서 첫 명령어를 페치하여 실행을 시작합니다.
POST (Power-On Self-Test)
BIOS가 가장 먼저 수행하는 작업은 하드웨어 자가 진단입니다. POST 실패 시 비프음(beep code)이나 POST 코드(디버그 포트 0x80)로 오류를 보고합니다.
/* POST 실행 순서 (간략화) */
/* 1. CPU 리셋 벡터 (0xFFFFFFF0) → BIOS ROM 진입
* - CS=0xF000, EIP=0xFFF0 (리얼 모드 + hidden base 0xFFFF0000)
* - 첫 명령어: 일반적으로 JMP far → BIOS 초기화 루틴으로 점프
*/
/* 2. CPU 자가 테스트 */
- 레지스터, ALU 기본 동작 확인
- CPUID 명령으로 CPU 모델/기능 확인
- 캐시 초기화 (CAR: Cache-As-RAM 설정 — 메모리 없이 스택 사용)
/* 3. 칩셋 초기화 */
- MCH/PCH (Memory/Platform Controller Hub) 설정
- PCI/PCIe 버스 초기 구성
- Super I/O 칩 감지 (직렬/병렬 포트, 키보드 컨트롤러)
/* 4. DRAM 초기화 (가장 복잡한 단계) */
- SPD(Serial Presence Detect) ROM에서 DIMM 파라미터 읽기 (I2C/SMBus)
- 메모리 컨트롤러 타이밍 설정 (CAS Latency, tRCD, tRP, tRAS)
- 메모리 트레이닝 (signal integrity 보정)
- 기본 메모리 테스트
/* 5. 주변 장치 초기화 */
- 8259A PIC (Programmable Interrupt Controller) 설정
- 8254 PIT (Programmable Interval Timer) 초기화
- 8042 키보드 컨트롤러 리셋 및 테스트
- VGA/비디오 어댑터 초기화 → Video BIOS (옵션 ROM) 실행
/* 6. 옵션 ROM 스캔 (0xC0000–0xEFFFF) */
- 2KB 단위로 시그니처 0x55AA 검색
- 발견 시 해당 ROM의 초기화 코드 실행 (예: 비디오 BIOS, RAID 컨트롤러)
/* 7. IPL (Initial Program Load) — 부팅 장치 선택 */
- CMOS/NVRAM에서 부팅 순서 읽기
- 해당 장치의 첫 섹터(MBR)를 0x7C00에 로드
POST 디버깅: 메인보드의 POST 코드 디스플레이(2자리 hex)나 I/O 포트 0x80에 쓰는 값으로 어떤 단계에서 멈췄는지 확인할 수 있습니다. 예: 0x10=OEM POST 시작, 0xD0=PCI 자원 할당 등. SeaBIOS에서는 debug.log를 통해 전체 POST 과정을 추적할 수 있습니다.
IVT와 BDA — BIOS 핵심 데이터 구조
/* IVT (Interrupt Vector Table): 0x0000:0000 ~ 0x0000:03FF (1KB)
*
* 256개 인터럽트 벡터 × 4바이트(segment:offset) = 1024바이트
* 리얼 모드에서 INT 명령 실행 시 CPU가 자동으로 참조
*/
struct ivt_entry {
uint16_t offset; /* ISR 오프셋 */
uint16_t segment; /* ISR 세그먼트 */
} ivt[256]; /* 물리 주소 0x00000000 */
/* 주요 BIOS 인터럽트 벡터 */
INT 0x10 /* 비디오 서비스 (텍스트/그래픽 출력, 커서 제어) */
INT 0x13 /* 디스크 서비스 (섹터 읽기/쓰기, 디스크 파라미터 조회) */
INT 0x15 /* 시스템 서비스 (메모리 맵 E820, A20 제어, APM) */
INT 0x16 /* 키보드 서비스 (키 입력, 상태 확인) */
INT 0x19 /* 부트스트랩 로더 (POST 완료 후 호출 → MBR 로드) */
INT 0x1A /* 시간/날짜 서비스 (RTC 접근) */
/* BDA (BIOS Data Area): 0x0040:0000 ~ 0x0040:00FF (256바이트)
*
* BIOS가 POST 중 수집한 하드웨어 정보를 저장
* 리얼 모드 프로그램(부트로더 등)이 직접 참조 가능
*/
/* 물리 주소 0x00400 */
0x0400 uint16_t com_port[4]; /* COM1~COM4 I/O 포트 주소 */
0x0408 uint16_t lpt_port[3]; /* LPT1~LPT3 I/O 포트 주소 */
0x0410 uint16_t equipment_word; /* 장비 플래그 (FPU, 마우스, 비디오 모드) */
0x0413 uint16_t mem_size_kb; /* 기본 메모리 크기 (KB 단위, ~640K) */
0x0417 uint8_t kbd_flags; /* 키보드 상태 (Shift, Ctrl, Alt, CapsLock) */
0x0449 uint8_t video_mode; /* 현재 비디오 모드 */
0x044A uint16_t screen_cols; /* 화면 열 수 */
0x046C uint32_t timer_ticks; /* 자정 이후 타이머 틱 (18.2Hz) */
0x0472 uint16_t reset_flag; /* 리부트 플래그: 0x1234=warm, 기타=cold */
0x0475 uint8_t hd_count; /* 감지된 하드 디스크 수 */
/* EBDA (Extended BIOS Data Area): 물리 주소 0x9FC00 근처 (BDA 0x040E에 세그먼트 저장)
* ACPI RSDP, MP Floating Pointer 등이 여기에 위치
* 커널 초기화 시 이 영역을 스캔하여 ACPI/SMBIOS 테이블을 찾음 */
BIOS INT 13h — 디스크 서비스
BIOS의 디스크 접근은 INT 13h 인터럽트를 통해 이루어집니다. 리얼 모드에서 부트로더가 커널 이미지를 메모리에 로드하는 유일한 수단입니다.
/* INT 13h, AH=02h: CHS(Cylinder-Head-Sector) 방식 읽기 — 전통적 방법 */
/* 최대 8GB(1024 cylinders × 255 heads × 63 sectors × 512 bytes) 제한 */
struct biosregs ireg;
ireg.ah = 0x02; /* 섹터 읽기 기능 */
ireg.al = num_sectors; /* 읽을 섹터 수 (1~128) */
ireg.ch = cylinder; /* 실린더 번호 하위 8비트 */
ireg.cl = sector; /* 섹터 번호(1~63) + 실린더 상위 2비트 */
ireg.dh = head; /* 헤드 번호 */
ireg.dl = drive; /* 드라이브 번호 (0x80=첫 번째 HDD) */
ireg.es = seg; /* 버퍼 세그먼트 */
ireg.bx = off; /* 버퍼 오프셋 */
intcall(0x13, &ireg, &oreg);
/* CF=1이면 오류, AH에 에러 코드 */
/* INT 13h, AH=42h: LBA(Logical Block Addressing) 확장 읽기
* "Extended Read Sectors" — 8GB 제한 극복
* 대부분의 현대 BIOS가 지원 (INT 13h Extensions, EDD)
*/
struct disk_address_packet { /* DAP (Disk Address Packet) */
uint8_t size; /* DAP 크기: 0x10 (16바이트) */
uint8_t reserved; /* 0 */
uint16_t num_sectors; /* 읽을 섹터 수 */
uint16_t buf_offset; /* 전송 버퍼 오프셋 */
uint16_t buf_segment; /* 전송 버퍼 세그먼트 */
uint64_t start_lba; /* 시작 LBA (최대 2^64 섹터) */
};
ireg.ah = 0x42; /* 확장 읽기 기능 */
ireg.dl = 0x80; /* 드라이브 번호 */
ireg.si = (uint16_t)&dap; /* DAP 포인터 */
intcall(0x13, &ireg, &oreg);
/* INT 13h, AH=48h: 확장 드라이브 파라미터 조회
* 총 섹터 수, 섹터 크기, CHS 지오메트리 등 반환
* GRUB가 디스크 크기 판단에 사용 */
INT 13h의 한계:
- 리얼 모드에서만 호출 가능 → 보호/롱 모드 전환 후에는 사용 불가
- 전송 버퍼가 1MB 미만의 리얼 모드 주소 공간에 있어야 함
- 64KB 세그먼트 경계를 넘는 DMA 전송 불가 (버퍼 정렬 필요)
- CHS 방식은 8GB 제한, LBA 확장으로 해결하나 BIOS 지원 필요
BIOS INT 15h — 시스템 서비스
INT 15h는 메모리 맵 조회, A20 제어 등 시스템 수준 기능을 제공합니다. 리눅스 커널 부트 코드가 가장 많이 호출하는 BIOS 인터럽트입니다.
/* INT 15h 주요 기능 */
/* AX=E820h: 시스템 메모리 맵 조회 (가장 중요) */
/* → e820-memory-map 섹션에서 상세 설명 */
/* AX=E801h: 확장 메모리 크기 (64MB 미만 시스템용 폴백) */
ireg.ax = 0xE801;
intcall(0x15, &ireg, &oreg);
/* AX/CX: 1MB~16MB 사이 메모리 (1KB 단위)
* BX/DX: 16MB 이상 메모리 (64KB 단위) */
/* AH=88h: 확장 메모리 크기 (레거시, 최대 64MB) */
ireg.ah = 0x88;
intcall(0x15, &ireg, &oreg);
/* AX: 1MB 이상 확장 메모리 크기 (1KB 단위, 최대 65535 = ~64MB) */
/* AX=2401h/2400h/2403h: A20 라인 제어 */
ireg.ax = 0x2401; /* A20 활성화 */
intcall(0x15, &ireg, &oreg);
ireg.ax = 0x2400; /* A20 비활성화 */
ireg.ax = 0x2403; /* A20 지원 상태 조회 */
/* AX=EC00h: 32/64비트 모드 전환 의도 통보 */
ireg.ax = 0xEC00;
ireg.bl = 0x02; /* 0x01=32-bit, 0x02=64-bit */
intcall(0x15, &ireg, &oreg);
/* BIOS에게 long mode 전환 예정임을 알림 → 일부 BIOS가 최적화 수행 */
/* AH=C0h: 시스템 설정 파라미터 테이블 */
ireg.ah = 0xC0;
intcall(0x15, &ireg, &oreg);
/* ES:BX → 시스템 설정 테이블 (모델, 서브모델, BIOS 리비전) */
/* arch/x86/boot/main.c의 detect_memory()에서 호출 순서:
* 1. detect_memory_e820() — AX=E820h (가장 상세)
* 2. detect_memory_e801() — AX=E801h (폴백)
* 3. detect_memory_88() — AH=88h (최후 수단)
* 순서대로 시도하여 첫 번째 성공한 결과를 사용 */
BIOS INT 10h — 비디오 서비스
/* INT 10h: 비디오 출력 — 부트 초기 화면 표시의 유일한 수단 */
/* AH=0Eh: TTY 문자 출력 (가장 기본적인 출력) */
ireg.ah = 0x0E; /* Teletype Output */
ireg.al = 'A'; /* 출력할 문자 */
ireg.bh = 0x00; /* 페이지 번호 */
ireg.bl = 0x07; /* 색상 (그래픽 모드에서만) */
intcall(0x10, &ireg, &oreg);
/* AH=00h: 비디오 모드 설정 */
ireg.ah = 0x00;
ireg.al = 0x03; /* 80×25 텍스트, 16색 */
intcall(0x10, &ireg, &oreg);
/* AH=4Fh: VESA VBE (Video BIOS Extensions) */
ireg.ax = 0x4F00; /* VBE Controller Information */
ireg.di = (size_t)&vbe_info;
intcall(0x10, &ireg, &oreg);
/* → VBE 버전, 비디오 메모리 크기, 지원 모드 목록 반환 */
ireg.ax = 0x4F01; /* VBE Mode Information */
ireg.cx = mode_num; /* 모드 번호 (예: 0x0118 = 1024×768×24bpp) */
intcall(0x10, &ireg, &oreg);
/* → 해상도, 색 깊이, 프레임버퍼 물리 주소 등 반환 */
ireg.ax = 0x4F02; /* VBE Set Mode */
ireg.bx = mode_num | 0x4000; /* bit 14: LFB(Linear Frame Buffer) 사용 */
intcall(0x10, &ireg, &oreg);
/* 커널의 set_video() (arch/x86/boot/video.c):
* 1. vesa_store_mode_params_graphics() → VBE 모드 정보 수집
* 2. boot_params.screen_info에 프레임버퍼 정보 저장
* 3. 나중에 vesafb/efifb/simplefb 드라이버가 이 정보를 사용
*
* 커널 커맨드라인 vga= 파라미터:
* vga=ask → 부팅 시 모드 선택 메뉴 표시
* vga=791 → 1024×768×16bpp
* vga=normal → 80×25 텍스트 모드 유지 */
MBR (Master Boot Record) 구조
/* MBR: 디스크의 첫 512바이트 (LBA 0) */
struct mbr {
uint8_t bootstrap_code[446]; /* 부트 코드 (GRUB Stage 1 등) */
struct {
uint8_t status; /* 0x80=active/bootable, 0x00=inactive */
uint8_t first_chs[3]; /* 파티션 시작 CHS */
uint8_t type; /* 파티션 타입 ID */
uint8_t last_chs[3]; /* 파티션 끝 CHS */
uint32_t first_lba; /* 파티션 시작 LBA */
uint32_t num_sectors; /* 파티션 섹터 수 */
} partitions[4]; /* 파티션 테이블 (4 × 16 = 64바이트) */
uint16_t signature; /* 0xAA55 — 유효한 MBR 시그니처 */
} __attribute__((__packed__)); /* 총 512바이트 */
/* 주요 파티션 타입 ID */
0x83 /* Linux (ext2/ext3/ext4/btrfs 등) */
0x82 /* Linux swap */
0x8E /* Linux LVM */
0xFD /* Linux RAID autodetect */
0x05 /* Extended partition (CHS) */
0x0F /* Extended partition (LBA) */
0x07 /* NTFS/exFAT */
0x0C /* FAT32 (LBA) */
0xEE /* GPT Protective MBR */
/* BIOS의 MBR 로드 과정:
*
* 1. INT 13h, AH=02h로 드라이브 0x80의 LBA 0을 0x7C00에 읽기
* 2. 마지막 2바이트가 0xAA55인지 확인 (아니면 다음 드라이브 시도)
* 3. DL에 부팅 드라이브 번호를 설정하고 JMP 0x0000:0x7C00 실행
*
* 주의: BIOS는 DL(드라이브 번호)만 전달 — 나머지는 부트로더가 알아서 처리
* MBR 코드 446바이트 내에서 해야 할 일:
* - 활성 파티션 찾기
* - 해당 파티션의 첫 섹터(VBR) 로드
* - VBR로 점프 (또는 GRUB Stage 1.5 위치로 점프) */
리얼 모드 메모리 맵 상세
/* x86 리얼 모드 (16-bit) 메모리 레이아웃 — 1MB 주소 공간 */
0x00000000 - 0x000003FF /* IVT (Interrupt Vector Table) — 256 × 4B = 1KB */
0x00000400 - 0x000004FF /* BDA (BIOS Data Area) — 256B */
0x00000500 - 0x00007BFF /* 사용 가능 (약 30KB) — 스택, 임시 데이터용 */
0x00007C00 - 0x00007DFF /* MBR 로드 위치 (512B) — BIOS가 고정 로드 */
0x00007E00 - 0x0007FFFF /* 사용 가능 (약 480KB) — 커널 setup 코드, 힙 */
0x00080000 - 0x0009FBFF /* 사용 가능 (EBDA 시작 전까지, ~128KB) */
0x0009FC00 - 0x0009FFFF /* EBDA (Extended BIOS Data Area) — 1~64KB 가변 */
0x000A0000 - 0x000BFFFF /* VGA 프레임버퍼 (128KB) */
/* 0xA0000: 그래픽 모드 프레임버퍼 */
/* 0xB0000: MDA 텍스트 모드 */
/* 0xB8000: CGA/VGA 텍스트 모드 (80×25×2B) */
0x000C0000 - 0x000C7FFF /* Video BIOS ROM (32KB) */
0x000C8000 - 0x000EFFFF /* 옵션 ROM 영역 (확장 카드 BIOS) */
0x000F0000 - 0x000FFFFF /* System BIOS ROM (64KB) */
/* 0xFFFF0: 리셋 벡터 (JMP → BIOS 시작) */
/* "640KB 장벽": 0x00000–0x9FFFF (640KB)만 사용 가능한 종래 메모리
* 0xA0000–0xFFFFF (384KB)는 VGA/ROM에 예약 = "Upper Memory Area (UMA)"
* A20 활성화 + 보호 모드 전환 후 1MB 이상 메모리 접근 가능
*
* 리얼 모드 주소 = segment × 16 + offset
* 최대 주소: 0xFFFF:0xFFFF = 0x10FFEF (1MB + ~64KB)
* A20 비활성 시 bit 20이 마스킹 → 0x100000 이상 주소가 0으로 wraparound */
BIOS PCI 열거와 리소스 할당
/* BIOS INT 1Ah, AX=B101h: PCI BIOS 존재 확인 */
ireg.ax = 0xB101;
intcall(0x1A, &ireg, &oreg);
/* EDX="PCI " (0x20494350)이면 PCI BIOS 지원
* AH=00: 지원, BH.BL=PCI 버전(예: 02.10=v2.1), CL=최대 버스 번호 */
/* BIOS PCI 열거 과정:
*
* 1. 버스 0부터 Configuration Space(0xCF8/0xCFC) 스캔
* - 각 bus/device/function의 Vendor ID 확인 (0xFFFF = 없음)
* - PCI 브리지 발견 시 → 하위 버스 번호 할당 후 재귀 스캔
*
* 2. BAR(Base Address Register) 크기 결정
* - BAR에 0xFFFFFFFF 쓰기 → 읽기 → 마스킹으로 크기 계산
* - MMIO BAR: bit 0=0, I/O BAR: bit 0=1
*
* 3. 리소스 할당
* - MMIO 영역: 보통 0xF0000000 이하에 할당 (4GB 미만)
* - I/O 포트: 0x1000 이상에 할당 (0x0000~0x0FFF은 레거시 예약)
* - IRQ 라우팅: $PIR 테이블 또는 ACPI _PRT 메서드
*
* 리눅스 커널은 BIOS의 PCI 할당을 대부분 수용하지만,
* pci=realloc 파라미터로 커널이 직접 재할당할 수도 있음
*/
/* PCI Configuration Space 직접 접근 (포트 I/O 방식) */
#define PCI_CONFIG_ADDR 0xCF8 /* 설정 주소 포트 */
#define PCI_CONFIG_DATA 0xCFC /* 설정 데이터 포트 */
/* 주소 레지스터 포맷 (0xCF8에 쓰는 32비트 값):
* bit 31: Enable bit (1)
* bit 23-16: 버스 번호
* bit 15-11: 디바이스 번호
* bit 10-8: 펑션 번호
* bit 7-0: 레지스터 오프셋 (4바이트 정렬) */
uint32_t addr = (1 << 31) | (bus << 16) | (dev << 11)
| (func << 8) | (reg & 0xFC);
outl(addr, PCI_CONFIG_ADDR);
uint32_t val = inl(PCI_CONFIG_DATA);
/* 현대 시스템: ECAM(Enhanced Configuration Access Mechanism)
* MMIO 기반 (PCI Express), 4KB per function, ACPI MCFG 테이블에 기술
* 기본 주소: 보통 0xE0000000 (BIOS가 MCFG에 기록) */
BIOS → 커널 정보 전달 (ACPI, SMBIOS)
BIOS는 하드웨어 정보를 여러 표준 테이블 형식으로 메모리에 기록하며, 커널이 이를 참조합니다.
/* 1. ACPI (Advanced Configuration and Power Interface)
*
* BIOS가 ACPI 테이블을 메모리에 배치 → 커널이 파싱
* RSDP(Root System Description Pointer) 탐색 위치:
* - EBDA 첫 1KB (물리 주소 ~0x9FC00)
* - 0xE0000 ~ 0xFFFFF (BIOS ROM 영역)
* - UEFI 환경: EFI_ACPI_TABLE_GUID로 직접 전달
*/
struct acpi_rsdp { /* RSDP v1 (ACPI 1.0) */
char signature[8]; /* "RSD PTR " */
uint8_t checksum; /* 처음 20바이트 체크섬 */
char oem_id[6]; /* OEM 식별자 */
uint8_t revision; /* 0=ACPI 1.0, 2=ACPI 2.0+ */
uint32_t rsdt_address; /* RSDT 물리 주소 (32-bit) */
/* ACPI 2.0+ 확장 필드 */
uint32_t length; /* RSDP 전체 길이 (36) */
uint64_t xsdt_address; /* XSDT 물리 주소 (64-bit) */
uint8_t ext_checksum; /* 확장 체크섬 */
uint8_t reserved[3];
};
/* RSDT/XSDT → 개별 테이블 포인터 배열:
* DSDT - Differentiated System Description Table (AML 바이트코드)
* FADT - Fixed ACPI Description Table (전원 관리 레지스터)
* MADT - Multiple APIC Description Table (인터럽트 라우팅)
* MCFG - PCI Express ECAM 기본 주소
* SRAT - System Resource Affinity Table (NUMA 토폴로지)
* HPET - High Precision Event Timer
* DMAR - DMA Remapping (IOMMU/VT-d)
* BGRT - Boot Graphics Resource Table (부팅 로고)
*/
/* 커널의 ACPI 테이블 파싱: drivers/acpi/tables.c
* acpi_table_init() → acpi_os_get_root_pointer() → RSDP 탐색
* → RSDT/XSDT에서 각 테이블 물리 주소 추출
* → acpi_get_table()로 개별 테이블 매핑/파싱 */
/* 2. SMBIOS (System Management BIOS) / DMI
*
* SMBIOS는 시스템 하드웨어 정보를 표준화된 테이블로 제공하는 규격.
* BIOS/UEFI가 부팅 시 테이블을 메모리에 배치하고, OS가 이를 읽어 활용.
*
* 버전 히스토리:
* SMBIOS 2.0 (1996) — DMTF 표준화, 32-bit Entry Point ("_SM_")
* SMBIOS 2.1~2.8 — 타입 확장 (메모리, TPM, 온보드 장치 등)
* SMBIOS 3.0 (2015) — 64-bit Entry Point ("_SM3_"), 4GB 이상 테이블 지원
* SMBIOS 3.7 (2023) — 최신, CXL 메모리 디바이스 타입 추가
*
* Entry Point 탐색 (Legacy BIOS):
* 물리 주소 0xF0000–0xFFFFF를 16바이트 경계로 스캔
* "_SM_" (2.x) 또는 "_SM3_" (3.x) 시그니처 탐색
*
* Entry Point 탐색 (UEFI):
* EFI Configuration Table에서 SMBIOS_TABLE_GUID 또는
* SMBIOS3_TABLE_GUID로 직접 포인터 전달 (메모리 스캔 불필요)
*/
/* === SMBIOS 2.x Entry Point (32-bit) === */
struct smbios_entry_point {
char anchor[4]; /* "_SM_" */
uint8_t checksum; /* 전체 EPS 체크섬 (합=0) */
uint8_t length; /* EPS 길이 (보통 0x1F=31) */
uint8_t major_ver; /* SMBIOS 주 버전 */
uint8_t minor_ver; /* SMBIOS 부 버전 */
uint16_t max_struct_size; /* 최대 개별 구조체 크기 */
uint8_t eps_revision; /* EPS 리비전 (0x00) */
uint8_t formatted[5]; /* 포맷 영역 (모두 0) */
/* DMI 앵커 (중간 체크포인트) */
char dmi_anchor[5]; /* "_DMI_" */
uint8_t dmi_checksum; /* DMI 영역 체크섬 */
uint16_t table_length; /* 구조체 테이블 전체 길이 */
uint32_t table_address; /* 구조체 테이블 물리 주소 (32-bit) */
uint16_t struct_count; /* 구조체 총 개수 */
uint8_t bcd_revision; /* BCD 리비전 (예: 0x28 = 2.8) */
};
/* === SMBIOS 3.x Entry Point (64-bit) ===
* 4GB 이상 주소에 테이블 배치 가능, struct_count 필드 제거 */
struct smbios3_entry_point {
char anchor[5]; /* "_SM3_" */
uint8_t checksum; /* EPS 체크섬 */
uint8_t length; /* EPS 길이 (0x18=24) */
uint8_t major_ver; /* SMBIOS 주 버전 */
uint8_t minor_ver; /* SMBIOS 부 버전 */
uint8_t docrev; /* 문서 리비전 */
uint8_t eps_revision; /* EPS 리비전 (0x01) */
uint8_t reserved;
uint32_t max_table_length; /* 구조체 테이블 최대 길이 */
uint64_t table_address; /* 구조체 테이블 물리 주소 (64-bit) */
};
/* === SMBIOS 구조체 헤더 (모든 타입 공통) === */
struct smbios_header {
uint8_t type; /* 구조체 타입 (0~127: SMBIOS 정의, 128~255: OEM) */
uint8_t length; /* formatted 영역 길이 (헤더 포함) */
uint16_t handle; /* 고유 핸들 (0x0000~0xFEFF) */
};
/* 바로 뒤에 타입별 고정 필드 → 그 뒤에 문자열 테이블 */
/* === Type 0: BIOS Information ===
* BIOS 벤더, 버전, 빌드 날짜, ROM 크기, 기능 플래그 */
struct smbios_type0 {
struct smbios_header hdr; /* type=0, length=0x1A+ */
uint8_t vendor; /* 문자열 #: BIOS 벤더 ("AMI", "Phoenix" 등) */
uint8_t version; /* 문자열 #: BIOS 버전 */
uint16_t start_segment; /* BIOS 시작 세그먼트 (보통 0xE000/0xF000) */
uint8_t release_date; /* 문자열 #: 릴리스 날짜 ("MM/DD/YYYY") */
uint8_t rom_size; /* (n+1)*64KB, 0xFF이면 ext_rom_size 참조 */
uint64_t characteristics; /* 기능 비트맵: PCI, APM, ACPI, USB 부팅 등 */
uint8_t ext_char1; /* 확장 특성 바이트 1 */
uint8_t ext_char2; /* 확장 특성 바이트 2 */
uint8_t bios_major; /* BIOS 주 릴리스 */
uint8_t bios_minor; /* BIOS 부 릴리스 */
uint8_t ec_major; /* EC 펌웨어 주 릴리스 */
uint8_t ec_minor; /* EC 펌웨어 부 릴리스 */
uint16_t ext_rom_size; /* 확장 ROM 크기 (MB/GB 단위) */
};
/* === Type 1: System Information ===
* 시스템 제조사, 제품명, UUID, Wake-up 타입 */
struct smbios_type1 {
struct smbios_header hdr; /* type=1, length=0x1B */
uint8_t manufacturer; /* 문자열 #: "Dell Inc.", "Lenovo" 등 */
uint8_t product_name; /* 문자열 #: "PowerEdge R750", "ThinkPad T14" 등 */
uint8_t version; /* 문자열 #: 제품 버전 */
uint8_t serial_number; /* 문자열 #: 시리얼 번호 */
uint8_t uuid[16]; /* RFC 4122 UUID (시스템 고유 식별자) */
uint8_t wakeup_type; /* Wake-up 유형 (전원 스위치, LAN, 타이머 등) */
uint8_t sku_number; /* 문자열 #: SKU */
uint8_t family; /* 문자열 #: 제품군 ("ThinkPad", "Latitude" 등) */
};
/* === Type 2: Baseboard (Motherboard) Information === */
struct smbios_type2 {
struct smbios_header hdr; /* type=2 */
uint8_t manufacturer; /* 문자열 #: 보드 제조사 */
uint8_t product; /* 문자열 #: 보드 모델 */
uint8_t version; /* 문자열 #: 보드 버전 */
uint8_t serial_number; /* 문자열 #: 보드 시리얼 */
uint8_t asset_tag; /* 문자열 #: 자산 태그 */
uint8_t feature_flags; /* 호스팅 보드, 교체 가능 등 */
uint8_t location; /* 문자열 #: 섀시 내 위치 */
uint16_t chassis_handle; /* Type 3 핸들 참조 */
uint8_t board_type; /* 0x0A=마더보드, 기타 */
};
/* === Type 4: Processor Information ===
* CPU 소켓, 모델, 클럭, 코어/스레드 수 */
struct smbios_type4 {
struct smbios_header hdr; /* type=4 */
uint8_t socket; /* 문자열 #: 소켓 명칭 ("LGA1700" 등) */
uint8_t proc_type; /* 03h=Central Processor */
uint8_t proc_family; /* 프로세서 패밀리 열거값 */
uint8_t manufacturer; /* 문자열 #: "Intel", "AMD" 등 */
uint64_t proc_id; /* CPUID 값 (EAX=1 결과) */
uint8_t version; /* 문자열 #: "Intel Core i9-13900K" 등 */
uint8_t voltage; /* 동작 전압 */
uint16_t ext_clock; /* 외부 클럭 (MHz) */
uint16_t max_speed; /* 최대 속도 (MHz) */
uint16_t current_speed; /* 현재 속도 (MHz) */
uint8_t status; /* CPU 소켓 상태 + 활성화 여부 */
uint8_t upgrade; /* 업그레이드 유형 */
uint16_t l1_cache; /* L1 캐시 핸들 (Type 7) */
uint16_t l2_cache; /* L2 캐시 핸들 (Type 7) */
uint16_t l3_cache; /* L3 캐시 핸들 (Type 7) */
/* SMBIOS 2.5+ */
uint16_t core_count; /* 물리 코어 수 */
uint16_t core_enabled; /* 활성화된 코어 수 */
uint16_t thread_count; /* 논리 스레드 수 */
uint16_t proc_char; /* 64-bit, 멀티코어, HT, NX 등 */
};
/* === Type 16: Physical Memory Array ===
* 물리 메모리 배열 (메모리 컨트롤러 단위) */
struct smbios_type16 {
struct smbios_header hdr; /* type=16 */
uint8_t location; /* 03h=시스템보드, 기타 */
uint8_t use; /* 03h=시스템 메모리, 기타 */
uint8_t error_correction; /* None/Parity/ECC/CRC 등 */
uint32_t max_capacity; /* 최대 용량 (KB), 0x80000000이면 ext 참조 */
uint16_t error_info_handle; /* Type 18 핸들 (없으면 0xFFFE) */
uint16_t num_devices; /* DIMM 슬롯 수 */
uint64_t ext_max_capacity; /* 확장 최대 용량 (바이트) */
};
/* === Type 17: Memory Device ===
* 개별 DIMM 슬롯 정보 (크기, 속도, 제조사, 파트넘버) */
struct smbios_type17 {
struct smbios_header hdr; /* type=17 */
uint16_t array_handle; /* Type 16 핸들 참조 */
uint16_t error_info_handle;
uint16_t total_width; /* 전체 비트 폭 (ECC 포함) */
uint16_t data_width; /* 데이터 비트 폭 (64-bit 등) */
uint16_t size; /* MB 단위 (0=미장착, 0x7FFF→ext) */
uint8_t form_factor; /* DIMM, SO-DIMM 등 */
uint8_t device_set;
uint8_t device_locator; /* 문자열 #: "DIMM_A1" 등 */
uint8_t bank_locator; /* 문자열 #: "BANK 0" 등 */
uint8_t memory_type; /* DDR4=0x1A, DDR5=0x22 등 */
uint16_t type_detail; /* Synchronous, Unbuffered 등 */
uint16_t speed; /* MT/s (DDR4-3200 → 3200) */
uint8_t manufacturer; /* 문자열 #: "Samsung", "SK Hynix" 등 */
uint8_t serial_number; /* 문자열 #: DIMM 시리얼 */
uint8_t asset_tag;
uint8_t part_number; /* 문자열 #: "M393A2K43DB3-CWE" 등 */
/* SMBIOS 2.7+ */
uint16_t configured_speed; /* 구성된 메모리 속도 (MT/s) */
uint16_t min_voltage; /* 최소 전압 (mV) */
uint16_t max_voltage; /* 최대 전압 (mV) */
uint16_t configured_voltage; /* 구성된 전압 (mV) */
/* SMBIOS 3.2+ */
uint32_t ext_size; /* 확장 크기 (MB, 32GB 이상) */
};
/* === 리눅스 커널의 SMBIOS/DMI 파싱 ===
*
* 소스: drivers/firmware/dmi_scan.c
*
* 초기화 흐름 (x86):
* setup_arch()
* → dmi_setup()
* → dmi_scan_machine()
* 1. UEFI: efi.smbios3 또는 efi.smbios 포인터 사용
* 2. Legacy: 0xF0000–0xFFFFF 스캔하여 Entry Point 탐색
* → dmi_present() / dmi_smbios3_present()
* → dmi_table(): 구조체 테이블을 ioremap하여 순회
* → dmi_decode(): 각 구조체 타입별 디코딩
* → dmi_memdev_walk(): Type 17 순회하여 메모리 정보 수집
* → dmi_check_system(): quirk 테이블 매칭
*/
/* 커널 내부 DMI 데이터 저장 구조 */
enum dmi_field {
DMI_NONE,
DMI_BIOS_VENDOR, /* Type 0: vendor */
DMI_BIOS_VERSION, /* Type 0: version */
DMI_BIOS_DATE, /* Type 0: release_date */
DMI_SYS_VENDOR, /* Type 1: manufacturer */
DMI_PRODUCT_NAME, /* Type 1: product_name */
DMI_PRODUCT_VERSION, /* Type 1: version */
DMI_PRODUCT_SERIAL, /* Type 1: serial_number */
DMI_PRODUCT_UUID, /* Type 1: uuid */
DMI_PRODUCT_SKU, /* Type 1: sku_number */
DMI_PRODUCT_FAMILY, /* Type 1: family */
DMI_BOARD_VENDOR, /* Type 2: manufacturer */
DMI_BOARD_NAME, /* Type 2: product */
DMI_BOARD_VERSION, /* Type 2: version */
DMI_BOARD_SERIAL, /* Type 2: serial_number */
DMI_BOARD_ASSET_TAG, /* Type 2: asset_tag */
DMI_CHASSIS_VENDOR, /* Type 3: manufacturer */
DMI_CHASSIS_TYPE, /* Type 3: type */
DMI_CHASSIS_VERSION, /* Type 3: version */
DMI_CHASSIS_SERIAL, /* Type 3: serial_number */
DMI_CHASSIS_ASSET_TAG, /* Type 3: asset_tag */
DMI_STRING_MAX,
};
/* dmi_get_system_info(DMI_PRODUCT_NAME)으로 접근 */
/* === DMI 기반 하드웨어 quirk 매칭 ===
*
* 특정 제조사/모델에 대한 버그 우회(quirk)를 DMI 정보로 매칭.
* 커널 전역에서 dmi_check_system()으로 사용.
* 예: 특정 노트북의 ACPI 버그 우회, 특정 서버의 메모리 매핑 수정 등
*/
static const struct dmi_system_id my_dmi_table[] = {
{
.callback = my_quirk_handler,
.ident = "Specific Laptop ACPI Fix",
.matches = {
DMI_MATCH(DMI_SYS_VENDOR, "LENOVO"),
DMI_MATCH(DMI_PRODUCT_NAME, "20UAS01234"),
},
},
{ } /* 종료 항목 */
};
dmi_check_system(my_dmi_table); /* 현재 시스템과 매칭 시 callback 호출 */
/* === /sys/class/dmi/id/ — sysfs DMI 인터페이스 ===
*
* 사용자 공간에서 DMI 정보를 파일로 접근 (drivers/firmware/dmi-id.c)
*
* /sys/class/dmi/id/bios_vendor → Type 0 벤더
* /sys/class/dmi/id/bios_version → Type 0 버전
* /sys/class/dmi/id/bios_date → Type 0 릴리스 날짜
* /sys/class/dmi/id/sys_vendor → Type 1 제조사
* /sys/class/dmi/id/product_name → Type 1 제품명
* /sys/class/dmi/id/product_serial → Type 1 시리얼 (root 전용)
* /sys/class/dmi/id/product_uuid → Type 1 UUID (root 전용)
* /sys/class/dmi/id/board_vendor → Type 2 보드 제조사
* /sys/class/dmi/id/board_name → Type 2 보드 모델
* /sys/class/dmi/id/chassis_type → Type 3 섀시 유형 번호
*
* dmidecode와 달리 root 권한 없이도 대부분 읽기 가능
* (serial_number, product_uuid 등 민감 정보는 0400 퍼미션) */
/* === dmidecode 명령어 상세 ===
*
* 사용자 공간 SMBIOS 디코더 (root 권한 필요)
*
* dmidecode → 전체 SMBIOS 테이블 덤프
* dmidecode -t bios → Type 0: BIOS 벤더, 버전, 날짜, 기능
* dmidecode -t system → Type 1: 제조사, 모델, UUID, 시리얼
* dmidecode -t baseboard → Type 2: 메인보드 제조사, 모델
* dmidecode -t chassis → Type 3: 섀시 유형 (랙마운트, 노트북 등)
* dmidecode -t processor → Type 4: CPU 모델, 소켓, 코어/스레드, 클럭
* dmidecode -t cache → Type 7: L1/L2/L3 캐시 크기, 연관도
* dmidecode -t memory → Type 16+17: DIMM 슬롯, 크기, 속도, 제조사
* dmidecode -t slot → Type 9: PCIe 슬롯 정보
* dmidecode -t connector → Type 8: 포트 커넥터 (USB, 시리얼 등)
* dmidecode -s system-uuid → 특정 필드만 출력 (스크립팅 용도)
* dmidecode -s bios-version
* dmidecode --type 17 → 타입 번호로 직접 지정
*
* 동작 원리: /dev/mem에서 Entry Point 탐색 → 테이블 매핑 → 디코딩
* (UEFI 시스템: /sys/firmware/dmi/tables/ 에서도 읽기 가능) */
product_serial, product_uuid 등은 시스템 고유 식별자입니다. 커널은 이들을 /sys/class/dmi/id/에서 0400(root 전용) 퍼미션으로 보호합니다. 가상화 환경(QEMU/KVM)에서는 -smbios type=1,manufacturer=...,product=... 옵션으로 게스트에 전달할 DMI 정보를 직접 지정할 수 있습니다.오픈소스 BIOS: coreboot과 SeaBIOS
/* coreboot (구 LinuxBIOS) — 오픈소스 펌웨어
*
* 아키텍처:
* bootblock → verstage(선택) → romstage → ramstage → payload
*
* bootblock: 리셋 벡터에서 실행, 최소 하드웨어 초기화
* romstage: DRAM 초기화 (Intel FSP 또는 자체 구현)
* CAR(Cache-As-RAM)으로 메모리 초기화 전 코드 실행
* ramstage: PCI 열거, 리소스 할당, ACPI 테이블 생성
* payload: SeaBIOS, GRUB2, LinuxBoot, TianoCore(EDK II) 등
*
* coreboot는 최소한의 초기화만 수행하고 payload에게 부팅을 위임
* → 부팅 시간 대폭 단축 (수 초 → 수백 ms)
*/
/* SeaBIOS — coreboot의 Legacy BIOS payload
*
* QEMU/KVM의 기본 BIOS로 사용
* 표준 BIOS 인터럽트(INT 10h/13h/15h 등)를 16-bit 리얼 모드로 구현
* → 일반 Legacy BIOS와 동일하게 부트로더 실행 가능
*
* 주요 소스:
* src/post.c — POST 메인 루프
* src/boot.c — IPL 장치 선택, MBR 로드
* src/disk.c — INT 13h 디스크 서비스 구현
* src/output.c — INT 10h 비디오 서비스 구현
* src/system.c — INT 15h 시스템 서비스 (E820 등)
*/
/* LinuxBoot — Linux 자체를 펌웨어 payload로 사용
*
* DRAM 초기화 후 Linux 커널을 직접 실행 → kexec로 실제 OS 부팅
* 장점:
* - BIOS의 복잡한 드라이버를 Linux 드라이버로 대체
* - 부팅 과정 전체를 Linux 도구로 디버깅/감사 가능
* - Google, Facebook 등 대규모 서버 환경에서 사용
*/
QEMU에서 BIOS 디버깅: qemu-system-x86_64 -bios /path/to/bios.bin -d int,cpu_reset -D qemu.log -s -S로 BIOS INT 호출과 CPU 리셋을 추적할 수 있습니다. -s -S로 GDB 연결 대기 상태로 시작하면 리셋 벡터부터 한 명령어씩 추적이 가능합니다.
Legacy BIOS의 한계와 UEFI 전환 이유
| 한계 | 상세 | UEFI 해결 방식 |
|---|---|---|
| 16-bit 리얼 모드 | 1MB 주소 공간, 64KB 세그먼트 제한 | 32/64-bit 보호/롱 모드 네이티브 실행 |
| MBR 2TB 제한 | 32-bit LBA × 512B = 2TB 최대 디스크 | GPT: 64-bit LBA → 9.4ZB |
| 4개 주 파티션 | MBR 파티션 테이블 = 4 엔트리 | GPT: 최대 128개 파티션 |
| 부트 코드 446B | MBR 부트 코드 영역이 극히 작음 | ESP의 FAT32에서 임의 크기 실행 파일 |
| 보안 부팅 없음 | MBR/VBR 코드 무결성 검증 불가 | Secure Boot: 서명 기반 검증 체인 |
| 드라이버 모델 없음 | INT 13h 폴링, 벤더별 비표준 확장 | UEFI 프로토콜 기반 표준 드라이버 모델 |
| 네트워크 부팅 | PXE (INT 18h/19h, 제한적) | UEFI HTTP Boot, IPv6 지원 |
UEFI 부팅
최신 시스템은 UEFI(Unified Extensible Firmware Interface)를 사용합니다. BIOS와 달리 UEFI는 64비트 모드에서 직접 실행되며, GPT 파티션과 ESP(EFI System Partition)를 활용합니다.
| 특성 | Legacy BIOS | UEFI |
|---|---|---|
| CPU 모드 | 16-bit 리얼 모드 | 32/64-bit 보호/롱 모드 |
| 파티션 | MBR (최대 2TB) | GPT (최대 9.4ZB) |
| 부트 엔트리 | 고정 LBA 0 | ESP의 EFI 바이너리 |
| 보안 부팅 | 미지원 | Secure Boot 지원 |
| 드라이버 | INT 13h | UEFI 프로토콜 드라이버 |
UEFI 부팅 흐름:
- SEC (Security): 플랫폼 초기 보안 검증, CPU 캐시를 임시 RAM으로 사용
- PEI (Pre-EFI Initialization): 메모리(DRAM) 초기화, 최소 하드웨어 설정
- DXE (Driver Execution Environment): UEFI 드라이버 로드, 프로토콜 서비스 등록
- BDS (Boot Device Selection): 부팅 장치 선택, ESP에서 부트로더 실행
- TSL (Transient System Load): 부트로더(GRUB EFI)가 커널 이미지 로드
CONFIG_EFI_STUB 옵션을 통해 자체적으로 UEFI 애플리케이션으로 동작할 수 있습니다. 이 경우 GRUB 없이 UEFI가 직접 커널을 로드합니다. UEFI Stub 진입점은 arch/x86/boot/compressed/efi_mixed.S와 drivers/firmware/efi/libstub/에 있습니다.
부트로더 (Bootloader)
부트로더는 펌웨어(BIOS/UEFI)와 운영체제 커널 사이의 중간 단계로, 핵심 역할은 세 가지입니다: (1) 커널 이미지(bzImage)를 스토리지에서 메모리로 로드, (2) 초기 램 디스크(initramfs)를 함께 로드, (3) 커널 커맨드 라인 파라미터를 전달하여 부팅 동작을 제어. 이 섹션에서는 가장 널리 사용되는 GRUB2를 중심으로, 내부 아키텍처부터 커널 핸드오프까지 상세히 살펴보고, systemd-boot 등 대안 부트로더도 다룹니다.
GRUB2 아키텍처
GRUB2(GRand Unified Bootloader 2)는 GRUB Legacy(0.9x)를 완전히 재작성한 프로젝트로, 모듈 기반 설계가 가장 큰 차이점입니다. Legacy에서는 Stage 1.5에 단일 파일시스템 드라이버만 포함할 수 있었지만, GRUB2는 수십 개의 모듈을 동적으로 로드하여 다양한 파일시스템, 파티션 스킴, 암호화, 네트워크 부팅을 지원합니다.
GRUB2 설정의 중심은 /etc/default/grub과 /etc/grub.d/ 디렉토리입니다:
# /etc/default/grub — 주요 변수
GRUB_DEFAULT=0 # 기본 메뉴 항목 (0부터 시작, 또는 "saved")
GRUB_TIMEOUT=5 # 메뉴 대기 시간 (초)
GRUB_TIMEOUT_STYLE=menu # menu, countdown, hidden
GRUB_CMDLINE_LINUX="quiet splash" # 모든 항목에 추가되는 커널 파라미터
GRUB_CMDLINE_LINUX_DEFAULT="" # 기본 항목에만 추가되는 파라미터
GRUB_DISABLE_RECOVERY=true # 복구 메뉴 항목 생성 여부
GRUB_ENABLE_CRYPTODISK=y # LUKS/GELI 암호화 디스크 지원
grub-mkconfig(또는 배포판별 update-grub) 명령은 /etc/grub.d/ 내의 스크립트를 번호 순서대로 실행하여 최종 grub.cfg를 생성합니다:
# /etc/grub.d/ 스크립트 실행 순서
00_header # 기본 설정 (timeout, default, 터미널 설정)
05_debian_theme # 배포판별 테마/배경 설정
10_linux # 로컬 리눅스 커널 탐지 → menuentry 생성
20_linux_xen # Xen 하이퍼바이저 항목
30_os-prober # 다른 OS 탐지 (Windows, macOS 등)
40_custom # 사용자 커스텀 항목
41_custom # 외부 파일 include
# 생성된 grub.cfg menuentry 구조 예시
menuentry 'Ubuntu, with Linux 6.8.0-45-generic' --class ubuntu {
recordfail
load_video
gfxmode $linux_gfx_mode
insmod gzio
insmod part_gpt
insmod ext2
set root='hd0,gpt2'
linux /vmlinuz-6.8.0-45-generic root=/dev/mapper/vg0-root ro quiet splash
initrd /initrd.img-6.8.0-45-generic
}
GRUB2 BIOS 로드 단계 상세
BIOS 환경에서 GRUB2는 세 단계로 로드됩니다. 각 단계의 바이너리 구성과 위치를 상세히 살펴봅니다.
boot.img (Stage 1, 446바이트): MBR의 부트 코드 영역에 설치됩니다. 소스는 grub-core/boot/i386/pc/boot.S이며, 유일한 역할은 core.img의 첫 섹터를 읽어 실행하는 것입니다. 파일시스템을 인식하지 못하므로, core.img의 LBA 주소가 설치 시 하드코딩됩니다(grub-install 이 수행).
core.img (Stage 1.5): 여러 컴포넌트를 결합한 복합 이미지입니다:
/*
* core.img 내부 구조
*
* +----------------------------------+
* | diskboot.img (512B) | ← grub-core/boot/i386/pc/diskboot.S
* | 나머지 core.img 섹터를 로드 | LBA 목록이 하드코딩됨
* +----------------------------------+
* | lzma_decompress.img | ← LZMA 압축 해제 루틴
* | 아래 압축 데이터를 해제 | 리얼→프로텍티드 모드 전환 포함
* +----------------------------------+
* | kernel.img (압축됨) | ← GRUB 자체 커널 (모듈 로더, 환경)
* +----------------------------------+
* | 초기 모듈들 (압축됨) | ← 최소 부팅에 필요한 모듈 세트
* | - biosdisk.mod | BIOS 디스크 I/O
* | - part_msdos.mod / part_gpt.mod| 파티션 테이블 파싱
* | - ext2.mod / xfs.mod 등 | /boot 파일시스템 드라이버
* +----------------------------------+
*/
core.img의 설치 위치는 파티션 스킴에 따라 다릅니다:
| 파티션 스킴 | 설치 위치 | 가용 크기 | 비고 |
|---|---|---|---|
| MBR + 전통 파티션 | 섹터 1~62 (MBR gap) | ~31KB | 첫 파티션이 섹터 63에서 시작하는 경우 |
| MBR + 1MiB 정렬 | 섹터 1~2047 | ~1MB | 최신 파티셔닝 도구 기본값 |
| GPT | BIOS Boot Partition | 1MB 권장 | 파티션 타입 GUID: 21686148-6449-6E6F-744E-656564454649 |
Stage 2 (/boot/grub/): core.img가 파일시스템을 인식할 수 있게 되면, /boot/grub/ 디렉토리에서 grub.cfg를 읽고 추가 모듈을 로드합니다. grubenv 파일(1KB 고정 크기)은 saved_entry, next_entry 등의 변수를 저장하여 재부팅 간 상태를 유지합니다.
/*
* BIOS 환경 GRUB2 디스크 레이아웃 (GPT)
*
* Sector 0 Sector 1 Sector 34 Sector 2048+
* +-----------+ +------------+ +------------+ +------------------+
* | MBR | | GPT Header | | GPT Entry | | Partition 1 |
* | (Protect | | | | Array | | (ESP 또는 /boot) |
* | ive MBR) | | | | | | |
* +-----------+ +------------+ +------------+ +------------------+
* | boot.img |
* | (446B) | BIOS Boot Partition (EF02)
* +----------------------------------+
* | core.img |
* | (diskboot + lzma + kernel + mods)|
* +----------------------------------+
*/
GRUB2 UEFI 모드
UEFI 환경에서는 BIOS의 다단계 로딩이 필요 없습니다. UEFI 펌웨어가 FAT 파일시스템(ESP)을 직접 읽을 수 있으므로, GRUB는 단일 PE32+ 실행 파일(grubx64.efi)로 제공됩니다.
| 항목 | BIOS 모드 | UEFI 모드 |
|---|---|---|
| 바이너리 | boot.img + core.img | grubx64.efi 단일 파일 |
| 설치 위치 | MBR + MBR gap / BIOS Boot Partition | ESP (/EFI/<distro>/) |
| 파일시스템 접근 | 자체 드라이버 (core.img 내장) | EFI Simple File System Protocol |
| 메모리 할당 | 직접 관리 (e820 맵 기반) | EFI Boot Services AllocatePages() |
| 비디오 출력 | VGA BIOS / VESA | EFI Graphics Output Protocol (GOP) |
| Stage 구분 | Stage 1 → 1.5 → 2 | 없음 (단일 단계) |
UEFI 모드에서 GRUB는 EFI System Table을 통해 Boot Services, Runtime Services에 접근합니다. 파일 I/O는 EFI_SIMPLE_FILE_SYSTEM_PROTOCOL을, 메모리 할당은 AllocatePages()/AllocatePool()을 사용합니다. 커널 로드 후 ExitBootServices()를 호출하면 펌웨어가 하드웨어 제어를 OS에 넘깁니다.
Secure Boot 환경에서는 Shim이 GRUB보다 먼저 로드되어 서명을 검증합니다. 체인은 shimx64.efi → grubx64.efi → vmlinuz 순서입니다. 자세한 내용은 UEFI: Shim 부트로더와 Secure Boot: Shim 검증 로직을 참고하십시오.
GRUB2 모듈 시스템
GRUB2의 모듈 아키텍처는 최소한의 코어로 부팅한 뒤 필요한 기능을 동적으로 로드하는 구조입니다. 모듈 디렉토리는 플랫폼에 따라 구분됩니다: BIOS는 /boot/grub/i386-pc/, UEFI는 /boot/grub/x86_64-efi/.
모듈은 두 가지 경로로 로드됩니다:
- core.img 임베디드:
grub-install시--modules옵션 또는 자동 감지로 core.img에 포함. 파일시스템 접근 전에 필요한 모듈(디스크, 파티션, 파일시스템 드라이버)이 여기에 해당 - 동적 insmod:
grub.cfg에서insmod명령으로 런타임에 로드. core.img가 파일시스템에 접근 가능한 이후부터 사용
| 카테고리 | 주요 모듈 | 설명 |
|---|---|---|
| 파일시스템 | ext2, xfs, btrfs, fat, ntfs, zfs | 파티션 내 파일 읽기 (ext2.mod는 ext2/3/4 모두 지원) |
| 파티션 | part_msdos, part_gpt, part_apple | 파티션 테이블 파싱 |
| 부트 | linux, chain, multiboot, bsd | OS별 로드 프로토콜 구현 |
| UI | gfxterm, gfxmenu, png, jpeg | 그래픽 터미널, 메뉴 테마, 이미지 렌더링 |
| 암호화 | luks, luks2, gcry_*, cryptodisk | 암호화 디스크 잠금 해제, 암호 알고리즘 |
| 네트워크 | net, efinet, http, tftp | PXE/HTTP 부팅, EFI 네트워크 스택 |
| 디스크 | biosdisk, ahci, nvme, lvm, mdraid* | 디스크 접근, LVM/RAID 볼륨 |
모듈 간 의존성은 자동으로 해결됩니다. 예를 들어 insmod btrfs를 실행하면 lzopio, zstd 등 압축 모듈이 자동 로드됩니다. 의존성 정보는 각 .mod 파일의 ELF 섹션에 기록되어 있습니다.
GRUB2 커널 로드 과정
grub.cfg의 linux 명령은 내부적으로 grub_cmd_linux() 함수를 호출합니다. 이 함수는 grub-core/loader/i386/linux.c에 구현되어 있으며, 다음 단계로 커널 이미지를 검증하고 로드합니다:
/* grub_cmd_linux() 내부 흐름 (간략화) */
/* 1. setup header 읽기 및 매직 검증 */
grub_file_read(file, &lh, sizeof(lh));
if (lh.header != 0x53726448) /* "HdrS" in little-endian */
return grub_error(GRUB_ERR_BAD_OS, "invalid magic");
/* 2. 프로토콜 버전 확인 (최소 2.00 이상) */
if (lh.version < 0x0200)
return grub_error(GRUB_ERR_BAD_OS, "too old protocol");
/* 3. loadflags 처리 */
if (!(lh.loadflags & LOADED_HIGH))
/* 0x100000(1MB) 이상에 로드 불가 → 에러 */
/* 4. 보호 모드 커널 메모리 할당: 1MB 이상 영역 */
prot_size = grub_file_size(file) - (lh.setup_sects + 1) * 512;
prot_mode_mem = grub_malloc(prot_size); /* 물리 주소 0x100000+ */
/* 5. 리얼 모드 코드 메모리 할당: 640KB 이하 영역 */
real_size = (lh.setup_sects + 1) * 512;
real_mode_mem = grub_malloc(real_size); /* 물리 주소 0x90000 부근 */
/* 6. 파일에서 읽어 각 영역에 복사 */
grub_file_read(file, real_mode_mem, real_size); /* setup 코드 */
grub_file_read(file, prot_mode_mem, prot_size); /* 압축 커널 */
/* 7. 커맨드 라인 파라미터 설정 */
lh.cmd_line_ptr = (__u32)cmd_line_addr;
lh.type_of_loader = 0x72; /* GRUB2 식별자 */
initrd 명령은 grub_cmd_initrd()를 호출하여 initramfs를 로드합니다:
/* grub_cmd_initrd() 핵심 동작 */
/* 1. initramfs 파일 읽기 (여러 파일 연결 가능) */
for (i = 0; i < nfiles; i++)
total_size += grub_file_size(files[i]);
/* 2. 메모리 할당: initrd_addr_max 이하 영역 */
addr = grub_malloc_below(total_size, lh.initrd_addr_max);
/* 3. setup header에 위치/크기 기록 */
lh.ramdisk_image = addr;
lh.ramdisk_size = total_size;
커널과 initramfs가 로드된 후의 메모리 레이아웃:
/*
* 커널 로드 후 물리 메모리 레이아웃
*
* 0 640KB 1MB ~End
* | | | |
* ───────┬───────────┬───────────┬──────────┬──────────┬──────────┤
* │ Real-mode │ Legacy │Protected │ │ │
* │ kernel │ Video │-mode │ (free) │ initramfs│
* │ + cmdline │ Memory │kernel │ │ │
* ───────┴───────────┴───────────┴──────────┴──────────┴──────────┤
* ↑0x90000 ↑0x100000 ↑ramdisk_image
* real_mode_mem prot_mode_mem
*/
Boot Protocol 상세
Linux Boot Protocol은 부트로더와 커널 간의 인터페이스를 정의합니다. 커널 소스의 Documentation/arch/x86/boot.rst에 공식 문서화되어 있으며, 핵심 데이터 구조는 boot_params입니다:
/* arch/x86/include/uapi/asm/bootparam.h */
struct boot_params {
struct screen_info screen_info; /* 0x000 */
struct apm_bios_info apm_bios_info; /* 0x040 */
__u8 _pad2[4]; /* 0x054 */
__u64 tboot_addr; /* 0x058 */
struct ist_info ist_info; /* 0x060 */
__u64 acpi_rsdp_addr; /* 0x070 */
__u8 _pad3[8]; /* 0x078 */
__u8 hd0_info[16]; /* 0x080 */
__u8 hd1_info[16]; /* 0x090 */
struct sys_desc_table sys_desc_table; /* 0x0a0 */
struct olpc_ofw_header olpc_ofw_header; /* 0x0b0 */
__u32 ext_ramdisk_image; /* 0x0c0 */
__u32 ext_ramdisk_size; /* 0x0c4 */
__u32 ext_cmd_line_ptr; /* 0x0c8 */
__u8 _pad4[112]; /* 0x0cc */
struct edid_info edid_info; /* 0x140 */
struct efi_info efi_info; /* 0x1c0 */
__u32 alt_mem_k; /* 0x1e0 */
__u8 e820_entries; /* 0x1e8 */
__u8 eddbuf_entries; /* 0x1e9 */
struct setup_header hdr; /* 0x1f1 — setup header 포함 */
__u8 _pad7[40];
struct edd_info eddbuf[EDDMAXNR]; /* EDD BIOS 디스크 정보 */
struct boot_e820_entry e820_table[E820_MAX_ENTRIES_ZEROPAGE];
};
boot_params.hdr에 포함된 setup_header의 주요 필드:
/* arch/x86/include/uapi/asm/bootparam.h — setup_header */
struct setup_header {
__u8 setup_sects; /* setup 코드의 섹터 수 */
__u16 root_flags; /* 루트 읽기 전용 플래그 */
__u32 syssize; /* 보호 모드 코드 크기 (16B 단위) */
__u16 ram_size; /* (미사용, 호환성) */
__u16 vid_mode; /* 비디오 모드 */
__u16 root_dev; /* 루트 디바이스 번호 */
__u16 boot_flag; /* 0xAA55 매직 */
__u16 jump; /* setup 진입 점프 명령어 */
__u32 header; /* "HdrS" (0x53726448) 매직 */
__u16 version; /* 부트 프로토콜 버전 */
__u32 realmode_swtch; /* 리얼 모드 전환 후크 */
__u16 start_sys_seg; /* (obsolete) */
__u16 kernel_version; /* 커널 버전 문자열 포인터 */
__u8 type_of_loader; /* 부트로더 식별자 */
__u8 loadflags; /* 로드 옵션 비트필드 */
__u16 setup_move_size; /* setup 코드 이동 크기 */
__u32 code32_start; /* 32-bit 진입점 주소 */
__u32 ramdisk_image; /* initrd 물리 주소 */
__u32 ramdisk_size; /* initrd 크기 */
__u32 bootsect_kludge; /* (obsolete) */
__u16 heap_end_ptr; /* setup 힙 끝 포인터 */
__u8 ext_loader_ver; /* 확장 로더 버전 */
__u8 ext_loader_type; /* 확장 로더 타입 */
__u32 cmd_line_ptr; /* 커맨드 라인 물리 주소 */
__u32 initrd_addr_max; /* initrd 최대 주소 */
__u32 kernel_alignment; /* 커널 정렬 요구사항 */
__u8 relocatable_kernel; /* 재배치 가능 여부 */
__u8 min_alignment; /* 최소 정렬 (2^n) */
__u16 xloadflags; /* 64-bit 관련 플래그 */
__u32 cmdline_size; /* 커맨드 라인 최대 크기 */
__u32 payload_offset; /* 압축 페이로드 오프셋 */
__u32 payload_length; /* 압축 페이로드 크기 */
__u64 setup_data; /* setup_data 연결 리스트 포인터 */
__u64 pref_address; /* 선호 로드 주소 */
__u32 init_size; /* 초기화 시 필요한 선형 메모리 크기 */
__u32 handover_offset; /* EFI 핸드오버 진입점 오프셋 */
};
type_of_loader 필드는 부트로더를 식별합니다. 상위 4비트가 로더 ID, 하위 4비트가 버전입니다:
| 값 | 부트로더 | 비고 |
|---|---|---|
0x0_ | LILO | 역사적 기본 로더 |
0x1_ | Loadlin | DOS에서 리눅스 부팅 |
0x2_ | bootsect-loader | (obsolete) |
0x3_ | SYSLINUX | FAT/NTFS/ext 부트로더 |
0x4_ | EtherBoot | 네트워크 부트 |
0x5_ | ELILO | EFI LILO |
0x7_ | GRUB | Legacy + GRUB2 |
0xD_ | kexec | 소프트 리부트 |
0xE_ | Extended | ext_loader_type 사용 |
loadflags 비트필드:
/* loadflags 비트 정의 */
#define LOADED_HIGH 0x01 /* 보호 모드 코드를 0x100000 이상에 로드 */
#define KASLR_FLAG 0x02 /* KASLR 사용 (커널 주소 공간 레이아웃 랜덤화) */
#define QUIET_FLAG 0x20 /* "quiet" 부트 파라미터 설정 */
#define KEEP_SEGMENTS 0x40 /* 32-bit 진입 시 세그먼트 재설정 안 함 */
#define CAN_USE_HEAP 0x80 /* heap_end_ptr 유효, setup 힙 사용 가능 */
setup_data 연결 리스트는 프로토콜 2.09+에서 도입되어, 부트로더가 커널에 확장 데이터를 전달하는 메커니즘입니다:
/* setup_data 노드 구조 */
struct setup_data {
__u64 next; /* 다음 노드의 물리 주소 (0이면 끝) */
__u32 type; /* SETUP_NONE, SETUP_E820_EXT, SETUP_DTB, ... */
__u32 len; /* data[] 크기 */
__u8 data[]; /* 가변 길이 페이로드 */
};
/* type 상수 */
#define SETUP_NONE 0
#define SETUP_E820_EXT 1 /* 확장 e820 메모리 맵 */
#define SETUP_DTB 2 /* Device Tree Blob */
#define SETUP_PCI 3 /* PCI ROM 데이터 */
#define SETUP_EFI 4 /* EFI 관련 데이터 */
#define SETUP_APPLE_PROPERTIES 5 /* Apple 펌웨어 속성 */
#define SETUP_JAILHOUSE 6 /* Jailhouse 하이퍼바이저 정보 */
#define SETUP_CC_BLOB 7 /* 기밀 컴퓨팅 Blob */
#define SETUP_IMA 8 /* IMA 측정 로그 */
프로토콜 버전 히스토리 (주요 변경사항):
| 버전 | 커널 | 주요 추가사항 |
|---|---|---|
| 2.00 | 1.3.73 | setup_header 도입, HdrS 매직 |
| 2.02 | 2.4.0 | cmd_line_ptr (32-bit 주소) |
| 2.05 | 2.6.20 | relocatable_kernel 필드 |
| 2.07 | 2.6.22 | hardware_subarch, 가상화 지원 |
| 2.09 | 2.6.26 | setup_data 연결 리스트 |
| 2.10 | 2.6.31 | kernel_alignment 강제 적용 |
| 2.11 | 3.0 | EFI 핸드오버 프로토콜 |
| 2.12 | 3.8 | xloadflags, 64-bit 부트 지원 |
| 2.13 | 3.14 | pref_address 필드 |
| 2.15 | 5.5 | E820 확장, IMA 로그 전달 |
부트 프로토콜 핸드오프
부트로더가 커널에 제어를 넘기는 순간은 부트 과정에서 가장 까다로운 전환점입니다. Documentation/arch/x86/boot.rst에 정의된 핸드오프 규약을 정확히 따르지 않으면 커널이 즉시 트리플 폴트로 리셋됩니다.
32-bit 부트 프로토콜 (Legacy BIOS) — 진입점: code32_start (기본 0x100000):
/* 32-bit 진입 시 CPU 레지스터 요구사항 */
CS = __BOOT_CS (0x10) /* 32-bit 읽기/실행, base 0, limit 0xFFFFFFFF */
DS = __BOOT_DS (0x18) /* 32-bit 읽기/쓰기, base 0, limit 0xFFFFFFFF */
ES = __BOOT_DS /* DS와 동일 (flat 세그먼트) */
SS = __BOOT_DS /* DS와 동일 */
FS = (don't care)
GS = (don't care)
ESP = boot_params 구조체 이후 적절한 스택 포인터
ESI = struct boot_params* /* boot_params의 물리 주소 */
EBP = 0
EDI = 0
EBX = 0
/* 필수 조건 */
- 인터럽트 비활성 (EFLAGS.IF = 0, CLI 실행)
- A20 게이트 활성화
- NMI 비활성 (port 0x70 bit 7 = 1)
- GDT: CS/DS 최소 엔트리 설정, flat 모델
64-bit 부트 프로토콜 — 진입점: code32_start + 0x200 (프로토콜 2.12+, xloadflags bit 0 설정):
/* 64-bit 진입 시 추가 요구사항 */
- CPU가 Long Mode (IA-32e)에 있어야 함
- CR0.PG = 1 (페이징 활성화)
- CR4.PAE = 1 (PAE 활성화)
- Identity mapping: 부트로더가 사용하는 메모리 영역
- 페이지 테이블은 4-level (PML4) 또는 5-level (PML5)
- CS: 64-bit 코드 세그먼트 (L=1, D=0)
- RSI = struct boot_params* /* 64-bit 물리 주소 */
핸드오프 시 부트로더의 필수 초기화:
- 반드시 초기화: e820 메모리 맵, 커맨드 라인(
cmd_line_ptr), VGA 정보(screen_info), EFI 정보(UEFI 부트 시) - 절대 금지: 보호 모드 커널 영역(
0x100000+) 덮어쓰기, A20 게이트 비활성 상태로 전달,boot_params외 영역에 중요 데이터 배치 - 선택 사항: APM BIOS 정보, EDD 디스크 정보,
setup_data리스트
bzImage 구조 상세
커널 빌드 시 생성되는 bzImage(big zImage)는 부팅 가능한 단일 이미지로, 리얼 모드 셋업 코드와 압축된 보호 모드 커널을 결합합니다.
빌드 파이프라인:
/*
* bzImage 빌드 과정 (arch/x86/boot/Makefile + arch/x86/boot/compressed/Makefile)
*
* vmlinux (ELF, ~20MB+)
* │
* ├─ objcopy -O binary -R .note -R .comment -S
* ▼
* vmlinux.bin (flat binary, 디버그 정보 제거)
* │
* ├─ 압축 (gzip/lzma/xz/lzo/lz4/zstd 중 선택)
* ▼
* vmlinux.bin.{gz,lzma,xz,lzo,lz4,zst}
* │
* ├─ objcopy → piggy.o (압축 데이터를 ELF 오브젝트로 래핑)
* │
* ├─ 링크: head_{32,64}.o + misc.o + piggy.o + 기타
* ▼
* vmlinux.bin (압축 해제 코드 + 압축 커널, flat binary)
* │
* ├─ setup.bin (header.S + main.c + pm.c + ...)
* │ + vmlinux.bin 결합
* ▼
* bzImage (최종 부팅 이미지)
*/
bzImage 바이너리 구조:
/*
* bzImage 내부 레이아웃
*
* Offset 0x000 setup_sects * 512
* ┌──────────────┬──────────────────────┬─────────────────────────────────┐
* │ Boot Sector │ Setup Code │ Protected-mode kernel │
* │ (512 bytes) │ (N × 512 bytes) │ (vmlinux.bin: 압축 해제 코드 │
* │ │ │ + 압축된 커널 페이로드) │
* │ header.S │ main.c, pm.c, │ │
* │ setup_header │ video.c, memory.c │ head_{32,64}.S → misc.c │
* │ 부트 서명 │ A20, 비디오, e820 │ → decompress → vmlinux │
* │ (0xAA55) │ 프로텍티드 모드 전환 │ │
* └──────────────┴──────────────────────┴─────────────────────────────────┘
* ↑
* (setup_sects + 1) × 512 바이트 오프셋
*/
지원 압축 알고리즘 (커널 설정 옵션):
| 알고리즘 | 커널 옵션 | 압축률 | 해제 속도 | 비고 |
|---|---|---|---|---|
| gzip | CONFIG_KERNEL_GZIP | 보통 | 보통 | 기본값, 가장 넓은 호환성 |
| LZMA | CONFIG_KERNEL_LZMA | 높음 | 느림 | 높은 압축률, 느린 해제 |
| XZ | CONFIG_KERNEL_XZ | 매우 높음 | 느림 | LZMA2 기반, 최고 압축률 |
| LZO | CONFIG_KERNEL_LZO | 낮음 | 빠름 | 빠른 해제, 임베디드에 적합 |
| LZ4 | CONFIG_KERNEL_LZ4 | 낮음 | 매우 빠름 | 최고속 해제 |
| Zstandard | CONFIG_KERNEL_ZSTD | 높음 | 빠름 | 압축률과 속도의 균형 (권장) |
PE/COFF 헤더 오버레이: CONFIG_EFI_STUB이 활성화되면, bzImage의 boot sector 영역에 PE/COFF(Portable Executable) 헤더가 오버레이됩니다. 이를 통해 UEFI 펌웨어가 bzImage를 PE 실행 파일로 인식하여 직접 로드할 수 있습니다. header.S의 MZ 매직(오프셋 0)과 PE 시그니처가 이 역할을 합니다. 자세한 내용은 UEFI: Linux EFI Stub을 참고하십시오.
vmlinux는 ELF 형식의 비압축 커널로, 디버깅과 심볼 분석에 사용됩니다. vmlinuz는 압축된 커널 바이너리를 가리키는 일반적인 이름입니다. bzImage는 boot sector + setup code + 압축 커널을 합친 부팅 가능 이미지이며, make bzImage 시 arch/x86/boot/Makefile이 조합합니다. 배포판에서 /boot/vmlinuz-*로 설치되는 파일이 실제로는 bzImage입니다.
systemd-boot (sd-boot)
systemd-boot(구 gummiboot)는 UEFI 전용 부트 매니저로, GRUB2와 정반대의 설계 철학을 가집니다: 스크립팅 없음, 자체 파일시스템 드라이버 없음, ESP(FAT)만 접근. UEFI 펌웨어의 기능에 최대한 의존하여 코드 복잡도를 최소화합니다.
부트 엔트리 유형:
Type #1 — Boot Loader Specification (BLS) 엔트리: ESP 내 /loader/entries/*.conf 파일로 정의됩니다:
# /boot/efi/loader/entries/linux-6.8.0.conf
title Arch Linux (6.8.0-arch1-1)
linux /vmlinuz-linux
initrd /initramfs-linux.img
options root=UUID=a1b2c3d4-... rw quiet
Type #2 — Unified Kernel Image (UKI): 커널, initramfs, 커맨드 라인, OS 릴리스 정보를 단일 PE 바이너리로 결합한 형태입니다. UEFI가 직접 실행할 수 있어 부트 매니저 없이도 부팅 가능합니다:
# UKI 내부 PE 섹션 구조
.osrel ← /etc/os-release (OS 식별 정보)
.cmdline ← 커널 커맨드 라인
.linux ← 리눅스 커널 (bzImage)
.initrd ← initramfs 이미지
.splash ← 부트 스플래시 이미지 (선택)
.dtb ← Device Tree Blob (선택)
.uname ← 커널 버전 문자열
# UKI 생성 (ukify 도구)
ukify build \
--linux=/boot/vmlinuz-linux \
--initrd=/boot/initramfs-linux.img \
--cmdline="root=UUID=... rw quiet" \
--output=/boot/efi/EFI/Linux/arch-linux.efi
관리 명령어:
# systemd-boot 설치 (ESP에 복사)
bootctl install
# 상태 확인 (펌웨어, 부트 엔트리, Secure Boot 상태)
bootctl status
# 커널 설치 자동화 (kernel-install 프레임워크)
kernel-install add 6.8.0 /boot/vmlinuz-6.8.0
# → /boot/efi/loader/entries/ 에 BLS 엔트리 자동 생성
# → 또는 /boot/efi/EFI/Linux/ 에 UKI 자동 빌드 (설정에 따라)
CONFIG_EFI_STUB 기능에 의존합니다. EFI Stub은 GRUB 같은 별도 부트로더 없이 UEFI 펌웨어가 커널을 직접 실행할 수 있게 합니다. 설정 방법은 UEFI: EFI Stub 직접 부팅 설정을 참고하십시오.
기타 리눅스 부트로더
SYSLINUX / ISOLINUX / PXELINUX: SYSLINUX 프로젝트는 특수 환경에 최적화된 경량 부트로더 패밀리입니다. SYSLINUX는 FAT 파일시스템(USB 스틱), ISOLINUX는 ISO 9660(라이브 CD/DVD), PXELINUX는 네트워크(PXE) 부팅을 담당합니다. 단순한 설정 파일(syslinux.cfg)로 구성되며, 라이브 미디어와 설치 이미지에서 널리 사용됩니다.
LILO (Linux Loader): 리눅스 초창기(1992~)의 표준 부트로더였습니다. 파일시스템을 인식하지 못하고 커널의 디스크 상 물리 위치(섹터 번호)를 맵 파일에 기록하는 방식이므로, 커널 업데이트마다 lilo 명령을 재실행해야 했습니다. 2015년 이후 사실상 개발이 중단되었습니다.
rEFInd: UEFI 환경의 부트 매니저로, 다른 부트로더나 OS를 선택하는 역할에 집중합니다. ESP를 자동 스캔하여 부팅 가능한 EFI 바이너리를 감지하고, 커널의 EFI Stub을 통해 직접 리눅스를 로드할 수도 있습니다. 그래픽 메뉴, 아이콘 테마, 멀티부트 환경에 특화되어 있습니다.
U-Boot (Das U-Boot): ARM, MIPS, RISC-V 등 임베디드 시스템의 사실상 표준 부트로더입니다. Device Tree를 커널에 전달하고, FIT(Flattened Image Tree) 이미지 포맷으로 커널 + DTB + initramfs를 하나로 묶을 수 있습니다. 스크립팅, 네트워크 부팅, NAND/NOR 플래시 지원 등 풍부한 기능을 제공합니다. 상세한 내용은 아래 U-Boot (Das U-Boot) 전용 섹션을 참고하십시오.
kexec: 엄밀히는 부트로더가 아닌 커널 내 소프트 리부트 메커니즘입니다. 실행 중인 커널에서 새 커널을 직접 로드하여 펌웨어(BIOS/UEFI)를 거치지 않고 부팅합니다. 두 가지 시스템 콜을 제공합니다:
/* kexec 시스템 콜 */
long kexec_load(
unsigned long entry, /* 새 커널 진입점 */
unsigned long nr_segments, /* 메모리 세그먼트 수 */
struct kexec_segment *segments, /* 커널/initrd 세그먼트 */
unsigned long flags /* KEXEC_ON_CRASH 등 */
);
/* 파일 기반 kexec (서명 검증 지원) */
long kexec_file_load(
int kernel_fd, /* 커널 파일 디스크립터 */
int initrd_fd, /* initrd 파일 디스크립터 */
unsigned long cmdline_len,
const char *cmdline,
unsigned long flags
);
kexec_file_load()는 Secure Boot/IMA 환경에서 커널 서명을 검증합니다. CONFIG_KEXEC_SIG이 활성화된 경우 서명되지 않은 커널 로드를 거부합니다. 자세한 내용은 Secure Boot: kexec 서명 검증을 참고하십시오.
U-Boot (Das U-Boot)
U-Boot(Universal Boot Loader)는 ARM, MIPS, RISC-V, PowerPC, x86 등 다양한 아키텍처를 지원하는 임베디드 시스템의 사실상 표준 부트로더입니다. 2000년 Wolfgang Denk가 8xxROM(PowerPC용)을 포크하여 PPCBoot로 시작했으며, 2002년 다중 아키텍처 지원을 추가하면서 U-Boot로 개명되었습니다. GPLv2 라이선스로 배포되며, Linux 커널과 유사한 Kconfig 기반 빌드 시스템을 사용합니다.
부팅 단계 아키텍처
U-Boot의 부팅 과정은 하드웨어 초기화를 여러 단계로 분리하여, 제한된 SRAM에서 시작하여 점진적으로 전체 DRAM을 활용하는 구조입니다:
SPL(Secondary Program Loader)은 U-Boot의 축소 버전으로, SoC 내장 SRAM(보통 64~256KB)에서 실행됩니다. BootROM이 SPL을 로드하면, SPL은 DRAM 컨트롤러를 초기화한 후 전체 U-Boot 이미지를 DRAM에 로드합니다. TPL(Tertiary Program Loader)은 SRAM이 극히 제한적인 플랫폼에서 SPL 이전에 실행되는 추가 단계로, DRAM 초기화만 담당하고 SPL에 제어를 넘깁니다.
/* U-Boot SPL 진입점: arch/arm/lib/crt0_aarch64.S */
_start:
/* 예외 벡터 테이블 설정 */
b reset
...
reset:
/* EL3에서 시작: 보안 설정 */
mrs x0, CurrentEL
cmp x0, #0xC /* EL3? */
b.ne 1f
/* GIC, 보안 레지스터 초기화 */
bl lowlevel_init
1:
/* 스택 포인터 설정 (SRAM 영역) */
ldr x0, =CONFIG_SPL_STACK
mov sp, x0
/* board_init_f(): DRAM 컨트롤러 초기화 */
bl board_init_f
/* DRAM 사용 가능: U-Boot proper 로드 */
bl board_init_r
빌드 시스템
U-Boot는 Linux 커널과 동일한 Kconfig + Makefile 빌드 시스템을 사용합니다. 각 보드마다 configs/ 디렉토리에 defconfig 파일이 존재하며, 크로스 컴파일러를 지정하여 빌드합니다:
# 빌드 예제: Raspberry Pi 4 (64-bit)
$ export CROSS_COMPILE=aarch64-linux-gnu-
$ make rpi_4_defconfig # 보드 설정 적용
$ make menuconfig # 선택적: GUI 설정 수정
$ make -j$(nproc) # 빌드
# 빌드 산출물
u-boot # ELF 바이너리 (디버깅용)
u-boot.bin # raw 바이너리 (U-Boot proper)
u-boot.img # mkimage 헤더 포함 이미지
spl/u-boot-spl.bin # SPL 바이너리
# NXP i.MX 계열: SPL + U-Boot 결합 이미지
$ make flash.bin # imx-mkimage로 통합 이미지 생성
# Rockchip 계열: idbloader 생성
$ tools/mkimage -n rk3399 -T rksd -d spl/u-boot-spl.bin idbloader.img
주요 빌드 설정 옵션:
| CONFIG 옵션 | 설명 |
|---|---|
CONFIG_SPL | SPL 빌드 활성화 |
CONFIG_TPL | TPL 빌드 활성화 (3단계 부팅) |
CONFIG_SYS_TEXT_BASE | U-Boot proper 로드 주소 |
CONFIG_SPL_TEXT_BASE | SPL 로드 주소 (SRAM 내) |
CONFIG_DEFAULT_FDT_FILE | 기본 Device Tree 파일명 |
CONFIG_BOOTDELAY | 자동 부팅 대기 시간 (초) |
CONFIG_DISTRO_DEFAULTS | 표준 배포판 부팅 지원 |
CONFIG_FIT | FIT 이미지 포맷 지원 |
CONFIG_FIT_SIGNATURE | FIT 이미지 서명 검증 |
CONFIG_CMD_NET | 네트워크 명령어 활성화 |
환경 변수 시스템
U-Boot 환경 변수는 부팅 동작을 제어하는 핵심 메커니즘입니다. 변수는 비휘발성 저장소에 영속되며, 셸에서 동적으로 수정할 수 있습니다:
# 환경 변수 저장 위치 (CONFIG로 선택)
CONFIG_ENV_IS_IN_MMC # eMMC/SD 카드의 고정 오프셋
CONFIG_ENV_IS_IN_FAT # FAT 파일시스템 내 파일
CONFIG_ENV_IS_IN_NAND # NAND 플래시
CONFIG_ENV_IS_IN_SPI_FLASH# SPI NOR 플래시
CONFIG_ENV_IS_NOWHERE # 영속 없음 (기본값만 사용)
주요 환경 변수:
| 변수 | 설명 | 예제 값 |
|---|---|---|
bootcmd | 자동 부팅 시 실행할 명령 시퀀스 | run distro_bootcmd |
bootargs | 커널 명령줄 인자 | console=ttyS0,115200 root=/dev/mmcblk0p2 rootwait |
bootdelay | 자동 부팅 대기 시간 (초) | 3 |
fdtfile | Device Tree Blob 파일명 | rockchip/rk3399-rock-pi-4.dtb |
kernel_addr_r | 커널 로드 주소 | 0x40080000 |
fdt_addr_r | DTB 로드 주소 | 0x4FA00000 |
ramdisk_addr_r | initramfs 로드 주소 | 0x4FF00000 |
serverip | TFTP 서버 IP 주소 | 192.168.1.100 |
ipaddr | 보드 자체 IP 주소 | 192.168.1.50 |
# U-Boot 셸에서 환경 변수 조작
=> env print bootargs # 특정 변수 출력
bootargs=console=ttyS0,115200 root=/dev/mmcblk0p2 rootwait
=> env set bootargs 'console=ttyS0,115200 root=/dev/mmcblk0p2 rootwait rw'
=> env save # 비휘발성 저장소에 저장
Saving Environment to MMC... Writing to MMC(0)... OK
=> env default -a # 모든 변수를 기본값으로 초기화
=> env export -t 0x4000000 # 환경을 텍스트로 메모리에 내보내기
핵심 명령어
U-Boot 셸은 부팅, 메모리, 저장장치, 네트워크 제어를 위한 다양한 명령어를 제공합니다:
| 카테고리 | 명령어 | 설명 |
|---|---|---|
| 부팅 | bootm [addr] | mkimage 헤더가 있는 이미지 부팅 (uImage/FIT) |
bootz [addr] [initrd] [fdt] | zImage (ARM 32비트 압축 커널) 부팅 | |
booti [addr] [initrd] [fdt] | Image (ARM64 커널) 부팅 | |
boot | bootcmd 환경 변수 실행 | |
| 메모리 | md [addr] [count] | 메모리 덤프 (hex) |
mw [addr] [val] [count] | 메모리 쓰기 | |
cp [src] [dst] [count] | 메모리 복사 | |
cmp [addr1] [addr2] [count] | 메모리 비교 | |
| 저장장치 | mmc info | 현재 MMC 장치 정보 |
mmc read [addr] [blk] [cnt] | MMC 블록 읽기 → 메모리 | |
load mmc 0:1 [addr] [file] | MMC 파티션에서 파일 로드 | |
nand read [addr] [off] [size] | NAND 플래시 읽기 | |
| 네트워크 | dhcp | DHCP로 IP 획득 + 파일 로드 |
tftp [addr] [file] | TFTP 서버에서 파일 다운로드 | |
ping [ip] | 네트워크 연결 확인 | |
nfs [addr] [ip:path] | NFS로 파일 다운로드 | |
| 기타 | fdt addr [addr] | DTB 작업 대상 주소 설정 |
fdt print [path] | Device Tree 노드 출력 | |
reset | 시스템 리셋 |
부트 스크립트와 Distro Boot
U-Boot는 부팅 과정을 자동화하기 위한 스크립트 기능을 제공합니다. 텍스트 명령 파일을 mkimage로 래핑하여 boot.scr 이미지를 생성합니다:
# boot.cmd — U-Boot 부트 스크립트 소스
setenv bootargs console=ttyS2,1500000 root=/dev/mmcblk1p2 rootwait rw
load mmc 1:1 ${kernel_addr_r} Image
load mmc 1:1 ${fdt_addr_r} dtbs/rockchip/rk3399-rock-pi-4b.dtb
load mmc 1:1 ${ramdisk_addr_r} initramfs.img
booti ${kernel_addr_r} ${ramdisk_addr_r}:${filesize} ${fdt_addr_r}
# mkimage로 boot.scr 생성
$ mkimage -C none -A arm64 -T script -d boot.cmd boot.scr
Distro Boot(CONFIG_DISTRO_DEFAULTS)는 다양한 리눅스 배포판을 표준화된 방식으로 부팅하기 위한 프레임워크입니다. 여러 부팅 매체(MMC, USB, PXE, DHCP)를 순서대로 탐색하며, 각 매체에서 extlinux/extlinux.conf 파일이나 boot.scr 스크립트를 찾아 실행합니다:
# /boot/extlinux/extlinux.conf — SYSLINUX 호환 설정
label linux
kernel /Image
fdt /dtbs/rockchip/rk3399-rock-pi-4b.dtb
append console=ttyS2,1500000 root=/dev/mmcblk1p2 rootwait rw
# Distro Boot 탐색 순서 (boot_targets 환경 변수)
boot_targets=mmc1 mmc0 usb0 pxe dhcp
# 각 매체에서의 탐색 순서:
# 1. boot.scr.uimg / boot.scr
# 2. extlinux/extlinux.conf
이미지 포맷과 mkimage
mkimage는 U-Boot이 인식하는 이미지 포맷을 생성하는 핵심 유틸리티입니다. U-Boot은 두 가지 주요 이미지 포맷을 지원합니다:
| 특성 | Legacy uImage | FIT Image (Flattened Image Tree) |
|---|---|---|
| 헤더 크기 | 64바이트 고정 | 가변 (DTB 구조) |
| 포함 가능 항목 | 단일 데이터 블롭 | 커널 + DTB + initramfs + 설정 등 다수 |
| 다중 설정 | 미지원 | 지원 (하나의 이미지로 여러 보드 대응) |
| 해시/서명 | CRC32만 | SHA-256, RSA 서명 (Verified Boot) |
| 권장 여부 | 레거시 (호환용) | 권장 (현재 표준) |
mkimage는 U-Boot 소스의 tools/ 디렉터리에서 빌드하거나, 배포판 패키지(u-boot-tools)로 설치할 수 있습니다:
# Debian/Ubuntu
$ sudo apt install u-boot-tools
# Fedora/RHEL
$ sudo dnf install uboot-tools
# U-Boot 소스에서 직접 빌드
$ make tools-only_defconfig && make tools-only
# → tools/mkimage 생성
Legacy uImage는 64바이트 image_header_t 헤더를 데이터 앞에 붙이는 단순한 형식입니다:
/* include/image.h — Legacy uImage 헤더 구조체 */
typedef struct image_header {
__be32 ih_magic; /* 매직 넘버: 0x27051956 */
__be32 ih_hcrc; /* 헤더 CRC32 체크섬 */
__be32 ih_time; /* 이미지 생성 타임스탬프 */
__be32 ih_size; /* 데이터 크기 (헤더 제외) */
__be32 ih_load; /* 로드 주소 (Data Load Address) */
__be32 ih_ep; /* 진입점 (Entry Point Address) */
__be32 ih_dcrc; /* 데이터 CRC32 체크섬 */
uint8_t ih_os; /* OS 타입 (IH_OS_LINUX 등) */
uint8_t ih_arch; /* 아키텍처 (IH_ARCH_ARM 등) */
uint8_t ih_type; /* 이미지 타입 (IH_TYPE_KERNEL 등) */
uint8_t ih_comp; /* 압축 방식 (IH_COMP_GZIP 등) */
uint8_t ih_name[32]; /* 이미지 이름 (널 종료 문자열) */
} image_header_t;
# Legacy uImage 생성 예시
$ mkimage -A arm -O linux -T kernel -C none \
-a 0x80008000 -e 0x80008000 \
-n "Linux-6.8.0" -d arch/arm/boot/zImage uImage
# 리눅스 커널 빌드 시스템 통합
$ make ARCH=arm CROSS_COMPILE=arm-linux-gnueabihf- \
LOADADDR=0x80008000 uImage
zImage는 자체 압축 해제 코드를 포함하므로 -C none을 사용합니다 (이미 압축된 상태). 원본 Image를 직접 gzip으로 압축한 경우에만 -C gzip을 사용합니다. 잘못된 압축 플래그는 부팅 실패의 흔한 원인입니다.
FIT (Flattened Image Tree)
FIT는 커널, DTB, initramfs, 설정 등 여러 구성 요소를 단일 이미지로 패키징하는 U-Boot의 고급 이미지 포맷입니다. Device Tree Source와 유사한 ITS(Image Tree Source) 파일로 정의합니다:
/* fitimage.its — FIT 이미지 정의 */
/dts-v1/;
/ {
description = "Linux kernel with DTB and initramfs";
#address-cells = <1>;
images {
kernel {
description = "Linux kernel";
data = /incbin/("Image.gz");
type = "kernel";
arch = "arm64";
os = "linux";
compression = "gzip";
load = <0x40080000>;
entry = <0x40080000>;
hash-1 {
algo = "sha256";
};
};
fdt-rk3399 {
description = "Rockchip RK3399 DTB";
data = /incbin/("rk3399-rock-pi-4b.dtb");
type = "flat_dt";
arch = "arm64";
compression = "none";
hash-1 {
algo = "sha256";
};
};
ramdisk {
description = "initramfs";
data = /incbin/("initramfs.cpio.gz");
type = "ramdisk";
arch = "arm64";
os = "linux";
compression = "gzip";
hash-1 {
algo = "sha256";
};
};
};
configurations {
default = "standard";
standard {
description = "Standard Boot";
kernel = "kernel";
fdt = "fdt-rk3399";
ramdisk = "ramdisk";
};
};
};
# FIT 이미지 빌드 및 부팅
$ mkimage -f fitimage.its fitimage.itb
# U-Boot 셸에서 FIT 이미지 부팅
=> load mmc 0:1 ${loadaddr} fitimage.itb
=> bootm ${loadaddr} # 기본 configuration 사용
=> bootm ${loadaddr}#standard # 특정 configuration 선택
네트워크 부팅
U-Boot의 네트워크 부팅은 임베디드 개발에서 빈번한 커널 교체를 위해 필수적입니다. TFTP, DHCP, NFS를 조합하여 완전한 네트워크 기반 개발 환경을 구성할 수 있습니다:
# 개발 환경 네트워크 부팅 설정
=> setenv serverip 192.168.1.100 # TFTP 서버 주소
=> setenv ipaddr 192.168.1.50 # 보드 IP 주소
=> setenv netmask 255.255.255.0
# 방법 1: TFTP로 커널 + DTB 로드 후 NFS root 부팅
=> tftp ${kernel_addr_r} Image
=> tftp ${fdt_addr_r} rk3399-rock-pi-4b.dtb
=> setenv bootargs 'console=ttyS2,1500000 root=/dev/nfs nfsroot=192.168.1.100:/srv/nfs/rootfs,v3 ip=dhcp rw'
=> booti ${kernel_addr_r} - ${fdt_addr_r}
# 방법 2: DHCP로 자동 IP + 파일 로드
=> dhcp ${kernel_addr_r} Image # DHCP + TFTP 파일 로드
=> tftp ${fdt_addr_r} rk3399-rock-pi-4b.dtb
=> booti ${kernel_addr_r} - ${fdt_addr_r}
# 자동 네트워크 부팅 스크립트 (boot.cmd)
dhcp ${kernel_addr_r} Image
tftp ${fdt_addr_r} ${fdtfile}
setenv bootargs console=ttyS2,1500000 root=/dev/nfs nfsroot=${serverip}:/srv/nfs/rootfs,v3 ip=dhcp rw
booti ${kernel_addr_r} - ${fdt_addr_r}
Device Model (DM)
U-Boot는 Linux 커널의 드라이버 모델에서 영감을 받은 Driver Model(DM)을 사용합니다. DM은 Device Tree 기반으로 장치를 자동 탐색하고, uclass(장치 클래스)별로 표준화된 인터페이스를 제공합니다:
/* U-Boot Driver Model 구조 */
/* uclass: 장치 클래스 (리눅스의 bus_type에 해당) */
UCLASS_DRIVER(mmc) = {
.id = UCLASS_MMC,
.name = "mmc",
.per_device_auto = sizeof(struct mmc_uclass_priv),
};
/* 드라이버 정의 (리눅스의 platform_driver에 해당) */
static const struct udevice_id rockchip_dwmmc_ids[] = {
{ .compatible = "rockchip,rk3399-dw-mshc" },
{ }
};
U_BOOT_DRIVER(rockchip_dwmmc) = {
.name = "rockchip_dwmmc",
.id = UCLASS_MMC,
.of_match = rockchip_dwmmc_ids, /* DT 바인딩 */
.probe = rockchip_dwmmc_probe,
.priv_auto = sizeof(struct rockchip_dwmmc_priv),
.ops = &dm_dwmci_ops, /* uclass 표준 ops */
};
uclass는 리눅스의 bus_type/class에, U_BOOT_DRIVER는 platform_driver에, of_match는 of_device_id에 대응합니다. 이 유사성 덕분에 리눅스 드라이버를 U-Boot으로 포팅하는 작업이 비교적 수월합니다.
Verified Boot (보안 부팅)
U-Boot의 Verified Boot는 FIT 이미지의 RSA/ECDSA 서명을 검증하여, 변조되지 않은 커널과 DTB만 실행되도록 보장합니다. 서명 키의 공개키는 U-Boot 자체의 제어 DTB에 내장됩니다:
# 1. RSA 키 쌍 생성
$ openssl genpkey -algorithm RSA -out dev.key \
-pkeyopt rsa_keygen_bits:2048
$ openssl req -batch -new -x509 -key dev.key -out dev.crt
# 2. FIT 이미지 생성 + 서명 (공개키를 u-boot.dtb에 삽입)
$ mkimage -f fitimage.its \
-K u-boot.dtb \ # 공개키가 여기에 기록됨
-k keys/ \ # dev.key, dev.crt 위치
-r \ # 'required' 플래그 설정
fitimage.itb
# 3. u-boot.dtb를 포함하여 U-Boot 재빌드
$ make EXT_DTB=u-boot.dtb
# 4. ITS 파일에 서명 노드 추가 예제
configurations {
default = "standard";
standard {
kernel = "kernel";
fdt = "fdt-rk3399";
ramdisk = "ramdisk";
signature-1 {
algo = "sha256,rsa2048";
key-name-hint = "dev";
sign-images = "kernel", "fdt", "ramdisk";
};
};
};
UEFI 서브시스템
U-Boot는 자체적으로 UEFI 인터페이스를 구현하여, 전통적으로 UEFI 펌웨어가 없는 ARM 플랫폼에서도 UEFI 호환 부팅을 가능하게 합니다. 이를 통해 GRUB2, systemd-boot 같은 EFI 애플리케이션을 U-Boot 위에서 실행할 수 있습니다:
# U-Boot UEFI 관련 설정
CONFIG_EFI_LOADER=y # UEFI 서브시스템 활성화
CONFIG_EFI_SECURE_BOOT=y # UEFI Secure Boot 지원
CONFIG_EFI_CAPSULE_UPDATE=y # UEFI 캡슐 업데이트 지원
# U-Boot 셸에서 EFI 애플리케이션 실행
=> load mmc 0:1 ${loadaddr} EFI/BOOT/BOOTAA64.EFI
=> bootefi ${loadaddr}
# EFI 부팅 매니저를 통한 자동 부팅
=> efidebug boot add -b 0001 'Linux' \
mmc 0:1 EFI/BOOT/BOOTAA64.EFI
=> efidebug boot order 0001
=> bootefi bootmgr
부트로더 비교 요약
| 부트로더 | BIOS | UEFI | 파일시스템 | 스크립팅 | Secure Boot | 주요 용도 |
|---|---|---|---|---|---|---|
| GRUB2 | O | O | ext, xfs, btrfs, zfs 등 | 자체 스크립트 언어 | Shim 연동 | 범용 데스크톱/서버 |
| systemd-boot | X | O | ESP(FAT)만 | 없음 | Shim 연동 / UKI | UEFI 단순 부팅 |
| SYSLINUX | O | O (제한적) | FAT, ext, iso9660 | 제한적 메뉴 | X | 라이브 미디어, 설치 |
| rEFInd | X | O | ESP(FAT), ext, btrfs | 없음 | 자체 키 / Shim | 멀티부트 선택 |
| U-Boot | N/A | O (일부) | ext, fat, ubifs, squashfs, btrfs | 자체 스크립트 | FIT 서명 | ARM/임베디드 |
| LILO | O | X | 없음 (섹터 맵) | 없음 | X | 역사적 (폐기) |
| kexec | N/A | N/A | 커널 VFS | 없음 | kexec_file_load 서명 | 빠른 재부팅, kdump |
리얼 모드 커널 (x86 Setup)
GRUB가 bzImage의 setup 코드를 메모리에 로드하면, 리얼 모드(16-bit)에서 실행이 시작됩니다. 진입점은 arch/x86/boot/header.S의 _start 레이블입니다.
header.S 진입
/* arch/x86/boot/header.S — 부트 섹터 시작 */
.code16
.section ".bstext", "ax"
.global bootsect_start
bootsect_start:
/* 직접 부팅 시 "Use a boot loader" 메시지 출력 */
ljmp $BOOTSEG, $start2
start2:
movw %cs, %ax
movw %ax, %ds
/* ... 메시지 출력 후 정지 ... */
.section ".header", "a"
/* setup_header 구조체 필드들 */
sentinel: .byte 0xff, 0xff
hdr:
setup_sects: .byte SETUPSECTS
/* ... 부트 프로토콜 필드 계속 ... */
main() — 하드웨어 탐지
header.S의 _start에서 C 코드 arch/x86/boot/main.c:main()으로 점프합니다. 리얼 모드에서 BIOS 인터럽트를 사용하여 하드웨어 정보를 수집합니다:
/* arch/x86/boot/main.c */
void main(void)
{
copy_boot_params(); /* boot_params 구조체 복사 */
console_init(); /* 초기 콘솔 설정 */
init_heap(); /* setup 코드용 힙 초기화 */
validate_cpu(); /* CPU 기능 확인 (long mode 등) */
set_bios_mode(); /* BIOS 모드 보고 */
detect_memory(); /* e820으로 메모리 맵 수집 */
keyboard_init(); /* 키보드 초기화 */
query_ist(); /* Intel SpeedStep 정보 */
set_video(); /* 비디오 모드 설정 */
go_to_protected_mode(); /* 보호 모드로 전환! */
}
E820 메모리 맵
detect_memory()는 BIOS INT 15h, AX=E820h를 호출하여 물리 메모리 맵을 수집합니다. 이 맵은 이후 커널의 메모리 관리 초기화에 핵심적입니다:
/* arch/x86/boot/memory.c */
static void detect_memory_e820(void)
{
struct biosregs ireg, oreg;
struct boot_e820_entry *desc = boot_params.e820_table;
static struct boot_e820_entry buf;
initregs(&ireg);
ireg.ax = 0xe820;
ireg.cx = sizeof(buf);
ireg.edx = SMAP; /* 0x534D4150 = "SMAP" */
ireg.di = (size_t)&buf;
do {
intcall(0x15, &ireg, &oreg);
/* 결과를 boot_params.e820_table에 저장 */
*desc++ = buf;
boot_params.e820_entries++;
} while (ireg.ebx && boot_params.e820_entries < E820_MAX_ENTRIES);
}
dmesg | grep e820으로 커널이 펌웨어로부터 받은 메모리 맵을 확인할 수 있습니다. UEFI 환경에서는 EFI: mem으로 검색합니다.
보호 모드 → 롱 모드 전환
보호 모드 진입
go_to_protected_mode()는 리얼 모드에서 32-bit 보호 모드로 전환합니다:
/* arch/x86/boot/pm.c */
void go_to_protected_mode(void)
{
realmode_switch_hook(); /* 리얼 모드 훅 실행 */
enable_a20(); /* A20 라인 활성화 (1MB+ 접근) */
reset_coprocessor(); /* FPU 리셋 */
mask_all_interrupts(); /* PIC 인터럽트 마스킹 */
setup_gdt(); /* 임시 GDT 설정 */
setup_idt(); /* IDT를 null로 설정 */
protected_mode_jump( /* 어셈블리로 점프! */
boot_params.hdr.code32_start,
(u32)&boot_params + (ds() << 4));
}
A20 라인 활성화
리얼 모드에서는 20-bit 주소 버스로 1MB까지만 접근 가능합니다. A20 라인을 활성화하면 1MB 이상의 메모리에 접근할 수 있습니다. 커널은 여러 방법을 순서대로 시도합니다:
/* arch/x86/boot/a20.c */
int enable_a20(void)
{
/* 1순위: BIOS INT 15h, AX=2401h */
enable_a20_bios();
if (a20_test_short()) return 0;
/* 2순위: 키보드 컨트롤러 (8042) */
enable_a20_kbc();
if (a20_test_long()) return 0;
/* 3순위: Fast A20 (포트 0x92) */
enable_a20_fast();
if (a20_test_long()) return 0;
return -1; /* 실패 */
}
64-bit 롱 모드 전환
보호 모드(32-bit)에서 롱 모드(64-bit)로의 전환은 압축 해제 코드의 arch/x86/boot/compressed/head_64.S에서 수행됩니다:
/* 롱 모드 전환에 필요한 단계 */
1. PAE(Physical Address Extension) 활성화 → CR4.PAE = 1
2. 4-level 페이지 테이블 구성 → CR3에 PML4 주소 설정
3. IA-32e 모드 활성화 → IA32_EFER.LME = 1
4. 페이징 활성화 → CR0.PG = 1
5. 64-bit 코드 세그먼트로 far jump
/* arch/x86/boot/compressed/head_64.S (간략화) */
.code32
startup_32:
/* 임시 4-level 페이지 테이블 구성 */
/* Identity mapping: 가상주소 = 물리주소 */
leal pgtable(%ebx), %edi
xorl %eax, %eax
movl $(BOOT_INIT_PGT_SIZE/4), %ecx
rep stosl /* 페이지 테이블 영역 0으로 초기화 */
/* PML4[0] → PDP 설정 */
leal pgtable + 0x1007(%ebx), %eax
movl %eax, pgtable + 0(%ebx)
/* CR4 → PAE 활성화 */
movl %cr4, %eax
orl $X86_CR4_PAE, %eax
movl %eax, %cr4
/* CR3 → 페이지 테이블 주소 */
leal pgtable(%ebx), %eax
movl %eax, %cr3
/* EFER.LME 활성화 */
movl $MSR_EFER, %ecx
rdmsr
btsl $_EFER_LME, %eax
wrmsr
/* CR0 → 페이징 활성화 */
movl %cr0, %eax
orl $X86_CR0_PG, %eax
movl %eax, %cr0
/* 64-bit 코드로 far jump */
ljmpl $__KERNEL_CS, $startup_64
커널 압축 해제
롱 모드 진입 후, 압축된 커널 이미지를 해제합니다. 해제 코드는 arch/x86/boot/compressed/ 디렉토리에 위치합니다.
압축 해제 흐름
/* arch/x86/boot/compressed/misc.c */
asmlinkage __visible void *extract_kernel(
void *rmode, unsigned char *output)
{
sanitize_boot_params(&boot_params);
init_default_io_ops();
/* 비디오/콘솔 초기화 */
console_init();
debug_putstr("early console in extract_kernel\n");
/* 출력 버퍼 위치 결정 (KASLR 적용) */
choose_random_location(...);
/* 압축 해제 실행 */
debug_putstr("\nDecompressing Linux... ");
__decompress(input_data, input_len,
NULL, NULL, output, output_len,
NULL, error);
debug_putstr("done.\nBooting the kernel.\n");
/* ELF 파싱하여 최종 위치에 배치 */
parse_elf(output);
return output;
}
KASLR (Kernel Address Space Layout Randomization)
보안을 위해 커널 로드 주소를 무작위화합니다. choose_random_location()에서 물리/가상 주소를 결정합니다:
/* arch/x86/boot/compressed/kaslr.c */
void choose_random_location(
unsigned long input, unsigned long input_size,
unsigned long *output, unsigned long output_size,
unsigned long *virt_addr)
{
/* 엔트로피 소스: RDTSC, RDRAND, i8254 타이머 */
unsigned long random = get_random_long("KASLR");
/* 물리 주소 무작위화 */
*output = find_random_phys_addr(input, input_size,
*output, output_size);
/* 가상 주소 무작위화 */
*virt_addr = find_random_virt_addr(
LOAD_PHYSICAL_ADDR, output_size);
}
nokaslr을 추가하면 KASLR을 비활성화할 수 있습니다. /proc/kallsyms의 주소가 고정되어 심볼 디버깅이 쉬워집니다.
start_kernel() — 커널 핵심 초기화
init/main.c의 start_kernel()은 아키텍처 독립적인 커널 초기화의 시작점입니다. 압축 해제 후 아키텍처별 초기화(arch/x86/kernel/head_64.S → x86_64_start_kernel())를 거쳐 여기에 도달합니다.
start_kernel() 주요 흐름
/* init/main.c */
asmlinkage __visible void __init start_kernel(void)
{
/* ---- 초기 설정 ---- */
set_task_stack_end_magic(&init_task);
smp_setup_processor_id();
cgroup_init_early();
local_irq_disable(); /* 인터럽트 비활성화 상태에서 초기화 */
boot_cpu_init();
page_address_init();
pr_notice("%s", linux_banner); /* "Linux version X.Y.Z ..." */
/* ---- 아키텍처별 초기화 ---- */
setup_arch(&command_line); /* 아키텍처 의존 설정 (매우 중요!) */
/* ---- 핵심 서브시스템 초기화 ---- */
setup_command_line(command_line);
setup_nr_cpu_ids();
setup_per_cpu_areas(); /* Per-CPU 영역 할당 */
smp_prepare_boot_cpu();
boot_cpu_hotplug_init();
build_all_zonelists(NULL); /* 메모리 존 리스트 구성 */
page_alloc_init();
pr_notice("Kernel command line: %s\n", saved_command_line);
parse_early_param(); /* 초기 커널 파라미터 파싱 */
setup_log_buf(0); /* printk 링 버퍼 설정 */
vfs_caches_init_early(); /* dentry/inode 해시 테이블 */
mm_core_init(); /* 메모리 관리 핵심 초기화 */
sched_init(); /* 스케줄러 초기화 */
preempt_disable();
radix_tree_init();
rcu_init(); /* RCU 초기화 */
early_irq_init(); /* IRQ 서술자 초기화 */
init_IRQ(); /* 아키텍처별 IRQ 설정 */
tick_init(); /* 틱 서브시스템 */
init_timers(); /* 타이머 초기화 */
softirq_init(); /* softirq 초기화 */
timekeeping_init(); /* 시간 관리 초기화 */
time_init(); /* 아키텍처별 시간 설정 */
local_irq_enable(); /* ★ 인터럽트 활성화! ★ */
console_init(); /* 콘솔 서브시스템 초기화 */
mm_init(); /* 메모리 관리 완전 초기화 */
kmem_cache_init(); /* Slab 할당자 초기화 */
pidmap_init(); /* PID 맵 초기화 */
anon_vma_init(); /* 익명 VMA 초기화 */
signals_init(); /* 시그널 큐 초기화 */
proc_root_init(); /* /proc 파일시스템 초기화 */
arch_call_rest_init(); /* → rest_init() 호출 */
}
setup_arch() — 아키텍처 의존 초기화
setup_arch()는 아키텍처별로 구현되며, x86_64에서는 arch/x86/kernel/setup.c에 위치합니다. 하드웨어 의존적인 초기화를 수행합니다:
/* arch/x86/kernel/setup.c (주요 호출만 발췌) */
void __init setup_arch(char **cmdline_p)
{
/* boot_params에서 정보 추출 */
setup_memory_map(); /* E820 메모리 맵 처리 */
parse_setup_data(); /* 부트로더 전달 데이터 파싱 */
init_hypervisor_platform(); /* 하이퍼바이저 탐지 (KVM 등) */
x86_init.resources.probe_roms(); /* ROM 탐색 */
/* CPU 기능 탐지 */
early_cpu_init(); /* CPUID 기반 CPU 식별 */
idt_setup_early_handler(); /* 초기 IDT 핸들러 설정 */
/* 메모리 설정 */
e820__memory_setup(); /* E820 테이블 최종 정리 */
init_mem_mapping(); /* 직접 매핑 영역 설정 */
initmem_init(); /* NUMA 메모리 초기화 */
/* ACPI, IOMMU, PCI 등 */
acpi_boot_init(); /* ACPI 테이블 파싱 */
x86_init.paging.pagetable_init(); /* 최종 페이지 테이블 */
}
rest_init() — 사용자 공간 진입
start_kernel()의 마지막에서 rest_init()을 호출합니다. 여기서 커널 스레드를 생성하고, 최종적으로 PID 1(init) 프로세스가 사용자 공간에서 실행됩니다.
/* init/main.c */
noinline void __init_refok rest_init(void)
{
struct task_struct *tsk;
int pid;
/* PID 1: kernel_init 스레드 생성 */
pid = user_mode_thread(kernel_init, NULL, CLONE_FS);
/* PID 2: kthreadd (커널 스레드 관리자) 생성 */
pid = kernel_thread(kthreadd, NULL,
CLONE_FS | CLONE_FILES);
kthreadd_task = find_task_by_pid_ns(pid, &init_pid_ns);
/* 부팅 CPU의 idle 루프 시작 */
complete(&kthreadd_done);
cpu_startup_entry(CPUHP_ONLINE); /* → idle loop (PID 0) */
}
kernel_init() → /sbin/init
/* init/main.c */
static int __ref kernel_init(void *unused)
{
/* 커널 초기화 완료 대기 */
wait_for_completion(&kthreadd_done);
kernel_init_freeable();
/* initcall 완료 후 ramdisk 해제 */
async_synchronize_full();
kprobe_free_init_mem();
ftrace_free_init_mem();
free_initmem(); /* __init 섹션 메모리 해제 */
system_state = SYSTEM_RUNNING;
/* 사용자 공간 init 실행 시도 */
if (ramdisk_execute_command) {
run_init_process(ramdisk_execute_command);
}
if (execute_command) {
run_init_process(execute_command); /* init= 파라미터 */
}
/* 기본 경로에서 init 탐색 */
try_to_run_init_process("/sbin/init");
try_to_run_init_process("/etc/init");
try_to_run_init_process("/bin/init");
try_to_run_init_process("/bin/sh");
panic("No working init found.");
}
ps -ef | head로 확인할 수 있습니다.
initramfs와 initrd
initramfs(initial RAM filesystem)는 커널이 루트 파일시스템을 마운트하기 전에 사용하는 임시 루트입니다. 디스크 드라이버, 파일시스템 모듈, 암호화 키 등 부팅에 필요한 요소들을 포함합니다.
initramfs vs initrd
| 특성 | initrd (구식) | initramfs (현재) |
|---|---|---|
| 형태 | 블록 디바이스 이미지 | cpio 아카이브 |
| 파일시스템 | ext2 등 (파일시스템 드라이버 필요) | tmpfs (커널 내장) |
| 크기 | 고정 크기 | 동적 크기 |
| 루트 전환 | pivot_root() | switch_root |
| 메모리 해제 | ramdisk 해제 필요 | 자동 해제 |
initramfs 로드 과정
/* init/initramfs.c */
static int __init populate_rootfs(void)
{
/* 1단계: 커널에 내장된 initramfs 해제 */
char *err = unpack_to_rootfs(
__initramfs_start, __initramfs_size);
/* 2단계: 부트로더가 전달한 외부 initramfs 해제 */
if (initrd_start) {
err = unpack_to_rootfs(
(char *)initrd_start,
initrd_end - initrd_start);
if (err) {
/* cpio 실패 → 구식 initrd로 처리 */
handle_initrd();
}
free_initrd_mem(initrd_start, initrd_end);
}
return 0;
}
rootfs_initcall(populate_rootfs);
initramfs 내용 확인
# initramfs 내용 확인
$ lsinitramfs /boot/initramfs-$(uname -r).img
# initramfs 풀기
$ mkdir /tmp/initramfs && cd /tmp/initramfs
$ unmkinitramfs /boot/initramfs-$(uname -r).img .
# 전형적인 initramfs 구조
/init # PID 1이 실행하는 스크립트/바이너리
/bin/busybox # 최소 유틸리티
/lib/modules/6.x.y/ # 부팅 필수 커널 모듈
├── kernel/drivers/ata/ # 디스크 드라이버
├── kernel/drivers/nvme/ # NVMe 드라이버
├── kernel/fs/ext4/ # 파일시스템
└── kernel/crypto/ # 암호화 (LUKS 등)
/etc/modprobe.d/ # 모듈 설정
/usr/lib/systemd/ # systemd 기반 init
cpio 아카이브 형식과 압축
initramfs는 newc 형식의 cpio 아카이브입니다. 커널은 init/initramfs.c의 내장 cpio 파서로 이를 해제합니다. 외부 라이브러리 없이 동작하므로 부팅 초기에 사용할 수 있습니다.
/* cpio newc 헤더 구조 (110바이트 고정, ASCII 16진수) */
struct cpio_newc_header {
char c_magic[6]; /* "070701" (newc) 또는 "070702" (crc) */
char c_ino[8]; /* inode 번호 */
char c_mode[8]; /* 파일 모드 (권한 + 타입) */
char c_uid[8]; /* 소유자 UID */
char c_gid[8]; /* 소유자 GID */
char c_nlink[8]; /* 하드 링크 수 */
char c_mtime[8]; /* 수정 시간 */
char c_filesize[8]; /* 파일 데이터 크기 */
char c_devmajor[8]; /* 디바이스 major */
char c_devminor[8]; /* 디바이스 minor */
char c_rdevmajor[8]; /* rdev major (특수 파일용) */
char c_rdevminor[8]; /* rdev minor */
char c_namesize[8]; /* 파일명 길이 (NUL 포함) */
char c_check[8]; /* CRC 체크섬 (070702만 사용) */
};
/* 아카이브 끝: "TRAILER!!!" 이름의 빈 엔트리 */
커널은 다양한 압축 포맷을 지원하며, 빌드 시 CONFIG_RD_* 옵션으로 선택합니다:
| 압축 포맷 | 커널 옵션 | 압축률 | 해제 속도 | 비고 |
|---|---|---|---|---|
| gzip | CONFIG_RD_GZIP | 보통 | 빠름 | 가장 범용적, 기본값 |
| bzip2 | CONFIG_RD_BZIP2 | 좋음 | 느림 | 메모리 사용 높음 |
| lzma | CONFIG_RD_LZMA | 매우 좋음 | 느림 | 높은 압축률, 느린 해제 |
| xz | CONFIG_RD_XZ | 매우 좋음 | 보통 | lzma 개선, 널리 사용 |
| lzo | CONFIG_RD_LZO | 낮음 | 매우 빠름 | 임베디드에서 선호 |
| lz4 | CONFIG_RD_LZ4 | 낮음 | 최고 | 부팅 속도 최적화 |
| zstd | CONFIG_RD_ZSTD | 좋음 | 빠름 | 균형 잡힌 선택, Fedora 기본 |
| 비압축 | (항상 지원) | - | - | 디버깅용, cpio 그대로 |
/* init/initramfs.c — 압축 포맷 자동 감지 */
/* decompress_fn은 매직 바이트로 압축 형식을 판별 */
static char * __init unpack_to_rootfs(char *buf, unsigned long len)
{
/* cpio 매직("070701") 확인 → 직접 해제 */
if (!memcmp(buf, "070701", 6) ||
!memcmp(buf, "070702", 6)) {
...
return do_header();
}
/* 압축된 경우: 매직 바이트로 디컴프레서 선택 */
decompress_fn decompress = decompress_method(buf, len, &compress_name);
if (decompress) {
/* 압축 해제 → cpio 아카이브 → rootfs에 풀기 */
decompress(buf, len, NULL, flush_buffer,
NULL, &my_inptr, error);
}
}
kernel/x86/microcode/)가 들어있고, 두 번째 아카이브(압축)에 실제 initramfs가 들어있습니다. 커널은 unpack_to_rootfs()를 반복 호출하여 두 아카이브를 순차적으로 처리합니다.
initramfs 생성 도구
배포판마다 initramfs 생성 도구가 다릅니다. 각 도구는 시스템 환경을 분석하여 부팅에 필요한 모듈과 도구를 자동으로 포함합니다.
| 도구 | 배포판 | 설정 위치 | 특징 |
|---|---|---|---|
dracut | Fedora, RHEL, SUSE, Arch(선택) | /etc/dracut.conf.d/ | 모듈 기반 아키텍처, hostonly 모드 |
mkinitramfs | Debian, Ubuntu | /etc/initramfs-tools/ | hook/script 기반, update-initramfs 연동 |
mkinitcpio | Arch Linux | /etc/mkinitcpio.conf | preset 시스템, HOOKS 배열로 구성 |
booster | Arch(선택), NixOS(선택) | /etc/booster.yaml | Go 구현, 빠른 생성/부팅 |
# === dracut (Fedora/RHEL) ===
# 현재 커널용 initramfs 재생성
$ dracut --force /boot/initramfs-$(uname -r).img $(uname -r)
# hostonly 모드: 현재 시스템 부팅에 필요한 것만 포함 (크기 최소화)
$ dracut --hostonly --force /boot/initramfs-$(uname -r).img
# 특정 모듈 추가/제외
$ dracut --add "lvm crypt" --omit "plymouth" --force
# dracut 모듈 구조 (각 모듈은 독립 디렉토리)
/usr/lib/dracut/modules.d/
├── 00systemd/ # systemd 기반 init
├── 10i18n/ # 키보드/로캘 설정
├── 90crypt/ # LUKS 암호화 해제
├── 90lvm/ # LVM 활성화
├── 90mdraid/ # MD RAID 조립
├── 95nfs/ # NFS 루트
├── 95iscsi/ # iSCSI 루트
└── 99base/ # 기본 유틸리티
# === mkinitramfs (Debian/Ubuntu) ===
# 모든 커널의 initramfs 업데이트
$ update-initramfs -u -k all
# hook 구조
/etc/initramfs-tools/
├── initramfs.conf # MODULES=most|dep|list, COMPRESS=zstd
├── modules # 강제 포함할 모듈 목록
├── hooks/ # initramfs 생성 시 실행 (파일 추가)
└── scripts/
├── init-top/ # /init 초기 단계
├── init-premount/ # 마운트 전 (암호화, LVM 등)
├── local-top/ # 로컬 디스크 마운트 전
├── local-premount/ # fsck 실행
├── local-bottom/ # 로컬 디스크 마운트 후
└── init-bottom/ # switch_root 직전
# === mkinitcpio (Arch Linux) ===
# /etc/mkinitcpio.conf 주요 설정
MODULES=(ext4 nvme) # 강제 포함 모듈
BINARIES=(fsck.ext4) # 강제 포함 바이너리
FILES=(/etc/crypttab.initramfs) # 강제 포함 파일
HOOKS=(base udev autodetect modconf kms keyboard keymap \
consolefont block encrypt lvm2 filesystems fsck)
# 재생성
$ mkinitcpio -P # 모든 preset 재생성
커널 내장 initramfs
initramfs는 부트로더가 전달하는 외부 파일 외에, 커널 이미지 자체에 내장할 수도 있습니다. 임베디드 시스템이나 단일 이미지 배포에서 유용합니다.
# .config 에서 내장 initramfs 설정
CONFIG_INITRAMFS_SOURCE="/path/to/initramfs_dir"
# 또는 cpio 아카이브 직접 지정
CONFIG_INITRAMFS_SOURCE="/path/to/initramfs.cpio"
# 또는 gen_init_cpio 형식의 파일 목록
CONFIG_INITRAMFS_SOURCE="/path/to/initramfs_list"
CONFIG_INITRAMFS_COMPRESSION_GZIP=y # 내장 initramfs 압축 방식
# gen_init_cpio 형식 파일 목록 예시 (usr/gen_initramfs.sh가 처리)
# <type> <name> <mode> <uid> <gid> ...
dir /dev 0755 0 0
dir /proc 0755 0 0
dir /sys 0755 0 0
dir /bin 0755 0 0
nod /dev/console 0600 0 0 c 5 1
nod /dev/null 0666 0 0 c 1 3
file /init /path/to/init 0755 0 0
file /bin/busybox /path/to/busybox 0755 0 0
slink /bin/sh busybox 0777 0 0
/* usr/gen_init_cpio.c — 커널 빌드 시 initramfs cpio 생성기 */
/* 커널 빌드 과정에서 CONFIG_INITRAMFS_SOURCE를 처리: */
/* 1. 디렉토리 → gen_init_cpio로 cpio 아카이브 생성 */
/* 2. cpio 파일 → 그대로 사용 */
/* 3. 파일 목록 → gen_init_cpio가 파싱하여 cpio 생성 */
/* 결과물은 .init.ramfs 섹션에 링크됨: */
/* 링커 스크립트(include/asm-generic/vmlinux.lds.h) */
.init.ramfs : {
__initramfs_start = .; /* populate_rootfs()가 참조 */
KEEP(*(.init.ramfs))
__initramfs_end = .;
}
CONFIG_INITRAMFS_SOURCE를 지정하지 않아도, 커널은 항상 최소한의 내장 initramfs(빈 /dev, /dev/console, /root 디렉토리)를 포함합니다. 이는 usr/default_cpio_list에 정의되어 있습니다.
/init 실행: Early Userspace 진입
populate_rootfs()로 initramfs가 해제된 후, 커널은 PID 1 프로세스로 /init을 실행합니다. 이것이 "early userspace"의 시작입니다.
/* init/main.c — 커널 → userspace 전환점 */
static int __ref kernel_init(void *unused)
{
...
/* initramfs 해제 완료 대기 */
wait_for_initramfs();
/* /init이 존재하면 실행 (initramfs가 있는 경우) */
if (!try_to_run_init_process("/init"))
return 0;
/* initramfs에 /init이 없으면 루트 파일시스템 직접 마운트 시도 */
/* root= 커널 파라미터 기반 */
prepare_namespace();
/* 루트 마운트 후 /sbin/init, /etc/init, /bin/init, /bin/sh 순서 시도 */
if (!try_to_run_init_process("/sbin/init"))
return 0;
if (!try_to_run_init_process("/etc/init"))
return 0;
if (!try_to_run_init_process("/bin/init"))
return 0;
if (!try_to_run_init_process("/bin/sh"))
return 0;
panic("No working init found.");
}
initramfs 내 /init은 일반적으로 셸 스크립트(busybox 기반) 또는 systemd 바이너리입니다. 전형적인 실행 흐름:
#!/bin/sh — 전형적인 initramfs /init 스크립트 (busybox 기반, 단순화)
# 1. 가상 파일시스템 마운트
mount -t proc proc /proc
mount -t sysfs sysfs /sys
mount -t devtmpfs devtmpfs /dev
# 2. 커널 파라미터 파싱
cmdline=$(cat /proc/cmdline)
for param in $cmdline; do
case "$param" in
root=*) ROOT="${param#root=}" ;;
rootfstype=*) ROOTFS="${param#rootfstype=}" ;;
rd.break) RDBREAK=1 ;; # 디버깅: switch_root 전 셸
esac
done
# 3. 커널 모듈 로드 (디스크, 파일시스템, 암호화)
modprobe nvme
modprobe ext4
# 4. 루트 디바이스 대기 (비동기 장치 인식 대응)
while [ ! -b "$ROOT" ]; do
sleep 0.1
done
# 5. 루트 파일시스템 마운트
mkdir -p /new_root
mount -t "$ROOTFS" -o ro "$ROOT" /new_root
# 6. 디버깅 브레이크포인트
[ "$RDBREAK" = 1 ] && exec /bin/sh
# 7. switch_root로 실제 루트 전환
exec switch_root /new_root /sbin/init
switch_root: 루트 파일시스템 전환
switch_root는 initramfs에서 실제 루트 파일시스템으로 전환하는 핵심 유틸리티입니다. 단순히 chroot와 달리, initramfs의 모든 내용을 삭제하고 메모리를 해제합니다.
/* switch_root의 핵심 동작 (util-linux 구현 기반) */
/* 1단계: initramfs의 모든 파일/디렉토리 재귀 삭제 */
/* - rootfs(tmpfs)의 내용을 지워 메모리 회수 */
/* - /new_root (마운트 포인트)와 /proc, /sys, /dev는 보존 */
static int recursiveRemove(int fd)
{
struct stat rb;
fstat(fd, &rb);
while ((d = readdir(dir))) {
if (is_mount_point(d)) /* 마운트 포인트 보존 */
continue;
if (d->d_type == DT_DIR)
recursiveRemove(dfd); /* 재귀 삭제 */
else
unlinkat(fd, d->d_name, 0);
}
}
/* 2단계: new_root를 / 로 이동 */
mount(newroot, "/", NULL, MS_MOVE, NULL);
/* 3단계: chroot + chdir */
chroot(".");
chdir("/");
/* 4단계: 새로운 /sbin/init을 exec (PID 1 유지) */
execv(init, initargs);
pivot_root() 시스템 콜을 사용하여 루트를 교체했습니다. initramfs에서는 rootfs(tmpfs)에 대해 pivot_root()가 동작하지 않으므로, switch_root 방식(삭제 후 MS_MOVE)을 사용합니다. pivot_root()는 새 루트와 이전 루트를 교환하지만, switch_root는 이전 루트를 완전히 제거합니다.
systemd 기반 initramfs
현대 배포판(Fedora, RHEL, Ubuntu 24.04+)은 initramfs 내부에서도 systemd를 PID 1로 사용합니다. busybox 셸 스크립트 대신 systemd의 유닛 파일과 제너레이터로 부팅 과정을 관리합니다.
# systemd 기반 initramfs의 주요 타겟/서비스
# 부팅 순서를 정의하는 타겟
sysinit.target # 기본 초기화
└→ initrd-root-fs.target # 루트 파일시스템 마운트 완료
└→ initrd-fs.target # /etc/fstab.initrd의 추가 마운트
└→ initrd.target # initrd 단계 완료
└→ initrd-switch-root.target # 루트 전환
# 핵심 서비스
systemd-udevd.service # 디바이스 감지/모듈 로드
systemd-modules-load.service # /etc/modules-load.d/ 처리
systemd-cryptsetup@*.service # LUKS 볼륨 열기
systemd-fstab-generator # root= 파라미터 → 마운트 유닛 생성
# systemd의 switch_root 처리
# systemd-shutdown이 initrd-switch-root.target 도달 시:
# 1. 모든 서비스 정지
# 2. 파일시스템 정리
# 3. systemctl switch-root /sysroot /sbin/init 실행
initramfs와 암호화/LVM/네트워크 부팅
initramfs가 반드시 필요한 대표적 시나리오입니다. 루트 파일시스템에 접근하기 전에 추가 준비 단계가 필요하기 때문입니다.
/* === LUKS 암호화 루트 (가장 일반적인 사용 사례) === */
/* 커널 파라미터: root=/dev/mapper/cryptroot rd.luks.uuid=<UUID> */
/* initramfs 내부에서의 처리 순서: */
1. udev가 암호화된 블록 디바이스 감지
2. cryptsetup으로 LUKS 헤더 읽기
→ crypt_activate_by_passphrase() 또는
→ crypt_activate_by_keyfile()
3. 사용자에게 패스프레이즈 요청 (plymouth 또는 systemd-ask-password)
4. /dev/mapper/cryptroot 생성
5. 이후 정상적인 마운트 진행
/* === LVM 루트 === */
/* 커널 파라미터: root=/dev/vgname/lvroot */
1. udev가 PV(물리 볼륨) 감지
2. pvscan --cache /* PV 스캔 */
3. vgchange -ay /* VG 활성화 */
4. /dev/vgname/lvroot LV 활성화
5. 마운트 진행
/* === MD RAID 루트 === */
/* 커널 파라미터: root=/dev/md0 rd.auto=1 */
1. udev가 RAID 멤버 디스크 감지
2. mdadm --assemble --scan /* RAID 배열 조립 */
3. /dev/md0 활성화
4. 마운트 진행
/* === 복합 구성: LUKS + LVM (일반적) === */
/* root=/dev/vg/root rd.luks.uuid=UUID */
물리 디스크 → LUKS 해제 → PV 스캔 → VG 활성화 → LV 마운트
/dev/sda2 → cryptsetup → /dev/mapper/crypt → vgchange → /dev/vg/root
# === NFS 루트 (디스크리스 부팅) ===
# 커널 파라미터:
# root=nfs:192.168.1.1:/export/root,vers=4 ip=dhcp
# initramfs에서 필요한 추가 모듈:
# - 네트워크 드라이버 (e1000e, igb, mlx5 등)
# - NFS 클라이언트 (nfs, nfsv4, sunrpc)
# - DHCP 클라이언트 (dhclient 또는 systemd-networkd)
# initramfs 처리 순서:
1. 네트워크 인터페이스 UP
2. DHCP로 IP 획득 (ip=dhcp)
3. NFS 마운트: mount -t nfs4 192.168.1.1:/export/root /new_root
4. switch_root
# === iSCSI 루트 ===
# 커널 파라미터:
# root=LABEL=rootfs rd.iscsi.initiator=iqn.2024-01.com.example:client
# rd.iscsi.target=iqn.2024-01.com.example:storage
# rd.iscsi.ip=192.168.1.100
# initramfs에서 iscsid 시작 → 타겟 로그인 → 블록 디바이스 생성 → 마운트
커스텀 initramfs 수동 생성
최소한의 initramfs를 직접 만들어보면 동작 원리를 깊이 이해할 수 있습니다. 커널 개발/디버깅이나 임베디드 시스템에서도 유용합니다.
#!/bin/bash — 최소 initramfs 생성 스크립트
# 1. 작업 디렉토리 준비
WORK=/tmp/my_initramfs
rm -rf "$WORK" && mkdir -p "$WORK"/{bin,dev,etc,lib,lib64,mnt/root,proc,sbin,sys}
# 2. 정적 링크된 busybox 복사 (외부 라이브러리 불필요)
cp /usr/bin/busybox "$WORK/bin/"
# busybox 심볼릭 링크 생성
for cmd in sh mount umount switch_root modprobe insmod \
ls cat echo mkdir sleep mdev; do
ln -s busybox "$WORK/bin/$cmd"
done
# 3. 필수 디바이스 노드 (devtmpfs 마운트 전 필요)
mknod -m 600 "$WORK/dev/console" c 5 1
mknod -m 666 "$WORK/dev/null" c 1 3
mknod -m 666 "$WORK/dev/zero" c 1 5
mknod -m 666 "$WORK/dev/tty" c 5 0
# 4. /init 스크립트 작성
cat > "$WORK/init" << 'INITEOF'
#!/bin/sh
# 가상 파일시스템 마운트
mount -t proc proc /proc
mount -t sysfs sysfs /sys
mount -t devtmpfs devtmpfs /dev
echo "=== Custom initramfs booted ==="
# 커널 파라미터에서 root= 파싱
ROOT=""
for param in $(cat /proc/cmdline); do
case "$param" in
root=*) ROOT="${param#root=}" ;;
esac
done
if [ -z "$ROOT" ]; then
echo "No root= parameter. Dropping to shell."
exec /bin/sh
fi
# 루트 디바이스 대기 (최대 10초)
count=0
while [ ! -b "$ROOT" ] && [ $count -lt 100 ]; do
sleep 0.1
count=$((count + 1))
done
# 루트 파일시스템 마운트 및 전환
mount "$ROOT" /mnt/root
umount /proc /sys /dev
exec switch_root /mnt/root /sbin/init
INITEOF
chmod 755 "$WORK/init"
# 5. 커널 모듈 복사 (필요 시)
KVER=$(uname -r)
mkdir -p "$WORK/lib/modules/$KVER"
# 예: NVMe + ext4 모듈
cp /lib/modules/$KVER/kernel/drivers/nvme/host/nvme*.ko* "$WORK/lib/modules/$KVER/" 2>/dev/null
cp /lib/modules/$KVER/kernel/fs/ext4/ext4.ko* "$WORK/lib/modules/$KVER/" 2>/dev/null
depmod -b "$WORK" "$KVER"
# 6. cpio 아카이브 생성
cd "$WORK"
find . -print0 | cpio --null -o -H newc 2>/dev/null | gzip -9 > /boot/initramfs-custom.img
echo "Created: /boot/initramfs-custom.img ($(du -h /boot/initramfs-custom.img | cut -f1))"
# QEMU에서 테스트:
# qemu-system-x86_64 -kernel /boot/vmlinuz-$(uname -r) \
# -initrd /boot/initramfs-custom.img \
# -append "console=ttyS0 root=/dev/sda1" \
# -hda rootfs.img -nographic
initramfs 디버깅
부팅 문제의 상당수는 initramfs 단계에서 발생합니다. 아래 기법으로 문제를 진단할 수 있습니다.
# === 커널 파라미터를 이용한 디버깅 ===
# 1. initramfs 셸 진입 (switch_root 전 중단)
# dracut:
rd.break # switch_root 직전 셸 제공
rd.break=pre-mount # 루트 마운트 전 중단
rd.break=pre-udev # udev 시작 전 중단
rd.break=cmdline # 커맨드라인 파싱 후 중단
rd.break=initqueue # initqueue 단계에서 중단
# mkinitramfs (Debian/Ubuntu):
break=top # /init 시작 직후
break=premount # 마운트 전
break=mount # 마운트 후
break=bottom # switch_root 직전
# 2. 상세 로그 활성화
rd.debug # dracut: 셸 디버그 모드 (set -x)
rd.udev.debug # udev 디버그 로그
systemd.log_level=debug # systemd-initrd 디버그
# 3. 셸 강제 진입
rd.shell # dracut: 부팅 실패 시 셸 제공
init=/bin/sh # initramfs 무시, 루트 직접 셸
rescue # systemd rescue 모드
# === initramfs 내용 분석 ===
# 압축 형식 확인
$ file /boot/initramfs-$(uname -r).img
/boot/initramfs-6.8.0.img: ASCII cpio archive (SVR4 with no CRC)
# → 비압축 early cpio (마이크로코드) + 압축된 메인 아카이브
# 상세 내용 확인 (dracut)
$ lsinitrd /boot/initramfs-$(uname -r).img
# → 포함된 dracut 모듈, 파일 목록, 크기 출력
# 특정 파일 추출
$ lsinitrd --unpack /boot/initramfs-$(uname -r).img
$ lsinitrd -f /etc/cmdline.d/root.conf /boot/initramfs-$(uname -r).img
# 수동 풀기 (multi-segment initramfs)
$ unmkinitramfs /boot/initramfs-$(uname -r).img /tmp/extracted/
# early/ → 마이크로코드
# main/ → 메인 initramfs
# === 일반적인 문제와 해결 ===
# 문제: "Unable to find root device"
# 원인: 디스크 드라이버 누락 (NVMe, AHCI 등)
# 해결: dracut --add-drivers "nvme ahci" --force
# 문제: "No working init found"
# 원인: initramfs 손상 또는 /init 없음
# 해결: initramfs 재생성, 또는 init= 파라미터로 경로 지정
# 문제: "VFS: Cannot open root device"
# 원인: root= 파라미터가 잘못되었거나, 파일시스템 모듈 누락
# 해결: root=UUID=xxx 형식 사용, 파일시스템 모듈 포함 확인
# 문제: LUKS 패스프레이즈 프롬프트가 표시되지 않음
# 원인: plymouth 또는 systemd-ask-password 누락
# 해결: dracut --add "crypt" --force, 또는 rd.luks.uuid= 확인
qemu-system-x86_64 -kernel vmlinuz -initrd initramfs.img -append "console=ttyS0 rd.break" -nographic로 시리얼 콘솔 환경에서 빠르게 확인할 수 있습니다.
initcall 메커니즘
커널 서브시스템과 드라이버의 초기화 함수는 __initcall 매크로를 통해 등록됩니다. do_initcalls()에서 레벨 순서대로 호출됩니다:
/* include/linux/init.h — initcall 레벨 */
#define pure_initcall(fn) __define_initcall(fn, 0)
#define core_initcall(fn) __define_initcall(fn, 1)
#define postcore_initcall(fn) __define_initcall(fn, 2)
#define arch_initcall(fn) __define_initcall(fn, 3)
#define subsys_initcall(fn) __define_initcall(fn, 4)
#define fs_initcall(fn) __define_initcall(fn, 5)
#define rootfs_initcall(fn) __define_initcall(fn, 5r)
#define device_initcall(fn) __define_initcall(fn, 6) /* = module_init */
#define late_initcall(fn) __define_initcall(fn, 7)
/* init/main.c — initcall 실행 */
static void __init do_initcalls(void)
{
int level;
for (level = 0; level < ARRAY_SIZE(initcall_levels) - 1; level++) {
/* 각 레벨의 등록된 함수들을 순서대로 호출 */
do_initcall_level(level, command_line);
}
}
initcall_debug를 추가하면 각 initcall의 호출 시간과 반환값이 dmesg에 출력됩니다. 부팅 지연의 원인을 찾을 때 유용합니다.
SMP 부팅 (멀티코어 초기화)
지금까지의 과정은 BSP(Bootstrap Processor)에서만 실행됩니다. 나머지 AP(Application Processor)는 smp_init()에서 깨워집니다.
/* kernel/smp.c */
void __init smp_init(void)
{
int num_nodes, num_cpus;
idle_threads_init(); /* AP별 idle 스레드 생성 */
cpuhp_threads_init(); /* CPU hotplug 스레드 생성 */
pr_info("Bringing up secondary CPUs ...\n");
bringup_nonboot_cpus(setup_max_cpus);
num_nodes = num_online_nodes();
num_cpus = num_online_cpus();
pr_info("Brought up %d node%s, %d CPU%s\n",
num_nodes, num_nodes > 1 ? "s" : "",
num_cpus, num_cpus > 1 ? "s" : "");
}
AP 깨우기: INIT-SIPI-SIPI
x86에서 AP를 깨우는 과정은 APIC(Advanced Programmable Interrupt Controller)의 IPI(Inter-Processor Interrupt)를 사용합니다:
/* AP 깨우기 순서 (x86) */
1. BSP가 AP에 INIT IPI 전송 /* AP를 리셋 상태로 */
2. 10ms 대기
3. BSP가 AP에 SIPI (Startup IPI) 전송 /* 시작 주소 지정 */
4. 200μs 대기
5. BSP가 AP에 두 번째 SIPI 전송 /* 실패 대비 재전송 */
6. AP가 리얼 모드에서 시작 → 보호 → 롱 모드 전환
7. AP가 start_secondary() 실행
/* arch/x86/kernel/smpboot.c */
static int wakeup_secondary_cpu_via_init(int phys_apicid,
unsigned long start_eip)
{
/* INIT IPI */
apic_icr_write(APIC_INT_LEVELTRIG | APIC_INT_ASSERT |
APIC_DM_INIT, phys_apicid);
udelay(10000);
/* SIPI × 2 */
for (j = 0; j < 2; j++) {
apic_icr_write(APIC_DM_STARTUP |
(start_eip >> 12), phys_apicid);
udelay(300);
}
}
ARM64 부팅 과정
ARM64(AArch64) 아키텍처의 부팅은 x86과 상당히 다릅니다. UEFI 또는 U-Boot 등의 부트로더를 사용하며, Device Tree를 통해 하드웨어 정보를 전달합니다.
커널 진입점
/* arch/arm64/kernel/head.S */
.section ".head.text", "ax"
_head:
b primary_entry /* 커널 진입 (branch 명령어) */
.long 0 /* reserved */
.quad _kernel_offset_le /* Image load offset (LE) */
.quad _kernel_size_le /* 커널 이미지 크기 */
.quad _kernel_flags_le /* 플래그 (little-endian 등) */
.quad 0 /* reserved */
.quad 0 /* reserved */
.quad 0 /* reserved */
.ascii "ARM\x64" /* 매직 넘버 */
.long pe_header - _head /* PE 헤더 오프셋 (UEFI용) */
primary_entry 초기화
/* arch/arm64/kernel/head.S */
primary_entry:
preserve_boot_args /* x0(DTB 주소) 등 부트 인자 보존 */
init_kernel_el /* Exception Level 확인/설정 */
create_idmap /* Identity mapping 페이지 테이블 */
create_kernel_mapping /* 커널 가상 주소 매핑 */
__primary_switch /* MMU 활성화 → __primary_switched */
__primary_switched:
adr_l x5, init_task /* swapper 태스크 설정 */
msr sp_el0, x5
adr_l x8, vectors /* 예외 벡터 테이블 설정 */
msr vbar_el1, x8
/* ... */
bl start_kernel /* C 코드로 진입 */
Device Tree (DTB)
ARM64에서는 하드웨어 정보를 Device Tree Blob(DTB)을 통해 커널에 전달합니다. 부트로더가 DTB의 물리 주소를 레지스터 x0에 넣어 전달합니다:
/* 예시: Device Tree Source (DTS) */
/ {
model = "Raspberry Pi 4 Model B";
compatible = "raspberrypi,4-model-b", "brcm,bcm2711";
memory@0 {
device_type = "memory";
reg = <0x0 0x0 0x0 0x40000000>; /* 1GB */
};
cpus {
#address-cells = <1>;
#size-cells = <0>;
cpu@0 {
device_type = "cpu";
compatible = "arm,cortex-a72";
reg = <0x0>;
enable-method = "psci";
};
/* cpu@1, @2, @3 ... */
};
timer {
compatible = "arm,armv8-timer";
interrupts = <GIC_PPI 13 IRQ_TYPE_LEVEL_LOW>,
<GIC_PPI 14 IRQ_TYPE_LEVEL_LOW>;
};
};
enable-method 속성으로 결정됩니다. 현대 시스템은 PSCI(Power State Coordination Interface)를 사용하며, spin-table 방식은 구형 플랫폼에서 사용됩니다.
커널 커맨드 라인 파라미터
부트로더가 커널에 전달하는 커맨드 라인은 부팅 동작을 제어하는 핵심 메커니즘입니다.
주요 부팅 파라미터
| 파라미터 | 설명 | 예시 |
|---|---|---|
root= | 루트 파일시스템 디바이스 | root=/dev/sda2, root=UUID=... |
rootfstype= | 루트 FS 타입 | rootfstype=ext4 |
init= | init 프로세스 경로 | init=/bin/bash (응급 복구) |
console= | 콘솔 디바이스 | console=ttyS0,115200 |
quiet | 부팅 메시지 억제 | loglevel=4와 동일 |
debug | 디버그 레벨 로깅 | loglevel=7과 동일 |
nokaslr | KASLR 비활성화 | 디버깅용 |
initcall_debug | initcall 타이밍 출력 | 부팅 지연 분석 |
earlyprintk= | 초기 콘솔 출력 | earlyprintk=serial,ttyS0,115200 |
nosmp | 단일 CPU로 부팅 | SMP 관련 문제 격리 |
maxcpus= | 사용할 최대 CPU 수 | maxcpus=1 |
mem= | 사용할 메모리 제한 | mem=512M |
crashkernel= | kdump용 메모리 예약 | crashkernel=256M |
early_param과 __setup 매크로
/* 커널 코드에서 파라미터 처리 등록 */
/* early_param: parse_early_param()에서 처리 (매우 이른 시점) */
static int __init setup_quiet(char *str)
{
console_loglevel = CONSOLE_LOGLEVEL_QUIET;
return 0;
}
early_param("quiet", setup_quiet);
/* __setup: do_initcall_level(0) 시점에서 처리 */
static int __init root_dev_setup(char *line)
{
strlcpy(saved_root_name, line, sizeof(saved_root_name));
return 1;
}
__setup("root=", root_dev_setup);
부팅 과정 디버깅
Early Printk
일반 콘솔이 초기화되기 전에 메시지를 출력하려면 early printk를 사용합니다:
# GRUB에서 커널 파라미터 추가
linux /vmlinuz-6.x root=/dev/sda2 \
earlyprintk=serial,ttyS0,115200 \
console=ttyS0,115200 \
initcall_debug \
loglevel=7
부팅 시간 분석
# systemd 기반 시스템의 부팅 시간 분석
$ systemd-analyze # 전체 부팅 시간
Startup finished in 3.2s (kernel) + 5.1s (userspace) = 8.3s
$ systemd-analyze blame # 서비스별 소요 시간
$ systemd-analyze critical-chain # 크리티컬 패스
$ systemd-analyze plot > boot.svg # SVG 타임라인
# 커널 내부 부팅 시간 (dmesg 타임스탬프)
$ dmesg | grep -E "^\[.*\]" | head -30
[ 0.000000] Linux version 6.x.y ...
[ 0.000000] Command line: root=/dev/sda2 ...
[ 0.004000] BIOS-provided physical RAM map:
[ 0.100000] Booting paravirtualized kernel on bare hardware
[ 0.523000] Freeing unused kernel image memory: 2340K
[ 0.530000] Run /sbin/init as init process
# initcall 분석
$ dmesg | grep "initcall" | sort -t= -k2 -n -r | head
# 가장 오래 걸린 initcall 확인
QEMU로 부팅 디버깅
# QEMU에서 커널 직접 부팅 + GDB 연결
$ qemu-system-x86_64 \
-kernel arch/x86/boot/bzImage \
-initrd initramfs.cpio.gz \
-append "console=ttyS0 nokaslr" \
-nographic \
-s -S # GDB 서버 (포트 1234)에서 대기
# 다른 터미널에서 GDB 연결
$ gdb vmlinux
(gdb) target remote :1234
(gdb) hbreak start_kernel # 하드웨어 브레이크포인트
(gdb) c # 실행 계속
(gdb) bt # 백트레이스 확인
nokaslr 파라미터를 사용하거나, /proc/kallsyms에서 실제 주소를 확인하세요.
부팅 관련 소스 트리
| 경로 | 설명 |
|---|---|
arch/x86/boot/ | x86 리얼 모드 부트 코드 (header.S, main.c, pm.c) |
arch/x86/boot/compressed/ | 압축 해제 코드 (head_64.S, misc.c, kaslr.c) |
arch/x86/kernel/head_64.S | 64-bit 진입점, 초기 페이지 테이블 |
arch/x86/kernel/head64.c | x86_64_start_kernel() |
arch/x86/kernel/setup.c | setup_arch() — x86 아키텍처 초기화 |
arch/x86/kernel/smpboot.c | SMP 부팅 (AP 깨우기) |
arch/arm64/kernel/head.S | ARM64 커널 진입점 |
init/main.c | start_kernel(), rest_init(), kernel_init() |
init/initramfs.c | initramfs 해제 |
init/do_mounts.c | 루트 파일시스템 마운트 |
drivers/firmware/efi/ | UEFI 런타임 서비스 |
drivers/firmware/efi/libstub/ | UEFI Stub 부트 코드 |