2010년 2월 10일 수요일

FreeBSD 7.2에서 커널모듈로 삽질한 이야기(2) - 커널 레벨 데몬만들기


대부분의 커널 모듈은 event handler이다. 이는 장치드라이버를 보면 알 수 있다. 장치 드라이버들과 같은 커널 모듈들은 읽기나 쓰기 함수등을 구현해서 커널이 요청할 때 필요한 기능을 제공한다. 하지만 이것으로는 우리가 생각하는 어플리케이션 만들기에는 부족하다. 모름지기 어플리케이션이란 것은 커널이 부를 때만 움직이는 수동적인 것이 아니라, 스스로 움직이는 능동적인 개체이어야 하기 때문이다 :) 그러기 위해서 필요한 것이 커널 스레드 만들기이다.

FreeBSD에서 커널레벨 프로세스를 시작하거나 종료하는 함수는
  • kproc_start
  • kproc_shutdown
이다. 이 외에, 커널 스레드와 관련된 함수로
  • kthread_create
  • kthread_exit
  • kthread_shutdown
과 같은 함수가 있다.

프로세스와 스레드? 유저레벨에서의 프로그래밍 경험을 살려서, 이름을 통해 기능을 유추하다가
혹시 커널 레벨 프로세스를 만들면 유저레벨 프로그램처럼
독립된 메모리공간을 사용하게 되지 않을까?
그래서 커널 API를 호출 못하는 것이 아닐까?
하는 고민을 하게 될 수도 있다. 특히나 Man Page에는 kproc_start함수를 "SYSINIT매크로와 함께 쓰여서, 컴퓨터가 부팅될 시에 자동으로 데몬을 실행시키도록 하는 용도로 만든 함수"로 기술하고 있다. 나의 경우, 실제로 이 man페이지 설명 때문에, SYSINIT 매크로에 겁먹어서 kproc_start함수를 쓰지 못했다. 그런데 사실 여기에 너무 얽매일 필요가 없다. 이에 대한 이유는 커널소스 /sys/kern/kern_kthread.c 에서 찾아볼 수 있다.

void
kproc_start(udata)

const void *udata;
{
const struct kproc_desc *kp = udata;
int error;

error = kthread_create((void (*)(void *))kp->func, NULL,
kp->global_procpp, 0, 0, "%s", kp->arg0);
if (error)
panic("kproc_start: %s: error %d", kp->arg0, error);
}

여기서도 알 수 있듯이, kproc_start는 단순히 kthread_create의 wrapper function에 불과하다. 따라서 둘은 같은 효과를 지니며, Man 페이지에서는 kproc_start와 kthread_create을 구분하고 있지만, 사실 이 둘은 같은 거라서 kproc_start로 생성된 커널 레벨 데몬도 커널과 같은 가상메모리 주소공간을 사용한다(즉, 어떤 함수로 생성되었는지에 상관 없이, 커널 데몬은 커널의 모든 메모리를 바로 엑세스 할 수 있다).

커널스레드에서 주의할 것은, 스레드를 마칠 때 kthread_exit함수를 꼭 호출해주어야 한다는 점이다. 만약 그렇지 않으면, 스레드 함수는 이상한 주소로 리턴하게 될 것이고, 커널은 페이지폴트 에러를 내며 장렬히 전사할 것이다.

자, 대강 준비는 끝났으니 이제 무언가 일을 하는 데몬을 만들어보자. 이번에 만들 모듈엔 다음의 기능을 구현할 것이다.

  • 드라이버가 로드됨과 동시에 데몬이 시작된다.
  • 데몬은 매 1초마다 이상한 메시지를 출력한다.
  • 드라이버가 언로드돼서 메모리에서 사라지기 전까지 데몬이 멈춘다.

이를 어떻게 하면 구현할 수 있을까?
먼저 매 1초마다 이상한 메시지를 출력하기 위해선 어떻게 해야할 지 살펴보자. 아무래도, 루프를 돌면서 이상한 메시지를 찍은 다음, 1초간 sleep하는 것이 좋을 것이다. 이상한 메시지를 찍는 것은 printf 함수를 이용하면 되는데, 1초간 sleep하는 것은 어떻게? 답은 tsleepmsleep이다. man 페이지를 살펴보자.

int msleep
    (void *chan, struct mtx *mtx, int priority, const char *wmesg, int timo);

int tsleep
    (void *chan, int priority, const char *wmesg, int timo);

