Computer >> 컴퓨터 >  >> 문제 해결 >> Android

Android 커널을 최신 리눅스 안정(Linux Stable) 버전으로 업데이트하는 방법

지금까지 '커스텀 커널 빌드 방법'이나 '안드로이드를 위한 최고의 커스텀 커널' 같은 안드로이드 커널 관련 가이드를 다뤄왔습니다. 오늘은 그 연장선에서, 자신의 안드로이드 커널을 최신 리눅스 안정(Linux Stable) 버전으로 업스트림(upstream)하는 방법을 알려드리겠습니다.

먼저 이 작업은 상당히 고급 난이도라는 점을 인지해야 합니다. 커널을 직접 컴파일해 본 경험이 없다면 앞서 언급한 '커스텀 커널 빌드 방법' 가이드를 먼저 따라 하는 것이 좋습니다. 이번 가이드에서는 최신 linux-stable 커널의 커밋들을 안드로이드 커널에 체리픽(cherry-pick)하거나 병합(merge)한 뒤 컴파일하는 과정을 다룹니다.

안드로이드 커널을 최신 리눅스 안정 버전으로 업스트림하면 최신 보안 패치와 버그 수정 사항을 그대로 받아올 수 있다는 큰 장점이 있습니다. 이 가이드 후반부에서 장단점을 더 자세히 설명하겠습니다.

리눅스 스테이블(Linux-Stable) 커널이란?

이름 그대로 linux-stable은 리눅스 커널의 '안정(stable)' 계열입니다. 반대쪽 계열은 '메인라인(mainline)', 즉 마스터 브랜치로 불립니다. 모든 리눅스 커널 개발은 메인라인에서 진행되며, 일반적으로 다음과 같은 흐름을 따릅니다.

  1. 리누스 토르발즈(Linus Torvalds)는 약 2주 동안 메인테이너들에게서 패치를 받아 모읍니다.
  2. 2주가 지나면 rc1 커널(예: 4.14-rc1)을 릴리즈합니다.
  3. 이후 6~8주 동안 매주 새로운 RC(예: 4.14-rc2, 4.14-rc3 등)를 릴리즈하며, 이 단계에는 오직 버그와 회귀(regression) 수정만 포함됩니다.
  4. 충분히 안정적이라고 판단되면 최종 버전이 tarball 형태로 kernel.org에 공개됩니다(예: 4.14).

LTS 커널이란?

매년 그렉 크로아-하트만(Greg Kroah-Hartman)은 특정 커널 하나를 골라 2년(LTS) 또는 6년(확장 LTS)간 유지보수합니다. LTS 커널은 안정성이 중요한 제품, 예컨대 안드로이드폰이나 각종 IoT 기기를 위해 설계된 것입니다. 릴리즈 과정 자체는 위에서 설명한 것과 동일하지만, 훨씬 긴 기간 동안 관리된다는 차이가 있습니다.

현재 존재하는 LTS 커널 목록(kernel.org 릴리즈 페이지에서 언제든 확인 가능)은 다음과 같습니다:

  • 4.14 (LTS) — Greg Kroah-Hartman 담당
  • 4.9 (LTS) — Greg Kroah-Hartman 담당
  • 4.4 (eLTS) — Greg Kroah-Hartman 담당
  • 4.1 (LTS) — Sasha Levin 담당
  • 3.16 (LTS) — Ben Hutchings 담당
  • 3.2 (LTS) — Ben Hutchings 담당

안드로이드 커널을 리눅스 스테이블로 업스트림하면 어떤 이점이 있나?

중요한 보안 취약점이 공개되거나 패치될 때, 안정 커널이 가장 먼저 해당 수정 사항을 반영합니다. 따라서 안드로이드 커널을 업스트림해 두면 각종 공격, 보안 결함, 일반적인 버그로부터 훨씬 안전해집니다.

리눅스 스테이블에는 내 안드로이드 기기가 사용하지 않는 드라이버 수정도 포함되어 있는데, 대부분 불필요한 것 아닌가?

'대부분'을 어떻게 정의하느냐에 따라 맞기도 하고 틀리기도 합니다. 리눅스 커널에는 안드로이드 시스템에서 실제로 사용되지 않는 코드가 많지만, 그렇다고 병합 시 해당 파일들에서 충돌이 절대 발생하지 않는다는 보장은 없습니다. 사실상 아무도 커널의 모든 부분을 빌드하지 않습니다. 우분투(Ubuntu)나 민트(Mint) 같은 대중적인 배포판조차 마찬가지입니다. 하지만 그렇다고 이런 수정 사항을 무시해서는 안 됩니다. 실제로 사용하고 있는 드라이버에 대한 수정도 분명히 포함되어 있기 때문입니다.

