IBM®
메인 컨텐츠로 가기
    Korea [국가변경]    이용약관
 
 
   
        제품    서비스 & 솔루션    고객지원 & 다운로드    회원 서비스    
메인 컨텐츠로 가기

한국 developerWorks  >  리눅스 | 오픈소스 프로젝트  >

Kprobes를 이용한 커널 디버깅

printk's를 리눅스 커널에 삽입하기

developerWorks
문서 옵션

JavaScript가 필요한 문서 옵션은 디스플레이되지 않습니다.


난이도 : 중급

Prasanna S. Panchamukhi, Developer, Linux Technology Center, IBM India Software Labs

2004 년 8 월 19 일

printk를 사용하여 리눅스 커널에서 디버깅 정보를 수집하는 것은 잘 알려진 방법이다. Kprobes를 사용하면 커널을 재부팅 할 필요가 없다. 2.6 커널과 결합된 Kprobes는 printk's를 동적으로 삽입할 때 경량의, 비파괴적인 강력한 메커니즘을 제공한다. 커널 스택 트레이스, 커널 데이터 구조, 레지스터 같은 디버그 정보를 기록하는 것은 결코 쉬운 것은 아니다.

Kprobes는 중단점을 실행 커널에 삽입할 수 있도록 하는 리눅스의 단순하고 간단한 메커니즘이다. Kprobes는 어떤 커널 루틴으로도 들어 갈 수 있고 인터럽트 핸들러에서 정보를 비파괴적으로 모을 수 있는 인터페이스를 제공한다. 프로세서 레지스트리와 글로벌 데이터 구조 같은 디버깅 정보는 Kprobes를 사용하여 쉽게 수집될 수 있다.

Kprobes는 실행 커널에서 주어진 주소에 중단점 명령을 동적으로 작성하여 프로브를 삽입한다. 검사된 명령을 실행하면 중단점 오류가 된다. Kprobes는 중단점 핸들러로 들어가서 디버깅 정보를 모은다.

설치

Kprobes를 설치하려면 Kprobes 홈페이지 (참고자료)에서 최신 패치를 다운로드 한다. kprobes-2.6.8-rc1.tar.gz에 tar 파일 이름이 붙여질 것이다. tar를 풀고 이를 리눅스 커널에 적용한다:

$tar -xvzf kprobes-2.6.8-rc1.tar.gz
$cd /usr/src/linux-2.6.8-rc1
$patch -p1 < ../kprobes-2.6.8-rc1-base.patch

Kprobes는 SysRq키를 사용하는데 이는 DOS 시절에 만들어진 것이다. (참고자료) Scroll Lock키의 왼쪽으로 SysRq키를 발견하게 될 것이다.종종 Print Screen이라는 라벨이 붙기도 한다. SysRq키를 Kprobes에서 실행하려면 kprobes-2.6.8-rc1-sysrq.patch 패치를 붙인다:

$patch -p1 < ../kprobes-2.6.8-rc1-sysrq.patch

커널을 make xconfig/ make menuconfig/ make oldconfig 로 설정하고 CONFIG_KPROBES and CONFIG_MAGIC_SYSRQ 플러그가 작동하도록 한다. 새로운 커널을 구현하여 부팅한다. 이제 printk's를 삽입하고 간단한 Kprobes 모듈을 작성하여 동적으로 디버깅 정보를 모을 준비가 된 것이다.




위로


Kprobes 모듈 작성하기

각 프로브에 struct kprobe kp; 구조를 할당해야 할 것이다. (include/linux/kprobes.h 참조)


Listing 1. 프리(pre), 포스트(post), 오류 핸들러 정의하기

 /* pre_handler: this is called just before the probed instruction is
  *	executed.
  */

int handler_pre(struct kprobe *p, struct pt_regs *regs) {
	printk("pre_handler: p->addr=0x%p, eflags=0x%lx\n",p->addr,
		regs->eflags);
	return 0;
}

 /* post_handler: this is called after the probed instruction is executed
  * 	(provided no exception is generated).
  */

void handler_post(struct kprobe *p, struct pt_regs *regs, unsigned long flags) {
	printk("post_handler: p->addr=0x%p, eflags=0x%lx \n", p->addr,
		regs->eflags);
}

 /* fault_handler: this is called if an exception is generated for any
  *	instruction within the fault-handler, or when Kprobes
  *	single-steps the probed instruction.
  */

int handler_fault(struct kprobe *p, struct pt_regs *regs, int trapnr) {
	printk("fault_handler:p->addr=0x%p, eflags=0x%lx\n", p->addr,
		regs->eflags);
	return 0;
}

