2025. 12. 9. 21:48ㆍLinux/리눅스 커널 모듈
Claud AI로 정리한 저장용 내용입니다. (고마워요 클로드 선생님)
목차
환경 준비
필수 패키지 설치
sudo apt-get update
sudo apt-get install -y \
qemu-system-x86 \
gdb \
busybox-static \
build-essential \
flex \
bison \
libssl-dev \
libelf-dev
커널 소스 다운로드
wget https://cdn.kernel.org/pub/linux/kernel/v6.x/linux-6.10.8.tar.xz
tar -xf linux-6.10.8.tar.xz
cd linux-6.10.8
커널 빌드
디버깅을 위한 커널 설정
GDB로 제대로 디버깅하려면 디버그 심볼이 포함된 커널을 빌드해야 합니다.
make menuconfig
다음 옵션들을 활성화하세요:
Kernel hacking --->
Compile-time checks and compiler options --->
[*] Compile the kernel with debug info (CONFIG_DEBUG_INFO=y)
[*] Provide GDB scripts for kernel debugging (CONFIG_GDB_SCRIPTS=y)
[*] Compile the kernel with frame pointers (CONFIG_FRAME_POINTER=y)
Generic Kernel Debugging Instruments --->
[*] KGDB: kernel debugger (CONFIG_KGDB=y) (선택사항)
또는 간단하게:
# 기본 설정 사용
make defconfig
# 디버그 옵션 추가
scripts/config --enable DEBUG_INFO
scripts/config --enable DEBUG_INFO_DWARF_TOOLCHAIN_DEFAULT
scripts/config --enable GDB_SCRIPTS
scripts/config --enable FRAME_POINTER
scripts/config --disable DEBUG_INFO_REDUCED
커널 컴파일
make -j$(nproc)
컴파일이 완료되면 다음 파일들이 생성됩니다:
- vmlinux: 디버그 심볼이 포함된 ELF 파일 (GDB용)
- arch/x86/boot/bzImage: 부팅 가능한 압축 커널 이미지 (QEMU용)
initramfs 생성
커널이 부팅되면 루트 파일시스템이 필요합니다. 간단한 initramfs를 만들어봅시다.
# initramfs 디렉토리 구조 생성
mkdir -p initramfs/{bin,sbin,etc,proc,sys,usr/bin,usr/sbin}
cd initramfs
# busybox 복사
cp /bin/busybox bin/
# init 스크립트 생성
cat > init << 'EOF'
#!/bin/busybox sh
# busybox 명령어 링크 생성
/bin/busybox --install -s
# 기본 파일시스템 마운트
mount -t proc none /proc
mount -t sysfs none /sys
mount -t devtmpfs none /dev
# 환경 설정
export PATH=/bin:/sbin:/usr/bin:/usr/sbin
# 메시지 출력
echo "Welcome to minimal Linux system!"
echo "Type 'help' for available commands"
# 쉘 실행
exec /bin/sh
EOF
chmod +x init
# initramfs 이미지 생성
find . | cpio -o -H newc | gzip > ../initramfs.img
cd ..
QEMU로 커널 실행
기본 실행
qemu-system-x86_64 \
-kernel arch/x86/boot/bzImage \
-initrd initramfs.img \
-append "console=ttyS0 nokaslr" \
-nographic
옵션 설명:
- -kernel: 부팅할 커널 이미지
- -initrd: 초기 램디스크
- -append: 커널 부트 파라미터
- console=ttyS0: 시리얼 콘솔 사용
- nokaslr: KASLR(커널 주소 랜덤화) 비활성화 (디버깅 용이)
- -nographic: 그래픽 창 없이 터미널에서 실행
네트워크 포함 실행
qemu-system-x86_64 \
-kernel arch/x86/boot/bzImage \
-initrd initramfs.img \
-append "console=ttyS0 nokaslr" \
-nographic \
-netdev user,id=net0 \
-device e1000,netdev=net0
네트워크가 추가되면 QEMU 안에서:
# 네트워크 인터페이스 확인
ip link
# 인터페이스 활성화
ip link set eth0 up
ip addr add 10.0.2.15/24 dev eth0
# 테스트
ping 10.0.2.2
QEMU 종료 방법
QEMU는 일반적인 Ctrl+C로 종료되지 않습니다. 다음 방법을 사용하세요:
방법 1: 키보드 단축키
Ctrl+A를 누른 후 X 입력
방법 2: QEMU 모니터
Ctrl+A를 누른 후 C 입력 (모니터 모드 진입)
(qemu) quit
기타 유용한 단축키:
- Ctrl+A H: 도움말
- Ctrl+A S: 화면 캡처
- Ctrl+A C: 모니터와 콘솔 간 전환
GDB 디버깅
GDB 서버 모드로 QEMU 실행
디버깅을 위해 QEMU를 GDB 서버 모드로 실행합니다.
qemu-system-x86_64 \
-kernel arch/x86/boot/bzImage \
-initrd initramfs.img \
-append "console=ttyS0 nokaslr" \
-nographic \
-s \ # GDB 서버를 포트 1234에서 시작
-S # 시작 시 CPU를 일시정지 (선택사항)
옵션 설명:
- -s: GDB 서버를 localhost:1234에서 시작
- -S: CPU를 일시정지 상태로 시작 (GDB 연결 후 수동으로 실행)
GDB 연결
새 터미널을 열어서:
cd ~/linux-6.10.8
gdb vmlinux
GDB에서:
# QEMU에 연결
(gdb) target remote :1234
Remote debugging using :1234
0x000000000000fff0 in ?? ()
# 브레이크포인트 설정
(gdb) break start_kernel
Breakpoint 1 at 0xffffffff82b4e320: file init/main.c, line 879.
# 실행 계속
(gdb) continue
Continuing.
Breakpoint 1, start_kernel () at init/main.c:879
879 {
TUI 모드로 소스 코드 보기
GDB의 TUI(Text User Interface) 모드를 사용하면 소스 코드를 보면서 디버깅할 수 있습니다.
# TUI 모드 활성화
(gdb) layout src # 소스 코드만
(gdb) layout split # 소스 + 어셈블리
(gdb) layout regs # 레지스터 + 소스
# TUI 모드 비활성화
(gdb) tui disable
# TUI 모드 토글
Ctrl+X A
TUI 모드 단축키:
- Ctrl+X A: TUI 모드 토글
- Ctrl+X 1: 단일 창
- Ctrl+X 2: 분할 창
- Ctrl+L: 화면 새로고침
- 화살표 키: 소스 코드 스크롤
브레이크포인트 설정
# 함수 이름으로 설정
(gdb) break start_kernel
(gdb) break do_fork
# 파일:라인번호로 설정
(gdb) break init/main.c:879
(gdb) break net/core/dev.c:4550
# 특정 조건에서만 멈춤
(gdb) break netif_receive_skb if skb->len > 1500
# 브레이크포인트 목록 확인
(gdb) info breakpoints
(gdb) info break
(gdb) i b # 축약형
# 브레이크포인트 삭제
(gdb) delete 1 # 1번 삭제
(gdb) delete 1 2 3 # 여러 개 삭제
(gdb) delete # 모두 삭제
# 브레이크포인트 비활성화/활성화
(gdb) disable 1 # 1번 비활성화
(gdb) enable 1 # 1번 활성화
실행 제어
# 계속 실행
(gdb) continue
(gdb) c # 축약형
# 한 줄 실행 (함수 내부로 들어가지 않음)
(gdb) next
(gdb) n # 축약형
# 한 줄 실행 (함수 내부로 들어감)
(gdb) step
(gdb) s # 축약형
# 현재 함수 끝까지 실행
(gdb) finish
(gdb) fin # 축약형
# 특정 라인까지 실행
(gdb) until 100 # 100번 라인까지
(gdb) u 100 # 축약형
변수 및 메모리 확인
# 변수 값 출력
(gdb) print cpu
(gdb) p cpu # 축약형
# 다양한 형식으로 출력
(gdb) print/x hash # 16진수
(gdb) print/d hash # 10진수
(gdb) print/t hash # 2진수
(gdb) print/c hash # 문자
# 포인터가 가리키는 값
(gdb) print *ptr
(gdb) print ptr->member
# 구조체 전체 출력
(gdb) print *skb
(gdb) print *(struct sk_buff *)skb
# 배열 출력
(gdb) print array[0]@10 # 10개 원소 출력
# 자동으로 값 표시 (매 스텝마다)
(gdb) display cpu
(gdb) display min_util
# 메모리 직접 확인
(gdb) x/10x 0xffff888000000000 # 16진수 10개
(gdb) x/s 0xffff888000000000 # 문자열
(gdb) x/10i $rip # 명령어 10개
스택 추적 및 프레임
# 콜 스택 확인
(gdb) backtrace
(gdb) bt # 축약형
(gdb) bt full # 로컬 변수 포함
# 특정 프레임으로 이동
(gdb) frame 0
(gdb) f 0 # 축약형
# 현재 프레임의 정보
(gdb) info frame
(gdb) info locals # 로컬 변수
(gdb) info args # 함수 인자
소스 코드 보기
# 현재 위치 주변 코드
(gdb) list
(gdb) l # 축약형
# 특정 함수
(gdb) list start_kernel
# 특정 파일:라인
(gdb) list init/main.c:100
# 라인 범위
(gdb) list 100,120
실전 디버깅 팁
예제 1: 네트워크 패킷 처리 함수 디버깅
커널에 커스텀 RPS(Receive Packet Steering) 함수를 추가했고, 이를 디버깅하려고 합니다.
// net/core/dev.c
static int custom_get_rps_cpu(struct net_device *dev, struct sk_buff *skb,
struct rps_dev_flow **rflowp)
{
struct rps_map *map;
int cpu = -1;
int min_cpu = -1;
unsigned long min_util = ~0UL;
map = rcu_dereference(dev->_rx->rps_map);
if (!map)
goto done;
for (int i = 0; i < map->len; i++) {
unsigned long util = my_cpu_util(map->cpus[i]);
printk(KERN_INFO "cpu = %d / util = %ld\n", map->cpus[i], util);
if (util < min_util) {
min_util = util;
min_cpu = map->cpus[i];
}
}
if (min_cpu != -1)
cpu = min_cpu;
done:
return cpu;
}
디버깅 세션:
# 터미널 1: QEMU 실행
qemu-system-x86_64 \
-kernel arch/x86/boot/bzImage \
-initrd initramfs.img \
-append "console=ttyS0 nokaslr" \
-nographic \
-netdev user,id=net0 -device e1000,netdev=net0 \
-s -S
# 터미널 2: GDB 연결
cd ~/linux-6.10.8
gdb vmlinux
(gdb) target remote :1234
# 브레이크포인트 설정
(gdb) break custom_get_rps_cpu
Breakpoint 1 at 0xffffffff81a2b3c0: file net/core/dev.c, line 4523.
# TUI 모드 활성화
(gdb) layout src
# 실행 시작
(gdb) continue
QEMU에서 네트워크 트래픽 생성:
/ # ip link set eth0 up
/ # ip addr add 10.0.2.15/24 dev eth0
/ # ping 10.0.2.2 -c 1
GDB에서 브레이크포인트 히트:
Breakpoint 1, custom_get_rps_cpu (dev=0xffff888003a00000, skb=0xffff888003b00000,
rflowp=0xffffc90000013e78) at net/core/dev.c:4523
# 변수 확인
(gdb) print dev->name
$1 = "eth0"
(gdb) print map->len
$2 = 4
# for 루프로 진입
(gdb) next
(gdb) next
# 각 iteration에서 값 확인
(gdb) print i
$3 = 0
(gdb) print map->cpus[i]
$4 = 0
(gdb) print util
$5 = 1234567
# 조건 변경 확인
(gdb) print min_util
$6 = 18446744073709551615
(gdb) continue
예제 2: 특정 코드 라인에 브레이크포인트
소스 코드의 특정 라인을 디버깅하고 싶을 때:
# 터미널에서 라인 번호 찾기
cd ~/linux-6.10.8
grep -n "ops->get_rps_cpu" net/core/dev.c
# 출력: 4550: cpu = ops->get_rps_cpu(skb->dev, skb, &rflow);
# GDB에서
(gdb) break net/core/dev.c:4550
Breakpoint 1 at 0xffffffff81a2b400: file net/core/dev.c, line 4550.
(gdb) continue

예제 3: 조건부 브레이크포인트
특정 조건에서만 멈추게 하기:
# 패킷 길이가 1500보다 클 때만
(gdb) break netif_receive_skb if skb->len > 1500
# CPU 번호가 2일 때만
(gdb) break custom_get_rps_cpu if cpu == 2
# 문자열 비교
(gdb) break net/core/dev.c:4550 if strcmp(dev->name, "eth0") == 0
예제 4: 워치포인트 (변수 값 변경 감지)
# 변수가 변경될 때 멈춤
(gdb) watch min_util
Hardware watchpoint 2: min_util
(gdb) continue
...
Hardware watchpoint 2: min_util
Old value = 18446744073709551615
New value = 1234567
커널 전용 GDB 명령어
Linux 커널에는 자체 GDB 스크립트가 포함되어 있습니다:
# 커널 스크립트 로드
(gdb) source scripts/gdb/vmlinux-gdb.py
# 프로세스 목록
(gdb) lx-ps
# 현재 태스크
(gdb) lx-task
# dmesg 출력
(gdb) lx-dmesg
# 커널 모듈 목록
(gdb) lx-lsmod
# 디바이스 목록
(gdb) lx-device-list-bus
자동 로드 설정:
echo "add-auto-load-safe-path ~/linux-6.10.8/scripts/gdb/vmlinux-gdb.py" >> ~/.gdbinit
GDB 설정 파일 (.gdbinit)
편의를 위한 설정:
cat > ~/.gdbinit << 'EOF'
# 페이징 비활성화 (긴 출력을 한번에 표시)
set pagination off
# 구조체를 보기 좋게 출력
set print pretty on
set print array on
# 배열 출력 제한 해제
set print elements 0
# 커널 소스 디렉토리
directory ~/linux-6.10.8
# 커널 GDB 스크립트 자동 로드
add-auto-load-safe-path ~/linux-6.10.8/scripts/gdb/vmlinux-gdb.py
# 유용한 alias
define psk
print *(struct sk_buff *)$arg0
end
define pdev
print *(struct net_device *)$arg0
end
define ptask
print *(struct task_struct *)$arg0
end
EOF
문제 해결
문제 1: "No symbol table is loaded"
증상:
(gdb) break start_kernel
No symbol table is loaded. Use the "file" command.
원인: vmlinux 파일을 로드하지 않음
해결:
(gdb) file vmlinux
Reading symbols from vmlinux...
(gdb) target remote :1234
또는 처음부터:
gdb vmlinux
(gdb) target remote :1234
문제 2: 소스 코드를 찾을 수 없음
증상:
(gdb) list
No source file for address 0xffffffff81a2b3c0.
해결:
(gdb) directory ~/linux-6.10.8
(gdb) directory ~/linux-6.10.8/net/core
또는 .gdbinit에 추가:
echo "directory ~/linux-6.10.8" >> ~/.gdbinit
문제 3: 브레이크포인트가 걸리지 않음
가능한 원인:
- 함수가 인라인됨
- # 함수가 존재하는지 확인 (gdb) info functions netif_receive_skb # 상위 함수에 브레이크포인트 시도 (gdb) break __netif_receive_skb_core
- 해당 코드 경로를 거치지 않음
- loopback 패킷은 특별한 경로를 탈 수 있음
- 여러 지점에 브레이크포인트를 걸어 확인:
(gdb) break netif_rx (gdb) break netif_receive_skb (gdb) break ip_rcv - 컴파일 최적화
- 디버그 빌드 확인: CONFIG_DEBUG_INFO=y
- 최적화 레벨 낮추기: CONFIG_CC_OPTIMIZE_FOR_SIZE=n
문제 4: QEMU가 너무 느림
해결 방법:
- KVM 사용 (호스트가 Linux인 경우)
- qemu-system-x86_64 -enable-kvm ...
- CPU 코어 수 줄이기
- qemu-system-x86_64 -smp 2 ...
- 메모리 늘리기
- qemu-system-x86_64 -m 2G ...
문제 5: 디버그 심볼이 없음
증상: 함수 이름이나 변수가 보이지 않음
확인:
# vmlinux에 디버그 정보가 있는지 확인
file vmlinux
# 출력에 "not stripped"가 있어야 함
readelf -S vmlinux | grep debug
# .debug_* 섹션들이 보여야 함
해결: 디버그 옵션을 활성화하고 재빌드
scripts/config --enable DEBUG_INFO
scripts/config --disable DEBUG_INFO_REDUCED
make -j$(nproc)
문제 6: GDB가 QEMU에 연결되지 않음
증상:
(gdb) target remote :1234
:1234: Connection refused.
확인:
- QEMU가 -s 옵션으로 실행 중인지 확인
- 포트가 사용 중인지 확인:
netstat -tlnp | grep 1234 - 다른 포트 사용:
# QEMUqemu-system-x86_64 -gdb tcp::5678 ...# GDB(gdb) target remote :5678
추가 리소스
QEMU 유용한 옵션
# 더 많은 메모리
-m 4G
# SMP (멀티코어)
-smp 4
# 시리얼 포트를 파일로 리다이렉트
-serial file:serial.log
# 그래픽 모드
-vga std # -nographic 대신
# 스냅샷 모드 (디스크 변경사항 저장 안함)
-snapshot
# USB 장치
-usb -device usb-tablet
GDB 치트시트
명령어 축약형 설명
| break | b | 브레이크포인트 설정 |
| continue | c | 실행 계속 |
| next | n | 다음 줄 (함수 내부로 안들어감) |
| step | s | 다음 줄 (함수 내부로 들어감) |
| finish | fin | 현재 함수 끝까지 |
| p | 변수 출력 | |
| backtrace | bt | 콜 스택 |
| info breakpoints | i b | 브레이크포인트 목록 |
| delete | d | 브레이크포인트 삭제 |
| watch | - | 변수 변경 감지 |
| list | l | 소스 코드 보기 |
| info locals | - | 로컬 변수 목록 |
| info args | - | 함수 인자 목록 |
| frame | f | 스택 프레임 선택 |
| up/down | - | 스택 프레임 이동 |
참고 자료
'Linux > 리눅스 커널 모듈' 카테고리의 다른 글
| 커널 패닉 원인 찾는 방법 (1) | 2025.12.09 |
|---|---|
| 커널 내부 흐름 엿보기 (0) | 2025.11.10 |
| Netfilter 이해하고 커널 모듈 만들기 (0) | 2025.11.10 |
| 최소 기능 모듈 만들기 (0) | 2025.11.10 |
| 개발 환경 설정 및 빌드 시스템 (0) | 2025.11.10 |