← Architecture 비교

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개 · 원문 218줄
기준 태그
Linux v6.18.37
분석 축
상태 · 접근 순서 · 객체 수명 · 지연 시간

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핵심 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. arm64 수집 콜백 세 가지같은 kunwind_state를 어느 adapter가 받느냐에 따라 수집값과 멈춤 조건이 달라진다.

일반 stack walk

arch_stack_walk()가 원래 콜백과 cookie를 묶어 kunwind_stack_walk()에 넘긴다. adapter는 state->common.pc를 수집기에 전달한다.

L374–390

reliable stack walk

state->source == KUNWIND_SOURCE_REGS_PC인 exception 경계에서는 false를 반환한다. 다른 경우 일반 adapter로 넘긴다.

L393–420

BPF adapter

BPF 콜백에 pc, sp = 0, fp를 전달한다. 0을 실제 stack address로 읽지 않는다.

L423–435
이 그림은 callback 경계만 보인다. 실제 frame record·PAC·shadow call stack 처리와 원래 함수의 return address 변경은 표시 범위 밖이다.
그림 2. x86 ORC 다음 프레임 검색의 네 갈래ORC 항목을 찾지 못한 경우와 정상 스택 끝을 구별한다.
① 먼저 종료·수명 확인unwind_done()이면 반환. 그 밖에는 guard(rcu)()로 ORC 자료를 읽는 동안 module unload를 막는다.L475–83
② 검색 주소 선택orc_find(state->signal ? ip : ip - 1). 일반 call frame의 return IP와 signal frame을 구분한다.L485–94
③ ORC 결과 분류항목이 없으면 orc_fp_entry로 추정하고 state->error = true. 정의되지 않은 type은 error, END_OF_STACK은 종료.L495–510
④ SP 기준 선택 시작sp_reg에 따라 SP/BP 직접·간접 또는 저장된 R10/R13/RDI 등을 선택한다. 이 발췌는 DX case의 시작에서 끝난다.L512–558
여기 보이는 switch는 다음 SP 계산의 앞부분이다. 최종 frame 검증과 return-address 갱신은 L558 뒤에 있다.
그림 3. RISC-V 사용자 프레임 한 단계저장 PC를 먼저 수집한 다음, 사용자 메모리에서 frame record를 안전하게 읽는다.
① 시작fp = regs->s0; regs->epc를 콜백에 보낸다. 콜백이 중단하면 바로 반환한다.L214–221
② 사용자 record 확인fp - sizeof(struct stackframe)를 계산하고 access_ok()와 __copy_from_user_inatomic() 중 하나라도 실패하면 0으로 멈춘다.L189–203
③ RA 선택첫 단계는 reg_ra가 있으면 그것을 쓰고, 없으면 복사한 buftail.ra를 쓴다. 다음 FP는 buftail.fp.L205–211
④ 반복 조건첫 호출에는 regs->ra를 넘긴다. 이후 fp != 0이고 8바이트 정렬일 때만 RA 인자 0으로 반복한다.L223–225
이 사용자 경로는 walk_stackframe()을 호출하는 kernel 경로(L179–183)와 분리되어 있다.

04 · SOURCE

Linux 6.18.37 원본 코드와 주요 구문 해설

소스 위치를 고정된 숫자로 복사하지 않고 Linux v6.18.37 tree에서 함수 선언을 다시 찾아 발췌했습니다. 원문 전체 발췌를 보존하고, 개별 해설은 근거가 있는 구문에만 붙였습니다.

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줄은 그대로 표시하며, 검토한 32개 구문에 설명을 붙였습니다.

L374static __always_inline bool

일반 스택 수집 콜백에 맞춰 unwind 상태를 바꾸는 내부 함수의 선언입니다. 반환 bool로 호출 스택 탐색을 계속할지 알려 줍니다.

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

현재 프레임의 unwind 상태와 원래 콜백·사용자 자료를 묶은 cookie를 받습니다. 전체 상태 중 PC만 일반 수집기로 전달합니다.

L377 struct kunwind_consume_entry_data *data = cookie;

