← Architecture 비교DUJINLABS.COM

Linux 6.18.37 LTS · Architecture comparison 19/21

Stack unwind, function tracing과 return address 보호

panic, perf, ftrace와 live debugging이 현재 instruction에서 caller chain을 복원하는 방법과 compiler가 제공해야 하는 metadata를 비교합니다.

비교 대상
arm64 / x86-64 / RISC-V
실제 원본
3 files · 219 annotated lines
기준 tag
Linux v6.18.37
분석 축
state · ordering · lifetime · latency

01 · QUESTION

무엇을 확인할 것인가

frame pointer가 없거나 interrupt가 별도 stack에서 들어왔을 때 kernel은 어떤 근거로 다음 stack frame과 return address를 신뢰하는가?

stack walker는 현재 PC/SP/FP와 task stack 경계를 받아 callback으로 frame을 전달한다. interrupt, exception, scheduler와 trampoline frame을 인식해 다른 stack segment로 넘어가며 invalid address와 loop를 차단한다.

live current-task unwind, stopped remote task, NMI/IRQ stack과 user stack을 구분한다. tracing instrumentation 자체가 stack을 바꾸거나 return address를 수정할 수 있으므로 ftrace/kretprobe metadata를 적용한다.

지연 시간 관점frame-pointer walk는 frame 수에 비례하고 ORC/DWARF-like state lookup은 metadata search가 더해진다. NMI에서 긴 unwind와 symbol lookup은 watchdog latency를 악화시킬 수 있다.

02 · CONTRACT

공통 계약과 architecture 구현

architecture핵심 mechanism실패 형태확인할 상태
arm64x29 frame record와 LR/PAC/SCS 처리깨진 x29 chain, PAC 미제거 또는 IRQ stack 경계 누락은 잘못된 symbol과 out-of-bounds stack read를 만든다.FP/SP 정렬, task/IRQ/overflow stack range, saved LR, PAC mask, SCS pointer와 unwind consumer를 확인한다.
x86-64ORC unwind table과 exception/IRQ stack metadata손상되거나 다른 build의 ORC table, noinstr 규칙 위반 또는 stack type 오판은 unwind stop과 잘못된 call trace를 만든다.IP의 ORC entry, SP/BP base와 offset, stack_info type, pt_regs 위치, module unwind table과 objtool warning을 본다.
RISC-Vs0 frame pointer chain 또는 compiler unwind 제한frame-pointer omission, leaf function의 RA 보존 방식 또는 손상된 s0가 call chain 단절과 잘못된 address를 만든다.s0/fp, sp, ra, task/IRQ stack range, pt_regs epc와 CONFIG_FRAME_POINTER/ftrace 설정을 확인한다.

03 · DIAGRAMS

세 그림으로 먼저 읽기

그림 1. 같은 목적, 서로 다른 mechanism각 ISA에서 실제로 추적할 state와 checkpoint를 한 줄에 맞췄습니다.

arm64

mechanism
x29 frame record와 LR/PAC/SCS 처리
state
AAPCS frame record의 previous FP와 saved LR을 따라간다. exception entry frame, fgraph return trampoline, pointer authentication signed LR와 shadow call stack 구성을 고려해 실제 return PC를 복원한다.
checkpoint
FP/SP 정렬, task/IRQ/overflow stack range, saved LR, PAC mask, SCS pointer와 unwind consumer를 확인한다.

x86-64

mechanism
ORC unwind table과 exception/IRQ stack metadata
state
objtool이 instruction별 SP/BP 복원 규칙을 ORC table로 만든다. walker는 current IP에 해당하는 ORC entry를 찾아 다음 register state를 계산하고 pt_regs, call, regs, signal frame 유형을 구분한다.
checkpoint
IP의 ORC entry, SP/BP base와 offset, stack_info type, pt_regs 위치, module unwind table과 objtool warning을 본다.

RISC-V

mechanism
s0 frame pointer chain 또는 compiler unwind 제한
state
frame pointer build에서는 s0가 가리키는 frame record에서 previous FP와 RA를 읽는다. exception pt_regs와 task stack 경계를 인식하고 instruction pointer를 callback에 넘긴다.
checkpoint
s0/fp, sp, ra, task/IRQ stack range, pt_regs epc와 CONFIG_FRAME_POINTER/ftrace 설정을 확인한다.
그림 2. 공통 kernel과 architecture hook의 소유권공통 정책이 hardware state를 직접 소유하지 않는 경계를 표시합니다.
Linux common contractstack walker는 현재 PC/SP/FP와 task stack 경계를 받아 callback으로 frame을 전달한다. interrupt, exception, scheduler와 trampoline frame을 인식해 다른 stack segment로 넘어가며 invalid address와 loop를 차단한다.
arm64x29 frame record와 LR/PAC/SCS 처리stack boundary 검사 뒤 LR의 PAC를 strip하고 kernel text 범위인지 확인한다. current task가 아니면 안정적으로 정지된 상태여야 한다.
x86-64ORC unwind table과 exception/IRQ stack metadatabinary와 ORC metadata가 정확히 일치해야 하며 runtime code patching과 module load는 해당 table을 함께 등록한다.
RISC-Vs0 frame pointer chain 또는 compiler unwind 제한compressed instruction과 ftrace return-address patching을 고려하고 stack range를 벗어나면 즉시 중단한다.
lifetime boundarytask stack과 unwind metadata는 walk가 끝날 때까지 유효해야 한다. remote running task를 정지하지 않고 walk하면 SP와 frame contents가 동시에 바뀌어 일관된 call chain을 보장할 수 없다.
그림 3. publication과 관찰 순서state를 준비한 뒤 architecture ordering을 거쳐 관찰 가능한 checkpoint가 됩니다.
arm64state 준비stack boundary 검사 뒤 LR의 PAC를 strip하고 kernel text 범위인지 확인한다. current task가 아니면 안정적으로 정지된 상태여야 한다.관찰: FP/SP 정렬, task/IRQ/overflow stack range, saved LR, PAC mask, SCS pointer와 unwind consumer를 확인한다.
x86-64state 준비binary와 ORC metadata가 정확히 일치해야 하며 runtime code patching과 module load는 해당 table을 함께 등록한다.관찰: IP의 ORC entry, SP/BP base와 offset, stack_info type, pt_regs 위치, module unwind table과 objtool warning을 본다.
RISC-Vstate 준비compressed instruction과 ftrace return-address patching을 고려하고 stack range를 벗어나면 즉시 중단한다.관찰: s0/fp, sp, ra, task/IRQ stack range, pt_regs epc와 CONFIG_FRAME_POINTER/ftrace 설정을 확인한다.

04 · SOURCE

Linux 6.18.37 원본 코드와 줄별 설명

소스 위치를 고정된 숫자로 복사하지 않고 Linux v6.18.37 tree에서 함수 선언을 다시 찾아 발췌했습니다. 아래 코드와 각 줄의 설명은 1:1로 대응합니다.

arm64 · Linux 6.18.37

x29 frame record와 LR/PAC/SCS 처리

AAPCS frame record의 previous FP와 saved LR을 따라간다. exception entry frame, fgraph return trampoline, pointer authentication signed LR와 shadow call stack 구성을 고려해 실제 return PC를 복원한다.

원본 코드: arch/arm64/kernel/stacktrace.c:373-435

373 
374static __always_inline bool
375arch_kunwind_consume_entry(const struct kunwind_state *state, void *cookie)
376{
377	struct kunwind_consume_entry_data *data = cookie;
378	return data->consume_entry(data->cookie, state->common.pc);
379}
380 
381noinline noinstr void arch_stack_walk(stack_trace_consume_fn consume_entry,
382			      void *cookie, struct task_struct *task,
383			      struct pt_regs *regs)
384{
385	struct kunwind_consume_entry_data data = {
386		.consume_entry = consume_entry,
387		.cookie = cookie,
388	};
389 
390	kunwind_stack_walk(arch_kunwind_consume_entry, &data, task, regs);
391}
392 
393static __always_inline bool
394arch_reliable_kunwind_consume_entry(const struct kunwind_state *state, void *cookie)
395{
396	/*
397	 * At an exception boundary we can reliably consume the saved PC. We do
398	 * not know whether the LR was live when the exception was taken, and
399	 * so we cannot perform the next unwind step reliably.
400	 *
401	 * All that matters is whether the *entire* unwind is reliable, so give
402	 * up as soon as we hit an exception boundary.
403	 */
404	if (state->source == KUNWIND_SOURCE_REGS_PC)
405		return false;
406 
407	return arch_kunwind_consume_entry(state, cookie);
408}
409 
410noinline noinstr int arch_stack_walk_reliable(stack_trace_consume_fn consume_entry,
411					      void *cookie,
412					      struct task_struct *task)
413{
414	struct kunwind_consume_entry_data data = {
415		.consume_entry = consume_entry,
416		.cookie = cookie,
417	};
418 
419	return kunwind_stack_walk(arch_reliable_kunwind_consume_entry, &data,
420				  task, NULL);
421}
422 
423struct bpf_unwind_consume_entry_data {
424	bool (*consume_entry)(void *cookie, u64 ip, u64 sp, u64 fp);
425	void *cookie;
426};
427 
428static bool
429arch_bpf_unwind_consume_entry(const struct kunwind_state *state, void *cookie)
430{
431	struct bpf_unwind_consume_entry_data *data = cookie;
432 
433	return data->consume_entry(data->cookie, state->common.pc, 0,
434				   state->common.fp);
435}

