Skip to content

필사 모드: 커널 소스에서 길 찾기 — clone부터 "이 동작을 하는 코드"를 찾아내기까지

한국어
0%
정확도 0%
💡 왼쪽 원문을 읽으면서 오른쪽에 따라 써보세요. Tab 키로 힌트를 받을 수 있습니다.

들어가며 — 이 글은 "읽는 법"이 아니라 "찾는 법"입니다

1편에서 커널이 4,300만 줄이고 그중 60퍼센트가 드라이버라는 것을 확인했습니다. 그 글의 결론은 커널은 순서대로 읽는 대상이 아니라 질문을 들고 들어가 필요한 파일만 여는 대상이라는 것이었습니다.

이 글은 그 "필요한 파일만 여는" 부분을 다룹니다. 구체적으로는 이런 상황입니다. 어떤 동작을 눈으로 봤습니다. 예를 들어 read 시스템 콜이 잘못된 파일 디스크립터에 대해 EBADF를 돌려주는 것을 봤습니다. 이제 그 EBADF를 실제로 반환하는 코드 줄을 찾아야 합니다. 이 글이 끝나면 그 작업이 몇 분짜리 일이 됩니다.

앞서 패킷의 여정 같은 글은 커널 안의 경로를 이미 정리된 형태로 보여 줍니다. 남이 정리해 준 경로를 읽는 것과, 아무도 정리해 주지 않은 경로를 스스로 찾아내는 것은 다른 능력입니다. 이 글은 후자입니다.

1. 어떤 트리를 받을 것인가

kernel.org의 릴리스 표 읽기

2026년 8월 19일에 kernel.org 첫 화면이 표시한 내용은 이랬습니다.

계열버전날짜
mainline7.22026-08-16
stable7.1.82026-08-09
longterm6.18.442026-08-09
longterm6.12.1032026-08-09
longterm6.6.1512026-08-09
longterm6.1.1822026-08-07
longterm5.15.2152026-08-07
longterm5.10.2642026-08-07
linux-nextnext-202608172026-08-17

세 계열의 성격이 다릅니다.

  • mainline은 리누스 토발즈의 트리입니다. 새 기능이 병합되는 곳이고, 여기 없는 코드는 어디에도 없습니다. 최신 구조를 보고 싶다면 여기입니다.
  • stable은 방금 나온 메인라인 릴리스에 버그 수정만 얹는 계열입니다. 세 번째 숫자가 올라갑니다.
  • longterm은 stable을 수년간 유지하는 계열입니다. 표에서 보듯 5.10 계열은 264번째 패치 릴리스까지 왔습니다. 배포판 커널은 대개 이 계열의 어느 지점에서 갈라져 나옵니다.
  • linux-next는 다음 병합 창구에 들어갈 후보들을 미리 합쳐 놓은 통합 트리입니다. 날짜가 붙은 것에서 알 수 있듯 매일 다시 만들어집니다. 읽기용이 아니라 충돌 검사용입니다.

어느 것을 받아야 하는가. 판단 기준은 하나입니다. 지금 눈앞의 서버에서 본 동작을 설명하려는 것인가, 아니면 커널이 어떻게 생겼는지 배우려는 것인가. 앞이면 그 서버의 uname -r이 가리키는 계열을, 뒤면 메인라인이나 최신 longterm을 받으면 됩니다. 이 시리즈의 코드 인용은 전부 메인라인 기준입니다.

실제로 받기

두 개의 대표적인 주소가 있습니다.

# 메인라인 (리누스 트리)
git clone https://git.kernel.org/pub/scm/linux/kernel/git/torvalds/linux.git

# stable/longterm 태그까지 포함하는 트리
git clone https://git.kernel.org/pub/scm/linux/kernel/git/stable/linux.git

커널 공식 문서인 Documentation/admin-guide/quickly-build-trimmed-linux.rst는 두 번째 쪽을 권합니다. 이 문서가 제시하는 형태는 이렇습니다.

git clone --depth 1 -b master \
  https://git.kernel.org/pub/scm/linux/kernel/git/stable/linux.git ~/linux/
cd ~/linux/