arm/arm64(가장 흔한 안드로이드 아키텍처)와 ext4(가장 흔한 안드로이드 파일시스템)를 예로 들어 보겠습니다. 4.4.78(오레오 CAF 태그 기준 최신 버전)부터 4.4.121(최신 업스트림 태그) 구간의 커밋 수는 다음과 같습니다.

nathan@flashbox ~/kernels/linux-stable (master) $ git log --format=%h v4.4.78..v4.4.121 | wc -l
2285
nathan@flashbox ~/kernels/linux-stable (master) $ git log --format=%h v4.4.78..v4.4.121 arch/arm | wc -l
58
nathan@flashbox ~/kernels/linux-stable (master) $ git log --format=%h v4.4.78..v4.4.121 arch/arm64 | wc -l
22
nathan@flashbox ~/kernels/linux-stable (master) $ git log --format=%h v4.4.78..v4.4.121 fs/ext4 | wc -l
18

가장 시간이 많이 걸리는 부분은 초기 bring-up 단계입니다. 한 번 최신 상태까지 끌어올리고 나면, 이후 새 릴리즈를 병합하는 데에는 거의 시간이 들지 않습니다. 새 릴리즈에는 보통 100개 남짓의 커밋만 포함되기 때문입니다. 이 과정이 가져다주는 이점(더 높은 안정성과 향상된 보안성)을 생각하면 충분히 할 만한 작업입니다.

리눅스 스테이블 커널을 안드로이드 커널에 병합하는 방법

우선 자신의 안드로이드 기기가 어떤 커널 버전을 사용 중인지 파악해야 합니다.

사소해 보이지만 시작 지점을 알아야 하므로 반드시 필요한 단계입니다. 커널 트리에서 다음 명령어를 실행하세요.

make kernelversion

현재 사용 중인 버전이 출력됩니다. 앞의 두 숫자는 필요한 브랜치를 결정하는 데 쓰이고(예: 4.4 계열이라면 linux-4.4.y), 마지막 숫자는 병합을 시작할 버전을 정하는 데 쓰입니다(예: 4.4.21을 사용 중이라면 다음은 4.4.22를 병합).

kernel.org에서 최신 커널 소스 가져오기

kernel.org는 최신 커널 소스를 linux-stable 저장소에서 호스팅합니다. 해당 페이지 하단에는 세 개의 fetch 링크가 있습니다. 필자의 경험상 구글 미러가 가장 빠르지만, 결과는 환경에 따라 다를 수 있습니다. 다음 명령어를 실행하세요.

git remote add linux-stable https://kernel.googlesource.com/pub/scm/linux/kernel/git/stable/linux-stable.git
git fetch linux-stable

전체 병합할 것인가, 체리픽할 것인가?

다음으로 커밋 전체를 병합(merge)할지, 아니면 체리픽(cherry-pick)할지 결정해야 합니다. 각 방식의 장단점과 적합한 상황은 다음과 같습니다.

참고: 커널 소스가 tarball 형태로 제공된다면 대부분 체리픽을 선택해야 합니다. 그렇지 않으면 수천 개의 파일 충돌이 발생합니다. git은 순수하게 업스트림 히스토리를 기반으로 동작하기 때문에 OEM이나 CAF가 변경한 내용을 알지 못하기 때문입니다. tarball 소스라면 바로 4단계로 넘어가세요.

체리픽(Cherry-picking)

장점:

  • 충돌 해결이 쉽습니다. 어떤 커밋이 문제를 일으키는지 정확히 알 수 있기 때문입니다.
  • 각 커밋이 독립적이므로 rebase가 쉽습니다.
  • 문제가 발생했을 때 bisect로 원인 추적이 용이합니다.

단점:

  • 모든 커밋을 하나씩 골라야 하므로 시간이 더 걸립니다.
  • 커밋이 업스트림에서 온 것인지 한눈에 파악하기가 다소 어렵습니다.

병합(Merge)

장점:

  • 클린 패치들이 병합되기를 기다릴 필요가 없어 속도가 빠릅니다.
  • 커밋의 커mitter가 본인이 아니라 업스트림 메인테이너로 표시되므로, 업스트림에서 온 커밋임을 확인하기 쉽습니다.

단점:

  • 충돌 해결이 다소 어려울 수 있습니다. git log/git blame으로 어떤 커밋이 충돌을 일으키는지 직접 찾아야 하며, 도구가 알려주지 않습니다.
  • 병합 커밋은 rebase할 수 없어 rebase가 어렵습니다. rebase를 시도하면 모든 커밋을 개별적으로 체리픽하겠다고 제안합니다. 다만 rebase는 자주 해서는 안 되며, 가능한 한 git revert와 git merge를 사용하는 것이 좋습니다.

