[리눅스 커널 구조 원리] #2. 커널 디버깅

문연수·2025년 3월 14일
0

1. objdump 유틸리티

 책에서는 Raspberry Pi 내에서 objdump 를 찍지만 필자는 호스트 머신에서 찍기 때문에 다른 이름의 프로그램을 사용해야 한다:

aarch64-linux-gnu-objdump -x vmlinux | grep

aarch64-linux-gnu-objdump			\
--start-address=0xffffffc080000000	\
--stop-address=0xffffffc080000020	\
-d vmlinux

 주소 범위로 어셈블리 코드를 찾는 것도 가능하다. 책에서는 다른 주소를 썼는데 필자는 64-bit 운영체제라서 하위 주소에서 아무 것도 나오는게 없다.

2. ftrace 를 통한 디버깅

 책을 따라서 코드를 작성하고 리빌드 한 뒤에 다시 실행시켜 봤는데 ftrace 로그에 결과가 찍히지 않는다. 그래서 이 문제 자체를 디버깅하는 과정을 적어보려 한다.

* 의식의 흐름

 우선 빌드가 제대로 되었는지 확인하기 위해서 vmlinux 파일의 symboldump 하여 rpi_get_interrupt_info 함수가 제대로 있는지 확인해보았다:

rpi_get_interrupt_info 함수는 확실히 존재한다. 그렇다면 컴파일에는 문제가 없었다는 뜻이다. 그렇다면 무엇이 문제였을까? 필자는 커널이 업데이트(변경)되지 않았다고 생각했다. 따라서 dmesgraspberry pi 의 커널 로그를 확인해보았다:

 맨 윗 줄만 보고 커널은 반드시 업데이트 되었다고 생각했다. 그렇게 생각한 이유는 우선 빌드 환경이 크로스 빌드임이 확인되었고 또 커널 업데이트 날짜가 완전히 최신이다. 그리고 다음의 로그를 보고 확신했다:

위에서 작성한 패치 코드는 분명히 적용되었다. 그렇다면 문제는 두 가지로 좁혀진다:

  1. 커널이 rpi_get_interrupt_info 함수를 부르지 못했다.
  2. ftrace 의 설정이 잘못 되어 함수는 호출되었으나 이를 캐치하지 못했다.

* rpi_get_interrupt_info 는 호출 되었는가?

필자는 이를 확인하기 위해 코드를 새롭게 한 줄 더 작성했다:

printk 함수는 dmesg 에 바로 붙잡히기 때문에 cat /proc/interrupts 이후에 찍어보면 바로 확인 가능하다:

자, 이제 커널은 변경 되었고 결과만 확인하면 된다...

* 결과는?

rpi_get_interrupt_info() was called!

 하지만 아직 한 가지 코너 케이스가 남아있다. 이것까지 로그로 찍어 보았으면 좋았을텐데 나의 판단에 아쉬움이 남는다. 그건 바로 irq_handlerNULL 인 경우이다. 이때에는 if 문이 실행되지 않아 trace_printk 함수도 호출되지 않을 것이다. 변경 전에 코드를 한번 더 살펴 보았다:

rpi_get_interrupt_info() 를 부르는 함수는 show_interrupts() 이고, 그 상위 함수를 찾아서 handler 가 어떻게 전달되는지 확인하려 했으나 위와 같은 구조로 맵핑되어 있어서 일단 찾는건 포기하고 다시 ftrace 쪽으로 넘어가기로 했다.

* ftrace 설정 문제?

ftrace 를 설정하는 과정에서 11번째 줄이 에러를 발생 시킨다. 이건 secondary_start_kernel 부분인데 처음에는 이곳이 문제인가? 했다가 바로 생각을 고쳐 먹었는데 그 이유는 다음과 같다:

 우선 show_interrupts 함수가 캐칭이 되고 호출된 횟수가 printk 를 호출한 횟수와 같다. 그래서 한번 ftracerfilter functionshow_interrupt 로 변경해보았다:

 진짜 띠용하다. 코드로 보아서는 확실히 rpi_get_interrupt_info 도 호출되었고, trace_printk 도 호출되었다.

3. 테스트 스크립트

 궁금해서 커널 변경 후, 전체 과정을 bash script 로 재작성하여 다시 수행해 보았다.

 제대로 출력된다!

 최종적인 테스트 스크립트는 위와 같다. 아무래도 재부팅 이전의 event, function 설정에 문제가 있었는데, 재부팅 이후에 설정이 다 날아간 것 같다. 그리고 하나 더, 전체 과정을 bash script 로 옮기는 과정에서 cat /proc/interrupts 명령을 수행하는 주체가 root 로 바뀌었다는 점도 있다. 책에서도 root 권한으로 위 명령을 수행하는데 이전 과정에서 필자는 그냥 일반 유저 권한으로 명령을 실행했다.

 궁금해서 다시 테스트 해 본 결과 아무래도 ftrace 설정 문제였던 것 같다. 일반 유저 권한으로도 문제가 없다.

