diff & patch — 소스 코드 차이 비교와 패치(Patch) 적용

diff는 두 파일(또는 디렉터리)의 차이를 계산하여 사람이 읽을 수 있는 형식으로 출력하는 유틸리티이며, patch는 그 diff 출력을 원본 파일에 적용하여 변경 사항을 재현하는 유틸리티입니다. 이 두 도구는 리눅스 커널 개발을 비롯한 모든 오픈소스 협업의 기본 인프라(Infrastructure)입니다. Normal/Context/Unified 세 가지 출력 형식, 디렉터리 비교, patch의 퍼지 매칭(Fuzz Matching)과 되돌리기, patchutils 보조 도구, quilt 패치 스택 관리, Git과의 연계까지 상세히 다룹니다.

전제 조건: Bash 셸 스크립팅Git 문서를 먼저 읽으면 diff/patch가 실제 워크플로에서 어떻게 쓰이는지 맥락을 이해하기 쉽습니다. 커널 패치(Patch) 제출 절차는 패치 제출 문서에서 상세히 다룹니다.
일상 비유: diff두 문서를 나란히 놓고 빨간 펜으로 다른 부분에 표시하는 것이고, patch그 빨간 펜 표시를 보고 원본 문서를 수정하는 것입니다. 편집자가 원고에 수정 지시를 적어 주면 저자가 그대로 반영하는 과정과 동일합니다.

핵심 요약

  • diff — 두 파일의 차이를 계산합니다. -u(Unified), -c(Context), 기본(Normal) 세 가지 출력 형식이 있으며, 커널 개발에서는 Unified 형식이 표준입니다.
  • patch — diff가 생성한 패치 파일을 원본에 적용합니다. -p 옵션으로 경로 깊이를 조절하고, -R로 되돌릴 수 있습니다.
  • Unified 형식---/+++ 헤더와 @@ 헝크(Hunk) 헤더로 구성되며, -는 삭제, +는 추가, 공백은 문맥(Context) 줄입니다.
  • 디렉터리 비교diff -ruN으로 두 디렉터리 트리 전체를 재귀적으로 비교할 수 있습니다.
  • patchutilsfilterdiff, lsdiff, interdiff, combinediff 등 패치 파일을 가공하는 보조 도구 모음입니다.

단계별 이해

  1. 차이 생성
    diff -u old.c new.c > change.patch로 변경 사항을 패치 파일로 저장합니다.
  2. 패치 검증
    patch --dry-run -p0 < change.patch로 실제 적용 전에 결과를 미리 확인합니다.
  3. 패치 적용
    patch -p0 < change.patch로 원본 파일에 변경 사항을 적용합니다.
  4. 패치 되돌리기
    patch -R -p0 < change.patch로 적용한 패치를 원래 상태로 되돌립니다.
  5. 커널 워크플로
    커널 개발에서는 git diffgit format-patch가 diff 역할을, git applygit am이 patch 역할을 수행합니다.
관련 표준: IEEE Std 1003.1-2024 (POSIX.1-2024) — diffpatch는 POSIX 표준 유틸리티입니다. GNU diffutils는 POSIX를 확장한 기능을 추가로 제공합니다.

diff 유틸리티 개요

diff는 1974년 Unix V5에서 Douglas McIlroy가 만든 유틸리티입니다. 초기에는 Hunt-McIlroy 알고리즘을 사용했으며, 이후 Eugene W. Myers가 1986년에 발표한 O(ND) 차이 알고리즘이 현대 diff 구현의 표준이 되었습니다. 이 알고리즘은 두 파일의 LCS(Longest Common Subsequence, 최장 공통 부분 수열)최단 편집 스크립트(Shortest Edit Script, SES)가 서로 연결된다는 사실을 이용해 최소 편집 경로를 찾습니다. GNU diffutils는 Myers 알고리즘 위에 가독성 보정 규칙을 더해 실제 패치 검토에 더 읽기 쉬운 헝크를 만듭니다.

# 기본 문법
diff [옵션] <원본 파일> <수정 파일>

# 종료 코드
# 0 — 차이 없음
# 1 — 차이 있음
# 2 — 오류 발생

diff/patch 역사와 발전

diff와 patch는 50년 이상의 역사를 가진 Unix 핵심 도구입니다. 각 세대마다 출력 형식과 알고리즘이 발전하면서 현대 소프트웨어 협업의 기반이 되었습니다.

diff / patch 역사와 발전 1974 Unix V5 diff Douglas McIlroy Hunt-McIlroy 알고리즘 Normal 형식 출력 1980s BSD diff -c Context 형식 도입 문맥 줄 추가 사람이 읽기 쉬움 1985 patch 탄생 Larry Wall 퍼지 매칭, 오프셋 원격 협업 가능 1986–91 Unified + GNU Myers O(ND) 알고리즘 Unified 형식 (-u) GNU diffutils 1.0 2005+ Git 시대 git diff / git apply Patience / Histogram format-patch / am 주요 이정표 1974 Hunt-McIlroy: 해싱 + LCS로 O(N log N) 달성. ed 스크립트 호환 Normal 형식 1980s BSD context diff: 문맥 줄 추가로 patch 적용 신뢰도 향상. 네트워크 시대 대비 1985 Larry Wall의 patch: Usenet으로 소스 코드 배포 → "전체 소스 대신 패치만 전송" 혁신 1986 Myers 알고리즘: O(ND) — 유사한 파일이면 사실상 O(N). 현대 diff의 핵심 1991 Unified 형식: Context 형식의 개선판. +/- 기호 사용, 더 작은 패치 크기 2005 Git: Linus Torvalds가 커널 개발용으로 설계. diff/patch를 버전 관리에 통합 2010s Patience/Histogram 알고리즘, AST 기반 구조적 diff(difftastic), delta/bat 등 시각화 도구
patch의 탄생 배경: 1985년 Larry Wall(Perl 창시자)이 patch를 만든 계기는 Usenet 뉴스그룹이었습니다. 당시 소프트웨어 업데이트를 위해 전체 소스 코드를 재배포하는 것은 네트워크 대역폭(Bandwidth) 낭비였기에, 변경 부분만 담은 diff 출력(패치)을 전송하고 수신자가 자동 적용하는 방식이 필요했습니다. 이 "패치를 보내고 적용하는" 워크플로는 오늘날 리눅스 커널의 이메일 기반 패치 리뷰 문화로 직접 이어집니다.

diff 출력 형식 비교

diff는 세 가지 주요 출력 형식을 제공합니다. 각 형식의 특성과 용도가 다르므로 상황에 맞게 선택해야 합니다.

diff 출력 형식 비교 Normal 형식 (기본) diff old new 3c3 < old line --- > new line a = 추가 (Add) d = 삭제 (Delete) c = 변경 (Change) 문맥 없음, 가장 간결 Context 형식 (-c) diff -c old new *** old 2024-01-01 --- new 2024-01-02 *************** *** 1,5 **** ! changed line ! = 변경, - = 삭제, + = 추가 기본 문맥 3줄 BSD 전통, 문맥 포함 Unified 형식 (-u) diff -u old new --- old 2024-01-01 +++ new 2024-01-02 @@ -1,5 +1,5 @@ -old line +new line - = 삭제, + = 추가 공백 접두사 = 문맥 줄 커널 개발 표준, 가장 많이 사용

Normal 형식 (기본)

옵션 없이 diff를 실행하면 Normal 형식으로 출력됩니다. ed 스크립트(ed script)와 유사한 형태로, 문맥 줄이 없어 가장 간결하지만 사람이 읽기에는 불편합니다.

# 예제 파일 준비
cat > old.c <<'EOF'
#include <stdio.h>

