Boot · Linux v6.18.37 · init/main.c verified

start_kernel(): Linux가 운영체제가 되는 순서

아키텍처별 어셈블리가 C 코드를 실행할 수 있게 만들었다고 스케줄러와 인터럽트, 파일 시스템까지 준비된 것은 아닙니다. start_kernel()은 아직 불완전한 환경에서 다음 초기화가 의존할 최소 조건을 하나씩 게시하고, 마지막에 PID 1과 kthreadd를 만든 뒤 boot CPU를 idle loop로 넘깁니다.

Baseline
Linux v6.18.37
Entry
init/main.c:908
Handoff
rest_init():711
End state
PID 1 + kthreadd + idle

핵심 질문

부팅 초기에 함수 호출 순서가 왜 그렇게 엄격할까요? 정답은 아직 사용할 수 없는 기능을 먼저 쓰지 않기 위해서입니다. allocator가 없을 때 동적 메모리를 요구하면 안 되고, scheduler가 준비되기 전에 timer interrupt가 task 전환을 요구해도 안 되며, kthreadd가 생기기 전에 PID 1이 kernel thread를 만들게 해서도 안 됩니다.

함수 이름 목록만 외우면 놓치는 것

start_kernel()은 모든 driver를 직접 초기화하지 않습니다. 여기서는 core infrastructure를 만들고, 이후 PID 1의 kernel_init_freeable()이 initcall을 실행하며 device와 filesystem을 본격적으로 올립니다. 두 단계를 섞으면 부팅 로그의 위치를 잘못 해석합니다.

1. 쉬운 모델: 공사 중인 도시의 개통 순서

도로가 없는데 버스를 운행할 수 없고, 신호등 제어실이 없는데 교차로 전원을 켤 수 없습니다. 부팅도 같습니다. start_kernel()의 호출 순서는 subsystem 이름의 나열이 아니라 의존성 그래프를 일렬로 편 결과입니다.

초반: 혼자서 기반을 만든다

boot CPU 하나가 interrupt를 막은 상태에서 architecture, command line, 초기 메모리, scheduler 뼈대를 준비합니다.

중반: 외부 사건을 받기 시작한다

IRQ, tick, timer, timekeeping을 준비한 뒤에야 local_irq_enable()로 interrupt를 허용합니다.

후반: 일반 커널 객체를 만든다

fork, credential, namespace, VFS, security, cgroup 같은 장기 수명 객체와 cache를 준비합니다.

인계: 세 갈래로 나뉜다

PID 1은 userspace 준비로, PID 2 kthreadd는 kernel thread 관리로, 원래 boot task는 idle로 갑니다.

선행 조건을 만든 뒤 외부 사건과 동시성을 연다

주소·메모리 기반setup_arch → per-CPU
trap → mm_core_initCPU와 메모리의 기본 주소·관리 구조
사건을 받을 준비sched_init → RCU
IRQ → timer → timekeepinglocal_irq_enable은 이 뒤에 위치
task와 driver 실행rest_init → PID 1
workqueue_init → initcall나머지 CPU·worker·device로 실행 확장
그림 1. 화살표는 대표 선행 관계이며 중간 호출을 생략했다. scheduler 준비, local IRQ 허용, secondary CPU 시작, userspace 실행은 서로 다른 경계다.근거: init/main.c:908–1110, 1563–1592.

2. 실제 초기화 순서와 각 단계의 산출물

