Egloos | Log-in
F/OSS study
F/OSS study
[Linux] kprobes 동작 방식 (3)
Linux: 2.6.34
arch: x86_64

이전 글 보기:

이번에는 2.6.34에서 새로이 추가된 kprobe jump optimization에 대해서 살펴보도록 한다.
앞서 살펴보았듯이 kprobe를 실행하기 위해서는 기존의 실행 흐름에서 최소한
두 번의 exception이 발생되어야 한다. (jprobe의 경우에는 세 번이 필요했다.)
jump optimization은 이를 개선하여 exception을 발생시키지 않고도 kprobe를 동작하게 해 준다.

jump optimization의 기본 아이디어는 breakpoint instruction을 이용하는 대신
(relative) jump를 수행하는 instruction을 이용하여
실행 과정에서 자연스럽게 handler가 호출되도록 하는 것이다.
대신 jump를 통해 handler를 수행하는 과정에서 필요한 instruction들을 저장하기 위해
일반 kprobe보다는 조금 더 많은 메모리를 소모한다.

먼저 jump optimization을 적용하려면 커널 설정 시 CONFIG_OPTPROBES 옵션을 선택해야 한다.
jump optimization을 적용하기 위해서는 몇 가지 제약 사항을 가지는데
x86 아키텍처의 경우 relative jmp instruction은 5 바이트를 차지하기 때문에
kprobe를 적용하는 함수 내의 해당 위치 이후로 5 바이트의 공간이 존재해야지만 적용이 가능하다.
(따라서 함수의 거의 마지막 부분에 등록된 kprobe의 경우에는 적용이 불가능하다.)
그리고 등록할 handler가 아키텍처에서 relative jump의 target으로 지정할 수 있는
범위 내에 있어야만 적용할 수 있다. (x86에서는 4바이트를 사용하므로 +/-2GB 범위이면 족하다)

또한 함수 내의 다른 instruction에서 변경될 instruction으로 jump하는 경우가 있다면
올바로 동작하지 않을 것이므로 이런 경우가 있는지 검사하여 적용 여부를 판단해야 한다.
비슷한 이유로 exception table을 검색하여 변경될 instruction에 대한 fixup 루틴이
등록되어 있는지 검사하고 이 경우에도 jump optimization을 적용하지 않도록 한다.

또한 instruction의 종류에 따라 jump optimization을 적용하지 못하는 경우도 있으며
kprobe에 post_handler 혹은 break_handler가 등록된 경우에도 적용을 할 수가 없다.

register_kprobe() 함수는 kprobe를 등록한 직후에 바로 try_to_optimize_kprobe() 함수를 호출하여
등록한 kprobe가 위에서 언급한 조건에 해당하지 않는지 검사한 후 optimization을 시도한다.

그 전에 서로 다른 kprobe가 동일한 위치에 함께 등록될 수 있다는 사실을 기억하도록 하자.
만약 kprobe를 등록할 때 지정한 위치에 이미 다른 kprobe가 등록되어 있다면
aggregate kprobe라는 별도의 kprobe를 새로 할당하여 해당 위치에 등록한 뒤
기존의 kprobe와 등록하려는 kprobe를 aggregate kprobe의 list 필드로 연결한다.
aggregate kprobe의 handler들은 aggr_[pre/post/fault/break]_handler라는 이름을 가지며
단순히 list 내의 모든 kprobe들의 해당 handler들을 차례로 호출해 주는 일을 한다.

CONFIG_OPTPROBES 옵션이 선택된 경우 aggregate kprobe에도 optimization을 적용하기 위해
aggregate kprobe 할당 시 미리 jump optimization에 사용되는 optimized_kprobe 구조체를 할당하고
여기에 포함된 kprobe 구조체를 aggregate kprobe로 사용한다.
optimized_kprobe 구조체는 다음과 같이 정의되어 있다.

include/linux/kprobes.h:
struct optimized_kprobe {
    struct kprobe kp;
    struct list_head list;    /* list for optimizing queue */
    struct arch_optimized_insn optinsn;
};

optinsn는 아키텍처 별로 최적화된 instruction들을 저장하기 위한 버퍼를 포함하는 구조체이다.
이는 상대적으로 많은 instruction들을 저장해야 하기 때문에 기존에 사용되던
arch_specific_insn 구조체에 비해 많은 공간을 필요로 하며, 이를 detour buffer라고 부른다.