필자의 추천 방법은 처음에는 체리픽으로 문제가 되는 충돌을 파악하고, 이후 병합(merge)으로 전환한 뒤 문제가 되는 커밋만 revert하는 것입니다. 최신 상태를 유지하고 나면 병합이 훨씬 빠르기 때문에 업데이트가 수월해집니다.

커밋을 소스에 추가할 때는 반드시 한 버전씩 진행하세요

이 과정에서 가장 중요한 것은 '한 번에 한 버전씩' 진행한다는 원칙입니다. 업스트림 커밋 시리즈 중에는 부팅 실패를 일으키거나 사운드, 충전 같은 기능을 망가뜨릴 수 있는 문제 패치가 숨어 있을 수 있습니다(팁 섹션에서 설명). 이 때문에 증분(incremental) 버전 업그레이드가 중요합니다. 50개 커밋에서 문제를 찾는 것이 2000개가 넘는 커밋에서 찾는 것보다 훨씬 쉽기 때문입니다. 모든 문제 커밋과 충돌 해결 방법을 파악한 후에야 전체 병합을 권장합니다.

체리픽 실행 방법

형식:

git cherry-pick <previous_tag>..<next_tag>

예시:

git cherry-pick v3.10.73..v3.10.74

병합 실행 방법

형식:

git merge <next_tag>

예시:

git merge v3.10.74

병합 커밋에서는 # 주석 표시를 제거하여 충돌 내역을 기록해 두는 것을 권장합니다.

충돌 해결 방법

모든 충돌에 대한 단계별 해결 가이드를 제공할 수는 없습니다. C 언어에 대한 깊은 이해가 필요하기 때문입니다. 하지만 몇 가지 유용한 힌트를 드리겠습니다.

병합 중이라면 먼저 어떤 커밋이 충돌을 일으키는지 파악하세요. 두 가지 방법이 있습니다.

  1. git log -p v$(make kernelversion)..<latest_tag>를 실행하면 현재 버전과 업스트림 최신 버전 사이의 변경 사항을 확인할 수 있습니다. -p 플래그를 사용하면 커밋별 변경 내용이 함께 출력됩니다.
  2. 해당 파일에 git blame을 실행해 영역별 커밋 해시를 확인한 뒤, git show --format=fuller로 커mitter가 메인라인/스테이블 쪽인지, 구글인지, CodeAurora(CAF)인지 확인할 수 있습니다.
  • 이미 해당 커밋을 가지고 있는지 확인하세요. 구글이나 CAF 같은 벤더는 Dirty COW 수정처럼 치명적인 버그를 업스트림에서 찾아 백포트(backport)하는 경우가 있는데, 이 백포트가 업스트림 커밋과 충돌할 수 있습니다. git log --grep="<part_of_commit_message>"로 검색해 보면 됩니다. 이미 있다면 해당 커밋을 건너뛰면 됩니다(체리픽 중이라면 git reset --hard && git cherry-pick --continue). 또는 충돌 마커(<<<<<<<부터 ====== 사이, >>>>>>>까지)를 삭제하고 무시할 수도 있습니다.
  • 백포트가 해결을 방해하는지 확인하세요. 구글과 CAF는 스테이블이 백포트하지 않는 특정 패치를 백포트하는 경우가 있습니다. 스테이블은 메인라인 커밋의 해결 방식을, 구글이 백포트하지 않은 패치가 없는 상황에 맞게 조정해야 하는 경우가 많습니다. git show <hash>로 메인라인 커밋을 확인할 수 있습니다(메인라인 해시는 스테이블 커밋 메시지에 포함되어 있습니다). 백포트가 문제를 일으킨다면 변경 사항을 버리거나 메인라인 버전을 사용하면 됩니다(대부분 후자가 필요합니다).
  • 커밋이 의도한 바를 읽고 이미 수정되었는지 확인하세요. 때로 CAF가 업스트림과 독립적으로 버그를 수정하는 경우가 있습니다. 이 경우 위와 마찬가지로 CAF의 수정을 덮어쓰거나 버리면 됩니다.

그 외의 경우라면 단순히 CAF/구글/OEM이 추가한 코드 때문일 가능성이 높으니, 코드 위치를 적절히 조정하면 됩니다.

GitHub에는 linux-stable kernel.org 저장소의 미러가 있어, 커밋 목록과 diff를 조회하며 충돌을 해결하기에 더 편리합니다. 커밋 목록 화면에서 문제가 되는 커밋을 찾아 원본 diff를 확인하고 자신의 코드와 비교하는 것을 권장합니다.

