← 글 목록

라즈베리파이 커널 직접 빌드하고 이식하기 (feat. 파이 벽돌 사건) (연재 2/3)

/ 10분 분량

임베디드 리눅스 스터디 1주차 과제로 라즈베리파이 커널을 직접 크로스 컴파일해서 보드에 올려봤다. 결과부터 말하면 빌드는 성공했는데 파이가 먹통이 되는 사고를 쳤고, 그 원인을 추적하는 과정에서 커널-모듈 관계를 훨씬 제대로 이해하게 됐다.

학습 주제

  • 주제: 라즈베리파이 4용 리눅스 커널 크로스 컴파일 및 보드 이식
  • 날짜: 2026년 7월 17일
  • 배경: 1주차 과제 절차는 (1) RPI OS 설치 → (2) SSH 연결 → (3) 커널 소스 다운로드 → (4) 커널 빌드 후 라즈베리파이에 올려서 동작 확인, 이렇게 네 단계였다. 이번엔 3번과 4번을 실제로 해내는 게 목표였다.

탐구 과정

어디서 뭘 받아야 하는지부터 막혔다

커널 소스코드를 어느 컴퓨터에서 받아야 하는지가 첫 관문이었다. 라즈베리파이에서 직접 컴파일하지 말고 빌드 머신(개인 PC)에서 크로스 컴파일 후 이미지만 넘기라는 게 과제 방침이었다. 그리고 버전은 현재 RPI OS 기본 커널이 6.18이더라도 스터디 기준은 6.12.75였다. 즉 내 파이는 6.18인데 빌드는 6.12.75로 해야 하는 상황이었고, 이 버전 차이가 나중에 사고의 핵심 원인이 될 줄은 몰랐다.

크로스 컴파일을 해야 하니 윈도우가 아니라 WSL(우분투) 안에서 작업해야 한다는 것도 이 단계에서 확인했다.

git clone --depth=1 --branch rpi-6.12.y https://github.com/raspberrypi/linux.git

CMD, PowerShell, WSL을 헷갈려서 삽질

git clone을 했더니 error: invalid path 'drivers/gpu/drm/nouveau/nvkm/subdev/i2c/aux.c'라는 에러가 났다. 알고 보니 지금 터미널이 PowerShell도 WSL도 아니고 그냥 윈도우 명령 프롬프트(CMD) 상태였다. aux라는 이름이 윈도우 예약어와 충돌해서 생긴 문제였다. ls, rm 같은 명령어가 하나도 안 먹혀서 한참 헤맸는데, 결국 wsl 명령어로 리눅스 환경에 제대로 들어가서 클론하니 깔끔하게 해결됐다.

여기서 배운 것: CMD / PowerShell / WSL은 프롬프트 모양이 다르고, 각자 명령어 문법이 완전히 다르다. master@DESKTOP... 형태면 CMD, PS C:\>면 파워쉘, user@computername:~$면 WSL 리눅스라는 걸 구분하는 습관이 필요했다.

.bashrc에 함수 추가하다 문법 에러

소스코드 네비게이션용으로 함수 찾기(ff), 구조체 찾기(fs), 매크로 찾기(fm) 스크립트를 .bashrc에 추가했는데, 복사-붙여넣기 과정에서 줄바꿈이 깨져서 syntax error near unexpected token 'newline' 에러가 났다. 원인은 파일 상단의 case {after_lines:-10} 같은 변수 기본값 문법이 깨져서 rg에 빈 문자열이 전달되는 에러가 반복됐다. 결국 함수 전체를 깨끗하게 다시 붙여넣고 나서야 ff printk가 정상적으로 커널 소스 안의 printk 정의를 찾아줬다.

빌드 스크립트 실행, 그리고 처음 겪는 진짜 에러들

빌드 스크립트를 처음 돌렸을 때는 flex: not found, aarch64-linux-gnu-gcc: Not a directory 같은 에러가 떴다. 필수 빌드 도구와 크로스 컴파일러가 아예 설치되어 있지 않았던 것.

sudo apt update && sudo apt install -y bc bison flex libssl-dev make libc6-dev libncurses5-dev crossbuild-essential-arm64

이걸 설치하고 나니 빌드가 제대로 굴러갔다. 결과물을 scp로 라즈베리파이에 전송하는 것까지는 무사히 끝났다.

압축 풀다가 폴더인 줄 알았던 게 파일이었던 사건

라즈베리파이에서 cd linux를 했더니 Not a directory 에러가 났다. ls -l로 확인해보니 linux라는 이름이 폴더가 아니라 방금 받은 압축 파일이었다. ls 결과에서 파일과 폴더 색이 비슷해서 헷갈렸던 것. 확장자를 붙여서 옮기고 tar -xzvf로 풀어서 해결했다.

진짜 사고: 부팅 파일까지 올리고 재부팅했는데 파이가 죽었다

kernel8.img, .dtb, 오버레이 파일들을 /boot/firmware/에 복사하고 sudo reboot을 쳤는데, 그대로 SSH 연결이 끊기고 다시는 접속이 안 됐다. Connection timed out만 반복. 이 시점에서 정말 멘탈이 흔들렸다.

핵심 학습 내용

뭐가 문제였나 — 모듈 누락

압축 파일(img_for_rpi.tar.gz) 안을 열어봤더니 boot 폴더만 있고 lib/modules가 있어야 할 자리가 통째로 비어 있었다. 즉:

  • **부팅 핵심 파일(커널 이미지, DTB)**은 정상적으로 /boot/firmware/에 들어갔다.
  • **커널 모듈(드라이버 파일들, /lib/modules/)**은 아예 파이로 전송되지 않았다.