범위대표 호출다음 단계가 얻는 조건
914–930smp_setup_processor_id(), local_irq_disable(), boot_cpu_init()boot CPU identity와 interrupt-off 초기 문맥
931–941setup_arch(), static call/key, early_security_init(), per-CPU, early NUMAarchitecture memory map, command line 입력, per-CPU 주소
944–976parameter parse, early random, log buffer, early VFS cache, trap, mm_core_init(), ftraceallocator 핵심과 예외·초기 추적 기반
978–1007sched_init(), housekeeping, early workqueue, RCU, traceinterrupt보다 먼저 동작 가능한 scheduler와 deferred work의 골격
1012–1038IRQ, tick, timer, hrtimer, softirq, timekeeping, random, perf 후 local_irq_enable()interrupt를 받아도 되는 상태
1040–1078late slab, console, lockdep/selftest, NUMA, ACPI early, sched clock, delay calibration관찰 가능한 console과 후반 객체 생성 조건
1080–1107PID allocator, fork, credentials, namespaces, security, network namespace, VFS, cgrouptask와 namespace, filesystem core 객체
1109–1110rest_init()PID 1, kthreadd, boot idle thread

setup_arch()는 generic 초기화보다 앞선 architecture 계약입니다.

arm64에서는 DT/ACPI, memory map, reserved memory, paging과 CPU 정보를 준비하지만 세부 호출은 architecture와 config에 따라 달라집니다. generic start_kernel() 본문만 읽고 firmware input의 lifetime까지 단정하면 안 됩니다.

초기화 함수가 끝났다는 말의 정확한 범위

mm_core_init()으로 메모리 관리의 핵심이 준비되어도 곧바로 모든 blocking allocation이 안전한 것은 아닙니다. 함수가 존재하는 것, 내부 자료구조가 준비된 것, 잠들었다 깨어날 실행 문맥이 있는 것은 다른 조건입니다. kernel_init_freeable()은 scheduler 준비 이후에야 gfp_allowed_mask = __GFP_BITS_MASK로 허용 범위를 넓힙니다.

경계준비된 것아직 가정하면 안 되는 것잘못 쓰면
setup_arch 이후architecture 초기 주소·memory map범용 C subsystem의 전체 준비초기 메모리 접근 오류
sched_init 이후boot runqueue와 scheduler 기본 상태SMP 전체와 모든 worker 실행실행할 worker를 기다리며 정지
workqueue_init_early 이후workqueue 생성·queueing의 기반queue한 work의 즉시 실행완료되지 않는 flush/wait
local_irq_enable 이후boot CPU의 local IRQ 허용다른 CPU와 모든 device의 가동IRQ enable을 SMP ready로 오해
PID 1의 workqueue_init 이후worker 실행 환경 구축모든 driver probe 완료아직 없는 device를 사용
그림 2. 부팅 시 기능 사용 가능 범위. 이 표는 API 사용 전 확인할 경계를 보여 주며 모든 allocation flag를 허용하는 표가 아니다.근거: init/main.c:978–1007의 주석, 1037–1048, 1563–1592.

workqueue의 두 단계가 좋은 예입니다. 앞 단계는 다른 subsystem이 work를 등록할 수 있도록 장부를 만듭니다. 실제로 그것을 처리할 kthread를 만들고 실행시키는 것은 뒤 단계입니다. 초기화 함수에서 “등록했으니 완료될 때까지 기다리자”라고 생각하면 아직 생기지 않은 worker를 기다리는 의존성 순환을 만들 수 있습니다. CPU 하나로 실행 중이라도 이런 대기 관계는 생깁니다.

3. 원문에서 순서를 확인한다

architecture와 security의 실제 순서 · init/main.c 908–941