라인 바이 라인 주석

빈 줄과 전처리 경계도 생략하지 않았습니다. 원본의 63개 줄에 각각 설명을 붙였습니다.

L373(blank)

빈 줄은 arm64 Stack unwind, function tracing과 return address 보호 경로에서 한 상태 묶음이 끝나는 위치다. 위쪽에서 만든 값이 아래쪽에서 소비되는지 구간을 나눠 읽는다.

L374static __always_inline bool

선언 또는 macro 확장 일부다. type의 폭과 signedness, per-CPU/task/object 중 어느 수명을 따르는 값인지 확인한다.

L375arch_kunwind_consume_entry(const struct kunwind_state *state, void *cookie)

이 함수의 진입 계약이 시작된다. arm64에서 caller context, argument ownership과 반환 시 보장할 architecture state를 먼저 적는다.

L376{

C block의 시작 또는 끝이다. lock, RCU, preemption과 interrupt-disabled 범위를 이 중괄호 바깥 호출까지 넘겨 추정하지 않는다.

L377 struct kunwind_consume_entry_data *data = cookie;

계산한 pointer, flag, register image 또는 generation을 다음 단계가 읽을 위치에 저장한다. 값의 단위, address space와 publication ordering을 확인한다.

L378 return data->consume_entry(data->cookie, state->common.pc);

이 함수가 Stack unwind, function tracing과 return address 보호 단계의 결과 또는 오류를 상위 계층에 전달한다. 반환 전에 lock, interrupt state, reference와 hardware active state가 정리됐는지 확인한다.

L379}

C block의 시작 또는 끝이다. lock, RCU, preemption과 interrupt-disabled 범위를 이 중괄호 바깥 호출까지 넘겨 추정하지 않는다.

L380(blank)

빈 줄은 arm64 Stack unwind, function tracing과 return address 보호 경로에서 한 상태 묶음이 끝나는 위치다. 위쪽에서 만든 값이 아래쪽에서 소비되는지 구간을 나눠 읽는다.

L381noinline noinstr void arch_stack_walk(stack_trace_consume_fn consume_entry,

이 줄이 arm64의 현재 상태에서 읽는 register와 memory, 그리고 다음 줄에 남기는 값을 적는다. Stack unwind, function tracing과 return address 보호의 공통 kernel 계약과 architecture 전용 side effect를 분리해 해석한다.

L382 void *cookie, struct task_struct *task,

이 줄이 arm64의 현재 상태에서 읽는 register와 memory, 그리고 다음 줄에 남기는 값을 적는다. Stack unwind, function tracing과 return address 보호의 공통 kernel 계약과 architecture 전용 side effect를 분리해 해석한다.

L383 struct pt_regs *regs)

이 줄이 arm64의 현재 상태에서 읽는 register와 memory, 그리고 다음 줄에 남기는 값을 적는다. Stack unwind, function tracing과 return address 보호의 공통 kernel 계약과 architecture 전용 side effect를 분리해 해석한다.

L384{

C block의 시작 또는 끝이다. lock, RCU, preemption과 interrupt-disabled 범위를 이 중괄호 바깥 호출까지 넘겨 추정하지 않는다.

L385 struct kunwind_consume_entry_data data = {

계산한 pointer, flag, register image 또는 generation을 다음 단계가 읽을 위치에 저장한다. 값의 단위, address space와 publication ordering을 확인한다.

L386 .consume_entry = consume_entry,

계산한 pointer, flag, register image 또는 generation을 다음 단계가 읽을 위치에 저장한다. 값의 단위, address space와 publication ordering을 확인한다.

L387 .cookie = cookie,

계산한 pointer, flag, register image 또는 generation을 다음 단계가 읽을 위치에 저장한다. 값의 단위, address space와 publication ordering을 확인한다.

L388 };

C block의 시작 또는 끝이다. lock, RCU, preemption과 interrupt-disabled 범위를 이 중괄호 바깥 호출까지 넘겨 추정하지 않는다.

L389(blank)

빈 줄은 arm64 Stack unwind, function tracing과 return address 보호 경로에서 한 상태 묶음이 끝나는 위치다. 위쪽에서 만든 값이 아래쪽에서 소비되는지 구간을 나눠 읽는다.

L390 kunwind_stack_walk(arch_kunwind_consume_entry, &data, task, regs);

helper 또는 architecture operation을 실행한다. arm64에서 이 호출이 register write, cache/TLB operation, callback 또는 object lifetime 중 무엇을 바꾸는지 call site와 callee를 연결해 본다.

L391}

C block의 시작 또는 끝이다. lock, RCU, preemption과 interrupt-disabled 범위를 이 중괄호 바깥 호출까지 넘겨 추정하지 않는다.

L392(blank)

빈 줄은 arm64 Stack unwind, function tracing과 return address 보호 경로에서 한 상태 묶음이 끝나는 위치다. 위쪽에서 만든 값이 아래쪽에서 소비되는지 구간을 나눠 읽는다.

L393static __always_inline bool

선언 또는 macro 확장 일부다. type의 폭과 signedness, per-CPU/task/object 중 어느 수명을 따르는 값인지 확인한다.

L394arch_reliable_kunwind_consume_entry(const struct kunwind_state *state, void *cookie)

이 함수의 진입 계약이 시작된다. arm64에서 caller context, argument ownership과 반환 시 보장할 architecture state를 먼저 적는다.

L395{

C block의 시작 또는 끝이다. lock, RCU, preemption과 interrupt-disabled 범위를 이 중괄호 바깥 호출까지 넘겨 추정하지 않는다.

L396 /*

Linux 원본 주석이다. 바로 아래 코드의 호출 조건, hardware 제약 또는 예외 처리를 설명하므로 실행 줄과 함께 읽는다.

L397 * At an exception boundary we can reliably consume the saved PC. We do

Linux 원본 주석이다. 바로 아래 코드의 호출 조건, hardware 제약 또는 예외 처리를 설명하므로 실행 줄과 함께 읽는다.

L398 * not know whether the LR was live when the exception was taken, and

Linux 원본 주석이다. 바로 아래 코드의 호출 조건, hardware 제약 또는 예외 처리를 설명하므로 실행 줄과 함께 읽는다.

L399 * so we cannot perform the next unwind step reliably.

Linux 원본 주석이다. 바로 아래 코드의 호출 조건, hardware 제약 또는 예외 처리를 설명하므로 실행 줄과 함께 읽는다.

L400 *

Linux 원본 주석이다. 바로 아래 코드의 호출 조건, hardware 제약 또는 예외 처리를 설명하므로 실행 줄과 함께 읽는다.

L401 * All that matters is whether the *entire* unwind is reliable, so give

Linux 원본 주석이다. 바로 아래 코드의 호출 조건, hardware 제약 또는 예외 처리를 설명하므로 실행 줄과 함께 읽는다.

L402 * up as soon as we hit an exception boundary.

Linux 원본 주석이다. 바로 아래 코드의 호출 조건, hardware 제약 또는 예외 처리를 설명하므로 실행 줄과 함께 읽는다.

L403 */

Linux 원본 주석이다. 바로 아래 코드의 호출 조건, hardware 제약 또는 예외 처리를 설명하므로 실행 줄과 함께 읽는다.

L404 if (state->source == KUNWIND_SOURCE_REGS_PC)

이 조건이 arm64 fast path와 fallback/error path를 가른다. 조건에 쓰인 flag가 어느 CPU 또는 object의 상태인지, 동시에 바뀔 수 있는지 확인한다.

L405 return false;

이 함수가 Stack unwind, function tracing과 return address 보호 단계의 결과 또는 오류를 상위 계층에 전달한다. 반환 전에 lock, interrupt state, reference와 hardware active state가 정리됐는지 확인한다.

L406(blank)

빈 줄은 arm64 Stack unwind, function tracing과 return address 보호 경로에서 한 상태 묶음이 끝나는 위치다. 위쪽에서 만든 값이 아래쪽에서 소비되는지 구간을 나눠 읽는다.

L407 return arch_kunwind_consume_entry(state, cookie);

이 함수가 Stack unwind, function tracing과 return address 보호 단계의 결과 또는 오류를 상위 계층에 전달한다. 반환 전에 lock, interrupt state, reference와 hardware active state가 정리됐는지 확인한다.

L408}

C block의 시작 또는 끝이다. lock, RCU, preemption과 interrupt-disabled 범위를 이 중괄호 바깥 호출까지 넘겨 추정하지 않는다.

L409(blank)

빈 줄은 arm64 Stack unwind, function tracing과 return address 보호 경로에서 한 상태 묶음이 끝나는 위치다. 위쪽에서 만든 값이 아래쪽에서 소비되는지 구간을 나눠 읽는다.

