라즈베리파이 SD카드 부팅 복구와 커스텀 커널 재도전기 (연재 3/3)
라즈베리파이에 커스텀 커널을 올렸다가 부팅이 먹통이 되어버린 사건을 처음부터 끝까지 복구한 기록입니다. 커널 모듈 버전 불일치라는 원인을 찾아내고, SD카드를 Windows에서 포렌식하듯 뜯어보며 정상 커널을 되살린 다음, 결국 원래 하려던 커스텀 커널 배포까지 완주했습니다.
학습 주제
- 주제: 라즈베리파이 SD카드 부팅 실패 진단 및 커스텀 커널 재배포
- 날짜: 2026년 7월 17일
라즈베리파이에 직접 빌드한 6.12.x 커널을 올렸다가 SSH가 완전히 먹통이 되는 사고를 겪었습니다. 이전에 원인은 어느 정도 파악해뒀던 상태(모듈 없이 커널만 덮어써서 생긴 vermagic 불일치)였는데, 이번엔 그 진단을 실제 복구 작업으로 이어가야 했습니다.
탐구 과정
가장 먼저 막힌 지점은 "SD카드가 정말 죽은 건지, 아니면 부팅만 안 되는 건지"를 구분하는 것이었습니다. SD카드를 파이에서 빼서 Windows PC의 카드리더에 꽂고 파티션 구조부터 확인했습니다. Get-Disk, Get-Volume, Get-Partition으로 살펴보니 MBR 파티션 테이블은 멀쩡했고, 디스크 시그니처가 cmdline.txt의 PARTUUID와 정확히 일치해서 파티션 테이블 손상은 아니라는 걸 확인할 수 있었습니다.
그다음엔 정말로 뭐가 문제인지 파고들었습니다. kernel8.img와 dtb 파일이 사고 당일 새벽에 수정된 걸 확인했고, ARM64 Image 헤더를 직접 파싱해봤더니 파일이 자체 선언 크기보다 651.5KB 작다는 걸 발견했습니다. 이걸 보고 처음엔 "전송이 중간에 끊겨서 파일이 잘렸다"고 판단했는데, 나중에 이 판단이 틀렸다는 걸 알게 됩니다(이 부분은 뒤에서 다시 다룹니다).
여기서 진짜 막힌 부분은 ext4로 포맷된 루트 파티션이었습니다. Windows는 ext4를 직접 못 읽으니, 안에 있는 커널 모듈 버전 정보를 확인할 방법이 없었습니다. 우회 방법을 고민하다가, 손상되지 않은 initramfs8 파일 안에 lib/modules/<버전> 같은 문자열이 그대로 들어있다는 걸 떠올렸고, 이 파일을 바이너리로 grep해서 마운트 없이도 원래 커널 버전이 6.18.34+rpt-rpi-v8이라는 걸 역추적했습니다. 파일시스템을 마운트하지 않고도 필요한 정보를 뽑아낼 수 있다는 게 꽤 인상적이었습니다.
그래도 결국 ext4 파티션을 직접 열어봐야 하는 순간이 왔습니다. WSL2로 마운트를 시도했는데 wsl --mount가 계속 실패했습니다. 비관리자 권한 에러부터 시작해서, 관리자 권한으로 다시 시도해도 0x8007000f(드라이브를 찾을 수 없음) 에러가 반복됐습니다. USB 카드리더 특유의 제약으로 보였습니다. 이 과정에서 D 드라이브가 통째로 사라지는 해프닝도 있었는데, Set-Disk -IsOffline $false로 복구했습니다.
결국 wsl --mount를 포기하고 다른 길을 찾았습니다. usbipd-win이라는 도구를 설치해서 USB 장치 자체를 WSL로 전달하는 방식이었는데, 이건 파티션이 아니라 USB 컨트롤러 레벨에서 장치를 통째로 넘기는 방식이라 카드리더의 제약을 우회할 수 있었습니다. usbipd list로 BUSID를 확인하고 bind, attach로 연결하니 WSL 안에서 정상적으로 파티션이 잡혔습니다.
핵심 학습 내용
ARM64 Image 헤더의 image_size에 대한 오해와 정정
가장 솔직하게 기록해두고 싶은 부분입니다. 처음엔 커스텀 커널 파일이 헤더에 선언된 크기보다 작다는 걸 "전송 중 파일이 잘렸다"는 증거로 삼았습니다. 그런데 재배포 과정에서 같은 빌드 산출물이 매번 정확히 동일한 크기(27,709,952바이트)로 재현되는 걸 확인하면서, 이 판단이 틀렸다는 걸 알게 됐습니다.
ARM64 Image 헤더의 image_size 필드
= 디스크상의 파일 크기가 아니라
런타임에 필요한 유효 이미지 크기 (BSS 등 초기화 영역 포함)
즉 파일 크기가 헤더의 선언값보다 작은 건 정상일 수 있고, 곧바로 "손상/잘림"으로 단정하면 안 된다는 걸 배웠습니다. 결과적으로 취한 복구 조치(스톡 커널 정확한 버전 복원)는 옳았지만, 그 근거로 들었던 세부 증거 하나가 잘못된 해석이었던 셈입니다.
진짜 원인: vermagic 불일치
실제 부팅 실패의 원인은 처음부터 끝까지 하나였습니다.
커널 이미지(6.12.95)와 /lib/modules(6.18.34)의 버전이 안 맞음
→ 네트워크 드라이버 모듈을 찾지 못함
→ 네트워크 스택이 안 올라옴
→ SSH 응답 없음 (부팅 자체는 진행 중이었을 가능성)
커널 이미지만 교체하고 그에 맞는 모듈을 함께 넣지 않으면 이렇게 됩니다. 커널과 모듈은 반드시 짝을 맞춰서, 그것도 모듈을 먼저 반영해야 안전하다는 걸 확실히 알게 됐습니다.
정확한 버전의 공식 바이너리 확보하기
원래 스톡 커널로 롤백하려면 정확히 같은 버전을 구해야 했습니다. 방법은 이랬습니다.
# 1. 설치된 정확한 버전 확인
grep -A 15 "^Package: linux-image-..." /var/lib/dpkg/status
# 2. apt 저장소 인덱스에서 그 버전의 다운로드 경로/체크섬 확보
curl -s <repo>/Packages.gz -o Packages.gz
gunzip -c Packages.gz > Packages
grep -A 20 "^Package: ..." Packages | grep -E "^Version:|^Filename:|^SHA256:"
# 3. 다운로드 후 체크섬 검증
sha256sum <file>.deb
# 4. .deb는 ar 아카이브 포맷 -> 풀어서 필요한 파일만 추출
ar x <file>.deb
tar xf data.tar.xz boot/vmlinuz-... dtb/broadcom/... dtb/overlays/...
apt 최신 버전을 그냥 받으면 안 되고, dpkg status로 확인한 정확한 버전과 일치하는 걸 저장소 인덱스에서 찾아야 한다는 점이 핵심이었습니다.
심볼릭 링크와 scp -r의 함정
커스텀 커널 모듈을 다시 옮기는 과정에서 사고가 하나 있었습니다.
scp -r out/lib/modules/6.12.95-v8+ jcw@host:/tmp/new_modules
이렇게 통째로 옮기려다가 파이의 디스크가 순식간에 꽉 차버렸습니다. 원인은 make modules_install이 만드는 /lib/modules/<버전>/build가 커널 빌드 트리 전체(수 GB)를 가리키는 심볼릭 링크였는데, 고전적인 scp -r이 이 링크를 따라가서 실체를 통째로 복사해버린 것이었습니다. 실제 필요한 모듈+메타파일은 29MB밖에 안 됐는데 말이죠.
해결은 tar로 build 링크만 제외하고 압축하는 것이었습니다.
tar --exclude=6.12.95-v8+/build -czf modules.tar.gz lib/modules/6.12.95-v8+
이후 이 tar만 scp로 옮기고, 파이에서 풀어서 /lib/modules/에 넣고 depmod -a를 돌려서 모듈 의존성 데이터베이스를 재생성했습니다.
git clone --depth=1 --branch의 함정
애초에 참고했던 자료에는 커널 버전이 6.12.75로 적혀 있었는데, 실제 빌드된 건 6.12.95였습니다. git clone --depth=1 --branch <branch>는 그 시점의 브랜치 HEAD를 받아오는 것이기 때문에, 시간이 지나면 자료에 적힌 특정 버전과 달라질 수 있다는 걸 알게 됐습니다. 같은 6.12 계열이라 기능상 문제는 없었지만, 버전 문자열이 다르다고 당황할 필요는 없다는 걸 확인한 셈입니다.
이해한 내용
이번 복구 과정에서 새로 이해하게 된 것들을 정리하면:
- 마운트 없이 파일시스템 내부 정보 확인하기: initramfs 같은 바이너리 파일 안에도 경로 문자열이 그대로 들어있어서, grep만으로도 마운트 없이 필요한 정보를 뽑아낼 수 있다는 걸 처음 제대로 활용해봤습니다.
- wsl --mount의 한계와 usbipd-win이라는 대안: USB 카드리더처럼 특정 환경에서
wsl --mount가 막힐 때, USB 장치 자체를 전달하는 방식으로 우회할 수 있다는 걸 배웠습니다. - ARM64 부트 이미지 헤더 구조: 헤더에 있는
image_size가 파일 크기와 다른 의미를 가진다는 걸 직접 실수해보고서야 제대로 이해했습니다. 이런 저수준 포맷은 문서만 봐서는 체감이 잘 안 되는데, 실제로 틀려보니 확실히 남았습니다. - 심볼릭 링크와 원격 복사 도구의 상호작용:
scp -r이 심볼릭 링크를 얼마나 "성실하게" 따라가는지 몸으로 겪었습니다. 이후로는 대용량 디렉터리를 옮길 때 링크가 섞여 있는지부터 확인하는 습관이 생겼습니다.
실전 적용
이번에 정리한 방법들은 앞으로 임베디드 리눅스나 SBC(싱글보드컴퓨터) 다룰 때 계속 써먹을 수 있을 것 같습니다.
- 커널 업데이트/커스텀 빌드를 배포할 땐 커널 이미지 + 정확히 매칭되는 모듈을 항상 한 세트로 취급하고, 모듈부터 반영하는 순서를 지킬 것
- 부팅 안 되는 장치를 진단할 때, 파일시스템을 직접 마운트하기 전에 initramfs 같은 정적 파일 안의 문자열부터 grep해보는 걸 첫 단계로 삼을 것
- 대용량 디렉터리를 원격으로 옮기기 전에
du -sh --exclude=...나ls -la로 심볼릭 링크가 섞여있는지 미리 확인할 것 - 정확한 버전의 패키지가 필요할 때는
dpkg --status→ 저장소Packages.gz인덱스 대조 순서를 습관화할 것
다음에 비슷한 작업을 할 땐, 새 커널을 올리기 전에 롤백용 스톡 커널을 미리 기기 내부에도 백업해두는 절차를 아예 표준 루틴으로 만들어두려고 합니다. 이번엔 운 좋게 SD카드를 분리해서 복구할 수 있었지만, 물리적으로 접근이 어려운 원격 장치였다면 훨씬 곤란했을 겁니다.
추가 학습 계획
- ARM64 부트 프로토콜과 Image 헤더 포맷을 좀 더 정식으로 문서를 찾아 읽어볼 것 (커널 문서의
Documentation/arm64/booting.rst같은 자료) - initramfs의 실제 구조(cpio 아카이브, 압축 방식)를 제대로 파헤쳐서, grep 말고도 어떤 정보를 더 뽑아낼 수 있는지 확인해볼 것
- usbipd-win의 동작 원리(USB/IP 프로토콜)를 좀 더 깊이 살펴보고, WSL2의 하드웨어 접근 모델과 비교해볼 것
- vermagic 체크가 커널 내부적으로 정확히 어떤 방식으로 이루어지는지 소스 레벨에서 확인해볼 것
- dtoverlay 시스템과 dtb 파일들이 커널 부팅 과정에서 정확히 어떤 역할을 하는지 더 깊이 공부할 것