stable 트리를 권하는 이유는 이 트리 하나로 메인라인과 모든 stable/longterm 태그를 오갈 수 있기 때문입니다. 나중에 "6.12에서는 이 함수가 어땠지"를 확인하고 싶어질 때, 트리를 새로 받을 필요가 없습니다.

얕은 복제에 대해 한 가지 경고. 위 명령의 depth 옵션은 최근 커밋 하나만 받아 옵니다. 빌드만 할 목적이라면 훌륭한 선택이고 내려받는 양이 크게 줍니다. 하지만 코드를 읽을 목적이라면 저는 권하지 않습니다. 커널을 읽다 보면 반드시 "이 조건문은 왜 여기 있는가"라는 질문이 생기고, 그 답은 거의 항상 커밋 메시지에 있습니다. 얕은 복제에서는 그 조회가 불가능합니다.

전체 이력이 부담스럽지만 이력 조회는 하고 싶다면 절충안이 있습니다.

# 커밋 이력은 전부 받되 과거 파일 내용은 필요할 때 받아 오는 방식
git clone --filter=blob:none \
  https://git.kernel.org/pub/scm/linux/kernel/git/stable/linux.git

태그와 브랜치 표기

받고 나면 태그 이름의 규칙이 눈에 들어옵니다.

git tag -l 'v6.12*' | head          # v6.12, v6.12.1, v6.12.2 ...
git tag -l 'v7.2-rc*'               # 개발 주기 중의 후보 릴리스
git checkout v6.12                  # 특정 릴리스 시점의 트리로 이동
git log --oneline -1                # 지금 어디에 있는지 확인

v6.12처럼 숫자가 둘이면 메인라인 릴리스이고, v6.12.103처럼 셋이면 그 계열의 패치 릴리스입니다. -rc가 붙은 것은 개발 중인 다음 릴리스의 후보입니다.

트리를 오갈 때 잊기 쉬운 것이 하나 있습니다. 태그를 바꾸면 코드는 바뀌지만 빌드 산출물과 인덱스는 그대로 남습니다. 태그를 옮긴 뒤에는 인덱스를 다시 만들어야 합니다. 4절에서 다시 이야기합니다.

2. 디렉터리를 지도로 쓰기

1편에서 최상위 디렉터리 목록을 봤습니다. 찾기의 관점에서 다시 정리하면, 중요한 것은 각 디렉터리에 무엇이 들어 있는가보다 어떤 질문이 어느 디렉터리로 가는가입니다.

질문의 형태먼저 볼 곳
이 시스템 콜은 무엇을 하는가fs/, kernel/, mm/, net/ 중 해당 서브시스템
시스템 콜 번호가 어디에 정의돼 있는가arch/x86/entry/syscalls/, scripts/syscall.tbl
이 구조체의 필드가 무슨 뜻인가include/linux/
사용자 공간과 공유하는 상수는 어디 있는가include/uapi/
이 CONFIG 옵션은 무엇을 켜는가각 서브시스템의 Kconfig 파일
이 기능의 공식 설명은Documentation/
이 동작이 어떻게 테스트되는가tools/testing/selftests/
예제 코드가 있는가samples/

특히 마지막 두 줄이 저평가되어 있다고 생각합니다. selftests에는 각 기능이 무엇을 보장해야 하는지가 실행 가능한 형태로 적혀 있고, 이는 문서보다 정확합니다.

Kconfig 파일도 마찬가지입니다. 어떤 CONFIG 옵션이 무엇인지 궁금하면 문서를 찾기 전에 정의를 찾는 편이 빠릅니다.

# CONFIG_DEBUG_ATOMIC_SLEEP이 무엇이고 무엇에 의존하는지
grep -rn --include='Kconfig*' '^config DEBUG_ATOMIC_SLEEP' -A 12 .

3. 찾기의 기본기 — grep과 ripgrep

실제로 쓰는 패턴들

커널 트리에서 grep -r을 아무 옵션 없이 돌리면 .git 디렉터리와 빌드 산출물까지 뒤져서 느립니다. 실전에서 쓰는 형태는 이렇습니다.

# C 소스와 헤더만, 줄 번호와 함께
grep -rn --include='*.c' --include='*.h' 'vfs_read' .

# 함수 정의처럼 보이는 것만 (줄 시작에 반환형이 오는 형태)
grep -rn --include='*.c' -E '^[a-z_]+ \*?vfs_read\(' .