불투명 cookie를 콜백과 사용자 자료가 들어 있는 구조체로 해석합니다. 새 메모리를 만드는 것이 아니라 호출자가 전달한 묶음을 다시 읽습니다.

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

현재 프레임의 PC와 원래 cookie를 수집기에 전달합니다. 수집기의 계속·중단 판단을 그대로 돌려주어 버퍼 한도 등에 맞춰 탐색을 멈출 수 있습니다.

L381noinline noinstr void arch_stack_walk(stack_trace_consume_fn consume_entry,

커널 호출 스택의 PC들을 consume_entry로 전달하는 외부 함수입니다. 인라이닝과 계측을 제한해 스택 탐색 자체가 추적에 끼어들지 않게 합니다.

L382 void *cookie, struct task_struct *task,

cookie는 수집기에 그대로 돌려줄 사용자 자료이고 task는 탐색할 태스크입니다. 출력 버퍼 같은 수집 상태를 cookie에 연결할 수 있습니다.

L383 struct pt_regs *regs)

저장된 예외 레지스터를 시작점으로 사용할 때의 regs입니다. NULL이면 unwind 코드가 현재 실행 위치나 태스크 저장 문맥에서 시작합니다.

L385 struct kunwind_consume_entry_data data = {

unwind 엔진에 넘길 콜백과 사용자 자료를 로컬 구조체로 묶습니다. 어댑터는 이 한 포인터로 둘 다 접근합니다.

L386 .consume_entry = consume_entry,

호출자가 제공한 PC 수집 함수를 어댑터 자료에 보관합니다. 각 프레임에서 이 함수가 호출됩니다.

L387 .cookie = cookie,

호출자가 제공한 자료 포인터를 그대로 보관합니다. 수집 중 프레임이 바뀌어도 같은 출력 상태를 이어서 사용할 수 있습니다.

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

task와 선택적 시작 레지스터 regs를 기준으로 커널 호출 스택을 풉니다. 어댑터 콜백은 각 프레임의 PC를 원래 consume_entry에 전달하고 cookie를 유지하므로, 호출자는 원하는 형식으로 주소를 수집하거나 중단할 수 있습니다.

L393static __always_inline bool

신뢰 가능한 전체 스택을 요구하는 경로의 콜백 선언입니다. 프레임을 확실하게 이어 갈 수 없으면 false로 중단합니다.

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

일반 수집에 앞서 이 unwind 상태가 신뢰성을 잃는 예외 경계인지 검사하는 어댑터입니다.

L404 if (state->source == KUNWIND_SOURCE_REGS_PC)

현재 PC가 예외 레지스터 프레임에서 나온 것인지 확인합니다. 이 경계에서는 저장 LR이 당시 유효했는지 몰라 다음 프레임의 신뢰성을 보장할 수 없습니다.

L405 return false;

예외 경계를 발견했으므로 신뢰 가능한 전체 unwind를 계속하지 않습니다. 저장 PC 하나를 알 수 있어도 이후 호출 관계까지 확실하다고 보고하지 않습니다.

L407 return arch_kunwind_consume_entry(state, cookie);

신뢰성을 막는 예외 경계가 아니므로 일반 어댑터로 PC를 수집합니다. 최종 계속 여부는 사용자 수집 콜백의 반환값을 따릅니다.

L410noinline noinstr int arch_stack_walk_reliable(stack_trace_consume_fn consume_entry,

신뢰 가능한 호출 스택 수집을 요청하는 외부 함수입니다. 일반 형태와 달리 int 결과로 unwind 엔진의 완료·실패 상태를 전달합니다.

L411 void *cookie,

각 PC를 수집할 때 돌려줄 사용자 자료 포인터입니다. 수집 버퍼나 현재 기록 위치를 이 포인터에 연결합니다.

L412 struct task_struct *task)

신뢰 가능한 스택을 조사할 태스크를 지정합니다. 이 함수는 별도 예외 regs 인자를 받지 않습니다.

L414 struct kunwind_consume_entry_data data = {

신뢰성 검사 어댑터가 쓸 수집 함수와 사용자 자료를 하나로 묶습니다.

L415 .consume_entry = consume_entry,

실제 PC 수집 콜백을 보관합니다. 신뢰성 검사를 통과한 프레임만 이 콜백으로 이어집니다.

L416 .cookie = cookie,

실제 수집기에 다시 전달할 cookie를 보관합니다. 신뢰성 검사 자체의 상태와 출력 자료를 혼동하지 않게 합니다.

L419 return kunwind_stack_walk(arch_reliable_kunwind_consume_entry, &data,

신뢰성 검사 어댑터로 커널 스택을 탐색하고 엔진의 결과를 그대로 반환합니다. 예외 경계 등에서 수집을 중단하는 정책은 앞의 어댑터가 적용합니다.

L420 task, NULL);

대상 태스크와 NULL 레지스터 시작점을 넘깁니다. 엔진이 task의 현재 또는 저장 실행 문맥에서 시작하도록 지정합니다.

L423struct bpf_unwind_consume_entry_data {

BPF용 스택 수집 함수와 그 함수에 되돌려줄 자료를 묶는 구조체입니다. 일반 PC 수집과 달리 IP·SP·FP를 받는 콜백 형식을 사용합니다.

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

BPF 스택 수집에 사용할 콜백 함수 포인터 필드의 선언입니다. cookie와 명령·스택·프레임 포인터를 받아 계속 수집할지 bool로 돌려주는 형식을 정합니다. 실제 아래 어댑터는 SP를 0으로 전달합니다.

L425 void *cookie;

BPF 수집 콜백의 사용자 자료를 보관하는 포인터입니다. unwind 엔진은 내용을 해석하지 않고 수집기에 그대로 전달합니다.

L428static bool

BPF 수집 형식으로 프레임을 전달하는 내부 bool 콜백의 선언입니다. 반환값은 다음 프레임으로 계속 진행할지 나타냅니다.

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

커널 unwind 상태와 BPF 수집 자료를 받아 현재 프레임의 주소들을 꺼내는 어댑터입니다.

L431 struct bpf_unwind_consume_entry_data *data = cookie;

cookie를 BPF 콜백과 사용자 자료가 저장된 구조체로 해석합니다.

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

BPF 수집기에 원래 cookie, 현재 PC와 SP 자리의 0을 넘깁니다. 이 어댑터는 유효한 SP를 제공하지 않으므로 0을 실제 스택 주소로 해석하면 안 됩니다.

L434 state->common.fp);

마지막 인자로 현재 프레임 포인터를 넘깁니다. 콜백 결과를 그대로 반환하여 BPF 수집기가 탐색의 계속 여부를 결정합니다.

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줄은 그대로 표시하며, 검토한 57개 구문에 설명을 붙였습니다.

L460 if (state->prev_regs) {

현재 레지스터 프레임에서 값을 얻기 어려울 때 이전의 완전한 레지스터 저장 영역이 있는지 확인합니다. 있으면 그곳에서 요청한 값을 읽습니다.

L462 return true;

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

L465 return false;

현재·이전 어느 저장 영역에서도 필요한 레지스터를 구할 수 없음을 반환합니다. 이 레지스터를 기준으로 스택을 계산하려던 호출자는 unwind를 중단해야 합니다.

L468bool unwind_next_frame(struct unwind_state *state)

ORC 메타데이터로 호출자의 다음 프레임을 복원하며 state를 갱신합니다. 더 진행할 프레임이 없거나 오류가 나면 false를 반환합니다.

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

복귀 주소 저장 위치 ip_p, 계산할 SP와 임시값을 준비하고 이전 IP·SP를 보관합니다. 이후 이동이 타당한지 검사하고 오류 위치를 보고할 기준입니다.

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

현재 태스크·IRQ·예외 스택 등의 종류를 보관합니다. 다음 프레임에서 스택 종류가 바뀌었는지 판단하는 기준입니다.

L472 struct orc_entry *orc;

현재 명령 위치에 대응하는 ORC 복원 규칙을 가리킵니다. 규칙은 이전 SP의 기준 레지스터와 오프셋, 프레임 종류를 설명합니다.

L473 bool indirect = false;

SP 값을 메모리에서 한 번 더 읽어야 하는지 표시합니다. 기본은 직접 계산이고 간접 규칙에서만 true로 바꿉니다.

L475 if (unwind_done(state))

이미 unwind가 끝난 상태인지 먼저 확인합니다. 끝난 뒤에는 ORC 자료를 읽거나 스택을 추가로 접근할 필요가 없습니다.

L476 return false;

이미 끝난 스택이므로 다음 프레임이 없다는 false를 반환합니다.

L479 guard(rcu)();

이 함수의 남은 범위를 RCU 읽기 임계 구역으로 만듭니다. ORC unwind 자료를 읽는 중 해당 모듈 자료가 해제되지 않게 보호하며, guard 매크로가 반환·분기 등으로 범위를 벗어날 때 읽기 잠금을 자동 해제합니다.

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

저장 레지스터가 사용자 모드를 가리키는지 검사합니다. 커널 스택 탐색이 사용자 경계까지 도달했으므로 여기서 끝냅니다.

L483 goto the_end;

사용자 모드 경계를 만났으므로 정상 종료 정리 위치로 이동합니다. 사용자 스택을 커널 ORC 규칙으로 계속 풀지 않습니다.

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

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

L495 if (!orc) {

현재 PC에 맞는 ORC 항목을 찾지 못했는지 검사합니다. BPF 같은 생성 코드일 수 있어 프레임 포인터 방식으로 추정을 시도합니다.

L502 orc = &orc_fp_entry;

일반 프레임 포인터 배치를 가정한 대체 ORC 규칙을 사용합니다. 원래 코드의 정확한 ORC 자료를 찾아낸 것은 아닙니다.

L503 state->error = true;

프레임 포인터 추정 경로로 넘어갔으므로 전체 결과의 신뢰성에 오류 표시를 남깁니다. 일부 주소를 더 얻더라도 확실한 unwind로 취급하지 않습니다.

L504 } else {

실제 ORC 항목을 찾은 경로입니다. 규칙의 종류가 유효한지와 정상 스택 끝인지 먼저 구분합니다.

L505 if (orc->type == ORC_TYPE_UNDEFINED)

ORC 항목이 정의되지 않은 프레임 종류인지 검사합니다. 이 규칙으로는 이전 스택 상태를 복원할 근거가 없습니다.

L506 goto err;

복원 규칙이 유효하지 않으므로 오류 정리로 이동합니다. 임의의 SP나 복귀 주소를 만들어 계속 진행하지 않습니다.

L508 if (orc->type == ORC_TYPE_END_OF_STACK)

ORC가 명시적으로 스택 끝이라고 표시했는지 확인합니다. 이 경우는 복원 실패가 아니라 정상 종료입니다.

L509 goto the_end;

ORC의 정상 끝 표지를 만났으므로 스택 탐색을 종료하는 위치로 이동합니다.

L512 state->signal = orc->signal;

프레임의 PC를 예외 지점 그대로 해석할지 여부를 ORC 규칙에서 가져옵니다. 다음 ORC 검색에서 일반 call 복귀 주소처럼 IP-1을 쓸지 구분하는 값입니다.

L515 switch (orc->sp_reg) {

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

L516 case ORC_REG_SP:

이전 SP를 현재 SP에 상대적인 값으로 복원하는 규칙입니다.

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

현재 SP에 ORC가 지정한 바이트 오프셋을 더해 이전 프레임의 SP를 계산합니다.

L518 break;

SP 상대 계산을 마쳤으므로 다른 기준 레지스터 case를 실행하지 않고 공통 복원 단계로 넘어갑니다.

L520 case ORC_REG_BP:

이전 SP를 현재 BP에 상대적인 값으로 복원하는 규칙입니다. 함수가 프레임 포인터를 기준으로 스택을 관리하는 경우에 해당합니다.

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

현재 BP에 ORC의 바이트 오프셋을 더해 이전 SP를 계산합니다.

L522 break;

BP 기준 계산을 마쳤으므로 기준 레지스터 선택 분기를 끝냅니다.

L524 case ORC_REG_SP_INDIRECT:

현재 SP가 가리키는 메모리에서 기준값을 읽은 뒤 오프셋을 적용하는 간접 규칙입니다.

L525 sp = state->sp;

먼저 역참조할 위치로 현재 SP를 잡습니다. 이 단계의 sp는 아직 최종 복원값이 아니라 읽을 메모리 주소입니다.

L526 indirect = true;

선택한 주소에서 SP 값을 읽어야 한다고 표시합니다. 공통 간접 처리에서 읽기가 성공한 뒤 sp_offset을 더합니다.

L527 break;

SP 간접 규칙의 주소 준비를 마치고 공통 역참조 처리로 이동합니다.

L529 case ORC_REG_BP_INDIRECT:

BP에 오프셋을 더한 주소에 이전 SP 값이 저장되어 있는 간접 규칙입니다.

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

현재 BP에 오프셋을 더해 복원할 SP가 저장된 메모리 위치를 구합니다. 직접 규칙과 달리 이 값을 다시 읽어야 합니다.

L531 indirect = true;

sp를 메모리 주소로 역참조하도록 표시합니다. BP 간접 규칙은 오프셋이 이미 주소 계산에 적용되었습니다.

L532 break;

BP 간접 규칙의 주소 준비를 끝내고 공통 읽기 단계로 진행합니다.

L534 case ORC_REG_R10:

저장된 R10 값이 이전 SP의 기준인 ORC 규칙입니다. 예외 레지스터 자료에서 R10을 찾아야 합니다.

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

pt_regs 내 R10 위치로 저장값을 가져옵니다. 현재 또는 이전 레지스터 프레임에서 그 값을 얻지 못하면 SP를 복원할 수 없습니다.

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

SP 복원에 필요한 R10이 없다는 경고를 출력합니다. 어떤 코드 위치의 ORC 규칙에서 실패했는지도 함께 기록합니다.

L537 (void *)state->ip);

경고의 %pB에 현재 IP를 넘겨 함수·복귀 주소 문맥을 알아볼 수 있게 합니다. 레지스터 값을 변경하는 표현은 아닙니다.

L538 goto err;

필요한 R10이 없으므로 오류 종료로 이동합니다. 불명확한 SP로 스택을 계속 읽지 않습니다.

L540 break;

R10 값을 정상적으로 얻었으므로 다른 레지스터 규칙을 건너뛰고 공통 복원으로 진행합니다.

L542 case ORC_REG_R13:

저장된 R13을 SP 복원의 기준으로 사용하는 ORC 규칙입니다.

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

레지스터 프레임에서 R13을 가져와 sp로 사용합니다. 해당 저장값이 없으면 이 규칙에 따른 복원이 불가능합니다.

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

필요한 R13 값이 없어 unwind할 수 없다는 경고를 준비합니다.

L545 (void *)state->ip);

R13 누락 경고에 현재 IP를 넘겨 실패한 코드 위치를 식별합니다.

L546 goto err;

R13이 없는 오류 경로로 이동해 스택 탐색을 정리합니다.

L548 break;

R13을 얻은 경우 레지스터 선택을 끝내고 다음 복원 단계를 진행합니다.

L550 case ORC_REG_DI:

저장된 RDI를 SP 기준으로 사용하는 ORC 규칙입니다. 일반 함수 인자라는 역할과 별개로 이 명령 위치에서는 스택 기준을 보관하고 있습니다.

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

pt_regs의 di 필드에서 RDI 저장값을 얻습니다. 필요한 레지스터 프레임이 없으면 sp를 정할 수 없어 오류로 처리합니다.

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

SP 기준이 될 RDI를 읽지 못했다는 경고입니다.

L553 (void *)state->ip);

RDI 누락 경고에 현재 IP를 전달하여 실패 위치를 표시합니다.

L554 goto err;

RDI 없이 복원을 계속할 수 없으므로 오류 정리로 이동합니다.

L556 break;

RDI로 SP 기준을 얻었으므로 기준 선택 분기를 끝냅니다.

L558 case ORC_REG_DX:

저장된 RDX를 SP 복원의 기준으로 사용하는 다음 ORC 규칙입니다. 아래에서 DX 필드를 읽고 실패하면 다른 경우와 동일하게 중단합니다.

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-226

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}

주요 구문 해설

원본 56줄은 그대로 표시하며, 검토한 34개 구문에 설명을 붙였습니다.

L172 if (!try_get_task_stack(task))

대상 태스크의 커널 스택 참조를 확보할 수 있는지 검사합니다. 스택이 해제되는 동안 접근하지 않도록 수명을 보호합니다.

L173 return 0;

스택 참조를 확보하지 못했으므로 대기 위치를 알 수 없다는 0을 반환합니다.

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

잠든 태스크의 호출 스택을 따라가며 save_wchan으로 각 PC를 검사합니다. 스케줄러 함수들을 건너뛴 첫 주소를 pc에 남겨, 태스크가 어떤 호출 경로에서 잠들었는지 나타냅니다.

L175 put_task_stack(task);

try_get_task_stack으로 확보한 스택 참조를 반환합니다. 스택을 읽는 동안 해제를 막았던 참조만 내려놓으며, 대상 태스크를 깨우거나 스택 내용을 지우는 작업은 아닙니다.

L176 return pc;

스케줄러 내부를 건너뛰어 찾은 대기 호출 경로의 PC를 반환합니다. 적절한 프레임을 얻지 못했으면 초기값 0입니다.

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

태스크의 커널 호출 스택을 지정한 콜백으로 전달하는 함수입니다. noinline·noinstr로 탐색 자체의 스택 형태와 계측 간섭을 제한합니다.

L180 struct task_struct *task, struct pt_regs *regs)