man page에 나와있듯이, tsleep은 일정 시간동안 기다리는 것이고, msleep은 인수로 넣어준 mutex의 lock을 얻을때까지 최대 timo의 시간만큼 기다린다(여기서 timo는 밀리초 단위다). man page에 찾아보면 chan이라는 인수는, "스레드가 sleep하게 된 이벤트를 구분하는 유일한 주소"라고 설명이 되어 있다. 또한, wakeup이라는 함수를 호출해서 chan에서의 이벤트를 기다리는 모든 스레드를 깨울 수 있다고 되어있다. 여기서 생기는 궁금증. chan의 정체는 무엇이란말인가? 만약 chan이 invalid한 메모리 주소를 가리키게 하면 어떻게 된다는 소리인가?

여기서 잠깐 팁! 커널 소스 볼 때에는 ctags를 이용하면 좋다!

 커널소스를 살펴보자. 여기서 할 일은 단순히 ctags를 이용해 함수의 definition을 좇아 들어가는 것 뿐이다. 여기에는 소스를 다 쫓아 들어가지 않고 결론을 간단히 제시하겠다. msleep함수와 tsleep함수는 _sleep함수의 래퍼함수이다. 그래서 두 함수는 사실 같다고 할 수 있다. 한편, msleep과 tsleep함수는 chan포인터를 _sleep함수의 ident로 넣어주는데, _sleep함수는 인수로 들어온 ident포인터를 /sys/kern/kern_synch.c의 203번째 줄에서

sleepq_add(ident, ident == &lbolt ? NULL : lock, wmesg, flags, 0);

와 같이, sleepq_add라는 함수에 넘겨주고 있다. 이름에서도 짐작할 수 있듯이, 이 함수는 잠자는 쓰레드 큐에다가 현재 스레드를 넣어준다. 무슨 소린지 전혀 감이 안올땐 ctags를 이용해서 같이 함수 콜을 따라 들어가보자. 감이 확확 올 것이다.
 아무튼, 여기서 알 수 있는 사실은 이것이다:FreeBSD에서, ident는 같은 이벤트를 기다리는 스레드 집단들을 구분하는 identifier로 쓰인다. 이 메모리 주소를 "채널"이라고 부르는 데, chan도 역시 channel의 약자이다. 그런데, 커널에서는 chan포인터가 가리키는 메모리 영역에서 일어난 변화를 감지하거나 하지 않으며, 그 값은 단순히 identifier로만 쓰이기 때문에 실제로 존재하지 않는 주소를 넣어주더라도 문제가 없다. 다만, 커널 소스 전체에 걸쳐 chan을 "어떤 이벤트가 발생하는 메모리 주소"로 생각하고 프로그래밍하고 있으며, chan이 가리키는 곳에 변경을 가했을 때 wakeup(chan)을 해줘서 chan주소에서 발생할 이벤트를 기다리는 스레드를 알아서 깨워주기 때문에, 이왕이면 이런 전통을 지키는 것이 좋겠다. 자세한 것은 커널소스에서 sleepqueue 스트럭트와 http://www.freebsd.org/doc/en/books/arch-handbook/smp-implementation-notes.html 을 참고하면 좋을것임 ㅋ

자, 그럼 소스코드를 보자.

#include <sys/types.h>
#include <sys/module.h>
#include <sys/systm.h>
#include <sys/errno.h>
#include <sys/param.h>

#include <sys/conf.h>
#include <sys/kernel.h>
#include <sys/malloc.h>
#include <sys/uio.h>
#include <sys/proc.h>
#include <sys/kthread.h>


struct mtx workermtx;
int shouldrun = 1;

static void worker(void* a)
{
int i=0;
while(shouldrun)
{
i++;
printf("Merrong(%d) curthread = %08x\n", i, curthread);
if (i>1000) break;
tsleep(worker, 0, "workerthread sleep", 1000);
}
kthread_exit(0);
}

static struct proc* g_proc;

static int drv_loader(struct module *m, int what, void* arg)
{
int err=0, i;
switch(what)
{
case MOD_LOAD:
i=kthread_create((void(*)(void*))worker, (void*)NULL, &g_proc, NULL, NULL,"drv");
printf("My driver is loaded(%d)\n", i);
break;
case MOD_UNLOAD:
shouldrun = 0;
wakeup(worker);
printf("My driver is unloaded\n");
break;
default:
err = EOPNOTSUPP;
break;
}
return (err);
}