# 특정 디렉터리로 좁히기 — 이게 가장 큰 차이를 만듭니다
grep -rn 'rw_verify_area' fs/

ripgrep이 설치돼 있다면 훨씬 편합니다. 기본적으로 .gitignore를 존중해서 빌드 산출물을 알아서 건너뜁니다.

rg -n 'SYSCALL_DEFINE3\(read'           # 정규식, 줄 번호
rg -t c -n 'copy_from_user' fs/         # C 파일만
rg -n --stats 'GFP_ATOMIC' mm/          # 몇 건 걸렸는지까지
rg -n -w 'kmalloc'                      # 단어 경계 — kmalloc_node는 제외

마지막 옵션이 커널에서 특히 유용합니다. 커널 함수 이름은 접미사가 붙은 변종이 많아서(같은 뿌리에 _node, _noprof, _bulk 등이 붙습니다) 단어 경계를 주지 않으면 결과가 몇 배로 늘어납니다.

순진한 grep이 실패하는 순간

여기가 이 글에서 가장 중요한 부분입니다.

read 시스템 콜의 구현을 찾겠다고 생각해 봅시다. 시스템 콜 테이블을 먼저 보면 진입점 이름이 sys_read라고 나와 있습니다. 그래서 이렇게 찾습니다.

grep -rn --include='*.c' 'sys_read' .

호출하거나 언급하는 곳은 잔뜩 나오는데 정의처럼 보이는 줄이 없습니다. 커널을 처음 읽는 사람이 거의 예외 없이 부딪히는 벽입니다.

이유는 그 함수가 소스에 그 이름으로 쓰여 있지 않기 때문입니다. 커널은 매크로로 함수 이름을 생성합니다. 실제 소스에 있는 것은 이 모양입니다.

SYSCALL_DEFINE3(read, unsigned int, fd, char __user *, buf, size_t, count)
{
	return ksys_read(fd, buf, count);
}

sys_read라는 토큰은 전처리기가 만들어 냅니다. 그러니 grep으로는 잡히지 않습니다. 대신 이렇게 찾아야 합니다.

# 매크로 형태로 찾기 — 인자 개수를 모르면 숫자를 와일드카드로
grep -rn --include='*.c' -E 'SYSCALL_DEFINE[0-6]\(read,' .

이런 "이름이 생성되는" 패턴은 커널 곳곳에 있습니다. 대표적인 것만 기억해 두면 헛수고를 크게 줄입니다.

소스에 쓰인 형태만들어지는 이름
SYSCALL_DEFINE3(read, ...)sys_read와 내부 보조 함수들
EXPORT_SYMBOL_GPL(foo)모듈이 링크할 수 있는 심볼 항목
module_param(bar, int, 0644)sysfs에 노출되는 파라미터
DEFINE_PER_CPU(type, name)per-CPU 변수
TRACE_EVENT(name, ...)추적점과 관련 함수 다수

규칙 하나로 요약하면: 심볼을 찾지 못했을 때 "없다"고 결론 내리기 전에, 그 이름이 매크로로 생성된 것은 아닌지 의심하세요. 의심하는 방법은 간단합니다. 이름의 접두사를 떼고 다시 찾아보는 것입니다. sys_read에서 sys_를 떼고 read,로 찾으면 위의 매크로가 걸립니다.

4. 인덱스 도구 — cscope, ctags, GNU GLOBAL

grep은 문자열을 찾습니다. 인덱스 도구는 심볼을 찾습니다. 차이는 이런 데서 납니다. "이 함수를 호출하는 곳 전부"는 grep으로는 정확히 답하기 어렵지만 cscope는 바로 답합니다.

커널은 이 인덱스들을 만드는 make 타깃을 트리 안에 이미 갖고 있습니다. 최상위 Makefile의 도움말이 표시하는 항목은 이렇습니다.

  tags/TAGS	  - Generate tags file for editors
  cscope	  - Generate cscope index
  gtags           - Generate GNU GLOBAL index

실제로 만드는 명령은 이렇습니다.

# vim/emacs용 tags 파일 — ARCH를 지정하지 않으면 모든 아키텍처가 들어갑니다
make ARCH=x86 tags