예시 URL: https://github.com/nathanchance/linux-stable/commits/linux-3.10.y/arch/arm64/mm/mmu.c

물론 명령줄에서도 동일한 작업이 가능합니다.

git log <current_version>..<version_being_added> <path>
git show <hash>

충돌 해결은 결국 맥락(context)의 문제입니다. 항상 해야 할 일은 최종 diff가 업스트림과 일치하는지 확인하는 것입니다. 두 개의 터미널 창을 열고 다음 명령어를 각각 실행해 비교하세요.

git diff HEAD
git diff v$(make kernelversion)..$(git tag --sort=-taggerdate -l v$(make kernelversion | cut -d . -f 1,2)* | head -n1)

rerere 기능 활성화하기

Git에는 rerere(Reuse Recorded Resolution, 기록된 해결 재사용)라는 기능이 있습니다. 충돌을 감지하면 해결 방법을 기록해 두었다가 나중에 동일한 충돌이 발생하면 자동으로 재사용하는 것입니다. 병합과 체리픽을 반복하는 개발자에게 특히 유용합니다. 업스트림 bring-up을 다시 할 때 git add . && git <merge|cherry-pick> --continue만 실행하면 이전에 해결했던 방식 그대로 충돌이 처리되기 때문입니다.

커널 저장소에서 다음 명령어로 활성화할 수 있습니다.

git config rerere.enabled true

컴파일 또는 런타임 에러 발생 시 git bisect 활용법

수많은 커밋을 추가하는 작업이다 보니 컴파일 에러나 런타임 에러가 생길 가능성이 충분히 있습니다. 포기하기 전에 git의 내장 bisect 도구를 사용해 문제의 근본 원인을 찾아보세요! 이상적으로는 커밋을 추가할 때마다 빌드와 플래싱을 수행하는 것이 좋습니다. 그러면 bisect가 필요할 때 시간이 크게 줄어들지만, 5000개 커밋이라도 문제없이 bisect할 수 있습니다.

git bisect는 문제가 존재하는 지점과 존재하지 않던 지점 사이의 커밋 범위를 잡은 뒤, 범위를 절반씩 줄여가며 빌드·테스트 결과(정상/비정상)를 입력받습니다. 이 과정을 반복해 문제를 일으키는 커밋을 찾아내면, 해당 커밋을 수정하거나 revert하면 됩니다.

  1. bisect 시작: git bisect start
  2. 현재 리비전을 bad로 표시: git bisect bad
  3. 정상이었던 리비전을 good으로 표시: git bisect good <sha1>
  4. 새 리비전으로 빌드
  5. 결과에 따라 git에 알림: git bisect good 또는 git bisect bad
  6. 문제 커밋이 발견될 때까지 4~5단계 반복!
  7. 문제 커밋을 revert하거나 수정

참고: 병합 방식을 사용하는 경우, 올바른 bisect를 위해 임시로 git rebase -i <good_sha>를 실행해 모든 패치를 브랜치에 적용해야 합니다. 병합 커밋이 그대로 있는 상태에서 bisect하면 업스트림 커밋으로 checkout되어 안드로이드 고유 커밋이 빠진 상태가 되기 때문입니다. 요청이 있다면 이 부분을 더 깊이 설명할 수 있지만, 일단 필요한 과정이라고 믿어 주세요. 문제 커밋을 찾은 후에는 revert하거나 병합에 다시 반영하면 됩니다.

업스트림 업데이트를 스쿼시(squash)하지 마세요

많은 초보 개발자가 '더 깔끔하고' '관리하기 쉽다'는 이유로 업데이트를 스쿼시하고 싶어 합니다. 그러나 이는 여러 면에서 좋지 않습니다.

  • 작성자 정보가 사라집니다. 다른 개발자들의 공로를 지우는 것은 공정하지 않습니다.
  • bisect가 불가능해집니다. 커밋 시리즈를 스쿼시한 뒤 그 안에 문제가 있다면, 어떤 커밋이 원인인지 알 수 없습니다.
  • 향후 체리픽이 어려워집니다. 스쿼시된 시리즈로 rebase해야 한다면 충돌의 출발점을 파악하기 어렵거나 불가능합니다.

적시에 업데이트 소식을 받으려면 리눅스 커널 메일링 리스트 구독

업스트림 업데이트가 있을 때마다 알림을 받으려면 linux-kernel-announce 메일링 리스트를 구독하세요. 새 커널이 릴리즈될 때마다 이메일을 받을 수 있어, 최대한 빠르게 업데이트하고 푸시할 수 있습니다.