asmlinkage __visible __init __no_sanitize_address __noreturn __no_stack_protector
void start_kernel(void)
{
	char *command_line;
	char *after_dashes;

	set_task_stack_end_magic(&init_task);
	smp_setup_processor_id();
	debug_objects_early_init();
	init_vmlinux_build_id();

	cgroup_init_early();

	local_irq_disable();
	early_boot_irqs_disabled = true;
	...
	boot_cpu_init();
	page_address_init();
	pr_notice("%s", linux_banner);
	setup_arch(&command_line);
	jump_label_init();
	static_call_init();
	early_security_init();
	setup_boot_config();
	setup_command_line(command_line);
	setup_nr_cpu_ids();
	setup_per_cpu_areas();
	smp_prepare_boot_cpu();
	early_numa_node_init();

v6.18.37에서 setup_arch()가 early_security_init()보다 먼저입니다. LSM의 early hook이 static key와 static call을 사용하므로 그 둘도 security 초기화보다 먼저 준비합니다. 이 순서는 과거 글의 코드 조각을 기억으로 옮기지 말고 해당 tag에서 확인해야 합니다.

scheduler가 interrupt보다 먼저인 이유 · init/main.c 978–1038

/* Set up the scheduler prior starting any interrupts ... */
sched_init();
...
workqueue_init_early();
rcu_init();
kvfree_rcu_init();
trace_init();
...
early_irq_init();
init_IRQ();
tick_init();
rcu_init_nohz();
timers_init();
srcu_init();
hrtimers_init();
softirq_init();
timekeeping_init();
time_init();
...
early_boot_irqs_disabled = false;
local_irq_enable();

이름이 비슷해도 이 버전의 함수는 init_timers()가 아니라 timers_init()입니다. timer interrupt가 시작되면 wakeup과 softirq, scheduler 상태를 건드릴 수 있으므로 sched_init()가 먼저 와야 합니다. workqueue_init_early()는 생성과 queueing을 허용할 뿐, worker 실행은 나중의 workqueue_init()과 kthread 준비까지 기다립니다.

generic core 초기화의 끝 · init/main.c 1080–1110

pid_idr_init();
anon_vma_init();
thread_stack_cache_init();
cred_init();
fork_init();
proc_caches_init();
uts_ns_init();
time_ns_init();
key_init();
security_init();
dbg_late_init();
net_ns_init();
vfs_caches_init();
pagecache_init();
signals_init();
seq_file_init();
proc_root_init();
nsfs_init();
pidfs_init();
cpuset_init();
mem_cgroup_init();
cgroup_init();
...
/* Do the rest non-__init'ed, we're now alive */
rest_init();

rest_init() 앞까지가 kernel이 task를 만들고 schedule할 수 있는 core state를 구축하는 구간입니다. device driver의 다수는 아직 올라오지 않았고, root filesystem도 아직 mount되지 않았습니다.

4. rest_init(): PID 1과 PID 2, 그리고 idle

세 실행 흐름을 만드는 곳 · init/main.c 711–758

static noinline void __ref __noreturn rest_init(void)
{
	struct task_struct *tsk;
	int pid;

	rcu_scheduler_starting();
	pid = user_mode_thread(kernel_init, NULL, CLONE_FS);
	...
	set_cpus_allowed_ptr(tsk, cpumask_of(smp_processor_id()));
	...
	pid = kernel_thread(kthreadd, NULL, NULL, CLONE_FS | CLONE_FILES);
	...
	kthreadd_task = find_task_by_pid_ns(pid, &init_pid_ns);
	...
	system_state = SYSTEM_SCHEDULING;
	complete(&kthreadd_done);

	schedule_preempt_disabled();
	cpu_startup_entry(CPUHP_ONLINE);
}
  1. PID 1을 먼저 예약user_mode_thread(kernel_init, ...)를 먼저 호출해 init task가 PID 1을 얻습니다.
  2. 임시 CPU 고정sched_init_smp() 전에는 migration이 완전히 준비되지 않아 init task를 boot CPU에 고정합니다.
  3. PID 2 생성kernel_thread(kthreadd, ...)가 이후 kernel thread 생성 요청을 담당할 task를 만듭니다.
  4. 동시성 문 열기complete(&kthreadd_done) 뒤에야 PID 1이 기다림을 끝내고 후속 초기화를 진행할 수 있습니다.
  5. boot task의 최종 역할원래 실행 흐름은 한 번 schedule한 뒤 cpu_startup_entry()에서 idle thread가 됩니다.

kernel_init()은 먼저 wait_for_completion(&kthreadd_done)에서 기다립니다. 그 뒤 kernel_init_freeable()로 initcall을 실행하고, asynchronous init 작업이 끝난 후 free_initmem()으로 __init 메모리를 반환합니다. rest_init()이 __init가 아닌 __ref 함수인 이유도 init thread와 root thread 사이의 free-initmem race를 피하기 위해서라고 원문 주석에 명시돼 있습니다.

먼저 생성하되 먼저 진행시키지는 않는다

PID 할당 순서와 실행 순서는 다릅니다. init을 먼저 생성해야 PID 1이 됩니다. 하지만 init이 kthread 생성을 요청할 때에는 요청을 처리하는 kthreadd_task가 이미 게시되어 있어야 합니다. 그래서 rest_init()은 PID 1의 생성과 후속 실행 사이에 completion을 둡니다.

논리 순서 ↓boot task · PID 0init task · PID 1kthreadd · PID 2
1user_mode_thread(kernel_init)task 생성, boot CPU에 임시 고정아직 없음
2kernel_thread(kthreadd)실행 기회를 얻어도 completion 전에는 대기task 생성
3kthreadd_task 게시 → SYSTEM_SCHEDULINGwait_for_completionkernel thread 생성 요청의 처리 주체
4complete(kthreadd_done)후속 초기화가 진행 가능CPU를 받으면 요청 처리
5schedule → cpu_startup_entrykernel_init_freeable → initcall요청 대기/처리 반복
그림 3. 이 표는 동기화상 필요한 순서이지 세 task가 같은 순간 병렬로 실행되는 실측 시간표가 아니다. 실제 CPU를 받는 시점은 스케줄링에 달려 있다.근거: init/main.c:711–758, 1473–1485.

completion은 순간적인 신호만 보내고 사라지는 장치가 아닙니다. PID 0이 먼저 complete()하고 나중에 PID 1이 wait하더라도 완료 상태를 소비하고 진행합니다. 반대로 PID 1이 먼저 기다리면 완료 시 깨웁니다. 그래서 이 코드는 “PID 1이 이미 wait 중이어야 한다”는 타이밍 가정이 필요 없습니다. 단, 이 completion은 모든 driver 초기화가 끝났다는 신호가 아니라 kthreadd 준비라는 특정 조건을 게시합니다.

system_state = SYSTEM_SCHEDULING도 같은 문맥에서 읽어야 합니다. 이 전역 상태를 바꾸는 대입문 자체가 scheduler를 구현하거나 CPU를 시작시키는 것은 아닙니다. 이미 준비한 조건을 다른 경로와 진단 코드에 알리는 단계입니다. 부팅이 끝났다는 SYSTEM_RUNNING 전환은 훨씬 뒤의 PID 1 경로에 있습니다.

4.1 PID 1이 initcall을 실행할 수 있는 이유

kernel_init_freeable()은 memory allocation 범위를 열고 smp_prepare_cpus(), workqueue_init(), early initcall을 거친 뒤 smp_init()과 sched_init_smp()를 호출합니다. 이후 do_basic_setup()에서 driver model을 준비하고 일반 initcall을 실행합니다. 따라서 early initcall과 level 0 이후의 initcall은 실행 전제부터 다릅니다.

early · do_pre_smp_initcalls__initcall_start → __initcall0_start
smp_init보다 앞
smp_init → sched_init_smpsecondary CPU와 scheduler topology 준비
0 pure → 1 core → 2 postcore → 3 archinitcall_levels의 각 연속 구간을 호출
4 subsys → 5 fs → 6 device → 7 late동일 level 안의 모든 항목을 순회
그림 4. initcall은 linker가 만든 함수 포인터 구간을 순회하는 구조다. 이름은 단계 구분이며 함수 호출 하나하나의 성공을 보장하지 않는다.근거: init/main.c:1302–1388, 1575–1592. 실제 비용 비율을 나타내지 않는 단계도.

한 initcall의 실행 경계 · init/main.c:1274–1295 중 핵심 발췌

int count = preempt_count();
...
do_trace_initcall_start(fn);
ret = fn();
do_trace_initcall_finish(fn, ret);
...
if (preempt_count() != count) {
    sprintf(msgbuf, "preemption imbalance ");
    preempt_count_set(count);
}
if (irqs_disabled()) {
    strlcat(msgbuf, "disabled interrupts ", sizeof(msgbuf));
    local_irq_enable();
}

왜 호출 전 preempt_count를 저장하는가

initcall이 preemption disable을 짝 맞춰 복구하지 않으면 다음 초기화 함수까지 잘못된 문맥을 물려받습니다. 호출자가 count를 비교하고 경고하며 복구하는 것은 그 오염을 드러내기 위해서입니다. 이것이 initcall의 락 누수나 자원 누수를 모두 회복해 준다는 뜻은 아닙니다.

반환값이 음수이면 부팅이 반드시 멈추는가

do_one_initcall()은 ret을 돌려주지만 일반 level 순회는 그것을 일괄 panic 조건으로 사용하지 않습니다. 장치가 없거나 초기화가 실패한 함수가 있어도 다음 함수로 갈 수 있습니다. 반대로 함수 안에서 panic하거나 기다림에 빠지면 다음 종료 기록이 없습니다. “음수 반환”과 “반환하지 않음”을 로그에서 구별해야 합니다.

level 순서로 모든 driver 의존성을 표현할 수 있는가

같은 level 안의 링크 순서를 안정적인 의존성 API로 쓰면 빌드 변화에 취약합니다. device 등록, bus 탐색, probe와 deferred probe는 별도의 관계를 갖습니다. initcall의 종료가 그 함수에서 요청한 모든 비동기 작업의 완료라는 보장도 없습니다. bootgraph의 한 막대가 subsystem의 전체 준비 시간을 의미하지 않는 이유입니다.

4.2 __init 메모리를 돌려주기 전 지켜야 할 것

부팅 전용 함수는 영구적으로 RAM을 차지할 필요가 없습니다. __init과 __initdata로 분리한 구간을 부팅 뒤 회수하면 다른 용도로 재사용할 수 있습니다. 문제는 코드를 가리키는 주소가 여전히 남아 있거나 다른 CPU가 그 코드를 실행 중인 경우입니다. allocator는 그것이 예전 함수 주소였다는 사실을 기억해 보호해 주지 않습니다.

부팅 중.text: 계속 쓰는 코드
.init.text / .init.data초기화 함수와 임시 데이터 사용 중
async init 작업반환 전 남은 실행이 있을 수 있음
회수 후.text: 여전히 유효
과거 init 영역다른 메모리 용도로 재사용 가능
오래된 함수 포인터는 유효하지 않음
영구 참조살아 있는 코드·데이터만 가리켜야 함
kernel_init_freeable 반환 → async_synchronize_full → 추적 hook 정리 → free_initmem → mark_readonly
그림 5. 실행 완료와 메모리 수명 종료는 별개의 경계다. async_synchronize_full()은 async 초기화 작업을 기다리는 것이며 모든 임의의 driver work를 자동으로 정리하지는 않는다.근거: init/main.c:1482–1505. 메모리 그림의 면적은 실제 용량과 무관하다.

rest_init()이 __ref인 이유는 root thread가 아직 그 함수를 빠져나오지 못했는데 init thread가 init 영역을 회수하는 경우를 피하기 위해서입니다. __ref는 일반적인 use-after-free 해결책이 아닙니다. 장기 사용 callback을 무심코 __init 함수로 등록했다면 callback의 수명을 바꾸거나 함수 배치를 수정해야 합니다. build의 section mismatch 경고를 숨기는 것만으로는 문제가 해결되지 않습니다.

4.3 PID 1이 어떤 프로그램으로 바뀌는가

PID 1은 처음에는 커널의 kernel_init()을 실행합니다. 이후 run_init_process()가 kernel_execve()를 호출하여 그 task의 프로그램을 userspace init으로 바꿉니다. 성공할 때마다 새 PID 1을 만드는 것이 아닙니다. 파일이 존재해도 CPU architecture, ELF interpreter, 실행 권한 등의 이유로 exec에 실패할 수 있습니다.

initramfs의 rdinit접근 가능하면 우선 실행 시도
실행 실패를 출력하고 다음 후보로
명시한 init=성공하면 종료 / 실패하면 panic
사용자가 지정한 요청을 조용히 대체하지 않음
CONFIG_DEFAULT_INIT설정되어 있으면 시도, 실패 시 계속
기본 후보/sbin/init → /etc/init → /bin/init → /bin/sh
모두 실패No working init found
그림 6. 위에서 아래로 실패 시 이동하되 명시적인 init= 실패는 panic으로 끝난다. initramfs의 rdinit 접근 여부에 따른 namespace 준비는 이 시도에 앞서 수행된다.근거: init/main.c:1391–1417, 1507–1541, 1596–1609.

Failed to execute /init가 나왔다면 커널 entry보다 initramfs 안의 실행 파일과 interpreter를 먼저 봅니다. 동적 ELF에 지정된 loader가 없으면 실행 파일이 존재해도 실패합니다. root filesystem 자체를 준비하지 못한 경우와 이미 rootfs가 있는데 init 실행이 실패한 경우는 조사 순서가 다릅니다. 소스의 분기를 부팅 로그와 연결해야 불필요한 MMU 디버깅으로 되돌아가지 않습니다.

5. 순서를 바꾸면 깨지는 불변 조건

  • interrupt gate: local_irq_enable() 전에는 IRQ, tick, timer, softirq, timekeeping의 최소 경로가 모두 초기화되어야 합니다.
  • allocator boundary: mm_core_init() 전후로 사용할 수 있는 allocator가 다릅니다. early code가 일반 blocking allocation을 요구하면 안 됩니다.
  • console은 부분 준비: console_init() 시점에는 PCI 등 모든 bus가 준비된 것이 아닙니다. console driver는 early 조건을 감당해야 합니다.
  • PID 1 before kthreadd: init을 먼저 생성해 PID 1을 보장하지만, 실행은 completion으로 막아 kthreadd보다 앞서 kernel thread를 만들지 못하게 합니다.
  • init memory lifetime: async init 작업과 참조가 끝나기 전에 free_initmem()을 호출하면 이미 반환된 코드로 실행할 수 있습니다.
  • configuration changes the path: ACPI, initrd, lockdep, tracing, security, cgroup 설정에 따라 호출의 효과와 비용이 달라집니다.

6. 마지막 로그로 멈춘 위치를 좁히기

관찰가능한 범위다음 확인
Linux banner도 없음start_kernel() 이전 또는 아주 초반architecture head code, stack, early mapping, UART early path
command line 이전에 정지setup_arch() 또는 firmware/memory map parseDTB/ACPI 주소, reserved memory, early page table
console 전 로그가 끊김IRQ/timer/timekeeping 또는 allocator 초기화earlycon, initcall이 아닌 core call에 임시 trace
Freeing unused kernel image가 보임PID 1이 initcall 대부분을 통과rootfs, init=, executable/loader, mount 실패
동일 지점에서 watchdog resetdeadlock뿐 아니라 interrupt/timer 미동작 가능IRQ mask, timer source, lockdep, watchdog pet 위치

마지막 출력 함수가 실패 함수라는 뜻은 아닙니다. console buffer, interrupt 상태, UART FIFO, CPU reset 때문에 실제 실패 이후의 로그가 전달되지 않을 수 있습니다. breakpoint나 memory signature로 실행 도달 여부를 별도로 확인해야 합니다.

7. 재현 가능한 관찰 방법

  • 초기 출력: 보드에 맞는 earlycon과 ignore_loglevel, 충분한 log_buf_len=을 사용합니다. 주소와 clock 설정이 틀리면 earlycon 자체가 침묵합니다.
  • initcall 시간: kernel command line에 initcall_debug를 넣고 함수별 시작·종료와 반환값을 수집합니다. 이것은 start_kernel()의 모든 core 호출을 자동 계측하는 옵션은 아닙니다.
  • boot graph: timestamp가 포함된 dmesg를 scripts/bootgraph.pl로 분석해 initcall 구간을 시각화합니다.
  • 함수 graph: 동일 baseline의 debug kernel에서 function graph tracer를 제한적으로 사용합니다. 초기 ftrace 준비 전 구간은 보이지 않을 수 있습니다.
  • QEMU/GDB: start_kernel, sched_init, rest_init와 IRQ enable 직전 소스 행에 breakpoint를 두고 CPU, SP, interrupt mask를 함께 기록합니다. local_irq_enable은 macro/inline이므로 독립 함수 symbol이 없을 수 있습니다.
  • 비교 실험: config, compiler, command line, DTB/ACPI table hash를 보존해야 두 boot log의 시간 차이를 의미 있게 비교할 수 있습니다.

관측 도구가 볼 수 있는 구간을 먼저 고른다

도구/근거architecture entrystart_kernel corePID 1 initcalluserspace
JTAG / QEMU GDB주소·registersymbol·소스 행task 문맥 확인별도 symbol 필요
earlycon설정 전 출력 불가초기화 시점 이후boot log일반 console 경로
initcall_debug대상 아님직접 호출은 대상 아님시작·종료·반환값대상 아님
bootgraph자료 없음자료 없음initcall 기록의 시간 그래프별도 도구
그림 7. 보이지 않는 구간은 실행되지 않았다는 뜻이 아니라 계측이 아직 준비되지 않았거나 해당 도구의 대상이 아닐 수 있다.근거: init/main.c의 trace_init·initcall 계측 위치. architecture별 debugger와 earlycon 설정은 별도 확인.

symbol이 있는 debug vmlinux와 일치하는 QEMU image를 사용한 GDB 절차

file vmlinux
target remote :1234
break start_kernel
break rest_init
break kernel_init
break kernel_init_freeable
continue
info threads
info registers
bt

QEMU는 선택한 architecture·machine에 맞게 시작하고, GDB 연결 포트와 멈춤 옵션을 설정해야 합니다. 위 절차는 이미 그 환경을 준비했다는 전제입니다. KASLR을 사용하는 image는 런타임 주소에 맞춘 symbol 처리가 필요하므로 통제된 부팅 실험에서는 nokaslr 여부도 기록합니다. 이 글에는 실제 보드/QEMU 실행 로그를 첨부하지 않았습니다. 소스 순서 검증과 실행 측정을 구분합니다.

결과 기록에는 최소한 kernel tag, .config, compiler, command line, firmware/DTB, 마지막 진입 함수와 복귀 여부를 포함합니다. 예를 들어 initcall 시작은 있지만 종료가 없다면 breakpoint로 대기 중 stack을 확인하고, 종료와 음수 ret이 있다면 해당 초기화의 오류 경로를 봅니다. console 출력 지연 때문에 마지막 로그만으로 PC를 추정하지 않습니다.

8. 이 글의 범위 밖

이 글은 generic init/main.c의 동기 초기화와 rest_init() 인계를 다룹니다. arm64 head.S의 MMU 전환, setup_arch() 내부의 DT/ACPI 세부, secondary CPU bring-up, 각 driver의 probe, root filesystem mount 내부와 userspace execve() 내부는 별도의 호출 경로입니다. 이 글에서는 initcall dispatch와 init 프로그램 선택 경계까지만 연결합니다.

특히 line number는 v6.18.37에만 고정됩니다. 다른 stable release에서 함수 이름이 같더라도 ordering과 config guard를 다시 확인해야 합니다.

원문과 검증 범위