운영체제 기본/x86-64 Linux의 BIOS 부팅
목차 · 관련 총론: 보호와 시스템 호출, 프로세스와 스레드
이 장은 실행 문맥, 프로세스 생성, 프로그램 교체를 Linux의 실제 소스에 연결하는 각론이다. 기준은 Linux v6.12이며, x86-64의 부트 CPU가 bzImage의 64비트 부트 프로토콜 진입점으로 제어를 넘겨받는 경로를 추적한다. 펌웨어의 리셋 벡터부터 모든 플랫폼의 부팅을 하나로 설명하는 장은 아니다.
핵심 질문은 “커널의 첫 명령어가 어떻게 init을 호출하는가?”보다 “어떤 실행 환경을 먼저 만들고, 어느 시점에 별도 태스크를 생성하며, 그 태스크가 언제 사용자 프로그램으로 바뀌는가?”이다. 함수 호출, 분기, 태스크 생성, exec는 서로 다른 사건이다. 아래 화살표도 이 차이를 구별해서 읽어야 한다.
1. 진입점은 하나가 아니다
64비트 부트 프로토콜에서 로더는 struct boot_params를 준비하고, CPU를 페이징이 활성화된 64비트 모드에 둔다. 필요한 영역의 identity mapping, 세그먼트 상태, 비활성화된 인터럽트 등도 진입 계약에 포함된다. %rsi에는 부트 매개변수 구조체의 주소가 들어간다. 따라서 이 경로의 커널 첫 명령어가 “리얼 모드에서 시작해 모든 CPU 상태를 처음부터 만든다”고 설명하면 틀린다.[1]
문서가 말하는 64비트 진입점은 적재된 커널의 시작 주소에 0x200을 더한 위치다. bzImage에서 이는 압축 해제기 쪽 startup_64에 해당한다. 여기서 “시작 주소”를 디스크상의 bzImage 파일 첫 바이트나 최종 커널의 가상 주소와 혼동하면 안 된다. 부트 프로토콜에 맞춰 적재한 이미지 부분을 기준으로 해석해야 한다.[1][2]
이름이 같은 심볼을 구별하는 가장 안전한 방법은 파일 경로까지 함께 적는 것이다.
| 위치 | 역할 |
|---|---|
arch/x86/boot/compressed/head_64.S:startup_64 |
압축된 커널을 풀어서 본체로 넘어가기 위한 진입점 |
arch/x86/kernel/head_64.S:startup_64 |
압축 해제된 커널 본체의 초기 진입점 |
32비트 프로토콜로 진입하면 앞에 startup_32와 모드 전환 과정이 붙는다. EFI stub은 펌웨어가 PE/COFF 이미지로 실행하는 별도 진입 경로이며, 전통적인 별도 로더의 역할 일부를 수행할 수 있다. BIOS, UEFI, 32비트 진입, 64비트 진입을 모두 반드시 통과하는 일렬의 단계로 그리지 않는다. 본체 소스는 이미 준비된 비압축 커널에 64비트 로더가 직접 들어오는 경우도 명시한다.[2][3][4]
2. 압축 해제기: 실행 중인 코드를 덮어쓰지 않고 본체를 만든다
압축 해제기에도 스택, 예외 처리, 주소 매핑 등 최소한의 실행 환경이 필요하다. 여기서의 준비를 이후 커널 전체의 메모리 관리 초기화와 동일시해서는 안 된다. 또한 압축 입력과 출력이 같은 메모리 공간을 사용하므로, 풀어 쓴 데이터가 아직 실행 중인 해제기나 읽지 않은 입력을 덮지 않도록 배치해야 한다.
compressed/head_64.S는 압축 이미지를 안전한 위치로 복사한 뒤 옮겨진 코드로 분기한다. 그 경로에서 BSS를 비우고 필요한 매핑을 마련한 다음 extract_kernel()을 호출한다. 반환값은 “압축 해제 성공 여부”가 아니라 다음에 실행할 커널 진입 주소다.[2]
arch/x86/boot/compressed/misc.c를 보면 이 과정이 단순한 압축 해제 라이브러리 호출보다 넓다는 것을 알 수 있다. extract_kernel()은 배치를 선택·검사하고, decompress_kernel()은 압축을 풀고 ELF를 해석하며 relocation을 처리한다. 최종적으로 출력 주소와 진입 오프셋으로 다음 진입 주소를 만든다. KASLR 같은 설정 때문에 “항상 특정 물리 주소에 본체를 놓는다”는 식의 상수 중심 설명은 피하는 편이 정확하다.[5]
다음은 호출·분기 관계만 남긴 의사 코드다. 레지스터 보존, 조건부 기능, 오류 처리 등은 생략했다.
압축 해제기 startup_64 → 안전한 위치로 이동한 코드 → entry = extract_kernel(boot_params, output) → boot_params를 %rsi에 두고 entry로 jump
3. 커널 본체: 아키텍처 초기화에서 공통 초기화로
본체의 startup_64도 처음부터 C 함수 하나를 평범하게 호출할 수 있는 환경을 전제하지는 않는다. 초기 스택과 GDT/IDT를 준비하고 CPU를 검사하며, __startup_64()를 통해 실제 적재 위치에 맞게 초기 페이지 테이블을 보정한다. 이어 CR3를 설정하고 커널 가상 주소의 공통 시작 코드로 분기한다. 즉, 압축을 푼다는 문제와 본체가 자신의 주소 공간에서 실행된다는 문제는 별개다.[3][6]
부트 CPU 경로에서 initial_code의 초기값은 x86_64_start_kernel이다. 공통 어셈블리 경로는 이를 간접 호출한다. 이 함수를 “처음 실행되는 모든 C 코드”라고 부르면 부정확하다. 앞에서도 페이지 테이블 보정 같은 C 코드가 실행되기 때문이다.[3][6]
x86_64_start_kernel()은 초기 페이지 테이블 정리, BSS 초기화, 초기 예외 처리, 부트 정보 복사 등을 수행하고 x86_64_start_reservations()를 거쳐 start_kernel()에 도달한다. real_mode_data라는 인자 이름이 남아 있어도 현재 CPU가 리얼 모드라는 뜻은 아니다. 전달되는 것은 앞에서부터 이어져 온 부트 정보다.[6]
start_kernel()은 setup_arch(), 메모리·스케줄러·인터럽트·타이머·VFS 등의 초기화를 진행한다. 아직 일반 사용자 프로세스가 실행되는 단계는 아니다. 마지막에는 rest_init()으로 넘어간다.[7]
4. rest_init: PID 0에서 PID 1을 만든다
rest_init()은 user_mode_thread(kernel_init, NULL, CLONE_FS)로 PID 1을 먼저 만들고, kernel_thread()로 kthreadd(PID 2)를 만든다. PID 1은 kthreadd_done을 기다린다. 기존 부트 태스크(PID 0)는 idle 경로로 들어간다. PID 0 자체가 init으로 바뀌는 것이 아니다.[7]
user_mode_thread라는 이름도 즉각 사용자 모드에서 실행한다는 뜻은 아니다. kernel/fork.c에서 이 함수는 시작 함수와 인자를 담아 kernel_clone()을 호출한다. kernel_thread()와 달리 이 생성 경로는 kthread 속성을 설정하지 않는다. 이는 나중에 exec할 태스크를 만드는 것과 계속 커널 스레드로 남을 태스크를 만드는 것의 차이다. fs/exec.c의 kernel_execve()는 실제로 PF_KTHREAD 태스크의 호출을 거부한다.[8][9]
아래는 동기화와 중간 호출을 생략한 실행 흐름도다. 세로선은 함수 호출만을 뜻하지 않으며, 분기점에서 태스크가 나뉜다.
부트 로더 → compressed/startup_64 → extract_kernel
|
v
kernel/startup_64 → x86_64_start_kernel
→ x86_64_start_reservations → start_kernel → rest_init
|
+------------------------------------------------+------+
| | |
기존 PID 0 새 PID 1 kthreadd
idle 경로 kernel_init 커널 스레드
|
kernel_init_freeable
|
run_init_process
|
kernel_execve
|
사용자 모드의 init 프로그램
kernel_init_freeable()은 SMP·initcall 등의 나머지 초기화와 initramfs 준비를 진행한다. 돌아온 kernel_init()은 비동기 초기화 완료를 기다리고 초기화용 메모리를 해제한 뒤 실행할 init을 고른다.[7]
5. /init, rdinit=, init=는 같은 선택지가 아니다
initramfs는 초기 사용자 공간에 필요한 파일들을 rootfs에 푸는 방식이다. init/initramfs.c의 populate_rootfs()는 rootfs_initcall로 등록되며, 비동기 작업에서 내장 아카이브와 조건에 따라 외부 이미지를 푼다. wait_for_initramfs()가 이 작업과 파일 접근 사이를 동기화한다. 따라서 “init이라는 디스크 파일을 열기 전에 파일 시스템을 어떻게 얻는가?”에 대한 첫 답은 이미 커널이 만든 초기 rootfs와 거기에 풀린 파일이다.[10]
아래는 선택 규칙의 요약이며 실제 코드 전체가 아니다.[7]
초기 후보 = rdinit= 지정 경로, 기본 /init 사전 실행 접근 검사 실패 → 초기 후보 해제; prepare_namespace() 초기 후보가 남아 있으면 실행 시도 → 성공하면 끝 init= 지정값 실행 시도 → 실패하면 panic CONFIG_DEFAULT_INIT가 비어 있지 않으면 시도 /sbin/init → /etc/init → /bin/init → /bin/sh 모두 실패하면 panic
초기 후보의 사전 검사를 통과한 뒤 exec만 실패한 경우, 그 실패가 prepare_namespace()를 다시 호출하지는 않는다. 후보 이름의 순서뿐 아니라 현재 루트가 무엇인지도 중요하다.[7]
prepare_namespace()는 장치 탐색을 기다리고 루트 장치를 해석하여 루트를 마운트하는 커널 측 경로다. 반면 initramfs의 /init이 실행되면 실제 루트를 찾고 전환하는 작업을 초기 사용자 공간이 맡을 수 있다. 루트 전환 후 다른 init으로 exec하는 것도 사용자 공간의 책임이다. “커널이 /init을 실행한 다음 무조건 /sbin/init도 실행한다”는 순서는 없다.[11][12]
6. exec와 사용자 모드 진입은 구분한다
run_init_process()는 인자·환경을 넘겨 kernel_execve()를 호출한다. 새 PID를 하나 더 만드는 것이 아니라 PID 1의 실행 프로그램을 교체하는 단계다.[7][9]
여기에도 흔한 함정이 있다. 사용자 프로그램에서 성공한 execve()는 원래 프로그램으로 돌아오지 않지만, 커널 내부의 kernel_execve()는 성공 상태를 반환할 수 있다. 이 반환을 “init 실행에 실패했거나 init 프로그램이 종료되었다”는 뜻으로 읽으면 안 된다. 실행 이미지와 사용자 레지스터 상태를 준비한 다음, 커널의 복귀 경로가 실제 권한 수준 전환을 마무리한다.[9]
x86에서는 ret_from_fork_asm이 ret_from_fork()를 호출한다. 이 C 함수가 시작 함수인 kernel_init을 실행하고, exec 성공 뒤 돌아오면 syscall_exit_to_user_mode()로 이어진다. 어셈블리 경로는 이어서 사용자 모드 복귀 코드로 분기한다. v6.12에는 FRED 사용 여부에 따른 대체 경로도 있으므로 마지막 명령을 모든 구성에서 한 가지로 고정하지 않는다.[13][14]
이제 PID 1은 사용자 공간의 프로그램을 실행한다. 그것이 초기 rootfs의 /init인지, 최종 루트의 init인지, 어떤 서비스 관리자인지는 구성과 선택 결과에 달려 있다. 여기까지가 커널 진입점에서 사용자 공간으로 책임을 넘기는 경계다. 이후 서비스 시작 순서는 커널 부팅 경로와 분리해서 다룬다.
소스 읽기 안내
심볼 검색은 반드시 v6.12로 고정하고 같은 이름의 파일을 구별한다. 다음 Elixir 링크는 탐색용이며, 본문의 검증 근거는 각주에 연결한 Linus Torvalds 저장소의 동일 태그 원문이다. 행 번호는 표시 방식에 따라 달라질 수 있어 고정하지 않았다.
- 압축 해제기 head_64.S
- 압축 해제·ELF·relocation
- 커널 본체 head_64.S
- 본체 초기 C 코드
- start_kernel부터 init 선택까지
- kernel_execve
각주
- ↑ 1.0 1.1 Linux v6.12: The Linux/x86 Boot Protocol, 특히 64-bit Boot Protocol 절.
- ↑ 2.0 2.1 2.2 Linux v6.12: arch/x86/boot/compressed/head_64.S.
- ↑ 3.0 3.1 3.2 Linux v6.12: arch/x86/kernel/head_64.S.
- ↑ Linux v6.12: The EFI Boot Stub.
- ↑ Linux v6.12: arch/x86/boot/compressed/misc.c, extract_kernel 및 decompress_kernel.
- ↑ 6.0 6.1 6.2 Linux v6.12: arch/x86/kernel/head64.c.
- ↑ 7.0 7.1 7.2 7.3 7.4 7.5 Linux v6.12: init/main.c, start_kernel, rest_init, kernel_init_freeable, kernel_init, run_init_process.
- ↑ Linux v6.12: kernel/fork.c, kernel_thread 및 user_mode_thread.
- ↑ 9.0 9.1 9.2 Linux v6.12: fs/exec.c, kernel_execve.
- ↑ Linux v6.12: init/initramfs.c, populate_rootfs, do_populate_rootfs, wait_for_initramfs.
- ↑ Linux v6.12: init/do_mounts.c, prepare_namespace.
- ↑ Linux v6.12: ramfs, rootfs and initramfs, What is initramfs? 절. 이 문서의 역사적 설명과 현행 세부 선택 규칙은 구별하며, 후자는 위 v6.12 코드를 기준으로 한다.
- ↑ Linux v6.12: arch/x86/kernel/process.c, ret_from_fork.
- ↑ Linux v6.12: arch/x86/entry/entry_64.S, ret_from_fork_asm.