탐색 대상 task와 선택적 시작 레지스터 regs를 받습니다. 실제 프레임 추적은 동일 인자를 받은 walk_stackframe이 수행합니다.

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

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

L189static unsigned long unwind_user_frame(stack_trace_consume_fn consume_entry,

사용자 프레임 하나를 읽어 복귀 주소를 수집하고 그 이전 프레임 포인터를 반환하는 함수입니다.

L190 void *cookie, unsigned long fp,

cookie는 수집기에 돌려줄 자료이고 fp는 현재 사용자 프레임 포인터입니다. fp 바로 아래에서 저장 FP·RA 쌍을 읽습니다.

L191 unsigned long reg_ra)

첫 프레임에서 사용할 수 있는 중단 당시 RA 저장값입니다. 0이면 스택 레코드의 RA를 사용합니다.

L193 struct stackframe buftail;

사용자 스택에서 읽어 온 저장 FP와 복귀 주소를 담을 커널 임시 구조체입니다. 검사된 복사 뒤 이 자료를 사용합니다.

L194 unsigned long ra = 0;

이번 프레임에서 수집할 복귀 주소를 보관합니다. 레지스터 RA 또는 스택 RA를 선택하기 전에는 0으로 초기화합니다.

L195 unsigned long __user *user_frame_tail =

