본문으로 이동
  • 현재 모든 문서는 작성 중입니다. 보시는 데 불편함이 있더라도 양해 부탁드립니다.

운영체제 기본/x86-64 Linux의 BIOS 부팅: 두 판 사이의 차이

~ldmsys
Linux v6.12 x86-64 부트 진입, 압축 해제, PID 1 생성과 사용자 init 실행 경로 설명
 
PID 1 첫 사용자 실행을 부팅 범위에 포함하도록 목차 정리
 
(같은 사용자의 중간 판 7개는 보이지 않습니다)
1번째 줄: 1번째 줄:
[[운영체제 기본|목차]] · 관련 총론: [[운영체제 기본/보호|보호와 시스템 호출]], [[운영체제 기본/프로세스와 스레드|프로세스와 스레드]]
[[운영체제 기본|운영체제 기본 목차]]


이 장은 '''실행 문맥, 프로세스 생성, 프로그램 교체'''를 Linux의 실제 소스에 연결하는 각론이다. 기준은 '''Linux v6.12'''이며, x86-64의 부트 CPU가 '''bzImage의 64비트 부트 프로토콜 진입점'''으로 제어를 넘겨받는 경로를 추적한다. 펌웨어의 리셋 벡터부터 모든 플랫폼의 부팅을 하나로 설명하는 장은 아니다.
BIOS 환경의 GRUB 2.12가 Linux v6.12의 x86-64 커널을 적재하고, 커널이 첫 사용자 프로그램인 init을 실행하기까지의 과정을 따라간다. GRUB의 일반 <code>linux</code> 명령이 사용하는 32비트 부트 프로토콜을 기준으로 한다.


핵심 질문은 “커널의 첫 명령어가 어떻게 init을 호출하는가?”보다 “'''어떤 실행 환경을 먼저 만들고, 어느 시점에 별도 태스크를 생성하며, 그 태스크가 언제 사용자 프로그램으로 바뀌는가?'''”이다. 함수 호출, 분기, 태스크 생성, exec는 서로 다른 사건이다. 아래 화살표도 이 차이를 구별해서 읽어야 한다.
== 읽는 순서 ==
# [[운영체제 기본/x86-64 Linux의 BIOS 부팅/GRUB에서 커널로|GRUB에서 커널로]]: 커널·initramfs 적재, boot_params와 CPU 상태의 인계.
# [[운영체제 기본/x86-64 Linux의 BIOS 부팅/진입 규약과 압축 해제|진입 규약과 압축 해제]]: startup_32에서 64비트 모드로 전환하고 커널 본체를 배치하는 과정.
# [[운영체제 기본/x86-64 Linux의 BIOS 부팅/GDT와 LDT 초기화|GDT와 LDT 초기화]]: 부트로더의 GDT에서 커널의 CPU별 GDT·TSS로 넘어가는 과정과 LDT의 초기 상태.
# [[운영체제 기본/x86-64 Linux의 BIOS 부팅/커널 초기화|커널 초기화]]: 초기 주소 공간과 스택, 예외 처리에서 start_kernel의 공통 초기화까지.
# [[운영체제 기본/x86-64 Linux의 BIOS 부팅/PID 1 생성|PID 1 생성]]: 부트 태스크와 PID 1의 분리, 나머지 초기화와 동기화.
# [[운영체제 기본/x86-64 Linux의 BIOS 부팅/루트 파일시스템과 init 선택|루트 파일시스템과 init 선택]]: rootfs와 initramfs, init 후보의 선택, PID 1의 첫 사용자 프로그램 실행.


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

2026년 10월 31일 (토) 14:26 기준 최신판

운영체제 기본 목차

BIOS 환경의 GRUB 2.12가 Linux v6.12의 x86-64 커널을 적재하고, 커널이 첫 사용자 프로그램인 init을 실행하기까지의 과정을 따라간다. GRUB의 일반 linux 명령이 사용하는 32비트 부트 프로토콜을 기준으로 한다.

읽는 순서

  1. GRUB에서 커널로: 커널·initramfs 적재, boot_params와 CPU 상태의 인계.
  2. 진입 규약과 압축 해제: startup_32에서 64비트 모드로 전환하고 커널 본체를 배치하는 과정.
  3. GDT와 LDT 초기화: 부트로더의 GDT에서 커널의 CPU별 GDT·TSS로 넘어가는 과정과 LDT의 초기 상태.
  4. 커널 초기화: 초기 주소 공간과 스택, 예외 처리에서 start_kernel의 공통 초기화까지.
  5. PID 1 생성: 부트 태스크와 PID 1의 분리, 나머지 초기화와 동기화.
  6. 루트 파일시스템과 init 선택: rootfs와 initramfs, init 후보의 선택, PID 1의 첫 사용자 프로그램 실행.

실행 파일의 형식 판별, 세그먼트 매핑, 초기 스택과 레지스터 준비의 공통 원리는 Linux의 ELF 로딩에서 설명한다.