int main(void) {
    printf("Hello\n");
    return 0;
}
EOF

cat > new.c <<'EOF'
#include <stdio.h>
#include <stdlib.h>

int main(int argc, char *argv[]) {
    printf("Hello, %s\n", argc > 1 ? argv[1] : "World");
    return EXIT_SUCCESS;
}
EOF

# Normal diff 실행
diff old.c new.c

출력 결과:

1a2
> #include <stdlib.h>
3c4
< int main(void) {
---
> int main(int argc, char *argv[]) {
4c5
<     printf("Hello\n");
---
>     printf("Hello, %s\n", argc > 1 ? argv[1] : "World");
5c6
<     return 0;
---
>     return EXIT_SUCCESS;

Normal 형식의 명령어 구문:

구문의미예시설명
LaR추가(Append)1a2원본 1번 줄 뒤에 수정본 2번 줄 추가
FdL삭제(Delete)3,5d2원본 3~5번 줄 삭제
FcT변경(Change)3c4원본 3번 줄을 수정본 4번 줄로 변경

Context 형식 (-c)

Context 형식은 변경 지점 주변의 문맥 줄을 함께 표시하여 사람이 변경 내용을 이해하기 쉽게 만듭니다. 1980년대 BSD에서 처음 도입되었습니다.

# Context diff 생성 (기본 문맥 3줄)
diff -c old.c new.c

# 문맥 줄 수 조절 (5줄)
diff -C 5 old.c new.c

출력 결과:

*** old.c	2024-01-01 10:00:00.000000000 +0900
--- new.c	2024-01-02 10:00:00.000000000 +0900
***************
*** 1,6 ****
  #include <stdio.h>

! int main(void) {
!     printf("Hello\n");
!     return 0;
  }
--- 1,7 ----
  #include <stdio.h>
+ #include <stdlib.h>

! int main(int argc, char *argv[]) {
!     printf("Hello, %s\n", argc > 1 ? argv[1] : "World");
!     return EXIT_SUCCESS;
  }

Context 형식의 접두사 기호:

기호의미설명
(공백)문맥변경 없는 줄 — 위치 확인용
!변경원본 블록(***)과 수정본 블록(---) 양쪽에 표시
-삭제원본 블록에만 존재
+추가수정본 블록에만 존재

Unified 형식 (-u) ★

Unified 형식은 Context 형식을 개선한 것으로, 원본과 수정본을 하나의 블록으로 합쳐 표시합니다. 가독성과 공간 효율이 모두 우수하여 리눅스 커널을 비롯한 대부분의 오픈소스 프로젝트에서 표준으로 사용합니다. git diff의 기본 출력도 Unified 형식입니다.

# Unified diff 생성 (기본 문맥 3줄)
diff -u old.c new.c

# 문맥 줄 수 조절 (5줄)
diff -U 5 old.c new.c

# 파일로 저장
diff -u old.c new.c > change.patch

출력 결과:

--- old.c	2024-01-01 10:00:00.000000000 +0900
+++ new.c	2024-01-02 10:00:00.000000000 +0900
@@ -1,6 +1,7 @@
 #include <stdio.h>
+#include <stdlib.h>

-int main(void) {
-    printf("Hello\n");
-    return 0;
+int main(int argc, char *argv[]) {
+    printf("Hello, %s\n", argc > 1 ? argv[1] : "World");
+    return EXIT_SUCCESS;
 }
Unified Diff 구조 분석 --- old.c 2024-01-01 10:00:00 +0900 +++ new.c 2024-01-02 10:00:00 +0900 파일 헤더(File Header) --- 원본, +++ 수정본 타임스탬프는 선택 사항 @@ -1,6 +1,7 @@ 헝크 헤더(Hunk Header) -시작,줄수 +시작,줄수 @@ -1,6 +1,7 @@ ↑ 분해: -1,6 → 원본 1번 줄부터 6줄 +1,7 → 수정본 1번 줄부터 7줄 줄 수가 1이면 생략 가능: @@ -3 +3 @@ @@ 뒤에 함수명 표시 가능 (git diff) #include <stdio.h> +#include <stdlib.h> -int main(void) { +int main(int argc, char *argv[]) { ... } 공백 접두사 = 문맥 줄 + 접두사 = 추가 줄 - 접두사 = 삭제 줄 patch는 문맥 줄로 적용 위치를 결정합니다

diff 주요 옵션 레퍼런스

옵션설명사용 예
-u, -U NUnified 형식 (문맥 N줄, 기본 3)diff -u old new
-c, -C NContext 형식 (문맥 N줄, 기본 3)diff -c old new
-r디렉터리 재귀 비교(Recursive)diff -r dir1/ dir2/
-N없는 파일을 빈 파일로 취급(New file)diff -ruN dir1/ dir2/
-p변경된 C 함수명 표시diff -up old.c new.c
-b공백 차이 무시(Ignore space change)diff -ub old new
-B빈 줄 차이 무시diff -uB old new
-w모든 공백 무시diff -uw old new
-i대소문자 무시diff -ui old new
--color컬러 출력 (GNU diffutils 3.4+)diff --color -u old new
-q다른 파일만 이름 표시(Brief)diff -rq dir1/ dir2/
-y나란히 비교(Side-by-side)diff -y old new
-W NSide-by-side 폭 지정diff -y -W 120 old new
--strip-trailing-crWindows CR 문자 무시diff -u --strip-trailing-cr old new
-x PAT패턴에 맞는 파일 제외(Exclude)diff -ruN -x '*.o' dir1/ dir2/
-X FILE제외 패턴 파일 지정diff -ruN -X .diffignore dir1/ dir2/
-L LABEL파일 이름 대신 라벨 표시diff -u -L a/file -L b/file old new

디렉터리 비교

커널 소스 트리처럼 대규모 디렉터리를 비교할 때는 -r(재귀)과 -N(새 파일 포함) 옵션을 함께 사용합니다.

# 커널 소스 트리 비교 (전형적인 커널 패치 생성)
diff -ruNp linux-6.8/ linux-6.8-patched/ > my-kernel-fix.patch

# 옵션 분해:
#   -r   재귀 비교
#   -u   Unified 형식
#   -N   새 파일을 빈 파일 대비로 포함
#   -p   변경된 C 함수명 표시

# 바이너리 파일과 빌드 산출물 제외
diff -ruNp -x '*.o' -x '*.ko' -x '.git' -x '.config' \
    linux-6.8/ linux-6.8-patched/ > my-kernel-fix.patch

# 제외 패턴을 파일로 관리
cat > .diffignore <<'EOF'
*.o
*.ko
*.mod
*.cmd
.git
.config
.tmp_versions
EOF
diff -ruNp -X .diffignore linux-6.8/ linux-6.8-patched/ > my-kernel-fix.patch

# 어떤 파일이 다른지만 빠르게 확인
diff -rq linux-6.8/ linux-6.8-patched/
# 출력 예:
# Files linux-6.8/drivers/net/foo.c and linux-6.8-patched/drivers/net/foo.c differ
# Only in linux-6.8-patched/drivers/net: bar.c

diff 고급 사용법

Side-by-Side 비교 (-y)

# 터미널에서 나란히 비교
diff -y -W 120 old.c new.c

# 출력 기호:
#   (빈칸)  동일한 줄
#   |       변경된 줄
#   <       원본에만 존재 (삭제)
#   >       수정본에만 존재 (추가)

# 다른 줄만 표시
diff -y --suppress-common-lines old.c new.c

통계 정보

# diffstat: diff 출력의 통계 요약 (별도 패키지)
diff -ruNp linux-6.8/ linux-6.8-patched/ | diffstat
# 출력 예:
#  drivers/net/foo.c   |   15 +++++++++------
#  drivers/net/bar.c   |   42 ++++++++++++++++++++++++++++++++++++++++++
#  include/net/baz.h   |    3 ++-
#  3 files changed, 50 insertions(+), 7 deletions(-)

# git에서의 유사 기능
git diff --stat

특수 비교

# 프로세스(Process) 치환으로 명령 출력 비교
diff <(sort file1.txt) <(sort file2.txt)

# 표준 입력과 파일 비교 (- 는 stdin)
cat modified.c | diff original.c -

# 바이너리 파일 비교 (16진수 덤프)
diff <(xxd file1.bin) <(xxd file2.bin)

# 커널 .config 비교 전용 도구
scripts/diffconfig .config.old .config
# 출력 예: CONFIG_DEBUG_INFO n -> y

diff3 — 3-way 머지

diff3는 세 파일을 동시에 비교하여 3방향 머지(3-way merge)를 수행합니다. 두 사람이 같은 원본에서 각자 수정한 경우, 양쪽 변경을 합칠 때 핵심적인 도구입니다. Git의 내부 머지 엔진도 이 3-way 머지 원리를 사용합니다.

diff3 — 3-way 머지 개념 공통 원본 (base) original.c 수정본 A (mine) my-version.c 수정본 B (yours) your-version.c diff3 -m mine base yours 양쪽 변경이 겹치지 않으면 자동 머지 겹치면 <<<<<<< 충돌 마커 삽입
# diff3 기본 문법: diff3 MINE OLDER YOURS
# (인수 순서 주의: 내 수정본, 공통 원본, 상대 수정본)
diff3 my-version.c original.c your-version.c

# 3-way 머지 결과를 stdout으로 출력 (-m)
diff3 -m my-version.c original.c your-version.c > merged.c

# 충돌이 없으면 종료 코드 0, 충돌 시 1
diff3 -m my.c base.c yours.c > merged.c
echo $?   # 0=성공, 1=충돌, 2=오류

# 충돌 발생 시 merged.c 내용:
# <<<<<<< my-version.c
#     printf("Hello, World!\n");
# ||||||| original.c
#     printf("Hello\n");
# =======
#     printf("Hi there\n");
# >>>>>>> your-version.c

# ed 스크립트 형식 출력 (기본)
diff3 my.c base.c yours.c
# 출력 기호:
# ==== — 세 파일 모두 다름 (충돌)
# ====1 — 파일 1(mine)만 다름
# ====2 — 파일 2(base)만 다름 → A와 B가 동일하게 수정
# ====3 — 파일 3(yours)만 다름

# 겹치지 않는 변경만 자동 적용, 충돌은 표시 (-A 옵션)
diff3 -A my.c base.c yours.c

# 실전: 두 커널 패치가 같은 파일을 수정한 경우
# 1. base를 기준으로 각각 패치 적용된 파일 준비
cp base.c mine.c && patch mine.c < patch-a.diff
cp base.c yours.c && patch yours.c < patch-b.diff
# 2. 3-way 머지
diff3 -m mine.c base.c yours.c > merged.c
# 3. 충돌 확인
grep -c '<<<<<<<' merged.c   # 0이면 충돌 없음

sdiff — 대화형 나란히 비교/머지

sdiff는 두 파일을 나란히(Side-by-side) 표시하며, -o 옵션으로 대화형 머지가 가능합니다.

# 나란히 비교 (diff -y와 유사)
sdiff old.c new.c

# 대화형 머지: 차이가 있는 부분마다 선택
sdiff -o merged.c old.c new.c
# 프롬프트에서:
#   l — 왼쪽(old) 선택
#   r — 오른쪽(new) 선택
#   e — 에디터로 직접 편집
#   el — 왼쪽을 에디터로 편집
#   er — 오른쪽을 에디터로 편집
#   eb — 양쪽을 에디터로 편집
#   s — 공통 줄 무음(표시 안 함)
#   v — 공통 줄 표시 (verbose)
#   q — 종료

# 폭 지정
sdiff -w 160 old.c new.c

# 공백 무시
sdiff -b old.c new.c

# 출력 기호:
# (빈칸)  동일
# |       변경
# <       왼쪽에만 존재 (삭제)
# >       오른쪽에만 존재 (추가)

cmp — 바이트 단위 비교

cmp는 두 파일을 바이트 단위로 비교합니다. diff가 텍스트 줄 단위 비교라면, cmp는 바이너리 파일을 포함한 모든 파일의 바이트 수준 비교 도구입니다.

# 기본: 첫 번째 다른 바이트 위치만 출력
cmp file1 file2
# file1 file2 differ: byte 1024, line 8

# 동일하면 출력 없이 종료 코드 0
cmp -s file1 file2 && echo "동일" || echo "다름"

# 스크립트에서 파일 동일성 검사 (가장 흔한 용도)
if cmp -s before.bin after.bin; then
    echo "변경 없음"
else
    echo "파일이 변경됨"
fi

# 모든 다른 바이트 나열 (-l)
cmp -l file1 file2
# 출력: 바이트_위치  8진수_file1  8진수_file2
#    1024 101 142
#    1025 157 163

# 처음 N바이트만 비교
cmp -n 512 file1 file2

# 바이트 오프셋 건너뛰기
cmp -i 1024 file1 file2          # 양쪽 모두 1024바이트 건너뜀
cmp -i 512:1024 file1 file2      # file1은 512, file2는 1024 건너뜀

# 커널 빌드 결과 검증: 동일한 .config로 빌드한 vmlinux 비교
cmp -s vmlinux.old vmlinux.new && echo "재현 가능 빌드 성공"

# diff vs cmp 차이점:
# diff — 텍스트 줄 단위, 차이 내용 표시, 패치 생성 가능
# cmp  — 바이트 단위, 위치만 표시, 바이너리 안전, 더 빠름

patch 유틸리티 개요

patch는 Larry Wall이 1985년에 만든 유틸리티로, diff가 생성한 차이 파일(패치)을 원본에 적용합니다. 원본 파일이 패치 생성 시점과 약간 다르더라도 퍼지 매칭(Fuzz Matching)오프셋(Offset) 조정(Offset Adjustment)으로 유연하게 적용할 수 있는 것이 핵심 강점입니다.

patch 적용 흐름 원본 파일 old.c 패치 파일 change.patch patch 1. 헝크 헤더 파싱 2. 문맥 줄 매칭 3. 오프셋/퍼지 조정 성공 파일 직접 수정 오프셋 적용 위치 조정 후 적용 실패 (reject) .rej 파일 생성 수정된 파일 new.c old.c.rej 수동 해결 필요

패치 파일 해부 — 멀티 파일 · 멀티 헝크

실제 커널 패치는 여러 파일에 걸쳐 여러 헝크(Hunk)를 포함합니다. 아래는 2개 파일, 총 3개 헝크로 구성된 패치의 전체 구조입니다.

패치 파일 구조 해부 --- a/drivers/net/ethernet/intel/e1000e/netdev.c +++ b/drivers/net/ethernet/intel/e1000e/netdev.c 파일 1 헤더 a/ = 원본, b/ = 수정본 @@ -127,7 +127,9 @@ static int e1000_open(struct net_device *netdev) struct e1000_adapter *adapter = netdev_priv(netdev); int err; + if (!adapter) { + return -EINVAL; + } 헝크 1 헤더 원본 127줄 7줄 → 수정본 127줄 9줄 @@ 뒤 함수명은 git diff가 추가 문맥 줄 (공백 접두사) patch가 위치를 찾는 앵커 추가 줄 (+ 접두사) @@ -245,6 +247,8 @@ static void e1000_down(struct net_device *netdev) e1000_clean_all_tx_rings(adapter); e1000_clean_all_rx_rings(adapter); + adapter->flags &= ~FLAG_IS_UP; + netdev_info(netdev, "device down\n"); 헝크 2 (같은 파일의 다른 위치) 줄 번호가 헝크 1 변경분만큼 시프트 --- a/drivers/net/ethernet/intel/e1000e/e1000.h +++ b/drivers/net/ethernet/intel/e1000e/e1000.h 파일 2 헤더 한 패치에 여러 파일 포함 가능 @@ -89,6 +89,7 @@ struct e1000_adapter { unsigned long flags; +#define FLAG_IS_UP BIT(16) struct work_struct reset_task; struct timer_list watchdog_timer; 이 패치: 2개 파일 × 3개 헝크 = 총 5줄 추가, 0줄 삭제 | patch -p1 적용 시 a/ 접두사 자동 제거

퍼지 매칭 동작 시각화

원본 파일이 패치 생성 시점과 달라졌을 때, patch는 문맥 줄을 기준으로 올바른 위치를 탐색합니다. 아래 다이어그램은 오프셋(Offset)퍼지(Fuzz)가 어떻게 동작하는지 보여줍니다.

patch의 매칭 전략: 정확 → 오프셋 → 퍼지 → 실패 1단계: 정확 매칭 (Exact Match) 헝크 헤더의 줄 번호 위치에서 문맥 줄이 정확히 일치하는지 확인 @@ -127,7 +127,9 @@ → 127번 줄에서 문맥 3줄 확인 결과: "Hunk #1 succeeded at 127." 2단계: 오프셋 탐색 (Offset Search) 127번 줄에서 실패 → 위아래로 확장 탐색 126번? 128번? 125번? 129번? ... 142번? ✓ 문맥 3줄이 정확히 일치하는 위치를 찾을 때까지 검색 결과: "Hunk #1 succeeded at 142 (offset 15 lines)." 3단계: 퍼지 매칭 (Fuzz Matching) 오프셋으로도 실패 → 문맥 줄의 상하 N줄을 무시하고 재시도 fuzz 1: 문맥 상단 1줄 무시, fuzz 2: 상하 각 1줄 무시 기본 최대 퍼지 팩터: 2 (문맥 3줄 중 2줄까지 무시 가능) 결과: "Hunk #1 succeeded at 142 with fuzz 2 (offset 15 lines)." 4단계: 실패 (Reject) 모든 탐색 실패 → .rej 파일 생성, 수동 해결 필요 결과: "Hunk #1 FAILED at 127." 퍼지(Fuzz) 동작 상세 패치의 문맥 줄 (기본 3줄): 문맥 줄 1 (상단) 문맥 줄 2 (중간) 문맥 줄 3 (하단) +변경 내용 -F 0 (퍼지 없음): 3줄 모두 일치해야 성공. 가장 안전합니다. -F 1 (퍼지 1): 상단/하단 문맥 1줄 무시. 중간 줄은 반드시 일치. 무시 매칭 무시 -F 2 (퍼지 2, 기본값): 상하 각 1줄씩 무시. 문맥 3줄 중 1줄만 매칭. ⚠ 퍼지가 높을수록 잘못된 위치에 적용될 위험 증가 ✓ 커널 개발: -F 0 권장, 실패 시 패치 재생성 백포트·레거시 유지보수: 기본값(-F 2) 또는 -F 3 허용

patch 기본 사용법

# 패치 적용 기본
patch < change.patch                  # 패치 파일의 헤더에서 파일명 자동 인식
patch -p0 < change.patch              # 경로에서 0단계 제거
patch -p1 < change.patch              # 경로에서 1단계 제거 (가장 흔함)

# -p 옵션 이해: 경로 접두사 제거 단계
# 패치 헤더: --- a/drivers/net/foo.c
#   -p0 → a/drivers/net/foo.c (전체 경로 사용)
#   -p1 → drivers/net/foo.c   (a/ 제거) ★ 커널 패치 표준
#   -p2 → net/foo.c           (a/drivers/ 제거)

# 대상 파일 직접 지정
patch old.c < change.patch

# 대상 디렉터리 변경
patch -d /usr/src/linux -p1 < kernel-fix.patch

-p 옵션 (경로 접두사 제거) 상세

-p 옵션은 패치를 올바른 파일에 적용하기 위해 경로 접두사를 제거하는 핵심 옵션입니다. 이 옵션을 잘못 지정하면 파일을 찾지 못해 패치가 실패합니다.

-p 옵션에 따른 경로 해석 패치 헤더: --- a/drivers/net/ethernet/intel/e1000e/netdev.c -p0 → a/drivers/net/ethernet/intel/e1000e/netdev.c --- 뒤 전체 경로 사용 -p1 a/drivers/net/ethernet/intel/e1000e/netdev.c ★ 커널 표준 (git diff 기본 a/ b/ 접두사 제거) -p2 a/drivers/net/ethernet/intel/e1000e/netdev.c 첫 2단계 제거 -p3 a/drivers/net/ethernet/intel/e1000e/netdev.c 첫 3단계 제거 주의: -p 없이 patch를 실행하면 전체 경로에서 마지막 파일명만 사용하여 현재 디렉터리에서 파일을 찾습니다.
# 커널 소스 루트에서 패치 적용 (가장 일반적)
cd /usr/src/linux
patch -p1 < /path/to/kernel-fix.patch

# diff -ruNp 로 만든 패치 (경로에 디렉터리명 포함)
# --- linux-6.8/drivers/net/foo.c
# +++ linux-6.8-patched/drivers/net/foo.c
cd linux-6.8
patch -p1 < ../kernel-fix.patch    # linux-6.8/ 접두사 제거

patch 주요 옵션 레퍼런스

옵션설명사용 예
-p NUM경로 접두사 NUM단계 제거patch -p1 < fix.patch
-R패치 되돌리기(Reverse)patch -R -p1 < fix.patch
--dry-run실제 변경 없이 시뮬레이션patch --dry-run -p1 < fix.patch
-d DIR대상 디렉터리 지정patch -d /usr/src/linux -p1 < fix.patch
-b백업 파일 생성 (.orig)patch -b -p1 < fix.patch
-V STYLE백업 파일 명명법patch -b -V numbered -p1 < fix.patch
-o FILE결과를 별도 파일에 저장patch -o new.c old.c < fix.patch
-F NUM퍼지 팩터(Fuzz factor) 설정patch -F 3 -p1 < fix.patch
-l느슨한 공백 매칭(Loose matching)patch -l -p1 < fix.patch
--verbose상세 출력patch --verbose -p1 < fix.patch
-s조용한 모드(Silent)patch -s -p1 < fix.patch
-f질문 없이 강제 적용(Force)patch -f -p1 < fix.patch
-E패치 후 빈 파일 삭제patch -E -p1 < fix.patch
--merge충돌 시 merge 마커 삽입 (GNU patch 2.7+)patch --merge -p1 < fix.patch

퍼지 매칭과 오프셋 조정

patch가 강력한 이유는 원본 파일이 패치 생성 시점과 달라도 유연하게 적용할 수 있기 때문입니다.

오프셋 조정 (Offset)

헝크의 줄 번호가 맞지 않으면 patch는 문맥 줄을 기준으로 위아래로 검색하여 올바른 위치를 찾습니다.

# 오프셋이 발생하면 patch가 알려줍니다
patch -p1 < fix.patch
# 출력:
# patching file drivers/net/foo.c
# Hunk #1 succeeded at 127 (offset 15 lines).
# Hunk #2 succeeded at 245 with fuzz 1 (offset 22 lines).

퍼지(Fuzz)

문맥 줄이 일부 다를 때 patch는 퍼지 팩터(Fuzz Factor)만큼 문맥 줄을 무시하고 매칭을 시도합니다. 기본 퍼지 팩터는 2입니다.

# 퍼지 없이 엄격하게 적용 (커널 개발 권장)
patch -F 0 -p1 < fix.patch

# 퍼지 팩터를 3으로 늘려 느슨하게 적용
patch -F 3 -p1 < fix.patch
주의: 퍼지 매칭은 잘못된 위치에 패치를 적용할 위험이 있습니다. 커널 개발에서는 -F 0으로 엄격하게 적용하고, 실패 시 패치를 재생성하는 것이 안전합니다.

거부(Reject) 파일 처리

patch가 헝크를 적용하지 못하면 .rej 파일을 생성합니다.

# 패치 적용 실패 시
patch -p1 < fix.patch
# 출력:
# patching file drivers/net/foo.c
# Hunk #1 FAILED at 42.
# 1 out of 3 hunks FAILED -- saving rejects to file drivers/net/foo.c.rej

# .rej 파일 확인 → 수동으로 해결
cat drivers/net/foo.c.rej
# 내용: 적용 실패한 헝크가 그대로 들어 있음
# → 해당 부분을 수동으로 편집해야 합니다

# 정리: .rej와 .orig 파일 제거
find . -name '*.rej' -o -name '*.orig' | xargs rm -f

패치 되돌리기 (-R)

# 적용한 패치를 되돌리기
patch -R -p1 < fix.patch

# 되돌리기 전 시뮬레이션
patch -R --dry-run -p1 < fix.patch

# 이미 적용된 패치를 다시 적용하려고 하면?
patch -p1 < fix.patch
# 출력:
# Reversed (or previously applied) patch detected!  Assume -R? [n]
# → patch가 자동으로 이미 적용되었음을 감지합니다

백업과 안전한 적용

# 백업 파일 생성 후 적용
patch -b -p1 < fix.patch
# drivers/net/foo.c.orig 생성 (원본 백업)

# 번호 붙은 백업
patch -b -V numbered -p1 < fix.patch
# drivers/net/foo.c.~1~ 생성

# 안전한 적용 패턴: dry-run → 백업 → 적용
patch --dry-run -p1 < fix.patch && \
patch -b -p1 < fix.patch

# 여러 패치를 순서대로 적용
for p in 0001-*.patch 0002-*.patch 0003-*.patch; do
    echo "적용 중: $p"
    patch -p1 < "$p" || { echo "실패: $p"; exit 1; }
done

실전 워크플로

커널 패치 워크플로: diff/patch vs Git 전통적 방법 (diff/patch) 소스 트리 복사 코드 수정 diff -ruNp 패치 생성 메일로 패치 전송 patch -p1 적용 현대적 방법 (Git) git clone / branch git commit git format-patch git send-email git am / git apply 동일 Unified 형식 git apply = patch + 검증

커널 패치 생성/적용 예제

# ===== 전통적 방법: diff/patch =====

# 1. 소스 트리 복사
cp -a linux-6.8 linux-6.8-fix
cd linux-6.8-fix

# 2. 코드 수정
vim drivers/net/ethernet/intel/e1000e/netdev.c

# 3. 패치 생성
cd ..
diff -ruNp linux-6.8/ linux-6.8-fix/ > fix-e1000e-null-deref.patch

# 4. 패치 검증 (다른 머신에서)
cd linux-6.8
patch --dry-run -p1 < ../fix-e1000e-null-deref.patch

# 5. 패치 적용
patch -p1 < ../fix-e1000e-null-deref.patch


# ===== 현대적 방법: Git =====

# 1. 브랜치에서 작업
git checkout -b fix/e1000e-null-deref v6.8

# 2. 코드 수정 & 커밋
vim drivers/net/ethernet/intel/e1000e/netdev.c
git add -p
git commit -s   # Signed-off-by 포함

# 3. diff 확인 (Unified 형식)
git diff v6.8..HEAD

# 4. 패치 파일 생성 (커밋 메타데이터 포함)
git format-patch -1 HEAD
# → 0001-e1000e-fix-null-deref-in-netdev-open.patch

# 5. 패치 적용 (수신자)
git apply --check 0001-*.patch   # 검증만
git am 0001-*.patch              # 적용 + 커밋 생성

Git의 diff/patch 관련 명령

Git 명령대응하는 전통 명령설명
git diffdiff -u작업 디렉터리 변경 사항 표시
git diff --cacheddiff -u스테이징된 변경 사항 표시
git diff HEAD~3..HEADdiff -ruNp최근 3개 커밋의 차이
git format-patchdiff -ruNp커밋을 이메일 형식 패치로 변환
git applypatch -p1패치 파일 적용 (커밋 없음)
git ampatch -p1 + 커밋이메일 형식 패치 적용 + 커밋 생성
git diff --statdiff | diffstat변경 통계 요약
git diff -wdiff -uw공백 무시 비교
git diff --color-words단어 단위 차이 하이라이트
# git diff 유용한 옵션들
git diff --stat                      # 변경 파일 목록과 통계
git diff --name-only                 # 변경 파일명만
git diff --name-status               # 파일명 + 상태 (M/A/D/R)
git diff -w                          # 공백 무시
git diff --color-words               # 단어 단위 하이라이트
git diff --word-diff                 # 단어 단위 차이 [{+추가+}] [-삭제-]
git diff --ignore-blank-lines        # 빈 줄 차이 무시
git diff -U10                        # 문맥 10줄로 확장
git diff -- '*.c' '*.h'              # 특정 파일 패턴만
git diff --diff-filter=M             # 수정된 파일만

# git apply 옵션
git apply --check patch.diff         # 적용 가능 여부만 검사
git apply --stat patch.diff          # 통계만 표시
git apply -3 patch.diff              # 3-way 머지 시도
git apply --reject patch.diff        # 실패 헝크를 .rej로 저장
git apply --whitespace=fix patch.diff # 공백 문제 자동 수정

patchutils — 패치 파일 가공 도구

patchutils 패키지는 패치 파일을 분석하고 가공하는 유틸리티 모음입니다.

# 설치
sudo apt install patchutils       # Debian/Ubuntu
sudo dnf install patchutils       # Fedora/RHEL

filterdiff — 패치 필터링

# 특정 파일의 변경만 추출
filterdiff -i '*/drivers/net/*' big-patch.patch > net-only.patch

# 특정 파일 제외
filterdiff -x '*/Documentation/*' big-patch.patch > code-only.patch

# 특정 헝크만 추출 (파일의 N번째 헝크)
filterdiff --hunks=1,3 -i '*/foo.c' big-patch.patch

# 패턴으로 필터링 (정규표현식)
filterdiff -I 'include.*net' big-patch.patch

lsdiff — 패치 파일 목록

# 패치에 포함된 파일 목록
lsdiff kernel-fix.patch
# 출력:
# drivers/net/foo.c
# drivers/net/bar.c
# include/net/baz.h

# 경로 접두사 제거 (patch -p1과 동일)
lsdiff -p1 kernel-fix.patch

# 통계 정보와 함께 표시
lsdiff -s kernel-fix.patch
# 출력:
# ! drivers/net/foo.c      (변경)
# + drivers/net/bar.c      (추가)
# - drivers/net/old.c      (삭제)

# 헝크 수 표시
lsdiff -n kernel-fix.patch

interdiff — 패치 간 차이

# 두 패치 버전 간의 차이 (v1 → v2 변경 사항)
interdiff fix-v1.patch fix-v2.patch > v1-to-v2.diff

# 커널 리뷰에서 v2 변경 사항만 확인할 때 유용
# 리뷰어: "v1에서 뭐가 바뀌었나요?"
interdiff 0001-v1-fix.patch 0001-v2-fix.patch

combinediff — 패치 합치기

# 두 패치를 순서대로 합침
combinediff patch1.diff patch2.diff > combined.diff

# patch1을 적용한 후 patch2를 적용한 것과 동일한 결과

splitdiff — 패치 분할

# 파일별로 패치 분할
splitdiff -a big-patch.patch
# 출력: big-patch-drivers-net-foo.c.patch, big-patch-include-net-baz.h.patch, ...

# 디렉터리별로 분할
splitdiff -d big-patch.patch

rediff — 헝크 번호 재계산

# 패치 파일을 수동 편집한 후 줄 번호 수정
# (헝크를 직접 수정하면 줄 번호가 맞지 않을 수 있음)
rediff edited-patch.patch > fixed-patch.patch

quilt — 패치 스택 관리

quilt는 여러 패치를 스택(Stack)으로 관리하는 도구입니다. Debian 패키지 유지보수, 배포판 커널 커스터마이징, upstream에 여러 패치를 순서대로 관리할 때 유용합니다.

quilt 패치 스택 0003-add-feature.patch ← top 0002-refactor-api.patch 0001-fix-bug.patch 원본 소스 코드 (base) quilt push 다음 패치 적용 (push) quilt pop 최상위 패치 제거 (pop) 주요 명령 quilt new 새 패치 생성 quilt add 파일을 현재 패치에 등록 quilt refresh 현재 패치 갱신 quilt push 다음 패치 적용 quilt pop 최상위 패치 제거 quilt series 패치 목록 표시 quilt diff 현재 패치 차이 표시 quilt applied 적용된 패치 표시
# 설치
sudo apt install quilt            # Debian/Ubuntu
sudo dnf install quilt            # Fedora/RHEL

# 초기화
cd linux-6.8
quilt init

# 새 패치 생성
quilt new fix-null-deref.patch

# 수정할 파일 등록 (수정 전에 등록해야 함)
quilt add drivers/net/foo.c

# 파일 수정
vim drivers/net/foo.c

# 패치 갱신 (변경 사항을 패치 파일에 반영)
quilt refresh

# 다른 패치 추가
quilt new add-logging.patch
quilt add drivers/net/foo.c
vim drivers/net/foo.c
quilt refresh

# 패치 스택 조작
quilt pop                         # 최상위 패치 제거
quilt pop -a                      # 모든 패치 제거
quilt push                        # 다음 패치 적용
quilt push -a                     # 모든 패치 적용

# 패치 목록과 상태
quilt series                      # 전체 패치 목록
quilt applied                     # 적용된 패치 목록
quilt unapplied                   # 미적용 패치 목록
quilt top                         # 현재 최상위 패치

# 중간 패치 수정
quilt pop fix-null-deref.patch    # 해당 패치까지 되돌림
vim drivers/net/foo.c             # 수정
quilt refresh                     # 패치 갱신
quilt push -a                     # 나머지 패치 재적용

대안 도구

도구특징설치
colordiffdiff 출력에 색상 추가 (diff 래퍼)apt install colordiff
icdiff터미널 Side-by-side 컬러 diffpip install icdiff
deltagit diff 페이저, 구문 하이라이트apt install git-delta
difftasticAST 기반 구조적 diff (Structural diff)cargo install difftastic
meldGUI 3방향 머지 도구apt install meld
vimdiffVim 기반 나란히 비교/머지Vim에 포함
kdiff3GUI 3방향 머지 도구apt install kdiff3
diffoscope재현 가능 빌드 검증용 심층 비교apt install diffoscope
wdiff단어 단위 diffapt install wdiff
diffstatdiff 출력의 통계 요약apt install diffstat
# colordiff: diff에 색상 추가
colordiff -u old.c new.c

# diff 출력을 colordiff로 파이프
diff -u old.c new.c | colordiff

# vimdiff: Vim에서 나란히 비교
vimdiff old.c new.c
# ]c — 다음 차이로 이동
# [c — 이전 차이로 이동
# do — diff obtain (상대 창에서 가져오기)
# dp — diff put (상대 창으로 보내기)

# delta: git pager로 설정
git config --global core.pager delta
git diff    # 이제 delta로 보기

# difftastic: 구조적 diff (AST 기반)
difft old.c new.c
# git difftool로 사용
git config --global diff.external difft

delta 실전 설정 — 커널 리뷰 최적화

dandavison delta 0.18+는 히스토그램 알고리즘을 기본으로 사용하고, side-by-side/라인 번호/구문 하이라이트를 제공합니다. git 기본 페이저를 대체하면 대용량 패치 리뷰가 눈에 띄게 개선됩니다.

# ~/.gitconfig
[core]
    pager = delta

[interactive]
    diffFilter = delta --color-only

[delta]
    navigate = true          ; n/N으로 hunk 이동
    line-numbers = true
    side-by-side = false      ; 터미널 폭이 좁으면 false 유지
    syntax-theme = "Monokai Extended"
    features = decorations

[delta "decorations"]
    commit-decoration-style = bold yellow box ul
    file-style = bold yellow ul
    file-decoration-style = none
    hunk-header-decoration-style = cyan box ul

[merge]
    conflictStyle = zdiff3   ; delta는 zdiff3 3-way 충돌 마커 이해

[diff]
    algorithm = histogram    ; delta + histogram 조합 (커널에 권장)
    colorMoved = zebra
    colorMovedWS = allow-indentation-change

difftastic 실전 설정 — 구조적 diff

Wilfred difftastic(0.60+)은 tree-sitter 파서로 AST를 생성한 뒤 Dijkstra 최단경로로 diff를 계산합니다. 순수 라인 기반 diff에서 "주석만 바뀌었는데 함수 전체가 달라 보이는" 문제를 해결합니다. 단, 대용량 파일에서는 라인 기반보다 느립니다.

# ~/.gitconfig
[diff]
    tool = difftastic

[difftool]
    prompt = false

[difftool "difftastic"]
    cmd = difft "$LOCAL" "$REMOTE"

[pager]
    difftool = true

[alias]
    dft = difftool
    ; 사용: git dft HEAD~..HEAD -- drivers/net/
# 특정 커밋만 구조적 diff로 보기
$ git dft HEAD~1..HEAD

# 환경 변수로 외부 diff 지정 (일회성)
$ GIT_EXTERNAL_DIFF=difft git diff drivers/foo/bar.c

# 두 파일 직접 비교
$ difft old.c new.c

Myers vs Histogram vs Patience — 동일 커밋 실측

동일한 커밋에 대해 세 알고리즘으로 diff를 실행하면 결과물의 "가독성"이 달라지는 것을 확인할 수 있습니다. 커널 리뷰 경험상 histogram이 코드 이동(move)과 함수 경계 인식이 가장 좋다는 점이 JGit/libxdiff 벤치마크와 2024년 Microsoft Research 리뷰 품질 연구에서 반복 확인되었습니다.

# 세 알고리즘으로 같은 커밋 diff 생성 + 크기 비교
for algo in myers minimal histogram patience; do
    lines=$(git diff --diff-algorithm=$algo HEAD~1 HEAD | wc -l)
    echo "$algo: $lines lines"
done

# 결과 예 (단일 리팩터링 커밋):
#   myers:     412 lines
#   minimal:   398 lines
#   histogram: 356 lines   ← 가독성 최고
#   patience:  361 lines

# 전역 기본값 변경 (권장)
$ git config --global diff.algorithm histogram
Git 기본 알고리즘의 미래: Git은 여전히 Myers를 기본값으로 사용하며, 2.47~2.49에 걸쳐 histogram으로 기본 전환하자는 실험이 있었으나 호환성 이유로 확정되지 않았습니다. 개인 리뷰 환경에서는 diff.algorithm=histogram을 명시적으로 설정하는 것을 권장합니다. 자세한 내용은 시퀀스 비교 알고리즘 페이지를 참고하세요.

패치 시리즈 관리

커널에 제출하는 변경이 크면 하나의 패치 대신 패치 시리즈(Patch Series)로 분리합니다. 각 패치는 독립적으로 컴파일되고 논리적 단위를 이루어야 합니다.

패치 시리즈 구조 [PATCH v2 0/3] e1000e: fix null deref series 커버 레터: 시리즈 전체 설명, 동기, 테스트 결과 [PATCH v2 1/3] e1000e: add null check in open() [PATCH v2 2/3] e1000e: add FLAG_IS_UP tracking [PATCH v2 3/3] e1000e: add device down logging 패치 시리즈 규칙 1. [PATCH n/N] — n번째 패치 / 총 N개 2. 0/N = 커버 레터 (코드 변경 없음, 설명만) 3. 각 패치는 독립적으로 컴파일 가능해야 함 4. 논리적 단위: 리팩터링·버그 수정·기능 추가 분리 5. v2, v3 = 리뷰 피드백 반영한 재제출 버전 6. 적용 순서: 1→2→3 (의존성 순서) 변경 이력 (커버 레터에 포함): v1→v2: 패치 2의 플래그명 변경 (리뷰어 A 요청) v1→v2: 패치 3에 netdev_info 로깅 추가 v2→v3: Signed-off-by 이메일 수정
# ===== git format-patch로 패치 시리즈 생성 =====

# 최근 3개 커밋을 패치 시리즈로 (커버 레터 포함)
git format-patch -3 --cover-letter -v2
# 생성 파일:
#   v2-0000-cover-letter.patch  ← 커버 레터 (Subject/내용 직접 편집)
#   v2-0001-e1000e-add-null-check.patch
#   v2-0002-e1000e-add-flag-tracking.patch
#   v2-0003-e1000e-add-logging.patch

# 커버 레터 편집
vim v2-0000-cover-letter.patch
# Subject: [PATCH v2 0/3] e1000e: fix null deref in open path
# --- 아래에 시리즈 설명, 변경 이력 작성 ---

# 수신자 자동 설정
scripts/get_maintainer.pl v2-000*.patch

# 전체 시리즈 전송 (커버 레터의 Message-Id를 In-Reply-To로 연결)
git send-email --to=... --cc=... v2-000*.patch


# ===== 전통적 diff/patch로 패치 시리즈 관리 =====

# 순서대로 적용
for p in 0001-*.patch 0002-*.patch 0003-*.patch; do
    echo "=== 적용: $p ==="
    patch --dry-run -p1 < "$p" || { echo "검증 실패: $p"; exit 1; }
    patch -p1 < "$p"
done

# 역순으로 되돌리기
for p in 0003-*.patch 0002-*.patch 0001-*.patch; do
    patch -R -p1 < "$p"
done

패치 파일 수동 편집

때로는 패치 파일을 직접 편집해야 합니다. 예를 들어 불필요한 변경을 제거하거나, 백포트를 위해 경로를 수정하거나, 패치에서 특정 헝크만 분리할 때입니다.

# ===== 헝크 제거 =====
# 패치에서 특정 헝크를 제거하려면:
# 1. 해당 @@ 줄부터 다음 @@ 줄(또는 파일 끝/다음 --- 줄) 직전까지 삭제
# 2. 파일 헤더의 줄 수는 보통 자동 무시됨 (patch가 실제 내용 기준)

# ===== 경로 수정 =====
# 패치의 파일 경로가 현재 소스 트리와 다른 경우
sed -i 's|a/old/path/|a/new/path/|g; s|b/old/path/|b/new/path/|g' fix.patch

# ===== 줄 번호 재계산 =====
# 헝크 내용을 수동 편집한 후 줄 번호가 맞지 않으면
rediff edited.patch > fixed.patch   # patchutils 필요

# ===== 패치 분리 (filterdiff 대안) =====
# 직접 에디터로 패치를 분리하는 방법:
# 1. 원본 패치를 복사
cp big.patch part-a.patch
cp big.patch part-b.patch
# 2. part-a.patch에서 불필요한 파일 섹션 삭제
# 3. part-b.patch에서 반대로 삭제
# 4. 각각 --dry-run으로 검증
patch --dry-run -p1 < part-a.patch
patch --dry-run -p1 < part-b.patch

# ===== 주의사항 =====
# - 문맥 줄(공백 접두사)은 절대 수정하면 안 됩니다
# - +/- 줄을 수정하면 해당 헝크의 줄 수(@@ 헤더)도 수정해야 합니다
# - 문맥 줄의 첫 번째 공백 문자가 실제 내용이 아닌 접두사임에 주의
# - 편집 후 반드시 patch --dry-run으로 검증
패치 편집 시 흔한 실수:
  • 탭/공백 혼동 — 문맥 줄의 접두사 공백(space)과 들여쓰기 탭(tab)을 혼동합니다. Unified 형식에서 문맥 줄의 첫 문자는 항상 공백(space)입니다.
  • 줄 수 불일치+ 줄을 추가/삭제하면 @@ 헤더의 줄 수가 맞지 않습니다. rediff로 자동 수정하거나, 직접 수정해야 합니다.
  • 불완전한 줄(No newline at end of file) — 파일 끝에 개행이 없으면 \ No newline at end of file 표시가 있습니다. 이 줄을 삭제하면 패치가 깨집니다.

커널 소스 내 diff/patch 관련 코드

리눅스 커널 소스 트리에는 diff/patch 워크플로를 지원하는 여러 스크립트와 도구가 포함되어 있습니다.

경로용도설명
scripts/checkpatch.pl패치 검증코딩 스타일(Coding Style), 커밋 메시지, Signed-off-by 검사. checkpatch.pl *.patch로 실행
scripts/get_maintainer.pl수신자 결정MAINTAINERS 파일 기반으로 패치 수신자(To/Cc) 결정
scripts/diffconfig.config 비교두 커널 설정 파일의 차이를 "CONFIG_X n -> y" 형식으로 표시
scripts/bloat-o-meter크기 비교두 vmlinux의 심볼별 크기 변화 표시. 패치의 코드 크기 영향 측정
scripts/decode_stacktrace.sh스택 디코딩패치 적용 전후 oops 주소를 소스 줄로 변환
Documentation/process/프로세스(Process) 문서submitting-patches.rst, email-clients.rst 등 패치 제출 가이드
# checkpatch.pl로 패치 검증 (제출 전 필수)
scripts/checkpatch.pl --strict v2-0001-*.patch
# ERROR: Missing Signed-off-by: line(s)
# WARNING: line over 100 characters

# 패치 파일이 아닌 소스 파일 직접 검사
scripts/checkpatch.pl --file drivers/net/foo.c

# get_maintainer.pl로 수신자 확인
scripts/get_maintainer.pl v2-0001-*.patch
# Jesse Brandeburg <...> (maintainer:INTEL ETHERNET DRIVERS)
# netdev@vger.kernel.org (open list:NETWORKING DRIVERS)

# bloat-o-meter로 패치의 코드 크기 영향 측정
scripts/bloat-o-meter vmlinux.before vmlinux.after
# add/remove: 2/0 grow/shrink: 1/0 up/down: 156/0 (156)
# Function                                     old     new   delta
# e1000_open                                   542     698    +156

실전 레시피 모음

stable 커널 패치 백포트

# mainline 패치를 stable 커널에 적용
cd linux-6.6
patch --dry-run -p1 < ../mainline-fix.patch

# 오프셋/퍼지로 적용 가능한 경우
patch -p1 < ../mainline-fix.patch

# 충돌 발생 시: .rej 확인 후 수동 해결
patch -p1 < ../mainline-fix.patch
# → FAILED 헝크가 있으면 .rej 파일 확인
cat drivers/net/foo.c.rej
# → 해당 부분을 에디터에서 수동으로 적용
vim drivers/net/foo.c
# → 정리
rm drivers/net/foo.c.rej drivers/net/foo.c.orig

큰 패치를 파일별로 분리

# patchutils의 splitdiff 사용
splitdiff -a big-patch.patch

# 또는 filterdiff로 특정 서브시스템만 추출
filterdiff -i '*/drivers/net/*' big-patch.patch > net-changes.patch
filterdiff -i '*/include/*' big-patch.patch > header-changes.patch

이메일로 받은 패치 적용

# 이메일 본문에서 패치 추출 (git format-patch 형식)
git am patch-email.mbox

# 일반 diff 패치를 이메일에서 저장한 경우
patch -p1 < saved-patch.diff

# b4 도구로 LKML에서 패치 시리즈 가져오기
b4 am 20240101120000.12345-1-author@example.com
git am *.mbx

바이너리 파일 차이 확인

# diff는 바이너리 파일이면 "Binary files differ"만 표시
diff firmware-v1.bin firmware-v2.bin
# Binary files firmware-v1.bin and firmware-v2.bin differ

# 16진수 덤프로 비교
diff <(xxd firmware-v1.bin) <(xxd firmware-v2.bin) | head -30

# cmp: 첫 번째 다른 바이트 위치
cmp firmware-v1.bin firmware-v2.bin
# firmware-v1.bin firmware-v2.bin differ: byte 1024, line 8

# cmp -l: 모든 다른 바이트 나열
cmp -l firmware-v1.bin firmware-v2.bin

커널 .config 비교

# scripts/diffconfig (커널 소스 내장)
scripts/diffconfig .config.old .config
# 출력 예:
#  CONFIG_DEBUG_INFO n -> y
# -CONFIG_DEBUG_INFO_NONE y
# +CONFIG_DEBUG_INFO_DWARF5 y
# +CONFIG_KASAN y

# 일반 diff로도 비교 가능 (주석/빈줄 제외)
diff <(grep -v '^#\|^$' .config.old | sort) \
     <(grep -v '^#\|^$' .config | sort)

diff 알고리즘 이해

diff는 원본과 수정본을 줄 시퀀스(Sequence)로 보고 최단 편집 스크립트(Shortest Edit Script, SES)를 찾습니다. 전통적인 diff 모델에는 치환 연산이 따로 없으므로, 공통으로 남는 줄들의 최대 집합이 곧 LCS(Longest Common Subsequence, 최장 공통 부분 수열)입니다. 즉 LCS는 "무엇을 남길지", SES는 "무엇을 삭제하고 추가할지"를 표현한 같은 정보의 다른 관점입니다.

원본 줄 수를 n, 수정본 줄 수를 m, LCS 길이를 L이라 하면 SES 길이 DD = n + m - 2L입니다. 따라서 LCS가 길수록 삭제와 추가가 줄어듭니다. Myers 알고리즘은 이 관계를 바탕으로 편집 그래프(Edit Graph)에서 SES를 직접 찾아 실제 diff 출력에 쓸 수 있는 결과를 빠르게 만듭니다.

diff 알고리즘: 공통 줄 유지 → SES 생성 원본 (A) 1: #include <stdio.h> 2: (빈 줄) 3: int main(void) 4: printf("Hello") 5: return 0; 6: } 수정본 (B) 1: #include <stdio.h> 2: #include <stdlib.h> 3: (빈 줄) 4: int main(int argc, ...) 5: printf("Hello, %s") 6: return 0; 7: } Myers 알고리즘 (1986) 1. 편집 그래프(Edit Graph)에서 경로 확장 2. D = 0, 1, 2 ... 순서로 SES 탐색 3. 대각선 = 공통, →삭제, ↓추가 복잡도: O(ND) N = 두 시퀀스 길이 합 D = SES 길이 유사한 파일일수록 D가 작아 빠름 GNU diff 가독성 보정: - 빈 줄 뒤로 헝크 경계 이동 - 함수 경계 인식 (-p 옵션) - Git은 patience/histogram 추가 제공
더 깊은 이론: 동적 계획법 표 채우기, D = n + m - 2L 관계식, 편집 그래프, Myers 경로 탐색은 시퀀스 비교 알고리즘 문서에서 단계별로 설명합니다.

Git의 diff 알고리즘

Git은 Myers 외에도 가독성을 높이기 위한 여러 diff 알고리즘을 제공합니다. 최소 편집 길이만 맞는다고 항상 읽기 좋은 패치가 되는 것은 아니므로, 코드 이동이나 반복 줄이 많은 변경에서는 다른 선택지가 더 유리할 수 있습니다.

알고리즘옵션특징
Myers (기본)--diff-algorithm=myersSES를 빠르게 찾는 기본 diff. LCS와 쌍대 관계
Patience--diff-algorithm=patience공통 고유 줄을 앵커로 사용, 코드 이동에 강함
Histogram--diff-algorithm=histogramPatience 확장판. 낮은 빈도 공통 줄 처리 개선
Minimal--diff-algorithm=minimalMyers를 더 오래 탐색해 더 작은 diff를 시도
# Patience diff 사용 (코드 리팩터링 시 권장)
git diff --patience

# Histogram diff (Git 기본 권장 후보)
git diff --histogram

# 기본 알고리즘 변경
git config --global diff.algorithm histogram

트러블슈팅

증상원인해결
can't find file to patch -p 값이 잘못됨 lsdiff -p1 patch.diff로 경로 확인 후 올바른 -p 값 사용
Hunk FAILED 원본이 패치 생성 시점과 다름 .rej 파일 확인 후 수동 적용, 또는 패치 재생성
Reversed patch detected 이미 적용된 패치를 다시 적용 -R로 되돌리거나, 이미 적용됨을 확인
줄바꿈 문제 (Windows ↔ Unix) CR/LF vs LF 차이 dos2unix 변환 후 diff, 또는 --strip-trailing-cr
인코딩 문제 UTF-8 vs EUC-KR 등 iconv로 인코딩 통일 후 diff
git apply 실패 공백 문제 git apply --whitespace=warn 또는 --whitespace=fix
patch: **** malformed patch 패치 파일 손상 (이메일 줄바꿈 등) git am --patch-format=mbox 또는 b4으로 재다운로드
빈 줄/공백만 다른 패치 에디터 설정 차이 diff -B -w로 의미 없는 차이 무시

diff/patch 모범 사례

커널 개발 모범 사례:
  • 항상 Unified 형식(-u)을 사용합니다. Normal/Context 형식은 레거시(Legacy)입니다.
  • 디렉터리 비교 시 -ruNp 옵션 조합을 사용합니다 (재귀 + Unified + 새 파일 + 함수명).
  • patch --dry-run으로 항상 먼저 검증합니다.
  • 커널 소스에서는 patch -p1이 표준입니다.
  • 퍼지(-F)를 높이기보다 패치를 재생성하는 것이 안전합니다.
  • .rej.orig 파일은 반드시 정리합니다.
  • Git 환경에서는 git format-patch / git am을 우선합니다.
  • 대규모 변경은 논리적 단위로 분리하여 별도 패치를 만듭니다.

참고 자료