4. 맵핑 스크립트

- 크로스빌드 & 재시작 스크립트

#!/bin/bash

BUILD_DIR=../../build/raspberrypi
TARGET=mythos@192.168.0.128
DEST_DIR=/home/mythos/build

KERNEL=kernel8 make ARCH=arm64 CROSS_COMPILE=aarch64-linux-gnu- Image modules dtbs O=$BUILD_DIR -j$(nproc)
KERNEL=kernel8 make ARCH=arm64 CROSS_COMPILE=aarch64-linux-gnu- O=$BUILD_DIR/ INSTALL_MOD_PATH=$BUILD_DIR modules_install -j$(nproc)

rm $BUILD_DIR/lib/modules/6.6.78-v8+/build

echo "Start send file"
sshpass -p $1 scp $BUILD_DIR/arch/arm64/boot/Image $TARGET:$DEST_DIR/Image
sshpass -p $1 scp -r $BUILD_DIR/lib/modules/6.6.78-v8+/ $TARGET:$DEST_DIR/modules/

sshpass -p $1 scp $BUILD_DIR/arch/arm64/boot/dts/broadcom/*.dtb $TARGET:$DEST_DIR/dtb/
sshpass -p $1 scp $BUILD_DIR/arch/arm64/boot/dts/overlays/*.dtb* $TARGET:$DEST_DIR/dtb/overlays/

echo "move sending files"
sshpass -p $1 ssh -t $TARGET "echo $1 | sudo -S cp $DEST_DIR/Image /boot/firmware/kernel8.img"
sshpass -p $1 ssh -t $TARGET "echo $1 | sudo -S cp -r $DEST_DIR/dtb/ /boot/firmware/"
sshpass -p $1 ssh -t $TARGET "echo $1 | sudo -S cp -r $DEST_DIR/modules/ /lib/modules/6.6.74+rpt-rpi-v8/"

sshpass -p $1 ssh -t $TARGET "sync"

sshpass -p $1 ssh -t $TARGET "echo $1 | sudo -S reboot -h now"

echo "done!"
./build_and_run.sh <password>

 이렇게 입력하면 커널을 빌드한 뒤에 빌드 결과물을 라즈베리 파이로 복사하고, 복사한 파일로 커널을 교체한 뒤 자동으로 재부팅한다.

- ftrace 래핑 스크립트

#!/bin/bash

FTRACE=/sys/kernel/debug/tracing

if (( $# <= 0 )); then
        echo "usage $0 <command>"
        echo -e "command list:
                on [timeout]
                off

                list-target
                set-target <target>
                get-target

                list-filter
                set-filter <target>
                get-filter

                list-option
                set-option <option> <value>
                get-option <option>

                result
        "
fi

case $1 in
        "on")
                echo 1 > $FTRACE/tracing_on

                if (( $# == 2 )); then
                        sleep $2
                        echo "tracing off"
                        echo 0 > $FTRACE/tracing_on
                fi
        ;;

        "off")
                echo 0 > $FTRACE/tracing_on
        ;;

        "list-target")
                cat $FTRACE/available_tracers
        ;;

        "set-target")
                echo $2 > $FTRACE/current_tracer
        ;;

        "get-target")
                cat $FTRACE/current_tracer
        ;;

        "list-filter")
                TARGET=$(cat $FTRACE/current_tracer)
                case $TARGET in
                        "function" | "function_graph")  cat $FTRACE/available_filter_functions  ;;
                        "event")                        cat $FTRACE/available_filter_events     ;;
                        *)                              echo "no filter provided for $TARGET"   ;;
                esac
        ;;

        "set-filter")
                echo $2 > $FTRACE/set_ftrace_filter
        ;;

        "get-filter")
                cat $FTRACE/set_ftrace_filter
        ;;

        "list-option")
                ls $FTRACE/options/
        ;;

        "set-option")
                echo $3 > $FTRACE/options/$2
        ;;

        "get-option")
                cat $FTRACE/options/$2
        ;;

        "result")
                cat $FTRACE/trace
        ;;
esac

ftrace 를 일일이 입력하는 것이 불편해서 만든 래핑 스크립트이다. 아래의 명령어를 순서대로 입력하면 위에서 수행했던 과정을 그대로 따라할 수 있다:

sudo ftrace set-target function
>
sudo ftrace set-filter rpi_get_interrupt_info
>
sudo ftrace set-option func_stack_trace 1
sudo ftrace set-option sym-offset 1
>
sudo ftrace on 5 &
>
cat /proc/interrupts
# 5 초간 대기...
>
cat ftrace result
profile
2000.11.30

0개의 댓글

관련 채용 정보