static moduledata_t drv_mod =
{
"drv",
drv_loader,
NULL
};

DECLARE_MODULE(drvmodule, drv_mod, SI_SUB_KLD, SI_ORDER_ANY);


무언가 엉성한 것을 확인할 수 있다. 애석하게도, 이 드라이버를 로드한 뒤 언로드하면...

페이지폴트를 내며 커널은 장렬히 전사한다. 이렇게 커널 패닉이 발생(?)하면 커널은 알아서 커널 코어덤프를 생성한다. 코어파일은 /var/crash에 생성된다. 이 코어파일을 분석하기 위해서는 커널디버거 kgdb를 이용해야 한다. kgdb의 사용법은 다음과 같다.

kgdb   [ 커널 파일 ]   [ 코어 덤프 ]

직접 커널을 컴파일하지 않은 경우에는 커널파일은 /boot/kernel/kernel에 들어있다. 코어파일의 이름은 /var/crash/vmcore.[번호] 인데, 여기서 [번호]는 클 수록 최근 것이다. 커널덤프는 크기가 꽤 크므로 지워주면 하드 용량이 부족할 때 지워주면 좋을 것이다.


이렇게 커널이 죽는 것은, 모듈이 언로드 되면서 모듈의 코드가 들어있는 메모리가 해제되기 때문이다. 이렇게 되면 실행되고 있는 스레드의 코드가 담겨있는 메모리가 없어져버리면서 cpu의 eip가 가리키는 주소는 invalid한 주소가 되어버린다 ㅋ 이를 막을려면, 언로드기능을 아예 구현하지 않는 방법이 있고, 모듈의 이벤트 핸들러가 스레드가 완전히 끝마쳐질 때까지 기다리도록 하는 방법이 있다. 처음 것은 너무 단순하니 두 번째 것을 시도해보자. 이는 간단히 구현할 수 있다. 위의 소스코드에서 다음의 줄을 추가하자.

                case MOD_UNLOAD:
shouldrun = 0;
wakeup(worker);
tsleep(g_proc, 0, "exit sleep", 1000);
printf("My driver is unloaded\n");
break;

이렇게 하면 스레드가 끝마쳐질 때까지 대기하도록 할 수 있다. 여기서 주목할 점은 g_proc을 chan으로 넣어주었다는 것이다.  kthread_exit함수를 열어보면 다음과 같다.

void
kthread_exit(int ecode)

{
struct thread *td;
struct proc *p;

td = curthread;
p = td->td_proc;
/*
* Reparent curthread from proc0 to init so that the zombie
* is harvested.
*/

sx_xlock(&proctree_lock);
PROC_LOCK(p);
proc_reparent(p, initproc);
PROC_UNLOCK(p);
sx_xunlock(&proctree_lock);
/*
* Wakeup anyone waiting for us to exit.
*/

wakeup(p);
/* Buh-bye! */
exit1(td, W_EXITCODE(ecode, 0));
}

여기에서 확인할 수 있듯, kthread_exit함수는 p에 대해 wakeup을 호출해서 g_proc에 대해 잠자고 있는 이벤트 핸들러를 깨워준다. 이를 통해서 스레드가 끝난 뒤에 커널 모듈이 언로드되는 것을 어느정도(?)보장해줄 수 있다.

이렇게 하고 커널 모듈을 빌드한 뒤, dmesg를 통해 이번 커널 모듈이 출력한 메시지를 확인해보자.

[root@CTF /]# dmesg | tail
Trying to mount root from ufs:/dev/ad0s1a
WARNING: / was not properly dismounted
GEOM_LABEL: Label ufsid/4b4d2753e0ce587a removed.
GEOM_LABEL: Label for provider ad0s1a is ufsid/4b4d2753e0ce587a.
GEOM_LABEL: Label ufsid/4b4d2753e0ce587a removed.
My driver is loaded(0)
Merrong(1) curthread = c29fd230
Merrong(2) curthread = c29fd230
Merrong(3) curthread = c29fd230
My driver is unloaded

우왕 ㅋ 성공한듯 ㅋ

사족을 붙이자면, 이 커널 드라이버에서 출력하는 curthread라는 포인터의 값은 현재 실행되고 있는 스레드의 TCB(thread control block)의 주소다. 타입은 struct thread* 이며, struct thread의 정의는 /sys/sys/proc.h에서 확인할 수 있다.

0 개의 댓글:

댓글 쓰기