리눅스 커널은 부팅할 때 자기 버전과 정확히 일치하는 /lib/modules/[버전]/ 폴더를 찾는다. 새 커널(6.12.75)은 떴는데 그에 맞는 모듈 폴더가 없으니, 네트워크 드라이버조차 못 올려서 SSH 접속 자체가 끊긴 것이었다.

왜 모듈이 빠졌나 — 빌드 스크립트의 경로 설정 오류

원래 빌드 스크립트는 모듈을 ${KERNEL_TOP_PATH}/out/ 아래에 설치하도록 되어 있었는데, 정작 압축은 img_for_rpi 폴더만 대상으로 했다.

# 문제였던 부분
MODULE_PATH="${KERNEL_TOP_PATH}/out/"          # 압축 대상 폴더 밖에 모듈이 생김
...
tar -czvf img_for_rpi.tar.gz img_for_rpi        # img_for_rpi만 압축, out은 누락

해결책은 모듈 설치 경로 자체를 압축할 폴더 안으로 넣는 것이었다.

# 수정본
MODULE_PATH="${KERNEL_TOP_PATH}/img_for_rpi/out"
make O={MODULE_PATH} modules_install -j15

추가로 cp $OUTPUT/arch/arm64/boot/dts/broadcom/.dtb ... 부분도 별표(*)가 빠진 오타였다. .dtb라는 이름의 숨김 파일을 찾으려 한 셈이라 정상적으로 파일이 복사되지 않았을 가능성이 있었다.

버전 불일치라는 근본 원인

더 근본적으로는, 내 파이의 실행 중인 OS 커널은 6.18인데 새로 빌드한 건 6.12.75였다는 점이 핵심이었다. 기존 6.18용 모듈을 그대로 두고 6.12.75 커널 이미지만 얹으려고 했던 것 자체가 애초에 성립할 수 없는 조합이었다. 리눅스는 커널 버전과 모듈 버전이 완전히 일치하지 않으면 드라이버 로드를 거부한다. 모듈 파일이 누락된 게 아니라 있었어도, 버전이 다르면 어차피 무용지물이었다는 뜻이다.

빌드 스크립트 전체 흐름 정리

[호스트 PC]                              [라즈베리파이]
1. 빌드 (Image, DTB, Modules)
2. 패키징 (한 폴더에 모아 tar.gz)  --전송-->  3. 배포
                                             - /boot/firmware/ 에 커널+DTB
                                             - /lib/modules/ 에 모듈
                                             - 재부팅

이렇게 3단계로 보니 어디서 무엇이 빠졌는지 훨씬 명확하게 보였다. 커널 이미지만 뇌라면, 모듈은 그 뇌와 연결된 신경망이라 둘 중 하나만 바꾸면 시스템이 아예 작동하지 않는다는 비유가 와닿았다.

이해한 내용

  • 크로스 컴파일의 목적: 라즈베리파이 본체는 느리니까, 개인 PC(WSL)에서 대신 컴파일하고 결과물만 옮기는 방식. ARCH=arm64, CROSS_COMPILE=aarch64-linux-gnu- 환경변수로 타겟 아키텍처를 지정한다.
  • INSTALL_MOD_PATH의 역할: 이 값이 시스템 경로(/lib/modules)가 아니라 임시 폴더를 가리키게 하면, 모듈을 실제 타겟 시스템이 아니라 패키징용 폴더에 모을 수 있다. 이걸 빼먹으면 모듈이 압축 대상 밖에 생겨서 통째로 누락된다.
  • 커널 버전과 모듈 버전은 완전히 일치해야 한다: 커널이 부팅 시 자기 버전 폴더(/lib/modules/[정확한 버전]/)를 찾는데, 하나라도 다르면 드라이버를 아예 로드하지 않는다.
  • 파이가 먹통이 됐다고 SD 카드를 다시 구울 필요는 없다: 문제는 부팅에 필요한 파일 몇 개일 뿐이고, 나머지 시스템과 개인 데이터는 멀쩡하다. SD 카드를 PC에 연결해서 문제가 된 파일만 원래대로 되돌리면 복구된다.
  • 터미널 환경 구분: CMD, PowerShell, WSL은 서로 다른 셸이고 명령어 문법도 다르다. 프롬프트 모양만 보고도 지금 내가 어떤 환경에 있는지 구분하는 습관이 생겼다.

실전 적용

  • 다음 빌드부터는 기존 커널을 백업해두고 (kernel8.img.bak), config.txt에 새 커널을 다른 이름으로 등록해서 멀티 부팅 테스트를 하는 방식으로 안전하게 진행할 계획이다.
  • 빌드 스크립트를 짤 때 패키징 대상 폴더 하나를 미리 정해두고, 모든 산출물(Image, DTB, modules)의 설치 경로를 그 폴더 기준으로 통일하는 습관을 들이려 한다.
  • 앞으로 커널 옵션을 하나씩 바꿔가며 재빌드/재이식하는 연습을 반복해서, 이번에 겪은 사고를 자연스러운 루틴으로 만들어보고 싶다.

추가 학습 계획

  • make modules_install이 내부적으로 심볼릭 링크(build, source)를 어떻게 만드는지, 이게 타겟 보드에서 왜 깨진 링크가 되는지 좀 더 파보고 싶다.
  • 커널 버전 체크(vermagic) 메커니즘이 정확히 어떤 방식으로 동작하는지 커널 소스 레벨에서 확인해보는 것.
  • bcm2711_defconfig처럼 라즈베리파이 모델별 defconfig가 실제로 어떤 옵션들을 켜고 끄는지 비교 분석.
  • 다음 주차 과제를 위해 디바이스 트리 오버레이(.dtbo) 구조를 조금 더 깊게 공부할 예정.