현재 fp 바로 아래의 사용자 프레임 레코드를 가리킬 포인터입니다. 다음 줄에서 레코드 크기만큼 뺀 주소를 계산합니다.

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

fp 바로 아래에 있는 저장 FP·복귀 주소 쌍의 위치를 계산해 사용자 포인터로 해석합니다. sizeof는 프레임 레코드 크기이며, 이 줄 자체는 메모리를 읽지 않고 뒤의 접근 검사와 복사를 준비합니다.

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

프레임 레코드 전체가 허용된 사용자 주소 범위인지 검사합니다. 범위를 벗어나면 역참조하지 않고 탐색을 끝냅니다.

L200 return 0;

프레임 주소가 사용자 범위 밖이므로 다음 FP 없음으로 해석할 0을 반환합니다.

L201 if (__copy_from_user_inatomic(&buftail, user_frame_tail,

사용자 프레임 레코드를 buftail로 읽습니다. inatomic 복사를 사용해 이 스택 탐색 중 페이지 폴트를 처리하느라 잠드는 경로에 의존하지 않습니다.

L202 sizeof(buftail)))

저장 FP·RA 구조체 전체 크기만큼 읽고, 복사하지 못한 바이트가 있으면 실패로 분기합니다.

L203 return 0;

사용자 프레임을 완전히 읽지 못했으므로 탐색을 중단할 0을 반환합니다.