이제 try_to_optimize_kprobe() 함수를 살펴보기로 하자.
이 함수는 다음과 같이 정의되어 있다.

kernel/kprobes.c:
static __kprobes void try_to_optimize_kprobe(struct kprobe *p)
{
    struct kprobe *ap;
    struct optimized_kprobe *op;

    ap = alloc_aggr_kprobe(p);
    if (!ap)
        return;

    op = container_of(ap, struct optimized_kprobe, kp);
    if (!arch_prepared_optinsn(&op->optinsn)) {
        /* If failed to setup optimizing, fallback to kprobe */
        free_aggr_kprobe(ap);
        return;
    }

    init_aggr_kprobe(ap, p);
    optimize_kprobe(ap);
}

앞서 말한대로 이 함수는 kprobe를 등록한 직후에 호출되며, 인자로 주어진 p는 등록한 kprobe를 가리킨다.
먼저 alloc_aggr_kprobe() 함수를 호출하여 aggregate kprobe를 할당하는데
jump optimization이 활성화 된 경우 이는 사실 optimized_kprobe 구조체를 반환하므로
container_of 매크로를 통해 원래의 구조체에 대한 포인터를 얻는다.
alloc_aggr_kprobe() 함수의 수행 과정에서 arch_prepare_optimized_kprobe() 함수를 호출하여
필요한 작업을 수행하는데 (이에 대해서는 바로 뒤에서 자세히 살펴볼 것이다.)
이 작업이 정상적으로 수행되었는지 arch_prepare_optinsn() 함수를 통해 검사하고
init_aggr_kprobe()를 호출하여 새로 할당한 optimized_kprobe 구조체를 원래의 kprobe 정보로 채운 뒤
마지막으로 optimize_kprobe() 함수를 호출한다.

arch_prepare_optimize_kprobe() 함수는 위에서 말한 조건들을 실제로 검사한 뒤
op->optinsn 버퍼를 할당하여 detour buffer를 구성한다.
detour buffer는 (kretprobe와 비슷하게) optprobe_template 및 기타 instruction으로 구성되는데
optprobe_template은 kprobe의 handler를 실행하기 위해 (exception이 발생한 상황과 비슷한) 환경을
맞추어주는 것이다. 그 뒤쪽으로는 원래의 함수에서 복사해 온 instruction들이 존재하고
마지막으로는 원래의 함수로 돌아가기 위한 (relative) jump instruction이 나오게 된다.

optprobe_template은 크게 4개의 부분으로 나누어지며 (optprobe_template_[start|val|call|end])
이 중 val과 call 부분은 등록된 kprobe에 따라 다르게 설정되므로 template 자체에는
해당 영역의 크기 만큼이 nop (혹은 이에 대응하는) instruction으로 채워져 있다.

call 부분은 선점을 금지한 채로 등록된 handler를 수행하는 optimized_callback() 함수를 호출하는
(relative) call instruction으로 채워지는데 이 함수는 해당하는 optimized_kprobe의 포인터를
인자로 받으므로 이를 설정하기 위한 (mov) instruction이 먼저 val 부분에 채워진다.

arch_prepare_optimize_kprobe() 함수가 정상적으로 실행된 후의
detour buffer는 다음과 같은 형태로 구성될 것이다.


