Kprobes
는 리눅스 커널의 디버깅과 성능 측정에 사용할 수 있는 좋은 인터페이스이다. Kprobes
는 커널 루틴에 동적으로 중단점(breakpoint
)을 삽입해 디버그와 성능 정보를 수집한다. 분석하고 싶은 커널 코드에 트랩(trap
)을 설치하면 중단점 도달 시, 설정해둔 처리 함수(handler
) 를 실행시킬 수 있다.kprobes
는 어떤 코드에도 삽입 가능하고, kretporbs
는 특정 함수가 반환될 때 동작한다. 백날 설명해봐야 한번 보는 것만 못하다. 이번 장에서는 예제 프로그램을 빌드하여 모듈을 삽입하고, 그 결과를 확인 해보겠다.
Kprobes
인터페이스 지원 여부Kprobes
를 사용하기 위해서는 만족해야 하는 몇 가지 조건이 있다. 대부분의 독자는 아래의 사항을 만족할 것이므로 큰 걱정할 필요는 없다.
CPU
아키텍쳐Kprobes
인터페이스는 아래의 CPU
아키텍쳐에서 동작한다.
i386
x86_64
ppc64
ia64
sparc64
arm
ppc
mips
s390
parisc
필자는 x86_64
를 사용하므로 지원 대상이다. 독자의 아키텍쳐 역시 대부분 위 목록 중 하나에 해당할 것이므로 걱정할 필요 없다.
빌드할 때 사용했던 .config
파일은 아래의 상수가 전부 활성화되어야 한다. 일부러 설정을 건드린 것이 아니라면 대체로 활성화 되어 있을테니 걱정할 필요 없다. 만일 비활성화 되어 있다면 활성화( = y
)로 변경하여 다시 빌드하면 그만이다. 상수는 아래와 같다:
CONFIG_KPROBES
CONFIG_MODULES
CONFIG_MODULE_UNLOAD
CONFIG_KALLSYMS
CONFIG_KALLSYMS_ALL
CONFIG_DEBUG_INFO
Kprobes
함수 목록 1번
의 설정을 모두 만족한다면 Kprobes
를 쓸 준비가 된 것이다. 우리가 사용할 Kprobes
함수들은 include/linux/kprobes.h
와 kernel/kprobes.c
에 선언 및 정의되어 있다. 공식 문서 또한 상세하게 잘 설명해놨으므로 이를 참조해도 좋을 것이다. 필자는 공식 문서와 같이 아주 디테일하게 설명하진 않고 간단하게 스케치만 해볼 것이다.
kprobe
관련 함수#include <linux/kprobes.h> // kprobe 를 등록/해제 하는 함수 int register_kprobe(struct kprobe *p); void unregister_kprobe(struct kprobe *p); // 중단점 앞에서 실행되는 처리 함수 typedef int (*kprobe_pre_handler_t) (struct kprobe *, struct pt_regs *); // 중단점 끝에서 실행되는 처리 함수 typedef int (*kprobe_post_handler_t) (struct kprobe *, struct pt_regs *, int trapnr); // 실행 중 예외를 처리하는 함수 typedef int (*kprobe_fault_handler_t) (struct kprobe *, struct pt_regs *, int trapnr); // kprobe 동작 활성/비활성화 함수 int disable_kprobe(struct kprobe *kp); int enable_kprobe(struct kprobe *kp);
kretprobe
관련 함수#include <linux/kprobes.h> // kretprobe 를 등록/해제 하는 함수 int register_kretprobe(struct kretprobe *rp); void unregister_kretprobe(struct kretprobe *rp); // kretprobe 동작 활성/비활성화 함수 int enable_kretprobe(struct kretprobe *rp); int disable_kretprobe(struct kretprobe *rp);
kprobe
는 위에서 본 것처럼 두 가지(kprobe
, kretprobe
) 종류가 있다. kprobe
는 어떠한 커널 코드에도 설치할 수 있는 반면,kretprobe
(혹은 return probe
라고 불리는) 는 특정 함수가 반환할 때 동작한다.
리눅스 커널은 kprobe
에 대한 샘플 소스 코드를 제공하므로 이를 빌드해서 그 내용을 테스트 해볼 것이다. samples/kprobes/
경로에서 그 내용을 확인할 수 있다. kprobe
샘플 코드는 모듈의 형태로 되어 있다. 따라서 모듈을 빌드하고 삽입하는 과정을 통해 그 결과를 확인할 수 있다. 이는 9 - 10 장 커널 모듈 프로그래밍
에서 설명했으므로 자세히 설명하진 않을 것이다. Makefile
은 아래와 같이 작성하면 된다:
kprobe_example.c
가장 먼저 kprobe_example.c
파일에 대해 분석해보겠다. kprobe_init
과 kprobe_exit
은 모듈 삽입과 삭제 시 실행되는 함수이다. 앞서 설명했듯이 kprobe
를 동작시키기 위해서는 register_kprobe
함수를 호출해서 트랩을 설치해야 한다. register_kprobe
는 kp
구조체의 주소를 매개변수로 받아서 트랩을 설치한다. 위의 3 행은 각각 트랩 발동 시(실행 전), 발동 후(끝난 후), 발동 중(예외)에 대한 처리를 하는 함수들이다.
그런데 뭔가 이상한 생각이 들지 않는가? 분명 트랩 시 호출되는 처리 함수는 kp
구조체에 담았지만, 정작 가장 중요한 트랩이 보이질 않는다. 이는 상단의 kp
구조체를 선언함과 동시에 초기화 한다.
보는 것처럼 struct kprobe
구조체는 .symbol_name
멤버 변수에 커널 코드의 심볼(함수) 를 저장하고, 상황에 맞춰 구조체에 등록된 처리 함수(handler
) 를 호출하게 된다.
전처리(handler_pre
), 후처리(handler_post
) 함수는 심볼의 이름(p->symbol_name
) 과, 프로브 혹은 함수 주소(p->addr
), 명령어 주소(ip
), 플래그(flags
) 등을 출력한다.
실제 코드에는 아키텍쳐별 출력 구문 나열되어 있었는데 필자는 x86_64
를 사용하기 때문에 해당 출력 구문만 남기고 모두 제거했다.
필자가 실행한 결과는 아래와 같았다:
(중략...)
kretprobe_example.c
kretprobe_example.c
역시 이전 예제와 구조적으로 다르진 않지만 전달하고 반환받는 구조체에서 약간의 차이를 보인다.
kretprobe_init
함수는 모듈 등록 시 호출되는 함수로, 중단점을 설치한다. kretprobe_exit
함수는 모듈 삭제 시 호출되는 함수로 등록했던 중단점을 해제한다.
entry_handler
는 중단점 진입 전에 실행되는 함수이고 ret_handler
는 중단점 반환 후 실행되는 함수이다. entry_handler
는 인자로 전달된 *ri
에 현재 시간을 저장한다.
kprobe
와는 달리 kretporbe
는 동시적으로 호출되는 경우, 각각의 probe
는 독자적인 데이터 공간을 가진다. *ri
는 다른 probe
들과는 공유하지 않는 독자적인 데이터 공간을 가르키는 변수이다. 따라서 해당 공간에 현재 시간을 저장할 수 있다. 위 사진에서 나온 struct kretprobe
는 register_kretprobe
에 전달하는 구조체로 각 probe
는 struct my_data
크기의 독자적인 데이터 공간을 가지며, 최대 20
개의 probe
를 병렬적으로 처리할 수 있음을 의미한다.
마지막으로 ret_handler
는 중단점의 반환 값을 읽어 들이고 현재 시간과 entry_handler
에서 받아왔던 시간의 차 (심볼 수행에 걸린 시간) 를 출력한다. 출력 결과는 아래와 같다:
(중략...)
[사이트] https://www.kernel.org/doc/Documentation/kprobes.txt
[사이트] https://elixir.bootlin.com/linux/v3.4/source/arch/x86/include/asm/ptrace.h
[사이트] https://en.wikipedia.org/wiki/FLAGS_register
[책] 리눅스 커널 소스 해설: 기초 입문 (정재준 저)