L205 ra = reg_ra ? : buftail.ra;

reg_ra가 0이 아니면 그것을 우선 선택하고, 아니면 스택에서 읽은 RA를 사용합니다. 첫 프레임과 그 이전 프레임의 복귀 주소 출처를 구분합니다.

L207 fp = buftail.fp;

스택 레코드가 가리키는 이전 프레임의 FP를 꺼냅니다. 현재 주소를 단순 증가시키는 대신 저장된 프레임 연결을 따라갑니다.

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

복귀 주소가 없거나 수집 콜백이 중단을 요청했는지 검사합니다. 버퍼를 다 채운 경우에도 불필요한 스택 접근을 이어가지 않습니다.

L209 return 0;

수집할 주소가 없거나 콜백이 종료를 원하므로 0을 돌려 반복을 끝냅니다.

L211 return fp;

다음에 읽을 이전 프레임 포인터를 반환합니다. 호출자가 0 여부와 정렬을 검사한 뒤 다음 단계를 수행합니다.

L214void arch_stack_walk_user(stack_trace_consume_fn consume_entry, void *cookie,

저장된 사용자 문맥에서 시작해 PC와 연속된 복귀 주소를 콜백에 전달하는 함수입니다.

L215 const struct pt_regs *regs)

탐색의 시작 PC·FP·RA를 담은 사용자 레지스터를 읽기 전용으로 받습니다. 사용자 실행 상태 자체를 고치는 함수가 아닙니다.