이렇게 detour buffer가 준비되고 나면 optimize_kprobe() 함수가 호출되는데
이 함수에서 직접 optimization을 적용하지는 않고 workqueue를 통해 이루어지도록 미룬다.
그 이유는 kprobe가 이미 등록되어 있는 상태이기 때문에 다른 cpu의 태스크가 kprobe의 handler를
수행하고 있는 도중일 수 있기 때문이다. 만약 정상적인 kprobe라면 breakpoint (#BP) exception이
발생하여 pre handler를 수행하고 single step으로 원래의 instruction을 수행한 뒤에
다시 원래 위치로 돌아오려고 하는 도중에 jump optimization으로 인해 원래 instruction이
relative jump로 변경되어 버렸다면 instruction의 중간부터 실행될 것이므로
어떠한 동작이 발생하게 될지 알 수 없을 것이다.

따라서 workqueue를 통해 실행되는 kprobe_optimizer() 함수는
이미 실행 중인 kprobe들이 모두 종료될 때까지 대기한 후에 리스트 내에 포함된 모든
kprobe들에 대해 arch_optimize_kprobe() 함수를 호출하여
breakpoint instruction을 detour buffer로 이동하는 relative jump instruction으로 교체한다.
(전과 비슷하게 실행 중에 커널의 코드를 변경하기 위해서 text_poke_smp() 함수가 사용된다.)

하지만 이 사이에는 약간의 틈이 존재하기 때문에 optimize_kprobe() 함수가 완료되었지만
아직 kprobe_optimizer()가 수행되기 전에 해당 kprobe가 실행되는 경우가 생길 수 있다.
이 시점에서 detour buffer 자체는 이용이 가능하기 때문에 새로 실행되는 kprobe는
pre_handler를 수행한 뒤에 setup_singlestep() 함수에서
single step 후에 debug (#DB) exception을 발생시키는 대신
detour buffer에 복사된 instruction을 직접 수행하도록 변경한다. (template 부분은 건너뛴다)
이러한 부수 효과가 적용된 경우를 kprobe가 boost되었다고 표현한다.

이제 jump optimization이 적용된 kprobe가 실행되면
detour buffer의 내용이 실행되는데 먼저 현재의 레지스터 상태를 모두 스택에 저장한 뒤
optimized_callback() 함수를 통해 kprobe의 pre_handler가 수행되고 레지스터를 복원한 뒤에
(복사된) 원래의 instruction들을 수행한 뒤 (원래 함수에서) 그 뒤의 위치로 jump하여 실행을 계속한다.
이러한 과정을 그림으로 나타내면 다음과 같다.


앞서 언급한대로 jump optimization은 kprobe가 post_handler와 break_handler를 포함하지 않을때만
적용이 가능하다. 만약 이미 optimization이 적용된 kprobe와 동일한 위치에 새로운 kprobe가 등록되고
이 kprobe가 post 혹은 break handler를 포함한다면 optimization은 제거되고
다시 원래의 int3 instruction (및 그 뒤의 4바이트)으로 복구된다.

마지막으로 덧붙이자면 debugfs (/sys/kernel/debug)의 kprobes 디렉터리에는
현재 kprobe의 적용 상태를 나타내는 enabled 파일과 등록된 모든 kprobe를 나타내는 list 파일이 있으며,
/proc/sys/debug/kprobes-optimization 파일을 통해서는
jump optimization의 적용 여부를 동적으로 변경할 수 있다.


=== 참고 문헌 ===

by namhyung | 2010/08/03 00:07 | Kernel | 트랙백 | 덧글(4)
트랙백 주소 : http://studyfoss.egloos.com/tb/5371124
☞ 내 이글루에 이 글과 관련된 글 쓰기 (트랙백 보내기) [도움말]
Commented by 뭉구 at 2011/02/07 16:47
글 잘보고 있습니다 많은 도움이 되네요 근데 kprobe 소스는 어디서 받을수 있나요?
Commented by namhyung at 2011/02/07 17:20
감사합니다. kprobe 소스는 리눅스 커널 소스 내에 포함되어 있습니다.
Commented by 뭉구 at 2011/02/07 18:54
우분투10.04를 쓰고 있는데 리눅스 커널 소스에 저는 없어서요. 패치된 소스를 받아야 하는거 같은데 받는 경로 좀 알려주시면 감사하겠습니다. ^^
Commented by namhyung at 2011/02/07 21:03
mainline 커널에 들어있습니다. http://kernel.org 에서 받으실 수 있구요.
우분투 커널에도 있을 것으로 생각되는데 만약 없다면 우분투 커널에 패치가 적용된 것이겠지요..
kernel/kprobes.c 파일이 있는지 확인해 보세요..

:         :

:

비공개 덧글

◀ 이전 페이지 다음 페이지 ▶

카테고리
General
Application
System
Kernel
Book
Tips
태그
bash sed blktrace memory algorithm script kernel git SMP build patch vcs scm gcc compiler block-layer CARM awk glibc documentation synchronization binutils CAaQA3 x86 perf linux computer-architecture elf C emacs
전체보기
이글루 파인더

최근 등록된 덧글
informsi yang bagus dan be..
by pordanaia at 08/05
Informsi yang bagus dan b..
by jamalsu at 08/01
as inforasi yang menarik http..
by jamalsu at 07/22
최근 등록된 트랙백
Tod's Ferrari Homme
by Tods Pas Cher,Kodak did ..
Mocassin Femme
by Mocassins Homme, I got so..
natural garcinia cambogia
by
rss

skin by jiinny


X