# cscope 인덱스
make ARCH=x86 cscope

# GNU GLOBAL
make ARCH=x86 gtags

ARCH를 지정하는 것이 중요합니다. 1편에서 본 대로 arch/만 480만 줄인데, 그중 대부분은 지금 보는 아키텍처와 무관합니다. 지정하지 않으면 인덱스가 몇 배로 커지고, 함수 정의로 점프했을 때 엉뚱한 아키텍처의 동명 함수로 가는 일이 생깁니다. 이 동작은 scripts/tags.sh가 담당하며, 최상위 Makefile이 이 스크립트를 호출합니다.

cscope를 쓸 때 실제로 유용한 질의는 다음 셋입니다.

cscope -d          # 인덱스를 다시 만들지 않고 열기

열고 나면 화면 아래에 질의 종류가 나옵니다. 그중 세 가지만 알면 충분합니다.

  • Find this C symbol — 이 이름이 나오는 모든 곳
  • Find functions calling this function — 호출자를 거꾸로 추적. 커널에서 가장 자주 쓰는 질의입니다.
  • Find this global definition — 정의 한 곳으로 바로 이동

에디터 통합을 쓴다면 별도 도구를 얹어도 됩니다. 다만 어느 도구를 쓰든 태그를 옮기거나 커널을 업데이트한 뒤에는 인덱스를 다시 만들어야 한다는 점만 기억하면 됩니다. 오래된 인덱스는 없느니만 못합니다. 존재하지 않는 줄 번호로 점프시켜 놓고 사용자를 혼란스럽게 만들기 때문입니다.

5. 실전 — 시스템 콜 하나를 코드까지 추적하기

앞의 도구들을 실제 질문 하나에 붙여 봅니다. 질문은 이것입니다. read(2)를 호출하면 커널의 어느 함수가 실행되는가.

1단계 — 시스템 콜 번호와 진입점 이름

x86_64의 시스템 콜 테이블은 arch/x86/entry/syscalls/syscall_64.tbl입니다. 파일 앞머리에 형식이 적혀 있습니다.

# 64-bit system call numbers and entry vectors
#
# The format is:
# <number> <abi> <name> <entry point> [<compat entry point> [noreturn]]
#
# The __x64_sys_*() stubs are created on-the-fly for sys_*() system calls
#
# The abi is "common", "64" or "x32" for this file.

그 아래 첫 줄이 우리가 찾는 것입니다.

0	common	read			sys_read
1	common	write			sys_write
2	common	open			sys_open
3	common	close			sys_close

번호 0번, 이름 read, 진입점 sys_read입니다.

여기서 버전 차이를 하나 짚어야 합니다. 커널 공식 문서 Documentation/process/adding-syscalls.rst는 6.11부터 아키텍처 공통 시스템 콜 테이블인 scripts/syscall.tbl이 도입됐다고 설명합니다. 제가 확인한 트리에도 두 파일이 모두 존재했습니다. 아키텍처별 테이블을 찾았는데 원하는 항목이 없다면 공통 테이블 쪽도 확인해야 합니다.

2단계 — 진입점 이름에서 실제 정의로

3절에서 설명한 함정이 여기서 나옵니다. sys_read로는 정의를 찾을 수 없습니다.

grep -rn --include='*.c' -E 'SYSCALL_DEFINE[0-6]\(read,' .

이 명령이 fs/read_write.c를 가리킵니다. 제가 확인한 트리에서는 723번째 줄이었습니다. 줄 번호는 버전마다 달라지므로 파일 이름만 기억하면 됩니다.

3단계 — 안으로 한 겹씩

파일을 열면 앞서 본 세 줄짜리 정의가 있고, 그것이 ksys_read를 호출합니다. 같은 파일 안에 있습니다. 그 함수는 다시 vfs_read를 호출하고, 이것도 같은 파일에 있습니다. 여기서 처음으로 파일이 갈라집니다. vfs_read는 파일시스템별 구현으로 넘기기 때문입니다.

호출 관계를 확인하는 명령은 이렇습니다.

# vfs_read를 호출하는 곳 전부 — cscope 없이 grep으로도 근사치는 나옵니다
grep -rn --include='*.c' -w 'vfs_read' . | head -20