L410noinline noinstr int arch_stack_walk_reliable(stack_trace_consume_fn consume_entry,

이 줄이 arm64의 현재 상태에서 읽는 register와 memory, 그리고 다음 줄에 남기는 값을 적는다. Stack unwind, function tracing과 return address 보호의 공통 kernel 계약과 architecture 전용 side effect를 분리해 해석한다.

L411 void *cookie,

선언 또는 macro 확장 일부다. type의 폭과 signedness, per-CPU/task/object 중 어느 수명을 따르는 값인지 확인한다.

L412 struct task_struct *task)

이 줄이 arm64의 현재 상태에서 읽는 register와 memory, 그리고 다음 줄에 남기는 값을 적는다. Stack unwind, function tracing과 return address 보호의 공통 kernel 계약과 architecture 전용 side effect를 분리해 해석한다.

L413{

C block의 시작 또는 끝이다. lock, RCU, preemption과 interrupt-disabled 범위를 이 중괄호 바깥 호출까지 넘겨 추정하지 않는다.

L414 struct kunwind_consume_entry_data data = {

계산한 pointer, flag, register image 또는 generation을 다음 단계가 읽을 위치에 저장한다. 값의 단위, address space와 publication ordering을 확인한다.

L415 .consume_entry = consume_entry,

계산한 pointer, flag, register image 또는 generation을 다음 단계가 읽을 위치에 저장한다. 값의 단위, address space와 publication ordering을 확인한다.

L416 .cookie = cookie,

계산한 pointer, flag, register image 또는 generation을 다음 단계가 읽을 위치에 저장한다. 값의 단위, address space와 publication ordering을 확인한다.

L417 };

C block의 시작 또는 끝이다. lock, RCU, preemption과 interrupt-disabled 범위를 이 중괄호 바깥 호출까지 넘겨 추정하지 않는다.

L418(blank)

빈 줄은 arm64 Stack unwind, function tracing과 return address 보호 경로에서 한 상태 묶음이 끝나는 위치다. 위쪽에서 만든 값이 아래쪽에서 소비되는지 구간을 나눠 읽는다.

L419 return kunwind_stack_walk(arch_reliable_kunwind_consume_entry, &data,

이 함수가 Stack unwind, function tracing과 return address 보호 단계의 결과 또는 오류를 상위 계층에 전달한다. 반환 전에 lock, interrupt state, reference와 hardware active state가 정리됐는지 확인한다.

L420 task, NULL);

이 줄이 arm64의 현재 상태에서 읽는 register와 memory, 그리고 다음 줄에 남기는 값을 적는다. Stack unwind, function tracing과 return address 보호의 공통 kernel 계약과 architecture 전용 side effect를 분리해 해석한다.

L421}

C block의 시작 또는 끝이다. lock, RCU, preemption과 interrupt-disabled 범위를 이 중괄호 바깥 호출까지 넘겨 추정하지 않는다.

L422(blank)

빈 줄은 arm64 Stack unwind, function tracing과 return address 보호 경로에서 한 상태 묶음이 끝나는 위치다. 위쪽에서 만든 값이 아래쪽에서 소비되는지 구간을 나눠 읽는다.

L423struct bpf_unwind_consume_entry_data {

이 줄이 arm64의 현재 상태에서 읽는 register와 memory, 그리고 다음 줄에 남기는 값을 적는다. Stack unwind, function tracing과 return address 보호의 공통 kernel 계약과 architecture 전용 side effect를 분리해 해석한다.

L424 bool (*consume_entry)(void *cookie, u64 ip, u64 sp, u64 fp);

helper 또는 architecture operation을 실행한다. arm64에서 이 호출이 register write, cache/TLB operation, callback 또는 object lifetime 중 무엇을 바꾸는지 call site와 callee를 연결해 본다.

L425 void *cookie;

선언 또는 macro 확장 일부다. type의 폭과 signedness, per-CPU/task/object 중 어느 수명을 따르는 값인지 확인한다.

L426};

C block의 시작 또는 끝이다. lock, RCU, preemption과 interrupt-disabled 범위를 이 중괄호 바깥 호출까지 넘겨 추정하지 않는다.

L427(blank)

빈 줄은 arm64 Stack unwind, function tracing과 return address 보호 경로에서 한 상태 묶음이 끝나는 위치다. 위쪽에서 만든 값이 아래쪽에서 소비되는지 구간을 나눠 읽는다.

L428static bool

선언 또는 macro 확장 일부다. type의 폭과 signedness, per-CPU/task/object 중 어느 수명을 따르는 값인지 확인한다.

L429arch_bpf_unwind_consume_entry(const struct kunwind_state *state, void *cookie)

이 함수의 진입 계약이 시작된다. arm64에서 caller context, argument ownership과 반환 시 보장할 architecture state를 먼저 적는다.

L430{

C block의 시작 또는 끝이다. lock, RCU, preemption과 interrupt-disabled 범위를 이 중괄호 바깥 호출까지 넘겨 추정하지 않는다.

L431 struct bpf_unwind_consume_entry_data *data = cookie;

계산한 pointer, flag, register image 또는 generation을 다음 단계가 읽을 위치에 저장한다. 값의 단위, address space와 publication ordering을 확인한다.

L432(blank)

빈 줄은 arm64 Stack unwind, function tracing과 return address 보호 경로에서 한 상태 묶음이 끝나는 위치다. 위쪽에서 만든 값이 아래쪽에서 소비되는지 구간을 나눠 읽는다.

L433 return data->consume_entry(data->cookie, state->common.pc, 0,

이 함수가 Stack unwind, function tracing과 return address 보호 단계의 결과 또는 오류를 상위 계층에 전달한다. 반환 전에 lock, interrupt state, reference와 hardware active state가 정리됐는지 확인한다.

L434 state->common.fp);

이 줄이 arm64의 현재 상태에서 읽는 register와 memory, 그리고 다음 줄에 남기는 값을 적는다. Stack unwind, function tracing과 return address 보호의 공통 kernel 계약과 architecture 전용 side effect를 분리해 해석한다.

L435}

C block의 시작 또는 끝이다. lock, RCU, preemption과 interrupt-disabled 범위를 이 중괄호 바깥 호출까지 넘겨 추정하지 않는다.

x86-64 · Linux 6.18.37

ORC unwind table과 exception/IRQ stack metadata

objtool이 instruction별 SP/BP 복원 규칙을 ORC table로 만든다. walker는 current IP에 해당하는 ORC entry를 찾아 다음 register state를 계산하고 pt_regs, call, regs, signal frame 유형을 구분한다.

원본 코드: arch/x86/kernel/unwind_orc.c:460-558

