커널 코드에 printk를 넣고 재빌드/재부팅하는 것은 확인할 지점이 늘어날수록 비용이 커진다. tracepoint와 kprobe는 커널을 재빌드하지 않고도 특정 함수의 호출, 인자, 리턴값을 실시간으로 들여다볼 수 있게 해주는 두 가지 트레이싱 메커니즘이다. tracepoint는 커널 개발자가 미리 코드에 박아둔 고정 지점이고, kprobe는 거의 모든 커널 함수에 임의로 붙일 수 있는 동적 브레이크포인트다. 이 글에서는 ftrace 인터페이스를 기준으로 두 가지를 켜고 읽는 방법을 정리한다.
사전 준비
대부분의 배포판 커널은 아래 옵션이 기본으로 켜져 있다. 직접 빌드한 커널이라면 확인이 필요하다.
CONFIG_FTRACE=y
CONFIG_KPROBE_EVENTS=y
CONFIG_TRACEPOINTS=y
ftrace의 tracefs는 보통 /sys/kernel/tracing에 마운트돼 있다(구버전 배포판은 /sys/kernel/debug/tracing). 모든 조작은 root 권한이 필요하다.
# mount | grep tracefs
tracefs on /sys/kernel/tracing type tracefs (rw,relatime)
# cd /sys/kernel/tracing
tracepoint 목록 확인하고 켜기
커널에 정의된 tracepoint는 events/ 아래에 서브시스템별 디렉토리로 정리돼 있다.
# ls events/
block ext4 kmem sched syscalls vmscan ...
# ls events/sched/
sched_switch sched_wakeup sched_process_exec ...
# cat events/sched/sched_switch/format
format 파일에는 해당 이벤트가 남기는 필드 목록이 나온다. 특정 이벤트만 켜려면 그 디렉토리의 enable에 1을 쓰면 된다.
# echo 1 > events/sched/sched_switch/enable
# echo 1 > tracing_on
# cat trace_pipe
서브시스템 전체를 한 번에 켜려면 서브시스템 디렉토리의 enable을 쓰면 된다(events/sched/enable). 다 끄고 처음 상태로 되돌리려면 echo 0 > events/enable과 echo > trace로 버퍼를 비운다.
kprobe 이벤트 등록하기
tracepoint가 없는 함수는 kprobe_events에 직접 프로브를 등록해서 추적한다. p:는 함수 진입 시점(kprobe), r:는 함수 리턴 시점(kretprobe)을 의미한다.
# echo 'p:myprobe do_sys_open' > kprobe_events
# echo 'r:myretprobe do_sys_open $retval' >> kprobe_events
# ls events/kprobes/
myprobe myretprobe
# echo 1 > events/kprobes/enable
# echo 1 > tracing_on
# cat trace_pipe
등록한 이벤트를 지우려면 이름 앞에 -:를 붙여서 쓰거나, kprobe_events에 빈 문자열을 쓰면 전체가 삭제된다.
# echo '-:myprobe' >> kprobe_events
# echo > kprobe_events
인자와 리턴값 캡처하기
fetchargs 문법으로 레지스터, 스택, 구조체 필드를 지정해 원하는 값을 이벤트에 함께 기록할 수 있다.
| 표기 | 의미 |
| %di, %si, %dx … | x86_64 호출 규약 기준 레지스터 직접 참조 |
| $argN | N번째 함수 인자 (커널이 디버그 정보로 타입 추론) |
| $retval | kretprobe에서 함수의 리턴값 |
| +offs(%reg) | 레지스터가 가리키는 주소에서 offs만큼 떨어진 메모리 참조 (구조체 필드 접근) |
| :type | 값의 타입 지정 (예: :u32, :string, :x64) |
예를 들어 do_sys_open이 열려는 파일 이름을 문자열로 함께 남기려면 인자 위치에 맞는 표기와 :string 타입을 함께 지정한다.
# echo 'p:myprobe do_sys_open filename=$arg2:string' > kprobe_events
# echo 1 > events/kprobes/myprobe/enable
# cat trace
# bash-1234 [002] .... 12345.678901: myprobe: (do_sys_open+0x0) filename="/etc/passwd"
구조체 포인터를 인자로 받는 함수라면 +offset(%reg)로 특정 필드만 뽑아낼 수도 있다. 다만 오프셋은 커널 버전마다 달라질 수 있으므로 대상 커널의 구조체 정의를 직접 확인해야 한다.
perf로 다루기
ftrace 파일을 직접 만지는 대신 perf probe로 같은 작업을 더 간단히 할 수 있다. perf는 내부적으로 kprobe_events에 등록하고 perf 이벤트로 노출해준다.
# perf probe --add do_sys_open
# perf probe -l
probe:do_sys_open (on do_sys_open)
# perf record -e probe:do_sys_open -a sleep 5
# perf script
함수의 특정 라인이나 리턴 시점만 잡고 싶으면 perf probe -L <func>로 라인 번호를 먼저 확인한 뒤 지정한다.
# perf probe -L do_sys_open | head
# perf probe --add 'do_sys_open:42'
# perf probe --add 'do_sys_open%return $retval'
# perf probe --del do_sys_open
디버그 정보(vmlinux-debuginfo 또는 /usr/lib/debug)가 설치돼 있으면 perf probe가 소스 라인·변수 이름을 그대로 인식해 fetchargs를 직접 계산할 필요가 없어진다.
주의사항 및 팁
- 모든 함수에 kprobe를 걸 수 있는 것은 아니다. 인라인 처리되거나
NOKPROBE_SYMBOL()로 명시적으로 막아둔 함수(주로 트레이싱 인프라 자체나 인터럽트 처리 경로)에는 등록이 거부된다. - 호출 빈도가 매우 높은 함수(예:
kmalloc, 스케줄러 내부 함수)에 kprobe를 걸면 오버헤드가 커지고 trace 버퍼가 순식간에 찬다. 먼저 tracepoint로 대체 가능한지 확인하는 편이 낫다 — tracepoint는 컴파일 타임에 최적화된 고정 지점이라 kprobe보다 오버헤드가 낮다. - trace 버퍼 크기는
buffer_size_kb로 조절한다. 짧은 시간에 많은 이벤트를 잡아야 한다면 미리 늘려두는 것이 좋다. - 작업이 끝나면
tracing_on을 0으로 내리고 등록한 이벤트를 정리한다. kprobe_events는 리부팅 전까지 계속 남아 있다.