여기까지가 찾기의 전형적인 형태입니다. 테이블에서 이름을 얻고, 매크로를 뚫고 정의에 도달하고, 호출 관계를 따라 한 겹씩 내려갑니다. 이 경로를 실제 코드와 함께 끝까지 읽는 것이 4편입니다.

다른 종류의 출발점

시스템 콜이 아니라 다른 것에서 출발할 때의 요령도 정리해 둡니다.

  • 에러 메시지에서 출발. dmesg에 찍힌 문구를 그대로 찾습니다. 커널 로그 문자열은 대개 소스에 리터럴로 들어 있습니다. 단, pr_fmt로 접두사가 붙는 경우가 있으므로 접두사를 뺀 부분으로 찾아야 합니다.
  • /proc이나 /sys 파일에서 출발. 파일 이름 문자열을 찾으면 그 파일을 만드는 코드가 나옵니다.
  • sysctl 이름에서 출발. vm.swappiness 같은 이름은 소스에 "swappiness" 형태로 등록돼 있습니다. 커널 파라미터 튜닝 글에서 다룬 파라미터들의 구현을 이렇게 찾을 수 있습니다.
  • 함수 이름을 이미 아는 경우. perf나 ftrace가 알려 준 이름이 여기 해당합니다. 6편에서 다룹니다.

6. MAINTAINERS — 누구의 코드인지 알아내기

파일 하나를 찾았습니다. 이제 그 파일이 어느 서브시스템 소관이고 누구에게 물어야 하는지 알아야 할 때가 옵니다. 답은 트리 최상위의 MAINTAINERS 파일에 있습니다.

이 파일은 앞부분에 필드 표기법을 설명해 두고, 그 뒤로 수천 개의 항목이 알파벳순으로 이어집니다. 자주 보게 되는 필드는 이렇습니다.

필드
M:패치를 보낼 관리자
R:참조로 넣어야 할 지정 리뷰어
L:관련 메일링 리스트
S:상태
T:소스 관리 트리의 종류와 위치
F:이 항목이 담당하는 파일과 디렉터리 패턴
X:담당에서 제외되는 경로
W:상태와 정보가 있는 웹 페이지

S: 필드의 값이 특히 정보량이 많습니다. 파일 자신의 설명을 옮기면 이렇습니다.

	S: *Status*, one of the following:
	   Supported:	Someone is actually paid to look after this.
	   Maintained:	Someone actually looks after it.
	   Odd Fixes:	It has a maintainer but they don't have time to do
			much other than throw the odd patch in.
	   Orphan:	No current maintainer.
	   Obsolete:	Old code. Something tagged obsolete generally means
			it has been replaced by a better system.

패치를 보내기 전에 이 줄을 확인하면 기대치를 조정할 수 있습니다. Orphan이나 Obsolete로 표시된 코드에 정성껏 패치를 만들어 봐야 리뷰해 줄 사람이 없습니다.

손으로 찾는 대신 스크립트를 쓰는 것이 정석입니다.

# 파일 하나의 담당자와 리스트
./scripts/get_maintainer.pl -f fs/read_write.c

# 만들어 둔 패치의 수신자 목록
./scripts/get_maintainer.pl 0001-my-change.patch

이 스크립트는 8편에서 다시 나옵니다. 지금은 "어느 코드가 누구 소관인지 물어보는 명령이 트리 안에 있다"는 사실만 기억하면 충분합니다.

7. Documentation 트리를 검색 대상으로 쓰기

1편의 표에서 Documentation이 167만 줄이었습니다. 이건 곁다리 자료가 아니라 커널의 일부이고, 코드와 함께 버전이 관리됩니다. 즉 손에 있는 트리의 문서는 그 트리의 코드와 일치합니다. 웹 문서를 보다 버전이 어긋나는 사고를 원천적으로 막는 방법이기도 합니다.

찾기 관점에서 특히 유용한 하위 디렉터리는 이렇습니다.

  • Documentation/process/ — 기여 절차 전반. 8편에서 대부분을 씁니다.
  • Documentation/core-api/ — 커널 내부 API 설명. 7편의 메모리 할당 규칙이 여기 있습니다.
  • Documentation/admin-guide/ — 관리자 관점. 빌드, 부트 파라미터, sysctl이 있습니다.
  • Documentation/dev-tools/ — 검사와 디버깅 도구.
  • Documentation/trace/ — ftrace, kprobes를 포함한 추적 인프라. 6편의 주 재료입니다.