460	if (state->prev_regs) {
461		*val = READ_ONCE_NOCHECK(((unsigned long *)state->prev_regs)[reg]);
462		return true;
463	}
464 
465	return false;
466}
467 
468bool unwind_next_frame(struct unwind_state *state)
469{
470	unsigned long ip_p, sp, tmp, orig_ip = state->ip, prev_sp = state->sp;
471	enum stack_type prev_type = state->stack_info.type;
472	struct orc_entry *orc;
473	bool indirect = false;
474 
475	if (unwind_done(state))
476		return false;
477 
478	/* Don't let modules unload while we're reading their ORC data. */
479	guard(rcu)();
480 
481	/* End-of-stack check for user tasks: */
482	if (state->regs && user_mode(state->regs))
483		goto the_end;
484 
485	/*
486	 * Find the orc_entry associated with the text address.
487	 *
488	 * For a call frame (as opposed to a signal frame), state->ip points to
489	 * the instruction after the call.  That instruction's stack layout
490	 * could be different from the call instruction's layout, for example
491	 * if the call was to a noreturn function.  So get the ORC data for the
492	 * call instruction itself.
493	 */
494	orc = orc_find(state->signal ? state->ip : state->ip - 1);
495	if (!orc) {
496		/*
497		 * As a fallback, try to assume this code uses a frame pointer.
498		 * This is useful for generated code, like BPF, which ORC
499		 * doesn't know about.  This is just a guess, so the rest of
500		 * the unwind is no longer considered reliable.
501		 */
502		orc = &orc_fp_entry;
503		state->error = true;
504	} else {
505		if (orc->type == ORC_TYPE_UNDEFINED)
506			goto err;
507 
508		if (orc->type == ORC_TYPE_END_OF_STACK)
509			goto the_end;
510	}
511 
512	state->signal = orc->signal;
513 
514	/* Find the previous frame's stack: */
515	switch (orc->sp_reg) {
516	case ORC_REG_SP:
517		sp = state->sp + orc->sp_offset;
518		break;
519 
520	case ORC_REG_BP:
521		sp = state->bp + orc->sp_offset;
522		break;
523 
524	case ORC_REG_SP_INDIRECT:
525		sp = state->sp;
526		indirect = true;
527		break;
528 
529	case ORC_REG_BP_INDIRECT:
530		sp = state->bp + orc->sp_offset;
531		indirect = true;
532		break;
533 
534	case ORC_REG_R10:
535		if (!get_reg(state, offsetof(struct pt_regs, r10), &sp)) {
536			orc_warn_current("missing R10 value at %pB\n",
537					 (void *)state->ip);
538			goto err;
539		}
540		break;
541 
542	case ORC_REG_R13:
543		if (!get_reg(state, offsetof(struct pt_regs, r13), &sp)) {
544			orc_warn_current("missing R13 value at %pB\n",
545					 (void *)state->ip);
546			goto err;
547		}
548		break;
549 
550	case ORC_REG_DI:
551		if (!get_reg(state, offsetof(struct pt_regs, di), &sp)) {
552			orc_warn_current("missing RDI value at %pB\n",
553					 (void *)state->ip);
554			goto err;
555		}
556		break;
557 
558	case ORC_REG_DX:

라인 바이 라인 주석

빈 줄과 전처리 경계도 생략하지 않았습니다. 원본의 99개 줄에 각각 설명을 붙였습니다.

L460 if (state->prev_regs) {

이 조건이 x86-64 fast path와 fallback/error path를 가른다. 조건에 쓰인 flag가 어느 CPU 또는 object의 상태인지, 동시에 바뀔 수 있는지 확인한다.

L461 *val = READ_ONCE_NOCHECK(((unsigned long *)state->prev_regs)[reg]);

Linux 원본 주석이다. 바로 아래 코드의 호출 조건, hardware 제약 또는 예외 처리를 설명하므로 실행 줄과 함께 읽는다.

L462 return true;

검증된 다음 frame을 consumer가 읽을 수 있음을 나타낸다.

L463 }

C block의 시작 또는 끝이다. lock, RCU, preemption과 interrupt-disabled 범위를 이 중괄호 바깥 호출까지 넘겨 추정하지 않는다.

L464(blank)

빈 줄은 x86-64 Stack unwind, function tracing과 return address 보호 경로에서 한 상태 묶음이 끝나는 위치다. 위쪽에서 만든 값이 아래쪽에서 소비되는지 구간을 나눠 읽는다.

L465 return false;

이 함수가 Stack unwind, function tracing과 return address 보호 단계의 결과 또는 오류를 상위 계층에 전달한다. 반환 전에 lock, interrupt state, reference와 hardware active state가 정리됐는지 확인한다.

L466}

C block의 시작 또는 끝이다. lock, RCU, preemption과 interrupt-disabled 범위를 이 중괄호 바깥 호출까지 넘겨 추정하지 않는다.

L467(blank)

빈 줄은 x86-64 Stack unwind, function tracing과 return address 보호 경로에서 한 상태 묶음이 끝나는 위치다. 위쪽에서 만든 값이 아래쪽에서 소비되는지 구간을 나눠 읽는다.

L468bool unwind_next_frame(struct unwind_state *state)

이 함수의 진입 계약이 시작된다. x86-64에서 caller context, argument ownership과 반환 시 보장할 architecture state를 먼저 적는다.

L469{

C block의 시작 또는 끝이다. lock, RCU, preemption과 interrupt-disabled 범위를 이 중괄호 바깥 호출까지 넘겨 추정하지 않는다.

L470 unsigned long ip_p, sp, tmp, orig_ip = state->ip, prev_sp = state->sp;

계산한 pointer, flag, register image 또는 generation을 다음 단계가 읽을 위치에 저장한다. 값의 단위, address space와 publication ordering을 확인한다.

L471 enum stack_type prev_type = state->stack_info.type;

계산한 pointer, flag, register image 또는 generation을 다음 단계가 읽을 위치에 저장한다. 값의 단위, address space와 publication ordering을 확인한다.

L472 struct orc_entry *orc;

선언 또는 macro 확장 일부다. type의 폭과 signedness, per-CPU/task/object 중 어느 수명을 따르는 값인지 확인한다.

L473 bool indirect = false;

계산한 pointer, flag, register image 또는 generation을 다음 단계가 읽을 위치에 저장한다. 값의 단위, address space와 publication ordering을 확인한다.

L474(blank)

빈 줄은 x86-64 Stack unwind, function tracing과 return address 보호 경로에서 한 상태 묶음이 끝나는 위치다. 위쪽에서 만든 값이 아래쪽에서 소비되는지 구간을 나눠 읽는다.

L475 if (unwind_done(state))

이 조건이 x86-64 fast path와 fallback/error path를 가른다. 조건에 쓰인 flag가 어느 CPU 또는 object의 상태인지, 동시에 바뀔 수 있는지 확인한다.

L476 return false;

이 함수가 Stack unwind, function tracing과 return address 보호 단계의 결과 또는 오류를 상위 계층에 전달한다. 반환 전에 lock, interrupt state, reference와 hardware active state가 정리됐는지 확인한다.

L477(blank)

빈 줄은 x86-64 Stack unwind, function tracing과 return address 보호 경로에서 한 상태 묶음이 끝나는 위치다. 위쪽에서 만든 값이 아래쪽에서 소비되는지 구간을 나눠 읽는다.

L478 /* Don't let modules unload while we're reading their ORC data. */

Linux 원본 주석이다. 바로 아래 코드의 호출 조건, hardware 제약 또는 예외 처리를 설명하므로 실행 줄과 함께 읽는다.

L479 guard(rcu)();

helper 또는 architecture operation을 실행한다. x86-64에서 이 호출이 register write, cache/TLB operation, callback 또는 object lifetime 중 무엇을 바꾸는지 call site와 callee를 연결해 본다.

L480(blank)

빈 줄은 x86-64 Stack unwind, function tracing과 return address 보호 경로에서 한 상태 묶음이 끝나는 위치다. 위쪽에서 만든 값이 아래쪽에서 소비되는지 구간을 나눠 읽는다.

L481 /* End-of-stack check for user tasks: */

Linux 원본 주석이다. 바로 아래 코드의 호출 조건, hardware 제약 또는 예외 처리를 설명하므로 실행 줄과 함께 읽는다.

L482 if (state->regs && user_mode(state->regs))

이 조건이 x86-64 fast path와 fallback/error path를 가른다. 조건에 쓰인 flag가 어느 CPU 또는 object의 상태인지, 동시에 바뀔 수 있는지 확인한다.

L483 goto the_end;

정상 직선 경로를 벗어나 cleanup, retry 또는 다음 항목으로 이동한다. 이동 대상에서 해제하는 resource와 현재까지 획득한 ownership을 맞춘다.

L484(blank)

빈 줄은 x86-64 Stack unwind, function tracing과 return address 보호 경로에서 한 상태 묶음이 끝나는 위치다. 위쪽에서 만든 값이 아래쪽에서 소비되는지 구간을 나눠 읽는다.

L485 /*

Linux 원본 주석이다. 바로 아래 코드의 호출 조건, hardware 제약 또는 예외 처리를 설명하므로 실행 줄과 함께 읽는다.

L486 * Find the orc_entry associated with the text address.

Linux 원본 주석이다. 바로 아래 코드의 호출 조건, hardware 제약 또는 예외 처리를 설명하므로 실행 줄과 함께 읽는다.

L487 *

Linux 원본 주석이다. 바로 아래 코드의 호출 조건, hardware 제약 또는 예외 처리를 설명하므로 실행 줄과 함께 읽는다.

L488 * For a call frame (as opposed to a signal frame), state->ip points to

Linux 원본 주석이다. 바로 아래 코드의 호출 조건, hardware 제약 또는 예외 처리를 설명하므로 실행 줄과 함께 읽는다.

L489 * the instruction after the call. That instruction's stack layout

Linux 원본 주석이다. 바로 아래 코드의 호출 조건, hardware 제약 또는 예외 처리를 설명하므로 실행 줄과 함께 읽는다.

L490 * could be different from the call instruction's layout, for example

Linux 원본 주석이다. 바로 아래 코드의 호출 조건, hardware 제약 또는 예외 처리를 설명하므로 실행 줄과 함께 읽는다.

L491 * if the call was to a noreturn function. So get the ORC data for the

Linux 원본 주석이다. 바로 아래 코드의 호출 조건, hardware 제약 또는 예외 처리를 설명하므로 실행 줄과 함께 읽는다.

L492 * call instruction itself.

Linux 원본 주석이다. 바로 아래 코드의 호출 조건, hardware 제약 또는 예외 처리를 설명하므로 실행 줄과 함께 읽는다.

L493 */

Linux 원본 주석이다. 바로 아래 코드의 호출 조건, hardware 제약 또는 예외 처리를 설명하므로 실행 줄과 함께 읽는다.

L494 orc = orc_find(state->signal ? state->ip : state->ip - 1);

현재 instruction이 call 이후인지 signal frame인지 반영해 대응하는 ORC rule을 찾는다.

L495 if (!orc) {

이 조건이 x86-64 fast path와 fallback/error path를 가른다. 조건에 쓰인 flag가 어느 CPU 또는 object의 상태인지, 동시에 바뀔 수 있는지 확인한다.

L496 /*

Linux 원본 주석이다. 바로 아래 코드의 호출 조건, hardware 제약 또는 예외 처리를 설명하므로 실행 줄과 함께 읽는다.

L497 * As a fallback, try to assume this code uses a frame pointer.

Linux 원본 주석이다. 바로 아래 코드의 호출 조건, hardware 제약 또는 예외 처리를 설명하므로 실행 줄과 함께 읽는다.

L498 * This is useful for generated code, like BPF, which ORC

Linux 원본 주석이다. 바로 아래 코드의 호출 조건, hardware 제약 또는 예외 처리를 설명하므로 실행 줄과 함께 읽는다.

L499 * doesn't know about. This is just a guess, so the rest of

Linux 원본 주석이다. 바로 아래 코드의 호출 조건, hardware 제약 또는 예외 처리를 설명하므로 실행 줄과 함께 읽는다.

L500 * the unwind is no longer considered reliable.

Linux 원본 주석이다. 바로 아래 코드의 호출 조건, hardware 제약 또는 예외 처리를 설명하므로 실행 줄과 함께 읽는다.

L501 */

Linux 원본 주석이다. 바로 아래 코드의 호출 조건, hardware 제약 또는 예외 처리를 설명하므로 실행 줄과 함께 읽는다.

L502 orc = &orc_fp_entry;

계산한 pointer, flag, register image 또는 generation을 다음 단계가 읽을 위치에 저장한다. 값의 단위, address space와 publication ordering을 확인한다.

L503 state->error = true;

계산한 pointer, flag, register image 또는 generation을 다음 단계가 읽을 위치에 저장한다. 값의 단위, address space와 publication ordering을 확인한다.

L504 } else {

이 줄이 x86-64의 현재 상태에서 읽는 register와 memory, 그리고 다음 줄에 남기는 값을 적는다. Stack unwind, function tracing과 return address 보호의 공통 kernel 계약과 architecture 전용 side effect를 분리해 해석한다.

L505 if (orc->type == ORC_TYPE_UNDEFINED)

이 조건이 x86-64 fast path와 fallback/error path를 가른다. 조건에 쓰인 flag가 어느 CPU 또는 object의 상태인지, 동시에 바뀔 수 있는지 확인한다.

L506 goto err;

정상 직선 경로를 벗어나 cleanup, retry 또는 다음 항목으로 이동한다. 이동 대상에서 해제하는 resource와 현재까지 획득한 ownership을 맞춘다.

L507(blank)

빈 줄은 x86-64 Stack unwind, function tracing과 return address 보호 경로에서 한 상태 묶음이 끝나는 위치다. 위쪽에서 만든 값이 아래쪽에서 소비되는지 구간을 나눠 읽는다.

L508 if (orc->type == ORC_TYPE_END_OF_STACK)

이 조건이 x86-64 fast path와 fallback/error path를 가른다. 조건에 쓰인 flag가 어느 CPU 또는 object의 상태인지, 동시에 바뀔 수 있는지 확인한다.

L509 goto the_end;

정상 직선 경로를 벗어나 cleanup, retry 또는 다음 항목으로 이동한다. 이동 대상에서 해제하는 resource와 현재까지 획득한 ownership을 맞춘다.

L510 }

C block의 시작 또는 끝이다. lock, RCU, preemption과 interrupt-disabled 범위를 이 중괄호 바깥 호출까지 넘겨 추정하지 않는다.

L511(blank)

빈 줄은 x86-64 Stack unwind, function tracing과 return address 보호 경로에서 한 상태 묶음이 끝나는 위치다. 위쪽에서 만든 값이 아래쪽에서 소비되는지 구간을 나눠 읽는다.

L512 state->signal = orc->signal;

계산한 pointer, flag, register image 또는 generation을 다음 단계가 읽을 위치에 저장한다. 값의 단위, address space와 publication ordering을 확인한다.

L513(blank)

빈 줄은 x86-64 Stack unwind, function tracing과 return address 보호 경로에서 한 상태 묶음이 끝나는 위치다. 위쪽에서 만든 값이 아래쪽에서 소비되는지 구간을 나눠 읽는다.

L514 /* Find the previous frame's stack: */

Linux 원본 주석이다. 바로 아래 코드의 호출 조건, hardware 제약 또는 예외 처리를 설명하므로 실행 줄과 함께 읽는다.

L515 switch (orc->sp_reg) {

ORC가 지정한 base register와 offset으로 caller SP를 계산한다.

L516 case ORC_REG_SP:

hardware reason, flag 또는 operation 종류를 개별 처리 경로로 분기한다. 각 case가 공통 cleanup과 completion 지점에 도달하는지 확인한다.

L517 sp = state->sp + orc->sp_offset;

계산한 pointer, flag, register image 또는 generation을 다음 단계가 읽을 위치에 저장한다. 값의 단위, address space와 publication ordering을 확인한다.

L518 break;

정상 직선 경로를 벗어나 cleanup, retry 또는 다음 항목으로 이동한다. 이동 대상에서 해제하는 resource와 현재까지 획득한 ownership을 맞춘다.

L519(blank)

빈 줄은 x86-64 Stack unwind, function tracing과 return address 보호 경로에서 한 상태 묶음이 끝나는 위치다. 위쪽에서 만든 값이 아래쪽에서 소비되는지 구간을 나눠 읽는다.

L520 case ORC_REG_BP:

hardware reason, flag 또는 operation 종류를 개별 처리 경로로 분기한다. 각 case가 공통 cleanup과 completion 지점에 도달하는지 확인한다.

L521 sp = state->bp + orc->sp_offset;

계산한 pointer, flag, register image 또는 generation을 다음 단계가 읽을 위치에 저장한다. 값의 단위, address space와 publication ordering을 확인한다.

L522 break;

정상 직선 경로를 벗어나 cleanup, retry 또는 다음 항목으로 이동한다. 이동 대상에서 해제하는 resource와 현재까지 획득한 ownership을 맞춘다.

L523(blank)

빈 줄은 x86-64 Stack unwind, function tracing과 return address 보호 경로에서 한 상태 묶음이 끝나는 위치다. 위쪽에서 만든 값이 아래쪽에서 소비되는지 구간을 나눠 읽는다.

L524 case ORC_REG_SP_INDIRECT:

hardware reason, flag 또는 operation 종류를 개별 처리 경로로 분기한다. 각 case가 공통 cleanup과 completion 지점에 도달하는지 확인한다.

L525 sp = state->sp;

계산한 pointer, flag, register image 또는 generation을 다음 단계가 읽을 위치에 저장한다. 값의 단위, address space와 publication ordering을 확인한다.

L526 indirect = true;

계산한 pointer, flag, register image 또는 generation을 다음 단계가 읽을 위치에 저장한다. 값의 단위, address space와 publication ordering을 확인한다.

L527 break;

정상 직선 경로를 벗어나 cleanup, retry 또는 다음 항목으로 이동한다. 이동 대상에서 해제하는 resource와 현재까지 획득한 ownership을 맞춘다.

L528(blank)

빈 줄은 x86-64 Stack unwind, function tracing과 return address 보호 경로에서 한 상태 묶음이 끝나는 위치다. 위쪽에서 만든 값이 아래쪽에서 소비되는지 구간을 나눠 읽는다.

L529 case ORC_REG_BP_INDIRECT:

hardware reason, flag 또는 operation 종류를 개별 처리 경로로 분기한다. 각 case가 공통 cleanup과 completion 지점에 도달하는지 확인한다.

L530 sp = state->bp + orc->sp_offset;

계산한 pointer, flag, register image 또는 generation을 다음 단계가 읽을 위치에 저장한다. 값의 단위, address space와 publication ordering을 확인한다.

L531 indirect = true;

계산한 pointer, flag, register image 또는 generation을 다음 단계가 읽을 위치에 저장한다. 값의 단위, address space와 publication ordering을 확인한다.

L532 break;

정상 직선 경로를 벗어나 cleanup, retry 또는 다음 항목으로 이동한다. 이동 대상에서 해제하는 resource와 현재까지 획득한 ownership을 맞춘다.

L533(blank)

빈 줄은 x86-64 Stack unwind, function tracing과 return address 보호 경로에서 한 상태 묶음이 끝나는 위치다. 위쪽에서 만든 값이 아래쪽에서 소비되는지 구간을 나눠 읽는다.

L534 case ORC_REG_R10:

hardware reason, flag 또는 operation 종류를 개별 처리 경로로 분기한다. 각 case가 공통 cleanup과 completion 지점에 도달하는지 확인한다.

L535 if (!get_reg(state, offsetof(struct pt_regs, r10), &sp)) {

이 조건이 x86-64 fast path와 fallback/error path를 가른다. 조건에 쓰인 flag가 어느 CPU 또는 object의 상태인지, 동시에 바뀔 수 있는지 확인한다.

L536 orc_warn_current("missing R10 value at %pB\n",

이 줄이 x86-64의 현재 상태에서 읽는 register와 memory, 그리고 다음 줄에 남기는 값을 적는다. Stack unwind, function tracing과 return address 보호의 공통 kernel 계약과 architecture 전용 side effect를 분리해 해석한다.

L537 (void *)state->ip);

이 줄이 x86-64의 현재 상태에서 읽는 register와 memory, 그리고 다음 줄에 남기는 값을 적는다. Stack unwind, function tracing과 return address 보호의 공통 kernel 계약과 architecture 전용 side effect를 분리해 해석한다.

L538 goto err;

정상 직선 경로를 벗어나 cleanup, retry 또는 다음 항목으로 이동한다. 이동 대상에서 해제하는 resource와 현재까지 획득한 ownership을 맞춘다.

L539 }

C block의 시작 또는 끝이다. lock, RCU, preemption과 interrupt-disabled 범위를 이 중괄호 바깥 호출까지 넘겨 추정하지 않는다.

L540 break;

정상 직선 경로를 벗어나 cleanup, retry 또는 다음 항목으로 이동한다. 이동 대상에서 해제하는 resource와 현재까지 획득한 ownership을 맞춘다.

L541(blank)

빈 줄은 x86-64 Stack unwind, function tracing과 return address 보호 경로에서 한 상태 묶음이 끝나는 위치다. 위쪽에서 만든 값이 아래쪽에서 소비되는지 구간을 나눠 읽는다.

L542 case ORC_REG_R13:

hardware reason, flag 또는 operation 종류를 개별 처리 경로로 분기한다. 각 case가 공통 cleanup과 completion 지점에 도달하는지 확인한다.

L543 if (!get_reg(state, offsetof(struct pt_regs, r13), &sp)) {

이 조건이 x86-64 fast path와 fallback/error path를 가른다. 조건에 쓰인 flag가 어느 CPU 또는 object의 상태인지, 동시에 바뀔 수 있는지 확인한다.

L544 orc_warn_current("missing R13 value at %pB\n",

이 줄이 x86-64의 현재 상태에서 읽는 register와 memory, 그리고 다음 줄에 남기는 값을 적는다. Stack unwind, function tracing과 return address 보호의 공통 kernel 계약과 architecture 전용 side effect를 분리해 해석한다.

L545 (void *)state->ip);

이 줄이 x86-64의 현재 상태에서 읽는 register와 memory, 그리고 다음 줄에 남기는 값을 적는다. Stack unwind, function tracing과 return address 보호의 공통 kernel 계약과 architecture 전용 side effect를 분리해 해석한다.

L546 goto err;

정상 직선 경로를 벗어나 cleanup, retry 또는 다음 항목으로 이동한다. 이동 대상에서 해제하는 resource와 현재까지 획득한 ownership을 맞춘다.

L547 }

C block의 시작 또는 끝이다. lock, RCU, preemption과 interrupt-disabled 범위를 이 중괄호 바깥 호출까지 넘겨 추정하지 않는다.

L548 break;

정상 직선 경로를 벗어나 cleanup, retry 또는 다음 항목으로 이동한다. 이동 대상에서 해제하는 resource와 현재까지 획득한 ownership을 맞춘다.

L549(blank)

빈 줄은 x86-64 Stack unwind, function tracing과 return address 보호 경로에서 한 상태 묶음이 끝나는 위치다. 위쪽에서 만든 값이 아래쪽에서 소비되는지 구간을 나눠 읽는다.

L550 case ORC_REG_DI:

hardware reason, flag 또는 operation 종류를 개별 처리 경로로 분기한다. 각 case가 공통 cleanup과 completion 지점에 도달하는지 확인한다.

L551 if (!get_reg(state, offsetof(struct pt_regs, di), &sp)) {

이 조건이 x86-64 fast path와 fallback/error path를 가른다. 조건에 쓰인 flag가 어느 CPU 또는 object의 상태인지, 동시에 바뀔 수 있는지 확인한다.

L552 orc_warn_current("missing RDI value at %pB\n",

이 줄이 x86-64의 현재 상태에서 읽는 register와 memory, 그리고 다음 줄에 남기는 값을 적는다. Stack unwind, function tracing과 return address 보호의 공통 kernel 계약과 architecture 전용 side effect를 분리해 해석한다.

L553 (void *)state->ip);

이 줄이 x86-64의 현재 상태에서 읽는 register와 memory, 그리고 다음 줄에 남기는 값을 적는다. Stack unwind, function tracing과 return address 보호의 공통 kernel 계약과 architecture 전용 side effect를 분리해 해석한다.

L554 goto err;

정상 직선 경로를 벗어나 cleanup, retry 또는 다음 항목으로 이동한다. 이동 대상에서 해제하는 resource와 현재까지 획득한 ownership을 맞춘다.

L555 }

C block의 시작 또는 끝이다. lock, RCU, preemption과 interrupt-disabled 범위를 이 중괄호 바깥 호출까지 넘겨 추정하지 않는다.

L556 break;

정상 직선 경로를 벗어나 cleanup, retry 또는 다음 항목으로 이동한다. 이동 대상에서 해제하는 resource와 현재까지 획득한 ownership을 맞춘다.

L557(blank)

빈 줄은 x86-64 Stack unwind, function tracing과 return address 보호 경로에서 한 상태 묶음이 끝나는 위치다. 위쪽에서 만든 값이 아래쪽에서 소비되는지 구간을 나눠 읽는다.

L558 case ORC_REG_DX:

hardware reason, flag 또는 operation 종류를 개별 처리 경로로 분기한다. 각 case가 공통 cleanup과 completion 지점에 도달하는지 확인한다.

RISC-V · Linux 6.18.37

s0 frame pointer chain 또는 compiler unwind 제한

frame pointer build에서는 s0가 가리키는 frame record에서 previous FP와 RA를 읽는다. exception pt_regs와 task stack 경계를 인식하고 instruction pointer를 callback에 넘긴다.

원본 코드: arch/riscv/kernel/stacktrace.c:171-227

171 
172	if (!try_get_task_stack(task))
173		return 0;
174	walk_stackframe(task, NULL, save_wchan, &pc);
175	put_task_stack(task);
176	return pc;
177}
178 
179noinline noinstr void arch_stack_walk(stack_trace_consume_fn consume_entry, void *cookie,
180		     struct task_struct *task, struct pt_regs *regs)
181{
182	walk_stackframe(task, regs, consume_entry, cookie);
183}
184 
185/*
186 * Get the return address for a single stackframe and return a pointer to the
187 * next frame tail.
188 */
189static unsigned long unwind_user_frame(stack_trace_consume_fn consume_entry,
190				       void *cookie, unsigned long fp,
191				       unsigned long reg_ra)
192{
193	struct stackframe buftail;
194	unsigned long ra = 0;
195	unsigned long __user *user_frame_tail =
196		(unsigned long __user *)(fp - sizeof(struct stackframe));
197 
198	/* Check accessibility of one struct frame_tail beyond */
199	if (!access_ok(user_frame_tail, sizeof(buftail)))
200		return 0;
201	if (__copy_from_user_inatomic(&buftail, user_frame_tail,
202				      sizeof(buftail)))
203		return 0;
204 
205	ra = reg_ra ? : buftail.ra;
206 
207	fp = buftail.fp;
208	if (!ra || !consume_entry(cookie, ra))
209		return 0;
210 
211	return fp;
212}
213 
214void arch_stack_walk_user(stack_trace_consume_fn consume_entry, void *cookie,
215			  const struct pt_regs *regs)
216{
217	unsigned long fp = 0;
218 
219	fp = regs->s0;
220	if (!consume_entry(cookie, regs->epc))
221		return;
222 
223	fp = unwind_user_frame(consume_entry, cookie, fp, regs->ra);
224	while (fp && !(fp & 0x7))
225		fp = unwind_user_frame(consume_entry, cookie, fp, 0);
226}
227 

라인 바이 라인 주석

빈 줄과 전처리 경계도 생략하지 않았습니다. 원본의 57개 줄에 각각 설명을 붙였습니다.

L171(blank)

빈 줄은 RISC-V Stack unwind, function tracing과 return address 보호 경로에서 한 상태 묶음이 끝나는 위치다. 위쪽에서 만든 값이 아래쪽에서 소비되는지 구간을 나눠 읽는다.

L172 if (!try_get_task_stack(task))

이 조건이 RISC-V fast path와 fallback/error path를 가른다. 조건에 쓰인 flag가 어느 CPU 또는 object의 상태인지, 동시에 바뀔 수 있는지 확인한다.

L173 return 0;

이 함수가 Stack unwind, function tracing과 return address 보호 단계의 결과 또는 오류를 상위 계층에 전달한다. 반환 전에 lock, interrupt state, reference와 hardware active state가 정리됐는지 확인한다.

L174 walk_stackframe(task, NULL, save_wchan, &pc);

helper 또는 architecture operation을 실행한다. RISC-V에서 이 호출이 register write, cache/TLB operation, callback 또는 object lifetime 중 무엇을 바꾸는지 call site와 callee를 연결해 본다.

L175 put_task_stack(task);

helper 또는 architecture operation을 실행한다. RISC-V에서 이 호출이 register write, cache/TLB operation, callback 또는 object lifetime 중 무엇을 바꾸는지 call site와 callee를 연결해 본다.

L176 return pc;

이 함수가 Stack unwind, function tracing과 return address 보호 단계의 결과 또는 오류를 상위 계층에 전달한다. 반환 전에 lock, interrupt state, reference와 hardware active state가 정리됐는지 확인한다.

L177}

C block의 시작 또는 끝이다. lock, RCU, preemption과 interrupt-disabled 범위를 이 중괄호 바깥 호출까지 넘겨 추정하지 않는다.

L178(blank)

빈 줄은 RISC-V Stack unwind, function tracing과 return address 보호 경로에서 한 상태 묶음이 끝나는 위치다. 위쪽에서 만든 값이 아래쪽에서 소비되는지 구간을 나눠 읽는다.

L179noinline noinstr void arch_stack_walk(stack_trace_consume_fn consume_entry, void *cookie,

이 줄이 RISC-V의 현재 상태에서 읽는 register와 memory, 그리고 다음 줄에 남기는 값을 적는다. Stack unwind, function tracing과 return address 보호의 공통 kernel 계약과 architecture 전용 side effect를 분리해 해석한다.

L180 struct task_struct *task, struct pt_regs *regs)

이 줄이 RISC-V의 현재 상태에서 읽는 register와 memory, 그리고 다음 줄에 남기는 값을 적는다. Stack unwind, function tracing과 return address 보호의 공통 kernel 계약과 architecture 전용 side effect를 분리해 해석한다.

L181{

C block의 시작 또는 끝이다. lock, RCU, preemption과 interrupt-disabled 범위를 이 중괄호 바깥 호출까지 넘겨 추정하지 않는다.

L182 walk_stackframe(task, regs, consume_entry, cookie);

초기 pt_regs 또는 task context에서 frame-pointer chain을 순회하고 callback으로 PC를 전달한다.

L183}

C block의 시작 또는 끝이다. lock, RCU, preemption과 interrupt-disabled 범위를 이 중괄호 바깥 호출까지 넘겨 추정하지 않는다.

L184(blank)

빈 줄은 RISC-V Stack unwind, function tracing과 return address 보호 경로에서 한 상태 묶음이 끝나는 위치다. 위쪽에서 만든 값이 아래쪽에서 소비되는지 구간을 나눠 읽는다.

L185/*

Linux 원본 주석이다. 바로 아래 코드의 호출 조건, hardware 제약 또는 예외 처리를 설명하므로 실행 줄과 함께 읽는다.

L186 * Get the return address for a single stackframe and return a pointer to the

Linux 원본 주석이다. 바로 아래 코드의 호출 조건, hardware 제약 또는 예외 처리를 설명하므로 실행 줄과 함께 읽는다.

L187 * next frame tail.

Linux 원본 주석이다. 바로 아래 코드의 호출 조건, hardware 제약 또는 예외 처리를 설명하므로 실행 줄과 함께 읽는다.

L188 */

Linux 원본 주석이다. 바로 아래 코드의 호출 조건, hardware 제약 또는 예외 처리를 설명하므로 실행 줄과 함께 읽는다.

L189static unsigned long unwind_user_frame(stack_trace_consume_fn consume_entry,

이 줄이 RISC-V의 현재 상태에서 읽는 register와 memory, 그리고 다음 줄에 남기는 값을 적는다. Stack unwind, function tracing과 return address 보호의 공통 kernel 계약과 architecture 전용 side effect를 분리해 해석한다.

L190 void *cookie, unsigned long fp,

이 줄이 RISC-V의 현재 상태에서 읽는 register와 memory, 그리고 다음 줄에 남기는 값을 적는다. Stack unwind, function tracing과 return address 보호의 공통 kernel 계약과 architecture 전용 side effect를 분리해 해석한다.

L191 unsigned long reg_ra)

이 줄이 RISC-V의 현재 상태에서 읽는 register와 memory, 그리고 다음 줄에 남기는 값을 적는다. Stack unwind, function tracing과 return address 보호의 공통 kernel 계약과 architecture 전용 side effect를 분리해 해석한다.

L192{

C block의 시작 또는 끝이다. lock, RCU, preemption과 interrupt-disabled 범위를 이 중괄호 바깥 호출까지 넘겨 추정하지 않는다.

L193 struct stackframe buftail;

선언 또는 macro 확장 일부다. type의 폭과 signedness, per-CPU/task/object 중 어느 수명을 따르는 값인지 확인한다.

L194 unsigned long ra = 0;

계산한 pointer, flag, register image 또는 generation을 다음 단계가 읽을 위치에 저장한다. 값의 단위, address space와 publication ordering을 확인한다.

L195 unsigned long __user *user_frame_tail =

이 줄이 RISC-V의 현재 상태에서 읽는 register와 memory, 그리고 다음 줄에 남기는 값을 적는다. Stack unwind, function tracing과 return address 보호의 공통 kernel 계약과 architecture 전용 side effect를 분리해 해석한다.

L196 (unsigned long __user *)(fp - sizeof(struct stackframe));

helper 또는 architecture operation을 실행한다. RISC-V에서 이 호출이 register write, cache/TLB operation, callback 또는 object lifetime 중 무엇을 바꾸는지 call site와 callee를 연결해 본다.

L197(blank)

빈 줄은 RISC-V Stack unwind, function tracing과 return address 보호 경로에서 한 상태 묶음이 끝나는 위치다. 위쪽에서 만든 값이 아래쪽에서 소비되는지 구간을 나눠 읽는다.

L198 /* Check accessibility of one struct frame_tail beyond */

Linux 원본 주석이다. 바로 아래 코드의 호출 조건, hardware 제약 또는 예외 처리를 설명하므로 실행 줄과 함께 읽는다.

L199 if (!access_ok(user_frame_tail, sizeof(buftail)))

이 조건이 RISC-V fast path와 fallback/error path를 가른다. 조건에 쓰인 flag가 어느 CPU 또는 object의 상태인지, 동시에 바뀔 수 있는지 확인한다.

L200 return 0;

이 함수가 Stack unwind, function tracing과 return address 보호 단계의 결과 또는 오류를 상위 계층에 전달한다. 반환 전에 lock, interrupt state, reference와 hardware active state가 정리됐는지 확인한다.

L201 if (__copy_from_user_inatomic(&buftail, user_frame_tail,

이 조건이 RISC-V fast path와 fallback/error path를 가른다. 조건에 쓰인 flag가 어느 CPU 또는 object의 상태인지, 동시에 바뀔 수 있는지 확인한다.

L202 sizeof(buftail)))

이 함수의 진입 계약이 시작된다. RISC-V에서 caller context, argument ownership과 반환 시 보장할 architecture state를 먼저 적는다.

L203 return 0;

이 함수가 Stack unwind, function tracing과 return address 보호 단계의 결과 또는 오류를 상위 계층에 전달한다. 반환 전에 lock, interrupt state, reference와 hardware active state가 정리됐는지 확인한다.

L204(blank)

빈 줄은 RISC-V Stack unwind, function tracing과 return address 보호 경로에서 한 상태 묶음이 끝나는 위치다. 위쪽에서 만든 값이 아래쪽에서 소비되는지 구간을 나눠 읽는다.

L205 ra = reg_ra ? : buftail.ra;

계산한 pointer, flag, register image 또는 generation을 다음 단계가 읽을 위치에 저장한다. 값의 단위, address space와 publication ordering을 확인한다.

L206(blank)

빈 줄은 RISC-V Stack unwind, function tracing과 return address 보호 경로에서 한 상태 묶음이 끝나는 위치다. 위쪽에서 만든 값이 아래쪽에서 소비되는지 구간을 나눠 읽는다.

L207 fp = buftail.fp;

계산한 pointer, flag, register image 또는 generation을 다음 단계가 읽을 위치에 저장한다. 값의 단위, address space와 publication ordering을 확인한다.

L208 if (!ra || !consume_entry(cookie, ra))

이 조건이 RISC-V fast path와 fallback/error path를 가른다. 조건에 쓰인 flag가 어느 CPU 또는 object의 상태인지, 동시에 바뀔 수 있는지 확인한다.

L209 return 0;

이 함수가 Stack unwind, function tracing과 return address 보호 단계의 결과 또는 오류를 상위 계층에 전달한다. 반환 전에 lock, interrupt state, reference와 hardware active state가 정리됐는지 확인한다.

L210(blank)

빈 줄은 RISC-V Stack unwind, function tracing과 return address 보호 경로에서 한 상태 묶음이 끝나는 위치다. 위쪽에서 만든 값이 아래쪽에서 소비되는지 구간을 나눠 읽는다.

L211 return fp;

이 함수가 Stack unwind, function tracing과 return address 보호 단계의 결과 또는 오류를 상위 계층에 전달한다. 반환 전에 lock, interrupt state, reference와 hardware active state가 정리됐는지 확인한다.

L212}

C block의 시작 또는 끝이다. lock, RCU, preemption과 interrupt-disabled 범위를 이 중괄호 바깥 호출까지 넘겨 추정하지 않는다.

L213(blank)

빈 줄은 RISC-V Stack unwind, function tracing과 return address 보호 경로에서 한 상태 묶음이 끝나는 위치다. 위쪽에서 만든 값이 아래쪽에서 소비되는지 구간을 나눠 읽는다.

L214void arch_stack_walk_user(stack_trace_consume_fn consume_entry, void *cookie,

이 줄이 RISC-V의 현재 상태에서 읽는 register와 memory, 그리고 다음 줄에 남기는 값을 적는다. Stack unwind, function tracing과 return address 보호의 공통 kernel 계약과 architecture 전용 side effect를 분리해 해석한다.

L215 const struct pt_regs *regs)

이 줄이 RISC-V의 현재 상태에서 읽는 register와 memory, 그리고 다음 줄에 남기는 값을 적는다. Stack unwind, function tracing과 return address 보호의 공통 kernel 계약과 architecture 전용 side effect를 분리해 해석한다.

L216{

C block의 시작 또는 끝이다. lock, RCU, preemption과 interrupt-disabled 범위를 이 중괄호 바깥 호출까지 넘겨 추정하지 않는다.

L217 unsigned long fp = 0;

계산한 pointer, flag, register image 또는 generation을 다음 단계가 읽을 위치에 저장한다. 값의 단위, address space와 publication ordering을 확인한다.

L218(blank)

빈 줄은 RISC-V Stack unwind, function tracing과 return address 보호 경로에서 한 상태 묶음이 끝나는 위치다. 위쪽에서 만든 값이 아래쪽에서 소비되는지 구간을 나눠 읽는다.

L219 fp = regs->s0;

계산한 pointer, flag, register image 또는 generation을 다음 단계가 읽을 위치에 저장한다. 값의 단위, address space와 publication ordering을 확인한다.

L220 if (!consume_entry(cookie, regs->epc))

이 조건이 RISC-V fast path와 fallback/error path를 가른다. 조건에 쓰인 flag가 어느 CPU 또는 object의 상태인지, 동시에 바뀔 수 있는지 확인한다.

L221 return;

이 함수가 Stack unwind, function tracing과 return address 보호 단계의 결과 또는 오류를 상위 계층에 전달한다. 반환 전에 lock, interrupt state, reference와 hardware active state가 정리됐는지 확인한다.

L222(blank)

빈 줄은 RISC-V Stack unwind, function tracing과 return address 보호 경로에서 한 상태 묶음이 끝나는 위치다. 위쪽에서 만든 값이 아래쪽에서 소비되는지 구간을 나눠 읽는다.

L223 fp = unwind_user_frame(consume_entry, cookie, fp, regs->ra);

helper 또는 architecture operation을 실행한다. RISC-V에서 이 호출이 register write, cache/TLB operation, callback 또는 object lifetime 중 무엇을 바꾸는지 call site와 callee를 연결해 본다.

L224 while (fp && !(fp & 0x7))

range, CPU mask, relocation 또는 descriptor를 반복 처리한다. 반복 상한과 중간 실패 때 이미 처리한 항목을 되돌리는 경로를 함께 본다.

L225 fp = unwind_user_frame(consume_entry, cookie, fp, 0);

helper 또는 architecture operation을 실행한다. RISC-V에서 이 호출이 register write, cache/TLB operation, callback 또는 object lifetime 중 무엇을 바꾸는지 call site와 callee를 연결해 본다.

L226}

C block의 시작 또는 끝이다. lock, RCU, preemption과 interrupt-disabled 범위를 이 중괄호 바깥 호출까지 넘겨 추정하지 않는다.

L227(blank)

빈 줄은 RISC-V Stack unwind, function tracing과 return address 보호 경로에서 한 상태 묶음이 끝나는 위치다. 위쪽에서 만든 값이 아래쪽에서 소비되는지 구간을 나눠 읽는다.

05 · WORKED EXAMPLE

숫자로 검산하기

01

interrupt가 중간에 끼어든 call chain

worker -> submit_io -> wait 실행 중 timer interrupt가 들어와 hrtimer_interrupt -> scheduler_tick을 실행했다고 가정한다.

  1. top현재 PC는 scheduler_tick 내부이고 첫 frame들은 IRQ handler stack에 있다.
  2. entry framewalker가 exception/interrupt pt_regs를 만나면 saved PC=wait와 interrupted SP를 읽는다.
  3. stack switch현재 IRQ stack range를 끝내고 task kernel stack range로 검사 기준을 바꾼다.
  4. callerwait, submit_io, worker frame을 이어서 출력하되 return PC가 kernel text/module text인지 확인한다.

결론단순히 stack word에서 text address를 찾는 것이 아니라 metadata와 stack 경계로 각 return address의 출처를 증명한다.

06 · DEEP DIVE

경계별 상세 분석

01

공통 kernel core와 architecture hook의 경계

stack walker는 현재 PC/SP/FP와 task stack 경계를 받아 callback으로 frame을 전달한다. interrupt, exception, scheduler와 trampoline frame을 인식해 다른 stack segment로 넘어가며 invalid address와 loop를 차단한다.

live current-task unwind, stopped remote task, NMI/IRQ stack과 user stack을 구분한다. tracing instrumentation 자체가 stack을 바꾸거나 return address를 수정할 수 있으므로 ftrace/kretprobe metadata를 적용한다.

02

arm64: x29 frame record와 LR/PAC/SCS 처리

AAPCS frame record의 previous FP와 saved LR을 따라간다. exception entry frame, fgraph return trampoline, pointer authentication signed LR와 shadow call stack 구성을 고려해 실제 return PC를 복원한다.

stack boundary 검사 뒤 LR의 PAC를 strip하고 kernel text 범위인지 확인한다. current task가 아니면 안정적으로 정지된 상태여야 한다. 디버깅할 때는 FP/SP 정렬, task/IRQ/overflow stack range, saved LR, PAC mask, SCS pointer와 unwind consumer를 확인한다.

03

x86-64: ORC unwind table과 exception/IRQ stack metadata

objtool이 instruction별 SP/BP 복원 규칙을 ORC table로 만든다. walker는 current IP에 해당하는 ORC entry를 찾아 다음 register state를 계산하고 pt_regs, call, regs, signal frame 유형을 구분한다.

binary와 ORC metadata가 정확히 일치해야 하며 runtime code patching과 module load는 해당 table을 함께 등록한다. 디버깅할 때는 IP의 ORC entry, SP/BP base와 offset, stack_info type, pt_regs 위치, module unwind table과 objtool warning을 본다.

04

RISC-V: s0 frame pointer chain 또는 compiler unwind 제한

frame pointer build에서는 s0가 가리키는 frame record에서 previous FP와 RA를 읽는다. exception pt_regs와 task stack 경계를 인식하고 instruction pointer를 callback에 넘긴다.

compressed instruction과 ftrace return-address patching을 고려하고 stack range를 벗어나면 즉시 중단한다. 디버깅할 때는 s0/fp, sp, ra, task/IRQ stack range, pt_regs epc와 CONFIG_FRAME_POINTER/ftrace 설정을 확인한다.

05

객체 수명과 소유권을 먼저 고정한다

task stack과 unwind metadata는 walk가 끝날 때까지 유효해야 한다. remote running task를 정지하지 않고 walk하면 SP와 frame contents가 동시에 바뀌어 일관된 call chain을 보장할 수 없다.

주소나 register 값이 맞는지만 확인하면 stale state를 놓친다. producer, publication, consumer와 폐기 지점을 같은 표에 기록한다.

06

latency upper bound는 hardware instruction 하나가 아니다

frame-pointer walk는 frame 수에 비례하고 ORC/DWARF-like state lookup은 metadata search가 더해진다. NMI에서 긴 unwind와 symbol lookup은 watchdog latency를 악화시킬 수 있다.

평균값 외에 interrupt-off 구간, remote CPU 응답, firmware 호출과 retry 횟수를 분리해야 최악 지연의 원인을 찾을 수 있다.

07 · FAILURE

실패를 어떤 증거로 나눌 것인가

분류관찰되는 결과첫 확인값
arm64깨진 x29 chain, PAC 미제거 또는 IRQ stack 경계 누락은 잘못된 symbol과 out-of-bounds stack read를 만든다.FP/SP 정렬, task/IRQ/overflow stack range, saved LR, PAC mask, SCS pointer와 unwind consumer를 확인한다.
x86-64손상되거나 다른 build의 ORC table, noinstr 규칙 위반 또는 stack type 오판은 unwind stop과 잘못된 call trace를 만든다.IP의 ORC entry, SP/BP base와 offset, stack_info type, pt_regs 위치, module unwind table과 objtool warning을 본다.
RISC-Vframe-pointer omission, leaf function의 RA 보존 방식 또는 손상된 s0가 call chain 단절과 잘못된 address를 만든다.s0/fp, sp, ra, task/IRQ stack range, pt_regs epc와 CONFIG_FRAME_POINTER/ftrace 설정을 확인한다.

08 · LAB

재현과 계측 절차

  1. 같은 call path를 frame-pointer/ORC 가능한 build에서 비교하고 interrupt stack 전환 frame을 표시한다.
  2. module load/unload와 동시에 stack sampling해 module text/unwind metadata lifetime 보호를 확인한다.
  3. 동일한 workload에서 세 architecture의 tracepoint 이름, CPU 번호, PC, stack pointer와 address-space identifier를 같은 열로 기록한다.
  4. 소스만 읽고 끝내지 않고 최종 vmlinuxobjdump -dr, readelf -SW 결과로 선택된 alternative와 section 배치를 확인한다.

09 · REFERENCES

원문 좌표

Linux kernel source: GPL-2.0-only. 이 글의 코드 발췌는 Linux v6.18.37 원문을 기준으로 하며, 분석 문장은 해당 코드의 실행 조건과 상태 경계를 설명합니다.