커널 루틴의 주소 얻기

등록하는 동안 프로브를 삽입 할 커널 루틴 주소를 지정해야 한다. 다음 메소드 중 하나를 사용하여 커널 루틴 주소를 얻는다:

  1. System.map 파일에서 직접 주소를 얻기
    예를 들어, do_fork의 주소를 받으려면, 명령행에서 $grep do_fork /usr/src/linux/System.map 을 실행한다.
  2. nm명령어 사용하기
    $nm vmlinuz |grep do_fork
  3. /proc/kallsyms 파일에서 주소 얻기
    $cat /proc/kallsyms |grep do_fork
  4. kallsyms_lookup_name() routine 사용하기
    이 루틴은 kernel/kallsyms.c 파일에서 정의되고 이를 사용하려면 CONFIG_KALLSYMS 를 실행시키면서 커널을 컴파일 해야 한다. kallsyms_lookup_name()은 커널 루틴 이름을 스트링으로 취하고 그 커널 루틴의 주소를 리턴한다. 예를 들면: kallsyms_lookup_name("do_fork");이다.

init_module에서 프로브를 등록한다:


Listing 2. 프로브 등록하기

 /* specify pre_handler address
  */
	kp.pre_handler=handler_pre;
 /* specify post_handler address
  */
	kp.post_handler=handler_post;
 /* specify fault_handler address
  */
	kp.fault_handler=handler_fault;
 /* specify the address/offset where you want to insert probe.
  * You can get the address using one of the methods described above.
  */
	kp.addr = (kprobe_opcode_t *) kallsyms_lookup_name("do_fork");

 /* check if the kallsyms_lookup_name() returned the correct value.
  */
	if (kp.add == NULL) {
		printk("kallsyms_lookup_name could not find address
					for the specified symbol name\n");
		return 1;
	}

 /*	or specify address directly.
  * $grep "do_fork" /usr/src/linux/System.map
  * or
  * $cat /proc/kallsyms |grep do_fork
  * or
  * $nm vmlinuz |grep do_fork
  */
	kp.addr = (kprobe_opcode_t *) 0xc01441d0;

 /* All set to register with Kprobes
  */
        register_kprobe(&kp);

일단 프로브가 등록되면 모든 쉘 명령어의 실행은 do_fork으로의 호출이 된다. 그리고 콘솔에 printk's가 보일 것이다. 또는 dmesg를 실행해서도 볼 수 있다. 끝났을 때 프로브의 등록을 해지하는 것을 기억하라:

unregister_kprobe(&kp);

다음 아웃풋은 Kprobes의 주소이다. eflags의 컨텐츠는 다음을 등록한다:

$tail -5 /var/log/messages

Jun 14 18:21:18 llm05 kernel: pre_handler: p->addr=0xc01441d0, eflags=0x202
Jun 14 18:21:18 llm05 kernel: post_handler: p->addr=0xc01441d0, eflags=0x196

오프셋

루틴의 시작 또는 함수의 오프셋에 printk's를 삽입할 수 있다. (오프셋은 명령 영역에 있어야 한다.) 다음 코드 샘플은 오프셋을 계산하는 방법이다. 우선, 객체 파일에서 머신 명령을 역어셈블하고 파일로 저장한다:

$objdump -D /usr/src/linux/kernel/fork.o > fork.dis

다음과 같이 된다:


Listing 3. 역어셈블 된 포크(fork)

000022b0 <do_fork>:
    22b0:       55                      push   %ebp
    22b1:       89 e5                   mov    %esp,%ebp
    22b3:       57                      push   %edi
    22b4:       89 c7                   mov    %eax,%edi
    22b6:       56                      push   %esi
    22b7:       89 d6                   mov    %edx,%esi
    22b9:       53                      push   %ebx
    22ba:       83 ec 38                sub    $0x38,%esp
    22bd:       c7 45 d0 00 00 00 00    movl   $0x0,0xffffffd0(%ebp)
    22c4:       89 cb                   mov    %ecx,%ebx
    22c6:       89 44 24 04             mov    %eax,0x4(%esp)
    22ca:       c7 04 24 0a 00 00 00    movl   $0xa,(%esp)
    22d1:       e8 fc ff ff ff          call   22d2 <do_fork+0x22>
    22d6:       b8 00 e0 ff ff          mov    $0xffffe000,%eax
    22db:       21 e0                   and    %esp,%eax
    22dd:       8b 00                   mov    (%eax),%eax

오프셋 0x22c4에 프로브를 삽입하려면, 0x22c4 - 0x22b0 = 0x14 루틴의 시작부터 관련 오프셋을 가져다가 이 오프셋을 do_fork 0xc01441d0 + 0x14의 주소에 추가한다. (do_fork의 주소를 확인하려면 $cat /proc/kallsyms | grep do_fork을 실행한다.)

do_fork 0x22c4 - 0x22b0 = 0x14의 관련 오프셋을 kallsyms_lookup_name("do_fork")의 아웃풋에 추가할 수도 있다; 따라서: 0x14 + kallsyms_lookup_name("do_fork");

커널 데이터 구조 덤핑하기

이제 시스템상에서 실행되는 모든 작업들의 몇몇 요소들을, 데이터 구조를 덤핑하기위해 변경했던 Kprobe post_handler를 사용하여 덤핑한다:


Listing 4. 데이터 구조를 덤핑하기 위해 변경된 Kprobe post_handler

void handler_post(struct kprobe *p, struct pt_regs *regs, unsigned long flags) {
	struct task_struct *task;
	read_lock(&tasklist_lock);
	for_each_process(task) {
		printk("pid =%x task-info_ptr=%lx\n", task->pid,
			task->thread_info);
		printk("thread-info element status=%lx,flags=%lx, cpu=%lx\n",
			task->thread_info->status, task->thread_info->flags,
			task->thread_info->cpu);
	}
	read_unlock(&tasklist_lock);
}

이 모듈은 do_fork의 오프셋에 삽입되어야 한다.


Listing 5. pids 1508 & 1509의 struct thread_info 아웃풋

$tail -10 /var/log/messages

Jun 22 18:14:25 llm05 kernel: thread-info element status=0,flags=0, cpu=1
Jun 22 18:14:25 llm05 kernel: pid =5e4 task-info_ptr=f5948000
Jun 22 18:14:25 llm05 kernel: thread-info element status=0,flags=8, cpu=0
Jun 22 18:14:25 llm05 kernel: pid =5e5 task-info_ptr=f5eca000

SysRq키 실행하기

SysRq키의 지원으로 이미 컴파일 했다. 이를 실행시켜 보자.

$echo 1 > /proc/sys/kernel/sysrq

이제 Alt+SysRq+W 를 사용하여 콘솔 또는 /var/log/messages 에서 모든 삽입된 커널 프로브를 볼 수 있다.


Listing 6. do_fork에 삽입된 Kprobes를 보여주는 /var/log/messages

Jun 23 10:24:48 linux-udp4749545uds kernel: SysRq : Show kprobes
Jun 23 10:24:48 linux-udp4749545uds kernel:
Jun 23 10:24:48 linux-udp4749545uds kernel: [<c011ea60>] do_fork+0x0/0x1de




위로


Kprobes를 이용한 더 나은 디버깅

프로브 이벤트 핸들러는 시스템 중단점 인터럽트 핸들러에 대한 확장으로서 실행되기 때문에 시스템 장치의 의존성이 거의 없거나 아예 없다. 또한 대부분의 적대적 환경-인터럽트, 태스크 타임부터 실행불가, 콘텍스트 간 변환, SMP 실행의 코드 경로 까지-에 삽입될 수 있다. 이 모두가 시스템 퍼포먼스에 나쁜 영향을 끼치지 않는다.

Kprobes를 사용할 때의 이점은 많다. printk's는 커널을 재구현 및 재부팅 하지 않고 삽입될 수 있다. 프로세서 레지스터는 기록될 될 수 있고, 심지어 디버깅을 위해 변경될 수도 있다. 물론 시스템 파괴는 전혀 없다. 이와 유사하게 리눅스 커널 데이터 구조 역시 기록될 수 있고 비파괴적으로 변경될 수 있다. Kprobes를 사용하여 SMP 시스템 상의 경쟁 조건을 디버깅할 수 있다. 이로서 재구현과 재부팅이라는 고통에서 해방되는 것이다. 커널 디버깅이 이전보다 빠르고 쉽게 될 수 있다는 것을 경험하게 될 것이다.




위로


참고자료




위로


필자소개

Prasanna S. Panchamukhi는 IBM Linux Technology Center (Bangalore, 인도)의 개발자이다. 현재 리눅스용 디버깅 툴 개선에 참여하고 있다. 이전에는 섬유 채널 장치 드라이버 작성과 네트워크 프로세서 애플리케이션 개발에 참여했으며 유닉스 OS를 관리했다. 그의 메일주소는 prasanna@in.ibm.com이다.





위로


기사에 대한 평가

매우 불만족 (1)
불만족 (2)
보통 (3)
만족 (4)
매우 만족 (5)




위로



    IBM 소개개인정보 보호정책문의