L217 unsigned long fp = 0;

사용자 프레임 연결을 따라갈 FP 변수를 준비합니다. 실제 시작점은 아래에서 저장된 s0로 설정합니다.

L219 fp = regs->s0;

RISC-V ABI에서 프레임 포인터로 쓰는 s0의 저장값을 탐색 시작점으로 삼습니다.

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

중단 당시 실행 중이던 PC를 먼저 수집기에 보냅니다. 콜백이 더 이상 필요 없다고 하면 스택 프레임 읽기를 시작하지 않습니다.

L221 return;

첫 PC에서 수집기가 중단을 요청했으므로 즉시 반환합니다.

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

첫 사용자 프레임을 풀 때는 중단 당시 regs->ra도 함께 넘깁니다. 값이 있으면 그것을 복귀 주소로 우선 사용하고, 프레임에서 읽은 이전 FP를 받아 다음 프레임으로 이어갑니다. 접근 실패나 수집 중단이면 0입니다.

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

이전 FP가 0이 아니고 8바이트 정렬을 만족하는 동안 프레임을 계속 풉니다. 종료값이나 잘못 정렬된 연결을 만나면 접근을 멈춥니다.

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

그다음 프레임들은 별도 레지스터 RA 없이 스택에 저장된 복귀 주소를 읽습니다. 반환된 이전 FP로 계속 이동하며, FP가 0이거나 정렬 조건을 벗어나면 바깥 반복이 끝납니다.

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

공통 커널 코드와 아키텍처별 함수의 역할 구분

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을 보장할 수 없다.

주소나 레지스터 값만 확인하면 갱신되지 않은 상태를 놓칠 수 있다. 상태를 만드는 코드, 다른 코드에 전달하는 시점, 사용하는 코드, 해제하는 시점을 함께 기록한다.

06

최대 지연 시간은 명령 하나의 실행 시간만으로 결정되지 않는다

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

평균값뿐 아니라 인터럽트를 비활성화한 시간, 다른 CPU의 응답 시간, 펌웨어 호출 시간, 재시도 횟수를 따로 확인해야 최대 지연의 원인을 찾을 수 있다.

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. 소스만 읽고 끝내지 않고 최종 vmlinux의 objdump -dr, readelf -SW 결과로 선택된 alternative와 section 배치를 확인한다.

09 · REFERENCES

원문 위치

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