Documentation/process/의 목록만 잠깐 보면 이 트리의 성격이 드러납니다.

1.Intro.rst          coding-style.rst      maintainers.rst
2.Process.rst        development-process   management-style.rst
3.Early-stage.rst    email-clients.rst     programming-language.rst
4.Coding.rst         howto.rst             stable-kernel-rules.rst
5.Posting.rst        license-rules.rst     submit-checklist.rst
6.Followthrough.rst  maintainer-*.rst      submitting-patches.rst

같은 내용이 docs.kernel.org에 렌더링되어 올라와 있습니다. 웹에서 읽을 때는 페이지 상단의 버전 표기를 확인하는 습관을 들이는 편이 좋습니다.

흔한 함정

얕은 복제로 받아 놓고 git log를 찾는다. 코드를 읽다 보면 반드시 이력이 필요해집니다. 처음부터 전체를 받거나, 최소한 blob 필터 방식으로 받으세요.

태그를 옮기고 인덱스를 그대로 쓴다. 정의로 점프했는데 엉뚱한 코드가 나오면 십중팔구 이 경우입니다. make ARCH=x86 cscope를 다시 돌리면 됩니다.

ARCH를 지정하지 않고 인덱스를 만든다. 같은 이름의 함수가 아키텍처마다 존재하는 경우가 흔해서, 지정하지 않으면 후보 목록이 열 개씩 나옵니다.

매크로가 생성한 이름을 찾다가 포기한다. 3절에서 다룬 대표적인 벽입니다. 접두사를 떼고 다시 찾아보는 습관 하나로 대부분 풀립니다.

웹에서 본 코드가 손에 있는 트리와 다르다. 여러 사이트가 특정 버전을 고정해 보여 줍니다. 웹 페이지의 버전과 내 트리의 버전이 같은지 먼저 확인하세요. 다르면 인용하지 말고 내 트리에서 다시 찾는 것이 안전합니다.

드라이버 이름으로 찾을 때 drivers/staging을 빠뜨린다. 반대로, staging에서 찾은 코드가 정식 위치의 코드와 다를 수도 있습니다. 두 곳에 비슷한 이름이 있으면 어느 쪽이 실제로 로드되는지 modinfo로 확인하는 편이 확실합니다.

마치며

찾기는 커널 학습에서 가장 저평가된 기술입니다. 사람들은 "커널을 이해한다"를 지식의 문제로 여기지만, 실제 작업의 대부분은 이 동작을 만들어 내는 코드가 어디 있는지 알아내는 일입니다. 알아내고 나면 읽는 것은 그냥 C를 읽는 일입니다.

이 글에서 다룬 것을 한 줄로 압축하면 이렇습니다. 트리는 stable 쪽에서 전체 이력과 함께 받고, 질문의 종류에 따라 디렉터리를 좁히고, ripgrep으로 문자열을 잡고, cscope로 호출 관계를 잡고, 이름이 안 나오면 매크로를 의심하고, 소관을 알아야 하면 MAINTAINERSget_maintainer.pl을 씁니다.

다음 편은 빌드입니다. 지금까지는 코드를 읽기만 했지만, 다음 편부터는 그 코드를 실제로 돌아가는 커널로 만듭니다. 그리고 그 커널을 어디서 부팅할 것인가에 대한 답은 하나뿐이라는 이야기를 하게 됩니다.

이 글의 명령과 파일 경로는 2026년 8월 19일에 메인라인 트리에서 직접 확인했습니다. 파일 위치는 버전에 따라 달라지므로, 다르면 3절의 방법으로 다시 찾으시면 됩니다.

직접 해보기

이전 편과 다음 편

참고 자료

모든 링크는 2026년 8월 19일에 확인했습니다.

현재 단락 (1/185)

[1편](/blog/linux/2026-08-19-kernel-where-to-start)에서 커널이 4,300만 줄이고 그중 60퍼센트가 드라이버라는 것을 확인했습니다. 그 글의...

작성 글자: 0원문 글자: 10,959작성 단락: 0/185