GCC (GNU Compiler Collection) 완전 가이드
커널 빌드에서 GCC가 만들어내는 산출물을 이해할 수 있도록 옵션 의미를 연결해 설명합니다. 전처리부터 링크까지 단계별 산출물, 경고 정책과 `-Werror` 운용, `-O2` 기반 최적화와 디버그 정보 균형, 아키텍처별 보안 완화 플래그, inline asm/`__builtin_*`/attribute 사용 시 주의점, 크로스 컴파일(Cross Compilation) 툴체인 구성, KCFLAGS를 포함한 Kbuild 통합 운용까지 실무 중심으로 상세히 정리합니다.
핵심 요약
- GCC — GNU Compiler Collection. C, C++, Fortran, Ada, Go, D, COBOL, Objective-C, Modula-2 등을 지원하는 통합 컴파일러 배포판.
- 컴파일 4단계 — 전처리(
-E) → 컴파일(-S) → 어셈블(-c) → 링크(기본), 각 단계에서 중단 가능. - 최적화 레벨 —
-O0(디버깅용) ~-O3(최대 속도),-Os(코드 크기),-Og(디버깅(Debugging) 친화적 최적화),-Ofast(표준 무시 최대 성능). - -ffreestanding — 호스트 환경 없이 동작하는 독립 구현 모드. 리눅스 커널 빌드의 핵심 플래그.
- 속성(attribute) —
__attribute__((noreturn)),__attribute__((packed))등 함수/변수/타입에 컴파일러 힌트 제공. - CPU 취약점(Vulnerability) 완화 —
-mindirect-branch=thunk-extern(Spectre v2 Retpoline),-mharden-sls=all(SLS),-mbranch-protection=pac-ret+bti(ARM64 PAC/BTI). - GCC 플러그인 — 커널 보안 플러그인: structleak(스택 0 초기화), randstruct(구조체(Struct) 순서 무작위화), stackleak(스택 독약값), latent_entropy(엔트로피 수집).
- 머신 의존 옵션 —
-mno-red-zone(레드존 금지),-mcmodel=kernel(커널 주소 모델),-mno-sse(FPU 금지) — 커널 필수 플래그.
단계별 이해
- 컴파일 4단계 구조 파악
소스 코드가 실행 파일이 되기까지의 각 단계와 중간 산출물(*.i, *.s, *.o)을 이해합니다. - 경고 옵션 적용
-Wall -Wextra -Werror로 코드 품질을 높이고, 커널에서 사용되는 경고 억제 패턴을 익힙니다. - 최적화 레벨 선택
개발 단계는-Og, 릴리스는-O2, 커널은 기본적으로-O2를 사용합니다. - GCC 확장 습득
커널 코드에서 광범위하게 쓰이는__attribute__,likely/unlikely, 인라인 어셈블리(Assembly) 문법을 익힙니다. - 크로스 컴파일 환경 설정
CROSS_COMPILE변수와ARCH환경 변수로 타깃 아키텍처별 빌드를 제어합니다. - 머신 의존 옵션 이해
커널은-mno-red-zone,-mcmodel=kernel,-mno-sse같은 아키텍처 전용 플래그가 필수입니다. 이를 빠뜨리면 런타임 오류가 발생합니다. - CPU 취약점 완화 적용
Spectre v2(Retpoline), SLS, ARM64 PAC/BTI 완화 옵션을 Kconfig와 연동하여 조건부 적용합니다. - GCC 플러그인 활성화
CONFIG_GCC_PLUGINS=y로 커널 보안 플러그인을 활성화합니다. 구조체 정보 누출, 스택 초기화, 무작위화를 컴파일러 수준에서 강화합니다.
GCC 개요 및 아키텍처
GCC는 "GNU Compiler Collection"의 약자로, GNU 프로젝트의 핵심 컴파일러 배포판입니다. 원래 "GNU C Compiler"를 의미했으나, C 이외의 언어를 지원하면서 "Collection"으로 확장되었습니다. GCC는 단일 언어에 종속되지 않는 언어 독립 공통 최적화 계층(middle-end)과 각 언어별 프론트엔드(front end), 타깃 아키텍처별 백엔드(back end)로 구성됩니다.
GCC가 호출될 때 기본적으로 전처리 → 컴파일 → 어셈블 → 링크의 4단계를 모두 실행합니다. -c 옵션으로 링크 이전 단계에서 멈추거나, -S로 어셈블리 출력, -E로 전처리 결과만 출력할 수 있습니다.
주요 전통 컴파일러 이름
| 언어 | GCC 드라이버 | 비고 |
|---|---|---|
| C | gcc | 기본 드라이버 |
| C++ | g++ | C++ 라이브러리 자동 링크 |
| COBOL | gcobol | GCC 15 신규 지원 |
| Ada | gnat | GNAT 컴파일러 |
| Fortran | gfortran | GNU Fortran |
| Go | gccgo | GCC 4.7.1+ |
| D | gdc | D 2.0 지원 |
| Objective-C | gcc -lobjc | GNU ObjC 런타임 |
지원 언어 및 표준
GCC는 각 언어에 대한 공식 표준을 따르며, GNU 확장이 포함된 버전도 지원합니다. -std= 옵션으로 표준 버전을 명시하고, -pedantic을 추가하면 표준에서 요구하는 진단을 모두 활성화합니다.
C 언어 표준
| 표준 | GCC 옵션 | GNU 확장 포함 | 특징 |
|---|---|---|---|
| C89 / C90 | -std=c90 / -ansi | -std=gnu90 | ANSI C, 가장 오래된 표준 |
| C94 / AMD1 | -std=iso9899:199409 | — | 다이그래프 추가 |
| C99 | -std=c99 | -std=gnu99 | 가변 배열, inline, // 주석, stdint.h |
| C11 | -std=c11 | -std=gnu11 | 원자 연산, 스레드(Thread), 정적 어설션 |
| C17 | -std=c17 | -std=gnu17 | C11 수정판, __STDC_VERSION__ 차이 |
| C23 | -std=c23 | -std=gnu23 | nullptr, _BitInt, constexpr, 2024년 제정 ISO/IEC 9899:2024 |
| C2Y (개발 중) | -std=c2y | -std=gnu2y | 실험적·미완성 지원 |
-std=gnu23을 기본으로 사용합니다 (GCC 15 기준). 커널 빌드에서는 -std=gnu11을 사용합니다.
C++ 언어 표준
| 표준 | GCC 옵션 | GNU 확장 포함 | 특징 |
|---|---|---|---|
| C++98 / C++03 | -std=c++98 | -std=gnu++98 | 원조 ISO C++ 표준 |
| C++11 | -std=c++11 | -std=gnu++11 | 람다, auto, range-for, 이동 의미론 |
| C++14 | -std=c++14 | -std=gnu++14 | 일반 람다, 변수 템플릿 |
| C++17 | -std=c++17 | -std=gnu++17 | 구조적 바인딩, if constexpr, std::optional |
| C++20 | -std=c++20 | -std=gnu++20 | 개념(Concepts), 코루틴, 모듈, ranges |
| C++23 | -std=c++23 | -std=gnu++23 | print, stacktrace, 추가 뷰, ISO/IEC 14882:2024 |
-std=gnu++17을 기본으로 사용합니다. 커널에서 Rust와 C++ 혼용은 금지되어 있으며, 커널 C++ 코드(있다면)는 제한적 서브셋만 허용합니다.
독립 구현 환경 (Freestanding Environment)
C 표준은 두 가지 구현 환경을 정의합니다. 리눅스 커널은 독립 구현 환경(freestanding environment)에서 동작합니다.
| 환경 | 설명 | GCC 옵션 | 예시 |
|---|---|---|---|
| Hosted | OS와 C 표준 라이브러리 전체 사용 가능. int main() 엔트리 포인트. | (기본값) | 일반 사용자 프로그램 |
| Freestanding | OS 없이 동작. <float.h>, <limits.h>, <stdarg.h>, <stddef.h>만 보장. | -ffreestanding | OS 커널, 부트로더(Bootloader), 임베디드 펌웨어(Firmware) |
# 리눅스 커널 Makefile에서 GCC 독립 환경 설정
KBUILD_CFLAGS += -ffreestanding # 독립 구현 모드
KBUILD_CFLAGS += -fno-builtin # 내장 함수 비활성화
KBUILD_CFLAGS += -fno-stack-protector # 기본 스택 보호 비활성화(커널이 직접 관리)
KBUILD_CFLAGS += -fno-PIE # 위치 독립 실행 파일 비활성화
컴파일 단계 상세
GCC는 하나의 명령으로 네 단계를 순차적으로 실행합니다. 각 단계에서 중단하면 중간 산출물을 확인할 수 있습니다.
# 단계별 중단 예시
$ gcc -E foo.c -o foo.i # 전처리만 (매크로 확장 결과 확인)
$ gcc -S foo.c -o foo.s # 어셈블리 출력 (최적화 결과 확인)
$ gcc -c foo.c -o foo.o # 오브젝트 파일 생성
$ gcc foo.o bar.o -o program # 링크
# 한 번에 컴파일 + 링크
$ gcc foo.c bar.c -o program
# 특정 언어 표준으로 컴파일
$ gcc -std=gnu11 -Wall -O2 foo.c -o foo
# 크로스 컴파일 (ARM64 타깃)
$ aarch64-linux-gnu-gcc -march=armv8-a foo.c -o foo.arm64
경고 옵션 (-W...)
GCC는 방대한 경고 체계를 제공합니다. 커널 개발에서는 경고를 오류로 취급(-Werror)하여 코드 품질을 강제하는 경우가 많습니다.
핵심 경고 옵션
| 옵션 | 설명 | 커널 사용 여부 |
|---|---|---|
-Wall | 가장 유용한 경고들의 묶음. 대부분의 일반적 버그 탐지. | ✓ 항상 사용 |
-Wextra | -Wall에 포함되지 않은 추가 경고 활성화. | ✓ 사용 |
-Werror | 모든 경고를 오류로 처리. CI/빌드 품질 강제. | 선택적 사용 |
-Wpedantic | 표준에서 요구하는 모든 진단 메시지 출력. | 부분 사용 |
-Wshadow | 변수 은닉(shadow) 경고. | ✓ 사용 |
-Wunused | 미사용 변수/함수/파라미터/라벨 경고. | ✓ 사용 |
-Wformat=2 | printf 형식 문자열 보안 강화 경고. | ✓ 사용 |
-Wmissing-prototypes | 프로토타입 없는 함수 정의 경고 (C only). | ✓ 사용 |
-Wimplicit-fallthrough | switch-case 폴스루(fallthrough) 경고. | ✓ 사용 |
-Wvla | 가변 길이 배열(VLA) 사용 경고. | ✓ 커널에서 금지 |
-Wstack-usage=N | 스택 사용량이 N 바이트를 초과하면 경고. | ✓ 사용 (커널: 1024) |
-Wno-unused-parameter | 미사용 파라미터 경고 억제 (커널 콜백(Callback)에 필요). | ✓ 사용 |
-Wstrict-prototypes | 엄격한 프로토타입 요구 경고. | ✓ 사용 |
# 리눅스 커널의 경고 옵션 일부 (scripts/Makefile.extrawarn 참고)
KBUILD_CFLAGS += -Wall
KBUILD_CFLAGS += -Wextra
KBUILD_CFLAGS += -Wno-unused-parameter
KBUILD_CFLAGS += -Wmissing-prototypes
KBUILD_CFLAGS += -Wstrict-prototypes
KBUILD_CFLAGS += -Wformat=2
KBUILD_CFLAGS += -Wimplicit-fallthrough=5
KBUILD_CFLAGS += -Wvla # VLA 금지
KBUILD_CFLAGS += -Wframe-larger-than=2048 # 스택 프레임 제한
정적 분석기 (Static Analyzer)
GCC 10+에서 -fanalyzer 옵션으로 절차 간 정적 분석을 활성화할 수 있습니다. 메모리 누수, 이중 해제(Double Free), 버퍼 오버플로(Buffer Overflow), null 역참조(Dereference) 등을 컴파일 타임에 탐지합니다.
$ gcc -fanalyzer foo.c # 정적 분석기 활성화
# 주요 분석기 경고 (Wno-analyzer-* 로 억제 가능)
# -Wanalyzer-null-dereference : null 역참조
# -Wanalyzer-malloc-leak : 메모리 누수
# -Wanalyzer-double-free : 이중 해제
# -Wanalyzer-out-of-bounds : 배열 경계 초과
# -Wanalyzer-use-after-free : 해제 후 사용
최적화 옵션 (-O...)
GCC 최적화는 수백 가지 개별 패스(pass)로 구성됩니다. -O 레벨은 이 패스들의 묶음을 활성화하는 단축키입니다.
최적화 레벨 비교
| 레벨 | 설명 | 주요 활성화 패스 | 용도 |
|---|---|---|---|
-O0 | 최적화 없음 (기본값). 컴파일 속도 최대, 디버깅 쉬움. | 없음 | 개발 초기 디버깅 |
-O1 | 기본 최적화. 실행 속도/크기 모두 개선, 컴파일 시간 크게 증가 없음. | DCE, CSE, 기본 인라이닝 | 일반 개발 |
-O2 | 권장 최적화. 대부분의 최적화 활성화. 루프 최적화, 인라이닝 강화. | -O1 + 루프 최적화, 별칭 분석, 스케줄링 | 리눅스 커널 기본값 |
-O3 | 적극적 최적화. 벡터화, 함수 복제, 투기적 최적화 포함. | -O2 + 벡터화, 함수 복제, loop unroll | HPC, 성능 중요 코드 |
-Os | 코드 크기 최적화. 실행 속도보다 바이너리 크기를 줄임. | -O2 중 크기 증가 패스 제외 | 임베디드, 부트로더 |
-Og | 디버깅 친화적 최적화. 디버거 경험을 해치지 않는 수준의 최적화. | -O1 서브셋 + 디버깅 정보 보존 | 개발 단계 권장 |
-Ofast | 표준 준수 무시 최대 성능. -ffast-math 등 포함. | -O3 + -ffast-math + 표준 위반 허용 | 수치 계산 (주의 필요) |
-Oz | 코드 크기 극한 최적화 (LLVM 유래, GCC도 지원). | -Os보다 더 강화된 크기 최소화 | 크기 극한 최소화 |
주요 개별 최적화 플래그
# 인라이닝 제어
-finline-functions # 모든 함수 인라인 시도 (-O3 포함)
-finline-limit=200 # 인라인 크기 한계 (기본 600)
-fno-inline # 인라이닝 비활성화
# LTO (Link-Time Optimization)
-flto # 링크 타임 최적화 활성화
-flto=auto # 병렬 LTO (코어 수 자동 결정)
-flto-partition=one # 단일 파티션 LTO
# 루프 최적화
-ftree-vectorize # 자동 벡터화 (-O3 포함)
-funroll-loops # 루프 언롤링
-floop-interchange # 루프 교환으로 캐시 친화성 향상
-fprefetch-loop-arrays # 루프 내 배열 프리페치
# 수학 최적화 (주의: 부동소수점 정확도 변경)
-ffast-math # IEEE 비준수, 공격적 수학 최적화
-fno-math-errno # errno 없이 수학 함수 최적화
# 함수/데이터 섹션 분리 (링커가 미사용 제거)
-ffunction-sections # 함수별 개별 섹션
-fdata-sections # 데이터별 개별 섹션
# 링커 옵션 (ld): --gc-sections # 미사용 섹션 제거
# PGO (Profile-Guided Optimization)
-fprofile-generate # 1단계: 프로파일 데이터 수집
-fprofile-use # 2단계: 프로파일 기반 최적화 적용
--param 세밀 조정
--param name=value 옵션으로 최적화 파라미터를 세밀하게 조정할 수 있습니다.
--param max-inline-insns-single=500 # 인라이닝 최대 명령어 수
--param max-unroll-times=8 # 루프 언롤 최대 횟수
--param large-function-insns=3000 # 큰 함수 기준
디버깅 옵션 (-g...)
디버깅 정보는 ELF 파일에 포함되며, GDB 같은 디버거가 소스 레벨 디버깅을 수행하는 데 사용됩니다. 커널에서는 -g로 빌드한 vmlinux를 KGDB나 crash 도구로 분석합니다.
| 옵션 | 설명 | 출력 형식 |
|---|---|---|
-g | 기본 디버깅 정보 생성 (OS 기본 형식). | 플랫폼 의존 |
-ggdb | GDB에 최적화된 디버깅 정보 (DWARF 확장 포함). | GDB 전용 |
-gdwarf | DWARF 형식 디버깅 정보 (기본은 DWARF 5). | DWARF |
-gdwarf-4 | DWARF 4 버전 명시 (넓은 호환성). | DWARF 4 |
-g1 | 최소 디버깅 정보 (줄 번호만). | DWARF 최소 |
-g3 | 매크로(Macro) 정의 포함 (가장 상세). | DWARF 최대 |
-gsplit-dwarf | DWARF 정보를 별도 .dwo 파일로 분리. 빌드 속도 향상. | 분산 DWARF |
-gbtf | BPF Type Format (BTF) 생성. eBPF 프로그램에 필요. | BTF |
-gctf | Compact C Type Format (CTF) 생성. | CTF |
# 커널 디버깅 빌드 예시
$ make ARCH=x86_64 CONFIG_DEBUG_INFO=y vmlinux
# → -g -gdwarf-4 로 빌드된 vmlinux 생성
# BTF 활성화 (eBPF CO-RE 지원)
$ make CONFIG_DEBUG_INFO_BTF=y vmlinux
# 분할 DWARF (빌드 속도 향상)
$ make CONFIG_DEBUG_INFO_SPLIT=y vmlinux
# GDB로 커널 디버깅
$ gdb vmlinux
(gdb) target remote :1234 # KGDB / QEMU gdbserver
(gdb) bt # 스택 트레이스
보안 & 새니타이저 옵션
GCC는 런타임 오류 탐지를 위한 새니타이저(Sanitizer)와 스택 보호, CFI(Control Flow Integrity) 등 다양한 보안 강화 옵션을 제공합니다. 커널은 일부 새니타이저를 자체 통합한 KASAN, KCSAN, KMSAN을 제공합니다.
-fsanitize 옵션
| 옵션 | 탐지 대상 | 커널 대응 |
|---|---|---|
-fsanitize=address | 힙/스택 버퍼 오버플로, use-after-free, use-after-return | KASAN |
-fsanitize=thread | 데이터 경쟁(data race) | KCSAN |
-fsanitize=memory | 미초기화 메모리 읽기 | KMSAN |
-fsanitize=undefined | 정의되지 않은 동작(UB): 정수 오버플로(Integer Overflow), null 역참조 등 | UBSAN |
-fsanitize=leak | 메모리 누수 | — |
-fsanitize=bounds | 배열 경계 초과 (정적 크기) | UBSAN 일부 |
-fsanitize=cfi | 간접 호출 타입 불일치 (CFI) | CFI (Clang) |
스택 보호 옵션
# 스택 보호 (Stack Canary)
-fstack-protector # 기본 스택 카나리 (취약한 함수만)
-fstack-protector-strong # 강화된 스택 카나리 (권장)
-fstack-protector-all # 모든 함수에 스택 카나리
# 스택 클래시 방어
-fstack-clash-protection # 스택-힙 충돌 방어
# 제어 흐름 무결성
-fcf-protection=full # Intel CET (IBT + SHSTK)
-fcf-protection=branch # 간접 분기만 보호
-fcf-protection=return # 반환 주소만 보호
# 포지션 독립 실행 (커널에서는 보통 비활성화)
-fPIC # 위치 독립 코드
-fPIE # 위치 독립 실행 파일
-fno-PIE # PIE 비활성화 (커널 코어)
# 하드닝 옵션 (-fharden-* GCC 14+)
-fharden-compares # 비교 연산 하드닝
-fharden-conditional-branches # 조건 분기 하드닝
-fhardcfr-check-exceptions # CFR 예외 경로 검사
CPU 취약점 완화 옵션 (Spectre / Meltdown)
Spectre, Meltdown 등 마이크로아키텍처 투기 실행 취약점을 컴파일러 수준에서 완화합니다. 커널은 CONFIG_RETPOLINE 등 Kconfig 기호로 조건부 적용합니다.
| 옵션 | 취약점 | 설명 |
|---|---|---|
-mindirect-branch=thunk | Spectre v2 | 간접 분기를 retpoline thunk로 치환 (x86) |
-mindirect-branch=thunk-extern | Spectre v2 | 외부 thunk 사용 — 커널 기본값 |
-mfunction-return=thunk-extern | Spectre v2 / RET | 함수 반환을 외부 thunk로 치환 |
-mindirect-branch-register | Spectre v2 | 간접 분기를 레지스터(Register) 기반으로만 허용 |
-mharden-sls=all | SLS (Straight-Line Speculation) | 직선 추측 실행 완화 — GCC 12+, 커널 사용 |
-mharden-sls=retbr | SLS | ret/br 명령어 뒤 INT3 삽입 |
-mbranch-protection=standard | ARM64 ROP/JOP | PAC + BTI 동시 활성화 (ARMv8.3+/8.5+) |
-mbranch-protection=pac-ret | ARM64 ROP | 반환 주소 포인터 인증(PAC-RET) |
-mbranch-protection=bti | ARM64 JOP | 분기 타깃 식별자(BTI) |
-mspeculative-load-hardening | Spectre v1 | 투기적 로드 하드닝 (주로 Clang) |
-ftrivial-auto-var-init=zero | 정보 누출 | 스택 변수 자동 0 초기화 (GCC 12+) |
# 커널 Spectre v2 완화 (arch/x86/Makefile 발췌)
ifdef CONFIG_MITIGATION_RETPOLINE
KBUILD_CFLAGS += -mindirect-branch=thunk-extern
KBUILD_CFLAGS += -mfunction-return=thunk-extern
KBUILD_CFLAGS += -mindirect-branch-register
endif
ifdef CONFIG_MITIGATION_SLS
KBUILD_CFLAGS += -mharden-sls=all
endif
# ARM64 PAC/BTI (arch/arm64/Makefile 발췌)
ifdef CONFIG_ARM64_PTR_AUTH_KERNEL
KBUILD_CFLAGS += -mbranch-protection=pac-ret
endif
ifdef CONFIG_ARM64_BTI_KERNEL
KBUILD_CFLAGS += -mbranch-protection=pac-ret+bti
endif
# 스택 변수 자동 초기화 (정보 누출 방지)
ifdef CONFIG_INIT_STACK_ALL_ZERO
KBUILD_CFLAGS += -ftrivial-auto-var-init=zero
endif
__attribute__ 30+개 완전 참조, typeof/statement-expr/computed-goto, 커널 핵심 매크로(container_of/likely/READ_ONCE), sparse 어노테이션, 커널 타입 시스템은
C 언어 완전 가이드 & 커널 C 관용어를 참조하세요.
GCC 확장 기능
GCC는 C 표준을 넘어서는 강력한 확장 기능을 제공합니다. 리눅스 커널은 이 확장들을 광범위하게 활용합니다. -pedantic 없이 -std=gnu11로 컴파일하면 대부분 사용 가능합니다.
함수 속성 (__attribute__)
/* 함수 속성 — 커널에서 자주 쓰이는 것들 */
/* noreturn: 함수가 반환하지 않음을 컴파일러에 알림 */
static __attribute__((noreturn)) void panic(const char *fmt, ...);
/* noinline: 인라이닝 금지 */
static __attribute__((noinline)) int slow_path(int x);
/* always_inline: 항상 인라인 강제 */
static inline __attribute__((always_inline)) u32 fast_read(void *p);
/* cold: 자주 호출되지 않는 함수 (오류 경로) */
static __attribute__((cold)) void error_handler(int err);
/* hot: 자주 호출되는 핫 함수 */
static __attribute__((hot)) int fast_path(int x);
/* pure: 부작용 없는 순수 함수 (같은 입력 → 같은 출력) */
static __attribute__((pure)) int hash(const char *key, int len);
/* const: pure보다 강함 — 메모리 접근도 없음 */
static __attribute__((const)) int popcount(u32 n);
/* visibility: 심볼 가시성 제어 */
__attribute__((visibility("hidden"))) void internal_func(void);
/* constructor / destructor: main() 전/후 실행 */
__attribute__((constructor)) static void init_on_load(void);
__attribute__((destructor)) static void cleanup(void);
/* format: printf 스타일 형식 문자열 검사 */
extern int myprintf(const char *fmt, ...)
__attribute__((format(printf, 1, 2)));
/* section: 특정 링커 섹션에 배치 */
__attribute__((section(".init.text"))) static int __init_func(void);
/* used: 미사용처럼 보여도 제거 금지 */
__attribute__((used)) static int debug_var = 0;
/* unused: 미사용 경고 억제 */
__attribute__((unused)) static int reserve;
변수 & 타입 속성
/* packed: 구조체 패딩 제거 */
struct __attribute__((packed)) ethernet_header {
u8 dest[6];
u8 src[6];
u16 ethertype;
};
/* aligned: 정렬 요구 사항 지정 */
u8 dma_buf[4096] __attribute__((aligned(4096)));
/* may_alias: 엄격한 별칭 분석 제외 */
typedef unsigned int __attribute__((may_alias)) u32_alias;
/* deprecated: 사용 시 경고 */
extern void old_api(void) __attribute__((deprecated("use new_api() instead")));
/*커널 매크로로 래핑된 형태 */
__cold /* = __attribute__((cold)) */
__hot /* = __attribute__((hot)) */
__pure /* = __attribute__((pure)) */
__packed /* = __attribute__((packed)) */
__aligned(n) /* = __attribute__((aligned(n))) */
분기 예측(Branch Prediction) 힌트
/* likely / unlikely — 분기 예측 힌트 */
#define likely(x) __builtin_expect(!!(x), 1)
#define unlikely(x) __builtin_expect(!!(x), 0)
static int process_packet(struct sk_buff *skb)
{
if (unlikely(skb == NULL)) /* 오류 경로: 거의 발생하지 않음 */
return -EINVAL;
if (likely(skb->len > 0)) /* 일반 경로: 거의 항상 참 */
return do_process(skb);
return -EMSGSIZE;
}
인라인 어셈블리 (Inline Assembly)
GCC의 인라인 어셈블리는 기본 asm과 확장 asm 두 가지 형식을 제공합니다. 커널에서는 주로 확장 asm을 사용합니다.
/* 확장 asm 문법:
asm [volatile] ( 어셈블리 코드
: 출력 피연산자
: 입력 피연산자
: 클러버 목록 ); */
/* 예제 1: 원자적 add 연산 (x86-64) */
static inline void atomic_add(int i, atomic_t *v)
{
asm volatile("lock addl %1, %0"
: "+m" (v->counter) /* 출력: 메모리 읽기+쓰기 */
: "ir" (i) /* 입력: 즉시값 또는 레지스터 */
: "cc"); /* 클러버: 조건 코드 레지스터 */
}
/* 예제 2: CPUID 명령어 */
static inline void native_cpuid(unsigned int *eax, unsigned int *ebx,
unsigned int *ecx, unsigned int *edx)
{
asm volatile("cpuid"
: "=a" (*eax), "=b" (*ebx),
"=c" (*ecx), "=d" (*edx)
: "0" (*eax), "2" (*ecx));
}
/* 예제 3: 메모리 배리어 */
static inline void mb(void)
{
asm volatile("mfence" ::: "memory");
/* "memory" 클러버: 컴파일러 재배치 금지 */
}
/* 피연산자 제약 문자 요약 */
/* r = 범용 레지스터, m = 메모리, i = 즉시 정수 */
/* a/b/c/d = 특정 레지스터 (eax/ebx/ecx/edx) */
/* "+" = 읽기+쓰기, "=" = 쓰기 전용, "" = 읽기 전용 */
GCC 내장 함수 (Built-in Functions)
/* 비트 연산 */
int n = __builtin_popcount(0xABCD); /* 설정된 비트 수 */
int z = __builtin_clz(0x1000); /* 선행 0 비트 수 */
int t = __builtin_ctz(0x1000); /* 후행 0 비트 수 */
int p = __builtin_parity(0xFF); /* 패리티 (1의 개수 홀짝) */
/* 바이트 스왑 */
uint16_t s = __builtin_bswap16(val); /* 16비트 바이트 스왑 */
uint32_t s = __builtin_bswap32(val); /* 32비트 바이트 스왑 */
uint64_t s = __builtin_bswap64(val); /* 64비트 바이트 스왑 */
/* 오버플로 체크 산술 (GCC 5+) */
int result;
if (__builtin_add_overflow(a, b, &result)) {
pr_err("오버플로 발생!\n");
}
__builtin_mul_overflow(a, b, &result);
__builtin_sub_overflow(a, b, &result);
/* 예측 분기 */
__builtin_expect(expr, expected_val); /* likely/unlikely 기반 */
__builtin_expect_with_probability(expr, val, 0.9);
/* 프레임 주소 / 반환 주소 */
void *fp = __builtin_frame_address(0); /* 현재 스택 프레임 */
void *ra = __builtin_return_address(0); /* 반환 주소 */
/* 메모리 조작 */
__builtin_memcpy(dst, src, n);
__builtin_memset(ptr, val, n);
/* 컴파일 타임 상수 판별 */
if (__builtin_constant_p(x)) {
/* x가 컴파일 타임 상수일 때 최적화 경로 */
}
/* 원자적 메모리 접근 (레거시 __sync 계열) */
__sync_fetch_and_add(&counter, 1);
__sync_bool_compare_and_swap(&ptr, old, new);
GCC C 언어 확장 — 커널 활용 예
/* 가변 길이 배열 (VLA) — 커널에서는 금지! */
/* void foo(int n) { int arr[n]; } */
/* 중첩 함수 (GCC 확장) */
void outer(void)
{
int x = 10;
void inner(void) { printf("%d\n", x); } /* GCC 확장 */
inner();
}
/* 구문 표현식 (statement expression) — 커널 max/min 매크로 기반 */
#define max(a, b) ({ \
typeof(a) _a = (a); \
typeof(b) _b = (b); \
_a > _b ? _a : _b; \
})
/* typeof — 타입 추론 */
typeof(x) tmp = x; /* x와 같은 타입의 변수 선언 */
/* 복합 리터럴 */
struct point p = (struct point){ .x = 1, .y = 2 };
/* __auto_type (C++ auto 유사, GCC 4.9+) */
__auto_type result = some_function();
/* 레이블 값 (computed goto) — 커널 디스패치 테이블에서 활용 */
void *dispatch_table[] = { &&case_a, &&case_b, &&case_c };
goto *dispatch_table[op];
case_a: handle_a(); goto done;
case_b: handle_b(); goto done;
case_c: handle_c(); goto done;
done:;
고급 함수 속성
/* naked: 프롤로그/에필로그 없는 순수 어셈블리 함수 */
__attribute__((naked))
static void raw_entry(void)
{
asm volatile(
"xorl %eax, %eax\n"
"retq\n"
);
}
/* target: 함수별 ISA 타깃 지정 */
__attribute__((target("avx2,bmi2")))
static void process_avx2(float *data, int n) { /* AVX2 명령어 사용 가능 */ }
/* target_clones: 자동 함수 멀티버저닝 — 런타임에 CPU 감지 후 최적 버전 선택 */
__attribute__((target_clones("avx2", "sse4.2", "default")))
int compute_sum(const int *arr, int n)
{
int sum = 0;
for (int i = 0; i < n; i++) sum += arr[i];
return sum;
}
/* GCC가 compute_sum.avx2 / compute_sum.sse4.2 / compute_sum 세 버전을 자동 생성 */
/* alias: 함수 별칭 (ABI 호환성 유지) */
int real_api(int x);
int __attribute__((alias("real_api"))) compat_api(int x);
/* ifunc: 런타임 리졸버 기반 간접 함수 */
static typeof(memcpy) *resolve_memcpy(void) {
if (__builtin_cpu_supports("avx")) return memcpy_avx;
return memcpy_generic;
}
typeof(memcpy) fast_copy __attribute__((ifunc("resolve_memcpy")));
/* optimize: 함수별 최적화 레벨 지정 */
__attribute__((optimize("O3,unroll-loops")))
static void hot_loop(int *data, int n) { /* 이 함수만 -O3 */ }
__attribute__((optimize("O0")))
static void debug_dump(void) { /* 이 함수만 최적화 없음 */ }
/* ms_abi / sysv_abi: Windows ↔ Linux ABI 전환 (x86-64) */
__attribute__((ms_abi))
typedef long (*win_fn_t)(void *arg); /* Windows x64 호출 규약 */
__attribute__((sysv_abi))
typedef long (*linux_fn_t)(void *arg); /* System V AMD64 호출 규약 */
/* availability / visibility 심볼 제어 */
__attribute__((visibility("default"))) void exported_func(void); /* 공개 심볼 */
__attribute__((visibility("hidden"))) void internal_func(void); /* 내부 심볼 */
__attribute__((visibility("protected")))void readonly_func(void); /* 재정의 불가 */
/* error / warning: 사용 시 컴파일 오류/경고 발생 */
extern void bad_function(void)
__attribute__((error("bad_function을 호출할 수 없습니다")));
/* access: 포인터 인수의 접근 패턴 선언 (GCC 10+) */
extern void safe_copy(void *dst, const void *src, size_t n)
__attribute__((access(write_only, 1, 3), access(read_only, 2, 3)));
추가 내장 함수 (__builtin_*)
/* 타입 호환성 검사 (컴파일 타임) */
if (__builtin_types_compatible_p(typeof(x), int)) {
/* x의 타입이 int인 경우에만 실행 (데드 코드 제거) */
}
/* 컴파일 타임 조건부 표현식 */
#define safe_abs(x) \
__builtin_choose_expr(__builtin_types_compatible_p(typeof(x), long), \
labs(x), abs(x))
/* CPU 기능 런타임 확인 */
__builtin_cpu_init(); /* CPU 기능 감지 초기화 (보통 자동) */
if (__builtin_cpu_supports("avx2")) { ... } /* AVX2 지원 여부 */
if (__builtin_cpu_supports("sha")) { ... } /* SHA 명령어 지원 */
if (__builtin_cpu_is("intel")) { ... } /* Intel CPU 여부 */
/* 도달 불가 표시 / 강제 트랩 */
__builtin_unreachable(); /* 이 지점 도달 불가 → 최적화 힌트 + 경고 제거 */
__builtin_trap(); /* 즉시 비정상 종료 (UD2 / BRK 명령어) */
/* 오브젝트 크기 (버퍼 오버플로 방어) */
size_t sz = __builtin_object_size(ptr, 0); /* 정적 상한 크기 */
size_t dz = __builtin_dynamic_object_size(ptr, 0); /* 동적 크기 (GCC 12+) */
/* 원자 연산 (C11 _Atomic 기반 권장; 하위 호환용 __atomic_* 계열) */
__atomic_fetch_add(&counter, 1, __ATOMIC_SEQ_CST);
__atomic_compare_exchange_n(&ptr, &old, new_val,
0, __ATOMIC_SEQ_CST, __ATOMIC_RELAXED);
__atomic_load_n(&flag, __ATOMIC_ACQUIRE);
__atomic_store_n(&flag, val, __ATOMIC_RELEASE);
/* x86 전용 내장 함수 */
__builtin_ia32_pause(); /* PAUSE 명령 (spinlock 효율화) */
__builtin_ia32_lfence(); /* LFENCE — 로드 배리어 */
__builtin_ia32_mfence(); /* MFENCE — 전체 배리어 */
__builtin_ia32_sfence(); /* SFENCE — 스토어 배리어 */
__builtin_ia32_rdtsc(); /* RDTSC — 타임스탬프 카운터 읽기 */
크로스 컴파일
크로스 컴파일이란 현재 실행 중인 플랫폼(호스트)과 다른 플랫폼(타깃)을 위한 코드를 생성하는 것입니다. 리눅스 커널은 x86-64 호스트에서 ARM64, RISC-V, MIPS 등 다양한 타깃으로 크로스 컴파일합니다.
크로스 컴파일러 툴체인 명명 규칙
# 형식: <arch>-<vendor>-<os>-<abi>-gcc
aarch64-linux-gnu-gcc # ARM64 리눅스 (glibc)
arm-linux-gnueabihf-gcc # ARM 32bit 하드 부동소수점
riscv64-linux-gnu-gcc # RISC-V 64bit 리눅스
mips-linux-gnu-gcc # MIPS 리눅스
powerpc64le-linux-gnu-gcc # PowerPC 64bit LE 리눅스
x86_64-w64-mingw32-gcc # x86-64 Windows (MinGW)
리눅스 커널 크로스 컴파일
# ARM64 커널 빌드
$ ARCH=arm64 \
CROSS_COMPILE=aarch64-linux-gnu- \
make defconfig
$ ARCH=arm64 \
CROSS_COMPILE=aarch64-linux-gnu- \
make -j$(nproc) Image.gz modules dtbs
# RISC-V 64bit 커널 빌드
$ ARCH=riscv \
CROSS_COMPILE=riscv64-linux-gnu- \
make defconfig
$ ARCH=riscv \
CROSS_COMPILE=riscv64-linux-gnu- \
make -j$(nproc)
# Clang 크로스 컴파일 (LLVM 툴체인)
$ make LLVM=1 ARCH=arm64 defconfig
$ make LLVM=1 ARCH=arm64 -j$(nproc)
# CROSS_COMPILE 변수가 컴파일러 prefix로 사용됨
# aarch64-linux-gnu-gcc, aarch64-linux-gnu-ld, aarch64-linux-gnu-objcopy ...
아키텍처별 GCC 주요 플래그
| 아키텍처 | 주요 옵션 | 설명 |
|---|---|---|
| x86-64 | -march=x86-64-v3 | AVX2 포함 x86-64 v3 마이크로아키텍처 |
| x86-64 | -mtune=native | 현재 CPU 특성에 맞게 튜닝 |
| ARM64 | -march=armv8-a | ARMv8-A 기본 아키텍처 |
| ARM64 | -march=armv8.5-a+memtag | Memory Tagging Extension(MTE) 활성화 |
| RISC-V | -march=rv64gc | RISC-V 64bit general + compressed |
| ARM 32bit | -mfpu=neon -mfloat-abi=hard | NEON SIMD + 하드 부동소수점 ABI |
-march vs -mtune vs -mcpu
| 옵션 | 역할 | 예시 |
|---|---|---|
-march=arch | 명령어 집합(ISA) 수준 지정. 해당 ISA 명령어를 생성 가능. | -march=armv8.2-a |
-mtune=cpu | 특정 CPU를 위한 코드 스케줄링 최적화. ISA는 변경 안 함. | -mtune=cortex-a72 |
-mcpu=cpu | -march와 -mtune을 동시에 설정. 특정 CPU 타깃. | -mcpu=cortex-a72 |
리눅스 커널에서의 GCC 활용
리눅스 커널은 GCC의 다양한 기능을 적극적으로 활용합니다. 커널 내부에는 GCC 버전에 따라 조건적으로 기능을 사용하는 코드가 많습니다.
커널 빌드의 핵심 GCC 플래그
# 커널 기본 CFLAGS (Makefile 참고)
-std=gnu11 # GNU C11 표준
-ffreestanding # 독립 구현 환경 (OS 없이 동작)
-fno-builtin # GCC 내장 함수 비활성화
-fno-stack-protector # 커널이 스택 보호를 직접 관리
-fno-strict-aliasing # 포인터 별칭 최적화 비활성화 (포인터 캐스팅 허용)
-fno-common # 공통 심볼 허용 안 함 (중복 전역 변수 오류)
-fno-asynchronous-unwind-tables # async unwind 테이블 제거 (크기 절감)
-fno-delete-null-pointer-checks # null 포인터 역참조 코드 삭제 방지
-O2 # 최적화 레벨 2 (기본)
-Wall -Wextra # 경고 활성화
-pipe # 임시 파일 대신 파이프 사용 (빌드 속도 향상)
-Werror=implicit-function-declaration # 함수 선언 누락을 오류로
__init / __exit 섹션
/* __init: 초기화 완료 후 메모리 해제 가능한 코드 */
static int __init my_driver_init(void)
{
/* 이 코드는 초기화 후 커널이 메모리 해제 가능 */
return 0;
}
/* __exit: 모듈이 내장될 때 제거되는 코드 */
static void __exit my_driver_exit(void)
{
/* built-in 커널 모듈에서는 이 코드 없음 */
}
/* __initdata: 초기화 후 제거되는 데이터 */
static const char __initconst version_str[] = "1.0";
/* section 확인 (GCC가 .init.text, .exit.text 섹션에 배치) */
#define __init __attribute__((section(".init.text"))) __cold
#define __exit __attribute__((section(".exit.text"))) __cold
#define __initdata __attribute__((section(".init.data")))
커널에서 자주 쓰이는 GCC 매크로 패턴
/* BUILD_BUG_ON: 컴파일 타임 어설션 */
BUILD_BUG_ON(sizeof(struct my_struct) != 64);
/* IS_ENABLED: Kconfig 심볼 컴파일 타임 확인 */
if (IS_ENABLED(CONFIG_SOME_FEATURE)) {
/* 데드 코드 제거(dead code elimination)로 최적화 */
}
/* ARRAY_SIZE: 배열 크기 (sizeof 기반) */
int arr[10];
pr_info("size = %zu\n", ARRAY_SIZE(arr)); /* = 10 */
/* container_of: 멤버 포인터로 구조체 포인터 획득 */
struct work_struct *work = ...;
struct my_data *data = container_of(work, struct my_data, work);
/* 내부: ((struct my_data *)((char *)(ptr) - offsetof(struct my_data, work))) */
/* READ_ONCE / WRITE_ONCE: volatile 메모리 접근 (컴파일러 재배치 방지) */
int val = READ_ONCE(shared_var);
WRITE_ONCE(shared_var, new_val);
/* data_race: 데이터 경쟁이 의도적임을 표시 (KCSAN 억제) */
int snapshot = data_race(counter);
/* 커널 GCC 버전 체크 */
#if GCC_VERSION >= 140000
/* GCC 14+ 전용 코드 */
#endif
컴파일러 최적화(Compiler Optimization)와 커널 이슈
커널은
void *나 char *를 다양한 타입 포인터로 캐스팅하는 경우가 많습니다. GCC의 엄격한 별칭 규칙(strict aliasing)을 적용하면 이런 코드를 잘못 최적화할 수 있습니다. 따라서 커널 빌드는 항상 -fno-strict-aliasing을 사용합니다.
GCC는 null 포인터 역참조 이후의 코드를 "도달 불가"로 간주하고 삭제할 수 있습니다. 커널에서는 의도적으로 null 포인터를 확인한 후 처리하는 패턴이 있어, 이 최적화를 비활성화해야 합니다.
커널 보안 강화 매크로 패턴
/* __randomize_layout: 구조체 필드 순서 무작위화 (CONFIG_GCC_PLUGIN_RANDSTRUCT) */
struct task_struct {
/* ... 필드들 ... */
} __randomize_layout;
/* __no_randomize_layout: 무작위화 제외 (ABI 고정 구조체) */
struct pt_regs {
u64 r15, r14, r13; /* ABI에서 순서 고정 */
/* ... */
} __no_randomize_layout;
/* OPTIMIZER_HIDE_VAR: 컴파일러 최적화로부터 변수 숨김 */
#define OPTIMIZER_HIDE_VAR(var) \
asm("" : "+r" (var)) /* 최적화기가 값을 추적 불가 */
OPTIMIZER_HIDE_VAR(secret_key);
/* barrier_data: 포인터 관련 컴파일러 최적화 방지 */
#define barrier_data(ptr) \
asm volatile("" : "+r"(ptr) :: "memory")
barrier_data(buf); /* buf 포인터 escape → 이후 접근 최적화 금지 */
/* noinstr: 어떠한 instrumentation도 금지 (인터럽트 비허용 임계 구간) */
noinstr void do_nmi(struct pt_regs *regs, long error_code)
{
/* KASAN/KCSAN/ftrace/lockdep 등 모든 계측 금지 */
}
/* instrumentation_begin/end: 임계 구간 내 특정 지점에서만 허용 */
noinstr void exception_entry(struct pt_regs *regs)
{
instrumentation_begin();
do_some_work(regs); /* 여기서는 계측 허용 */
instrumentation_end();
}
/* 새니타이저 선택적 제외 */
__no_sanitize_address /* = __attribute__((no_sanitize_address)) */
__no_kcsan /* KCSAN 제외 */
__no_sanitize_undefined /* UBSAN 제외 */
/* pragma를 이용한 경고 억제 */
#pragma GCC diagnostic push
#pragma GCC diagnostic ignored "-Wformat-nonliteral"
pr_info(dynamic_fmt_string, arg);
#pragma GCC diagnostic pop
/* GCC_VERSION 기반 조건부 기능 사용 */
#include <linux/compiler-version.h>
#if GCC_VERSION >= 150000 /* GCC 15.0+ */
/* GCC 15 신규 기능: 개선된 -fanalyzer, gcobol 지원 등 */
#elif GCC_VERSION >= 140000 /* GCC 14.0+ */
/* -fharden-compares, -fharden-conditional-branches */
#endif
커널 LTO (Link-Time Optimization)
LTO를 활성화하면 링크 타임에 전체 커널 소스를 한 번에 최적화합니다. 불필요한 심볼 제거, 크로스 파일 인라이닝, IPA(Interprocess Analysis)가 가능해집니다.
# 커널 LTO 활성화 (GCC WHOPR 방식)
# CONFIG_LTO_GCC=y → KBUILD_CFLAGS += -flto -fno-fat-lto-objects
$ make CONFIG_LTO_GCC=y -j$(nproc) vmlinux
# LTO 빌드 파이프라인:
# 1. gcc -flto -c foo.c → foo.o (GIMPLE IR + 심볼 메타 포함)
# 2. ld --plugin=liblto_plugin.so *.o → 전체 GIMPLE 최적화 (IPA)
# 3. 최종 머신 코드 생성 (크로스 파일 인라이닝, 전역 DCE)
# LTO 병렬 처리 옵션
-flto=auto # 사용 가능한 코어 수 자동 결정 (권장)
-flto=8 # 8개 병렬 파티션 고정
-flto=jobserver # make 잡 서버 활용
# LTO 디버그 (IR 덤프)
$ lto-dump -list foo.o # LTO 오브젝트의 심볼 목록
$ lto-dump -dump-body foo.o # GIMPLE IR 덤프
# 링크 맵으로 심볼 배치 확인
$ make LDFLAGS+="-Wl,-Map=vmlinux.map" vmlinux
$ grep __init_begin vmlinux.map
GCC 최소 버전 요구사항
리눅스 커널은 커널 버전마다 최소 GCC 버전을 요구합니다. 공식 기준은 Documentation/process/changes.rst와 커널 소스 내 scripts/min-tool-version.sh입니다.
| 커널 버전 | 최소 GCC | 비고 |
|---|---|---|
| 4.0 ~ 4.8 | 3.2 | x86 레거시 지원, 기본 C99/GCC 확장 |
| 4.9 ~ 5.1 | 4.6 | -std=gnu89에서 -std=gnu11으로 전환 시작 |
| 5.2 ~ 5.14 | 4.9 | C11 _Generic·_Static_assert 활용, -std=gnu11 기본 |
| 5.15 ~ 현재 | 5.1 | scripts/cc-version.sh가 빌드 전에 체크; scripts/min-tool-version.sh gcc → 5.1.0 |
/* include/linux/compiler-gcc.h — GCC_VERSION 매크로 정의 */
#define GCC_VERSION (__GNUC__ * 10000 \
+ __GNUC_MINOR__ * 100 \
+ __GNUC_PATCHLEVEL__)
/* 예: GCC 12.3.0 → GCC_VERSION = 120300 */
/* 버전 조건부 기능 선택 패턴 (커널 코드 내부) */
#if GCC_VERSION >= 120000 /* GCC 12.0+ */
# define __counted_by(m) __attribute__((__counted_by__(m)))
#else
# define __counted_by(m) /* noop — older GCC */
#endif
compiler-gcc.h의 #error가 아니라 빌드 시작 시 scripts/cc-version.sh가 scripts/min-tool-version.sh를 호출해 버전을 확인합니다. 기준 미달이면 *** C compiler is too old. 메시지와 함께 make가 즉시 중단됩니다.
# 현재 시스템 GCC 버전 확인
$ gcc --version
gcc (Ubuntu 12.3.0-1ubuntu1~22.04) 12.3.0
# 커널 소스에서 요구하는 최소 버전 확인
$ ./scripts/min-tool-version.sh gcc
5.1.0
# GCC_VERSION 구성 확인 (12*10000 + 3*100 + 0 = 120300)
$ gcc -dM -E - < /dev/null | grep -E '__GNUC__'
#define __GNUC__ 12
#define __GNUC_MINOR__ 3
#define __GNUC_PATCHLEVEL__ 0
빌드 시 GCC 플래그 추가 (KCFLAGS / KCPPFLAGS)
KCFLAGS는 커널 빌드 시 전체 C 파일에 GCC 플래그를 외부에서 추가하는 변수입니다. KBUILD_CFLAGS가 Makefile 내부 기본 플래그라면, KCFLAGS는 이를 덮어쓰지 않고 뒤에 이어 붙여 기존 플래그를 재정의할 수 있습니다.
| 변수 | 대상 | 용도 |
|---|---|---|
KCFLAGS | 전체 C 파일 | 최적화·디버그 플래그 추가 (KBUILD_CFLAGS 뒤에 추가) |
KCPPFLAGS | 전처리 단계 | -D 매크로 정의, 전처리기 전용 옵션 |
KBUILD_CFLAGS | 전체 C 파일 | Makefile 내부 기본 플래그 (직접 수정 비권장) |
# 특정 서브시스템 디버그 빌드: 최적화 해제 + 프레임 포인터 보존
$ make KCFLAGS="-O0 -fno-omit-frame-pointer" drivers/net/
# 전처리 매크로 추가 (CONFIG 변수가 아닌 임시 디버그용)
$ make KCPPFLAGS="-DDEBUG_SPINLOCK=1" kernel/locking/
# 복합 사용: 전처리 + 컴파일 플래그 동시 지정
$ make KCPPFLAGS="-DDEBUG_LOCK=1" \
KCFLAGS="-O1 -fno-inline" \
-j$(nproc) vmlinux
# V=1로 실제 gcc 커맨드라인 확인 (KCFLAGS 적용 위치 확인)
$ make V=1 init/main.o
# 출력 예: gcc [KBUILD_CFLAGS...] [KCFLAGS...] -c init/main.c -o init/main.o
# → KCFLAGS는 KBUILD_CFLAGS 뒤에 위치하여 앞의 플래그 재정의 가능
파일별 컴파일 플래그 (per-file CFLAGS)
서브시스템 Makefile에서 특정 파일에만 GCC 플래그를 추가하거나 제거할 수 있습니다. 성능 크리티컬 파일과 디버그 대상 파일을 개별 처리할 때 유용합니다.
# drivers/example/Makefile — per-file CFLAGS 활용 예
obj-$(CONFIG_EXAMPLE) := example.o
# 특정 파일에 플래그 추가 (최적화 해제 + 디버그 심볼)
CFLAGS_example_main.o := -O0 -g -DDEBUG_EXAMPLE
# 다른 파일에 고최적화 적용
CFLAGS_example_hot.o := -O3 -funroll-loops
# 특정 플래그 제거 후 교체 (-O2 제거 → -O0 대체)
CFLAGS_REMOVE_example_slow.o := -O2
CFLAGS_example_slow.o := -O0
# 서브디렉토리 전체에 인클루드 경로 추가
ccflags-y += -I$(src)/include
ccflags-y += -I$(srctree)/include/linux
# 어셈블리 파일별 플래그
AFLAGS_entry.o := -D__ASSEMBLY__
# 실제 커널 사례: ftrace 프로파일링 제외 (arch/x86/kernel/Makefile)
CFLAGS_REMOVE_cpufeature.o = -pg # -pg는 mcount 호출 삽입 → 재귀 방지
CFLAGS_REMOVE_cpu.o = -pg
$(KBUILD_CFLAGS) $(ccflags-y) $(CFLAGS_foo.o) 순으로 적용됩니다. CFLAGS_REMOVE_foo.o에 지정한 플래그는 이 전체 목록에서 제거됩니다. 따라서 ccflags-y로 추가된 플래그도 CFLAGS_REMOVE로 제거할 수 있습니다.
스택 사용량 분석 (-fstack-usage)
-fstack-usage 플래그를 사용하면 GCC가 각 함수의 스택 프레임(Stack Frame) 크기를 .su 파일로 출력합니다. 커널은 CONFIG_FRAME_WARN으로 과도한 스택 사용 함수를 빌드 시 감지합니다.
# -fstack-usage로 .su 파일 생성
$ gcc -fstack-usage -O2 -c foo.c
$ cat foo.su
foo.c:42:5:process_packet 224 static
foo.c:87:6:handle_error 48 static
# 형식: 파일:줄:열:함수명 TAB 바이트수 TAB [static|dynamic|bounded]
# static : 컴파일 타임에 스택 크기 확정 (VLA 없음)
# dynamic : VLA·alloca 등 런타임에 크기 결정
# bounded : 동적이지만 컴파일러가 상한을 파악
# 커널 빌드에 적용: 특정 서브시스템 스택 분석
$ make KCFLAGS="-fstack-usage" drivers/net/
$ find drivers/net/ -name '*.su' -exec sort -t' ' -k2 -rn {} + | head -20
# 스택을 가장 많이 사용하는 함수 상위 20개 출력
| 항목 | 기본값 | 설명 |
|---|---|---|
CONFIG_FRAME_WARN | 2048 (x86-64) / 1024 (arm64) | 스택 프레임 경고 임계값(바이트). 초과 시 빌드 경고 발생 |
-Wframe-larger-than=N | 자동 설정 | CONFIG_FRAME_WARN 값으로 Kbuild가 자동 적용 |
scripts/checkstack.pl | — | objdump 출력 파싱으로 스택 크기 추출 (LTO/최적화 결과 반영) |
# checkstack.pl로 커널 전체 스택 사용 상위 함수 출력
$ objdump -d vmlinux | scripts/checkstack.pl x86_64 | head -20
4096 process_one_work [vmlinux]:
3840 ext4_find_entry [vmlinux]:
...
# 특정 임계값 초과 함수만 필터 (1024 바이트 초과)
$ objdump -d vmlinux | scripts/checkstack.pl x86_64 \
| awk 'NR>1 && $1+0 > 1024'
# CONFIG_FRAME_WARN 경고 빌드 출력 예시
# drivers/gpu/drm/amd/display/dc/core/dc.c:1234:5:
# warning: the frame size of 2304 bytes is larger than 2048 bytes [-Wframe-larger-than=]
-Wvla 플래그로 VLA 사용을 금지합니다. VLA는 스택 크기를 컴파일 타임에 예측할 수 없어 -fstack-usage에서 dynamic으로 표시되며 스택 오버플로(Stack Overflow) 취약점의 원인이 됩니다. 가변 길이가 필요하면 kmalloc()이나 고정 크기 배열을 사용하세요.
gcov 코드 커버리지 도구
gcov는 GCC에 포함된 테스트 커버리지 프로그램입니다. 컴파일 시 계측(instrumentation) 코드를 삽입하여 실행 중 각 코드 줄이 몇 번 실행되었는지 기록합니다. 리눅스 커널은 CONFIG_GCOV_KERNEL로 gcov를 지원합니다.
gcov 사용 흐름
# 1단계: 커버리지 계측 빌드
$ gcc --coverage -O0 foo.c -o foo # --coverage = -fprofile-arcs -ftest-coverage
# foo.gcno 파일 생성 (노트 파일)
# 2단계: 프로그램 실행 (커버리지 데이터 수집)
$ ./foo
# foo.gcda 파일 생성 (실행 통계)
# 3단계: gcov 리포트 생성
$ gcov foo.c
# foo.c.gcov 파일 생성 (줄별 실행 횟수 주석 포함)
# lcov / genhtml로 HTML 리포트 생성
$ lcov --capture --directory . --output-file coverage.info
$ genhtml coverage.info --output-directory out/
$ xdg-open out/index.html
# gcov-tool: 오프라인 .gcda 처리 도구
$ gcov-tool merge -o merged dir1 dir2
# gcov-dump: .gcno / .gcda 덤프
$ gcov-dump foo.gcno
커널 gcov 활성화
# 커널 설정
CONFIG_GCOV_KERNEL=y
CONFIG_GCOV_FORMAT_AUTODETECT=y
# 개별 파일에 gcov 활성화
# drivers/mydriver/Makefile
GCOV_PROFILE_myfile.o := y
GCOV_PROFILE := y # 디렉터리 전체
# debugfs로 커버리지 데이터 접근
$ ls /sys/kernel/debug/gcov/
$ find /sys/kernel/debug/gcov -name "*.gcda" | xargs cp -t /tmp/gcov/
$ gcov -o /path/to/kernel/build /tmp/gcov/*.gcda
lto-dump: LTO 오브젝트 파일 덤프(Dump)
# LTO로 빌드된 오브젝트 파일 분석
$ gcc -flto -c foo.c -o foo.o
$ lto-dump -list foo.o # 심볼 목록
$ lto-dump -dump-body foo.o # GIMPLE IR 덤프
전처리기 옵션
전처리기(cpp)는 #include, #define, 조건부 컴파일 등을 처리합니다. GCC는 전처리기 옵션을 직접 전달할 수 있습니다.
| 옵션 | 설명 | 예시 |
|---|---|---|
-D macro | 매크로 정의 | -DDEBUG -DVERSION=3 |
-U macro | 매크로 정의 해제 | -UNDEBUG |
-I dir | 헤더 파일 검색 경로 추가 | -I./include -I/usr/local/include |
-include file | 소스 시작 전 파일 자동 포함 | -include config.h |
-M | Makefile 의존성 규칙 출력 | 빌드 시스템 통합 |
-MM | 시스템 헤더 제외 의존성 출력 | Kbuild에서 활용 |
-MD | 의존성 파일 생성 + 컴파일 계속 | *.d 파일 생성 |
-MMD | 시스템 헤더 제외 의존성 파일 생성 | Kbuild 기본 사용 |
-E | 전처리 결과만 출력 (컴파일 안 함) | 매크로 확장 디버깅 |
-fdebug-cpp | 전처리기 디버그 정보 출력 | 매크로 추적 |
-fmacro-prefix-map=old=new | 매크로 내 파일 경로 재맵핑 | 재현 가능 빌드 |
# 의존성 파일 생성 예시 (Kbuild 방식)
$ gcc -MMD -MP -c foo.c -o foo.o
# foo.d 생성:
# foo.o: foo.c foo.h bar.h
# foo.h:
# bar.h:
# 재현 가능 빌드를 위한 경로 정규화
$ gcc -fmacro-prefix-map=/home/user/linux=. \
-ffile-prefix-map=/home/user/linux=. \
-c foo.c -o foo.o
링크 옵션 (-l, -L, -Wl, ...)
GCC는 컴파일 후 링커(ld)를 내부적으로 호출합니다. -Wl,옵션으로 링커 옵션을 직접 전달하고, -l/-L로 라이브러리를 지정합니다. 커널은 arch/*/kernel/vmlinux.lds.S 링커 스크립트로 섹션 배치를 완전히 제어합니다.
기본 링크 옵션
| 옵션 | 설명 | 비고 |
|---|---|---|
-l name | 라이브러리 링크 (libname.a / libname.so 자동 탐색) | -lpthread -lm |
-L dir | 라이브러리 검색 경로 추가 | -L/usr/local/lib |
-static | 모든 라이브러리를 정적 링크 (.a 파일만 사용) | 임베디드, 독립 실행 파일 |
-shared | 공유 라이브러리(.so) 생성 | gcc -shared -fPIC -o libfoo.so |
-nostdlib | 표준 라이브러리·시작 파일(crt*.o) 링크 안 함 | ✓ 커널, 부트로더 |
-nodefaultlibs | libgcc 등 기본 라이브러리 링크 안 함 | 맞춤 런타임 |
-nostartfiles | crt0.o 등 시작 파일 링크 안 함 | 커스텀 엔트리 포인트 |
-rdynamic | 동적 심볼 테이블(Symbol Table)을 모두 내보냄 (dlopen 플러그인) | 백트레이스/플러그인 시스템 |
-T script | 링커 스크립트 명시적 지정 | 커널: arch/x86/kernel/vmlinux.lds |
-e symbol | 엔트리 포인트 심볼 지정 | 커널: -e startup_64 |
-Map=file | 링크 맵 파일 생성 (심볼 배치 확인) | -Wl,-Map=vmlinux.map |
-r | 재배치(Relocation) 가능 오브젝트 생성 (partial link) | 커널 built-in.a 빌드 |
-Wl 링커 옵션 전달
# 미사용 섹션 제거 (크기 최적화) — -ffunction-sections -fdata-sections와 쌍
-Wl,--gc-sections
# 링크 맵 생성 (심볼/섹션 배치 확인)
-Wl,-Map=vmlinux.map
# soname 설정 (공유 라이브러리 버전)
-Wl,-soname,libfoo.so.1
# rpath — 런타임 라이브러리 검색 경로
-Wl,-rpath,/opt/myapp/lib
# 심볼 버저닝 (ABI 호환 관리)
-Wl,--version-script=version.map
# 정적 라이브러리 전체 포함 (심볼 체인 문제 해결)
-Wl,--whole-archive libfoo.a -Wl,--no-whole-archive
# 미정의 심볼 오류 강제 (커널 빌드 품질)
-Wl,--no-undefined
# 빌드 ID 생성 (바이너리 식별자)
-Wl,--build-id=sha1
# PIE 링크
-Wl,-pie
# 동적 링커 명시적 지정
-Wl,--dynamic-linker=/lib64/ld-linux-x86-64.so.2
# 링크 순서에 무관한 정적 라이브러리 처리
-Wl,--start-group libA.a libB.a -Wl,--end-group
커널 링커 스크립트 구조
/* arch/x86/kernel/vmlinux.lds.S 발췌 (실제는 전처리 후 vmlinux.lds) */
OUTPUT_FORMAT("elf64-x86-64")
ENTRY(startup_64)
SECTIONS {
. = __START_KERNEL; /* 커널 가상 주소 시작 */
.text : {
_text = .;
*(.text.hot .text.hot.*) /* hot 코드 (캐시 친화) */
*(.text .text.*) /* 일반 코드 */
*(.text.cold .text.cold.*)/* cold 코드 (오류 경로) */
_etext = .;
}
.init.text : AT(ADDR(.init.text) - LOAD_OFFSET) {
__init_begin = .;
*(.init.text .init.text.*) /* __init 함수 → 초기화 후 해제 */
__init_end = .;
}
.data : {
_data = .;
*(.data .data.*)
_edata = .;
}
.bss : {
__bss_start = .;
*(.bss .bss.*)
__bss_stop = .;
}
}
머신 의존 옵션
GCC는 타깃 아키텍처별로 세밀한 옵션을 제공합니다. 커널 빌드에서는 -mno-red-zone, -mcmodel=kernel, -mno-sse 같은 옵션이 정확성과 보안에 직결됩니다.
x86 / x86-64 주요 옵션
| 옵션 | 설명 | 커널 활용 |
|---|---|---|
-m32 / -m64 | 32bit / 64bit 코드 생성 강제 | |
-mno-red-zone | x86-64 ABI의 128바이트 레드존 사용 금지 | ✓ 필수 — 인터럽트(Interrupt)/NMI 핸들러(Handler) 안전 |
-mcmodel=kernel | 커널 주소 공간(Address Space) 모델 (최상위 2GB 내 접근) | ✓ x86-64 커널 필수 |
-mcmodel=large | 모든 주소를 64bit 절대 주소로 접근 | BPF JIT 등 특수 목적 |
-mno-mmx | MMX 명령어 사용 금지 | ✓ 커널 코어 |
-mno-sse / -mno-sse2 | SSE/SSE2 사용 금지 (FPU 상태 저장 불필요) | ✓ 커널 코어 |
-mno-avx | AVX 사용 금지 | ✓ 커널 코어 |
-maccumulate-outgoing-args | 스택 아규먼트를 미리 할당 (프레임 크기 일정) | ✓ 커널 |
-mskip-rax-setup | 가변인수 함수 진입 시 %al 초기화 생략 | ✓ 커널 (System V ABI 불필요) |
-mpreferred-stack-boundary=3 | 스택 8바이트 정렬 강제 (2³=8) | ✓ 일부 아키텍처 |
-march=x86-64-v3 | AVX2+BMI+F16C 포함 x86-64 v3 마이크로아키텍처 | 최신 서버 최적화 |
-mtune=native | 현재 실행 CPU 특성에 맞게 스케줄링 최적화 | 호스트 빌드 |
-masm=intel | Intel 문법으로 어셈블리 출력 (-S와 함께) | 어셈블리 분석 |
ARM64 주요 옵션
| 옵션 | 설명 | 비고 |
|---|---|---|
-mgeneral-regs-only | 범용 레지스터만 사용 (FP/SIMD 레지스터 금지) | ✓ ARM64 커널 기본 |
-mabi=lp64 | LP64 ABI (long + pointer = 64bit) — Linux 기본 | |
-mabi=ilp32 | ILP32 ABI (int+long+pointer = 32bit) | 특수 목적 |
-mbranch-protection=standard | PAC + BTI 동시 활성화 (ARMv8.3+/ARMv8.5+) | 커널 CFI 강화 |
-mbranch-protection=pac-ret | 반환 주소 포인터 인증(PAC-RET)만 활성화 | ROP 방어 |
-moutline-atomics | 원자 연산을 런타임에 LSE/LL-SC 방식 선택 | 이식성 (커널 기본) |
-mno-outline-atomics | 원자 연산 인라인 고정 (LSE 또는 LL-SC) | 성능 최적화 |
-mfix-cortex-a53-835769 | Cortex-A53 에라타 835769 완화 | RaspberryPi 3 등 |
-mfix-cortex-a53-843419 | Cortex-A53 에라타 843419 완화 |
# x86-64 커널 핵심 머신 옵션 (arch/x86/Makefile 발췌)
KBUILD_CFLAGS += -mno-red-zone
KBUILD_CFLAGS += -mcmodel=kernel
KBUILD_CFLAGS += -mno-mmx -mno-sse -mno-sse2 -mno-sse3
KBUILD_CFLAGS += -mno-avx
KBUILD_CFLAGS += -maccumulate-outgoing-args
# ARM64 커널 핵심 머신 옵션 (arch/arm64/Makefile 발췌)
KBUILD_CFLAGS += -mgeneral-regs-only
KBUILD_CFLAGS += -mabi=lp64
KBUILD_CFLAGS += -mfix-cortex-a53-835769
KBUILD_CFLAGS += -mfix-cortex-a53-843419
중간 표현 덤프 & 컴파일 분석
GCC 내부 중간 표현(IR)인 GIMPLE과 RTL을 덤프하면 최적화 결과를 추적하거나 컴파일러 버그를 신고할 때 활용할 수 있습니다. 또한 -fopt-info로 벡터화·인라이닝 결정을 리포트받을 수 있습니다.
GIMPLE / RTL 덤프 옵션
# GIMPLE 패스 덤프 — 최적화 전/후 비교
$ gcc -O2 -fdump-tree-all foo.c -o foo # 모든 GIMPLE 패스 (*.NNN.* 파일군)
$ gcc -O2 -fdump-tree-optimized foo.c # 최적화 완료 후 최종 GIMPLE
$ gcc -O2 -fdump-tree-cfg foo.c # 제어 흐름 그래프 (CFG)
$ gcc -O2 -fdump-tree-dce foo.c # Dead Code Elimination 후
$ gcc -O2 -fdump-tree-vectorize foo.c # 자동 벡터화 결과
$ gcc -O2 -fdump-tree-inline foo.c # 인라이닝 적용 후
# RTL 덤프 — 어셈블리 직전 단계
$ gcc -O2 -fdump-rtl-final foo.c # 최종 RTL
$ gcc -O2 -fdump-rtl-combine foo.c # RTL combine 패스 후
# IPA (Interprocedural Analysis) 덤프
$ gcc -O2 -fdump-ipa-cgraph foo.c # 함수 호출 그래프
$ gcc -O2 -fdump-ipa-inline foo.c # IPA 인라이닝 결정
# 최적화 결정 리포트 (-fopt-info)
$ gcc -O3 -fopt-info-vec=vec.log foo.c # 벡터화된/못된 루프 목록
$ gcc -O2 -fopt-info-inline=inline.log foo.c # 인라이닝 결정 사유
$ gcc -O2 -fopt-info-loop=loop.log foo.c # 루프 최적화 정보
$ gcc -O2 -fopt-info-all=opts.log foo.c # 모든 최적화 정보
# GIMPLE DOT 그래프 출력 (Graphviz 시각화)
$ gcc -O2 -fdump-tree-cfg-graph foo.c
$ dot -Tpng foo.c.*.cfg.dot -o cfg.png
# 진단 메시지 제어
$ gcc -fdiagnostics-color=always foo.c # 컬러 진단 강제
$ gcc -fmax-errors=10 foo.c # 오류 10개 후 중단
$ gcc -fmessage-length=0 foo.c # 메시지 줄 바꿈 없음 (CI 파싱용)
$ gcc -fdiagnostics-format=json foo.c # JSON 형식 출력 (IDE 통합)
$ gcc -fdiagnostics-show-caret foo.c # 소스 내 오류 위치 캐럿(^) 표시
함수 멀티버저닝 (Function Multiversioning)
동일 함수를 여러 ISA 버전으로 컴파일하고 런타임에 CPU 기능을 감지하여 최적 버전을 자동 선택합니다 (x86에서 ifunc 메커니즘 기반).
/* target_clones: 자동 멀티버저닝 — GCC가 세 버전을 자동 생성 */
__attribute__((target_clones("avx512f,avx512bw", "avx2", "sse4.2", "default")))
int vector_sum(const int *arr, int n)
{
int sum = 0;
for (int i = 0; i < n; i++) sum += arr[i];
return sum;
}
/* 생성 결과: vector_sum.avx512f_avx512bw, vector_sum.avx2,
vector_sum.sse4.2, vector_sum (default)
+ ifunc 리졸버: __cpu_indicator_init() 호출 → 최적 버전 선택 */
/* target: 단일 함수에 다른 ISA 강제 */
__attribute__((target("avx2,bmi2,popcnt")))
static void process_avx2(float *data, int n)
{
/* 이 함수 내에서만 AVX2/BMI2/POPCNT 명령어 사용 가능 */
}
/* 수동 CPU 기능 감지 + 멀티버저닝 */
typedef void (*compress_fn_t)(void *dst, const void *src, size_t n);
static compress_fn_t resolve_compress(void)
{
if (__builtin_cpu_supports("avx512bw")) return compress_avx512;
if (__builtin_cpu_supports("avx2")) return compress_avx2;
return compress_generic;
}
compress_fn_t compress __attribute__((ifunc("resolve_compress")));
GCC 플러그인
GCC 플러그인 API는 컴파일 파이프라인(Pipeline)에 커스텀 GIMPLE 패스를 삽입합니다. 리눅스 커널은 scripts/gcc-plugins/에 보안 강화 플러그인들을 포함합니다 (CONFIG_GCC_PLUGINS=y).
커널 내장 GCC 플러그인
| 플러그인 파일 | Kconfig | 기능 |
|---|---|---|
structleak_plugin.so | CONFIG_GCC_PLUGIN_STRUCTLEAK | 스택 구조체 자동 0 초기화 — 커널↔유저스페이스 정보 누출 방지 |
structleak_plugin.so | CONFIG_GCC_PLUGIN_STRUCTLEAK_BYREF_ALL | 참조 전달 구조체도 모두 초기화 (강화 모드) |
latent_entropy_plugin.so | CONFIG_GCC_PLUGIN_LATENT_ENTROPY | 부팅 시 잠재 엔트로피 수집 보강 (RNG 초기화 개선) |
randomize_layout_plugin.so | CONFIG_GCC_PLUGIN_RANDSTRUCT | 구조체 필드 순서 무작위화 — exploit에서 필드 오프셋(Offset) 예측 불가 |
stackleak_plugin.so | CONFIG_GCC_PLUGIN_STACKLEAK | 함수 진입 시 스택 프레임을 독약값으로 채움 — 정보 누출 방지 |
# 커널 플러그인 빌드 활성화
$ make menuconfig # → Security options → GCC plugins 활성화
$ make CONFIG_GCC_PLUGINS=y -j$(nproc) vmlinux
# 플러그인 직접 사용법
$ gcc -fplugin=/path/to/myplugin.so source.c
# 플러그인에 인수 전달
$ gcc -fplugin=myplugin.so \
-fplugin-arg-myplugin-verbose=1 \
-fplugin-arg-myplugin-threshold=100 source.c
# 플러그인 API 버전 확인
$ gcc -print-file-name=plugin # 플러그인 include 경로
$ ls $(gcc -print-file-name=plugin)/include/
/* GCC 플러그인 최소 골격 (plugin_gcc_version 필수 확인) */
#include "gcc-plugin.h"
#include "tree.h"
#include "gimple.h"
#include "gimple-iterator.h"
int plugin_is_GPL_compatible; /* GPL 호환 선언 필수 */
static unsigned int my_gimple_pass(function *fun)
{
basic_block bb;
FOR_EACH_BB_FN(bb, fun) {
gimple_stmt_iterator gsi;
for (gsi = gsi_start_bb(bb); !gsi_end_p(gsi); gsi_next(&gsi)) {
gimple *stmt = gsi_stmt(gsi);
/* GIMPLE 문장 분석/변환 */
}
}
return 0;
}
static struct pass_data my_pass_data = {
.type = GIMPLE_PASS,
.name = "my_security_pass",
.execute = my_gimple_pass,
};
int plugin_init(struct plugin_name_args *info,
struct plugin_gcc_version *ver)
{
if (!plugin_default_version_check(ver, &gcc_version))
return 1; /* 버전 불일치 → 실패 */
register_callback(info->base_name,
PLUGIN_PASS_MANAGER_SETUP,
NULL, &my_pass_setup);
return 0;
}
make LLVM=1)가 점점 완성도가 높아지고 있습니다. Clang은 GCC 플러그인 대신 자체 -fsanitize=kernel-address, -fsanitize=cfi, LTO=thin 등을 제공합니다. 두 컴파일러 모두 CONFIG_CC_IS_GCC / CONFIG_CC_IS_CLANG으로 구분됩니다.
커널 모듈(Kernel Module) 빌드와 GCC
리눅스 커널 모듈은 커널 소스 트리 밖에서도 빌드할 수 있습니다(out-of-tree 빌드). Kbuild 시스템이 GCC 호출을 담당하며, 개발자는 간단한 Makefile만 작성하면 됩니다.
out-of-tree 모듈 Kbuild 구조
Kbuild는 obj-m 변수로 빌드할 모듈을 선언합니다. 멀티 소스 파일 모듈은 모듈명-y 변수에 오브젝트 파일을 나열합니다.
# Kbuild 파일 (또는 Makefile 상단 Kbuild 섹션)
# 단일 파일 모듈 (mymodule.c → mymodule.ko)
obj-m := mymodule.o
# 멀티 파일 모듈: mymodule.ko = a.o + b.o + c.o
obj-m := mymodule.o
mymodule-y := a.o b.o c.o
mymodule-$(CONFIG_MY_FEATURE) += feature.o # 조건부 오브젝트
# 모듈별 GCC 플래그 추가
ccflags-y += -I$(src)/include -DMODULE_VERSION="\"1.0\""
ldflags-y += -T $(src)/mymodule.lds # 커스텀 링커 스크립트
# 파일별 플래그 (최적화 개별 제어)
CFLAGS_a.o := -O0 -g # a.c만 디버그 빌드
CFLAGS_REMOVE_b.o := -O2 # b.c에서 -O2 제거
make -C $(KDIR) M=$(PWD) 패턴
out-of-tree 모듈의 표준 Makefile 골격입니다. KERNELRELEASE 변수로 Kbuild 내부/외부를 구분합니다.
# ~/mymodule/Makefile — 표준 out-of-tree 모듈 Makefile
KDIR ?= /lib/modules/$(shell uname -r)/build
PWD := $(shell pwd)
ifeq ($(KERNELRELEASE),)
# ─── 외부 호출: Kbuild 시스템으로 위임 ───────────────────────
all:
$(MAKE) -C $(KDIR) M=$(PWD) modules
modules_install:
$(MAKE) -C $(KDIR) M=$(PWD) modules_install
clean:
$(MAKE) -C $(KDIR) M=$(PWD) clean
else
# ─── Kbuild 내부 호출: 모듈 선언 ─────────────────────────────
obj-m := mymodule.o
mymodule-y := a.o b.o
endif
# 실행 중인 커널용 모듈 빌드 (기본)
$ make
# 특정 커널 헤더 트리 지정
$ make KDIR=/usr/src/linux-headers-6.8.0-100-generic
# KDIR 설정 방법
# 1. 설치된 커널 헤더 패키지 (권장, 배포판 커널용)
# apt install linux-headers-$(uname -r)
KDIR = /lib/modules/$(uname -r)/build
# 2. 자체 빌드한 커널 소스 트리 (make 후 디렉토리 지정)
KDIR = /usr/src/linux-6.8
크로스 컴파일 조합 (ARCH + CROSS_COMPILE + KDIR)
타겟 아키텍처용 커널 모듈을 빌드할 때 ARCH, CROSS_COMPILE, KDIR 세 변수를 조합합니다. KDIR은 타겟 아키텍처로 빌드된 커널 소스 트리 또는 헤더 경로를 지정합니다.
# ARM64 (aarch64) 모듈 크로스 빌드
$ make \
ARCH=arm64 \
CROSS_COMPILE=aarch64-linux-gnu- \
KDIR=/path/to/arm64-kernel-build \
-j$(nproc)
# RISC-V 64비트 모듈 크로스 빌드
$ make \
ARCH=riscv \
CROSS_COMPILE=riscv64-linux-gnu- \
KDIR=/path/to/riscv64-kernel-build
# ARM (32비트) + 스테이징 루트에 설치
$ make \
ARCH=arm \
CROSS_COMPILE=arm-linux-gnueabihf- \
KDIR=/path/to/arm-kernel-build \
INSTALL_MOD_PATH=/staging/rootfs \
modules_install
# LLVM 크로스 컴파일 (Clang 기반)
$ make \
LLVM=1 \
ARCH=arm64 \
KDIR=/path/to/arm64-llvm-kernel-build
모듈 설치 및 연계 도구
빌드된 모듈을 시스템에 설치하고 관리하는 표준 워크플로입니다.
# 스테이징 루트에 설치 (크로스 컴파일 배포 시)
$ make modules_install INSTALL_MOD_PATH=/staging/rootfs
# → /staging/rootfs/lib/modules/$(KERNELRELEASE)/extra/mymodule.ko
# 현재 시스템에 직접 설치
$ sudo make modules_install
$ sudo depmod -a # 모듈 의존성 데이터베이스 갱신 (modules.dep 재생성)
$ sudo modprobe mymodule # 모듈 로드 (의존성 자동 처리)
# 모듈 정보 확인
$ modinfo mymodule.ko
filename: /lib/modules/6.8.0/extra/mymodule.ko
version: 1.0
description: My kernel module
...
# 로드된 모듈 확인 및 제거
$ lsmod | grep mymodule
$ sudo rmmod mymodule
# 빌드 아티팩트 정리
$ make clean # .o, .ko, .mod.c 등 제거
CONFIG_MODULE_SIG=y 커널에서는 모듈에 서명이 필요합니다. scripts/sign-file로 수동 서명하거나 make modules_install 후 별도 서명 스텝을 추가합니다. Secure Boot 환경에서 CONFIG_MODULE_SIG_FORCE=y가 활성화된 경우 미서명 모듈 로드는 차단됩니다.
GCC 사용 시 주의사항 및 문제 해결
GCC 사용 중 발생하는 일반적인 문제와 그 원인, 해결 방법을 정리합니다.
자주 발생하는 문제
| 문제 | 원인 | 해결 방법 |
|---|---|---|
| mysterious 최적화 버그 | -O2/O3에서만 발생하는 버그 | UB 확인: -fsanitize=undefined, -fno-strict-aliasing 추가 |
| 부동소수점 결과 불일치 | -ffast-math 사용 시 IEEE 준수 무시 | -ffast-math 제거 또는 -fno-unsafe-math-optimizations |
| 스택 오버플로 | 재귀, 큰 로컬 배열, VLA | -Wframe-larger-than=N으로 탐지, VLA 제거 |
| LTO 링크 오류 | 버전 불일치 오브젝트 파일 혼합 | 동일 GCC 버전으로 모두 재빌드 |
| 미사용 코드 삭제 실패 | __attribute__((used)) 없는 심볼 | __attribute__((used)) 또는 링커 스크립트로 보존 |
| C++ name mangling 충돌 | C/C++ 혼용 헤더에 extern "C" 누락 | C 헤더에 extern "C" { ... } 추가 |
| 컴파일 속도 저하 | 헤더 파일 남발, LTO 사용 | 사전 컴파일 헤더(-fprecompiled-headers), -gsplit-dwarf, unity 빌드 |
유용한 진단 옵션
# 어떤 명령어가 실제로 실행되는지 확인
$ gcc -v foo.c -o foo # 상세 빌드 과정 출력
# 내부 spec 파일 확인
$ gcc -dumpspecs
# 기본 검색 경로 확인
$ gcc -print-search-dirs
# 전처리된 결과 확인 (매크로 확장)
$ gcc -E -dM foo.c # 정의된 매크로 목록
$ gcc -E foo.c | less # 전처리 결과 확인
# 어떤 최적화가 활성화되었는지 확인
$ gcc -O2 -Q --help=optimizers
# 특정 최적화 비활성화 (디버깅)
$ gcc -O2 -fno-tree-vectorize foo.c -o foo
# 어셈블리 출력으로 최적화 결과 확인
$ gcc -O2 -S -fverbose-asm foo.c -o foo.s
# 어떤 버전의 GCC인지 확인
$ gcc --version
$ gcc -dumpversion
$ gcc -dumpfullversion
- 빌드 시스템 (Kbuild/Kconfig) — GCC 옵션이 실제 커널 빌드에서 어떻게 조합되는지
- 어셈블리 — GCC 인라인 어셈블리와 커널 어셈블리 상세
- 커널 개발 도구 — GCC 외 Clang, sparse, coccinelle 등 전체 도구 생태계
- 성능 최적화 — GCC 최적화와 커널 성능 튜닝 연계
- GNU Make — GCC를 구동하는 빌드 도구
- GDB 완전 가이드 — GCC로 빌드한 커널·모듈 디버깅
- GNU Binutils — objdump/readelf로 GCC 출력 분석
- 메모리 배리어(Memory Barrier) — GCC 메모리 접근 순서와 컴파일러 배리어
GCC 컴파일 파이프라인
GCC 내부 파이프라인은 소스 코드를 여러 중간 표현(IR)으로 변환하면서 최적화를 적용합니다. 각 단계의 산출물과 역할을 이해하면 컴파일러 동작을 예측하고 디버깅할 수 있습니다.
GIMPLE과 SSA 형식
GIMPLE은 GCC의 핵심 중간 표현으로, 3-주소 코드(3-Address Code) 형태입니다. SSA(Static Single Assignment) 변환 후 각 변수는 한 번만 정의되어 최적화 분석이 용이해집니다.
/* 원본 C 코드 */
int example(int a, int b)
{
int x = a + b;
if (x > 10)
x = x * 2;
return x;
}
/* GIMPLE SSA 형식 (fdump-tree-ssa 출력) */
example (int a, int b)
{
int x;
int _1;
int _4;
<bb 2>:
_1 = a_2(D) + b_3(D); /* SSA: 각 변수에 고유 번호 */
if (_1 > 10)
goto <bb 3>;
else
goto <bb 4>;
<bb 3>:
_4 = _1 * 2;
<bb 4>:
# x_5 = PHI <_1(2), _4(3)> /* PHI 함수: 분기 합류점 */
return x_5;
}
# GIMPLE SSA 덤프 생성
$ gcc -O2 -fdump-tree-ssa example.c -c
$ cat example.c.*.ssa
# 최적화 전후 비교
$ gcc -O0 -fdump-tree-optimized example.c -c # 최적화 없는 최종 GIMPLE
$ gcc -O2 -fdump-tree-optimized example.c -c # 최적화된 최종 GIMPLE
# 특정 최적화 패스 전후 비교
$ gcc -O2 -fdump-tree-pre example.c -c # PRE(Partial Redundancy Elimination) 후
$ gcc -O2 -fdump-tree-vrp example.c -c # VRP(Value Range Propagation) 후
RTL (Register Transfer Language)
RTL은 GIMPLE 이후 단계에서 사용되는 하위 수준 중간 표현으로, 타깃 아키텍처의 레지스터와 명령어에 더 가깝습니다. 레지스터 할당, 명령어 스케줄링 등이 RTL 수준에서 이루어집니다.
# RTL 최종 형태 덤프
$ gcc -O2 -fdump-rtl-final example.c -c
$ cat example.c.*.final
# RTL 출력 예시 (x86-64):
# (insn 7 6 8 2 (set (reg:SI 0 ax [orig:82 _1] [82])
# (plus:SI (reg:SI 5 di [orig:83 a] [83])
# (reg:SI 4 si [orig:84 b] [84]))) "example.c":3:13
# 모든 RTL 패스 덤프 (파일 많음 주의)
$ gcc -O2 -fdump-rtl-all example.c -c
커널 필수 GCC 옵션
커널 빌드에서 정확성과 보안에 직결되는 핵심 옵션을 깊이 있게 설명합니다. 이 옵션들을 잘못 설정하면 커널 크래시나 보안 취약점이 발생합니다.
-mno-red-zone 상세
x86-64 System V ABI는 %rsp 아래 128바이트를 "레드존(Red Zone)"으로 정의합니다. 리프 함수는 이 영역을 스택 포인터 조정 없이 사용할 수 있습니다. 그러나 커널에서는 인터럽트/NMI가 언제든 발생하여 레드존을 덮어쓸 수 있으므로 반드시 비활성화해야 합니다.
/* 레드존 문제 시연 */
void leaf_function(void)
{
int local_vars[32]; /* 128B — 레드존에 배치될 수 있음 */
/* ... */
/* 여기서 인터럽트 발생 → ISR이 같은 스택에 프레임 push */
/* → local_vars 파괴 → 함수 복귀 시 잘못된 값 사용 → 크래시 */
}
/* 해결: -mno-red-zone → 모든 함수가 %rsp를 명시적으로 조정 */
-mcmodel=kernel 상세
| 코드 모델 | 주소 범위 | 생성 코드 | 용도 |
|---|---|---|---|
-mcmodel=small | 하위 2GB (0~0x7FFFFFFF) | 32비트 절대/RIP 상대 | 일반 사용자 프로그램 (기본) |
-mcmodel=kernel | 상위 2GB (...FFF80000000~...FFFFFFFF) | 부호 확장 32비트 절대 | x86-64 리눅스 커널 |
-mcmodel=medium | 코드: 하위 2GB, 데이터: 4GB | 데이터만 64비트 접근 | 큰 데이터 섹션 |
-mcmodel=large | 전체 64비트 주소 공간 | 모든 접근 64비트 | 특수 목적 (BPF JIT) |
-fno-common 상세
/* -fno-common의 효과 */
/* file1.c */
int global_var; /* 미초기화 전역 변수 */
/* file2.c */
int global_var; /* 동일 이름 미초기화 전역 변수 */
/* -fcommon (레거시 기본값):
* 두 변수가 BSS 공통 심볼(COMMON)로 생성
* 링커가 하나로 병합 → 조용히 성공
* 버그 원인: 의도하지 않은 공유 */
/* -fno-common (GCC 10+ 기본값, 커널 항상 사용):
* 두 변수 모두 BSS 일반 심볼로 생성
* 링커가 "multiple definition" 오류 발생
* → 버그를 조기 발견 */
최적화 레벨 비교
각 최적화 레벨이 활성화하는 주요 패스와 커널에서 -O2를 사용하는 구체적 이유를 분석합니다.
커널이 -O2를 사용하는 이유
| 고려 사항 | -O1 | -O2 (커널 기본) | -O3 | -Os |
|---|---|---|---|---|
| 인라이닝 | 기본적 | 적극적 (성능 핵심) | 매우 적극적 | 제한적 |
| 루프 최적화 | 기본 | 루프 불변식 이동, 강도 감소 | 벡터화, 언롤링 포함 | -O2와 유사 |
| 코드 크기 | 적당 | 적당 | 크게 증가 | 최소 |
| 빌드 시간 | 빠름 | 보통 | 느림 | 보통 |
| 디버깅 | 약간 어려움 | 어려움 | 매우 어려움 | 어려움 |
| 커널 안정성 | 검증 부족 | 가장 많이 테스트됨 | 적극적 최적화로 UB 노출 위험 | 일부 경로 미최적화 |
-O2는 성능과 안정성의 최적 균형점입니다. -O3는 벡터화와 함수 복제로 코드 크기가 증가하고, 커널의 의도적 UB(예: flexible array)를 최적화기가 잘못 해석할 수 있습니다. -Os는 인라이닝이 제한되어 hotpath 성능이 저하됩니다. 커널 CI/테스트 인프라가 -O2 기준으로 가장 광범위하게 검증되어 있습니다.
주요 개별 최적화 패스와 커널 영향
| 패스 | GCC 옵션 | 활성 레벨 | 커널 관련 영향 |
|---|---|---|---|
| Dead Code Elimination | -fdce | O1+ | IS_ENABLED() 패턴으로 미사용 코드 제거 |
| 인라이닝 | -finline-functions | O2+ | __always_inline 함수 강제 인라인 |
| Strict Aliasing | -fstrict-aliasing | O2+ | 커널: -fno-strict-aliasing으로 비활성화 (void* 캐스팅) |
| Null Pointer Opt | -fdelete-null-pointer-checks | O2+ | 커널: -fno-delete-null-pointer-checks로 비활성화 |
| 벡터화 | -ftree-vectorize | O3 | 커널 코어에서 미사용 (-mno-sse), crypto/에서 선택적 사용 |
| 함수 복제 | -fclone-functions | O3 | 커널 코드 크기 증가 → 캐시 압박 |
| Loop Unrolling | -funroll-loops | O3 | 커널에서 수동 언롤링 선호 (크기 제어) |
경고 옵션 — 커널 W=1/W=2/W=3
커널 빌드는 기본 경고 외에 W=1, W=2, W=3 레벨로 추가 경고를 점진적으로 활성화합니다. 패치 제출 시 최소 W=1 클린 빌드를 권장합니다.
| 레벨 | 빌드 명령 | 추가 경고 | 용도 |
|---|---|---|---|
| 기본 | make | -Wall -Wextra + 커널 기본 경고 | 정상 빌드 |
| W=1 | make W=1 | -Wunused-but-set-variable, -Wunused-const-variable, -Wpacked-not-aligned | 패치 제출 전 권장 |
| W=2 | make W=2 | -Wcast-align, -Wdisabled-optimization, -Wnested-externs | 코드 품질 |
| W=3 | make W=3 | -Wbad-function-cast, -Wcast-qual, -Wconversion, -Wsign-conversion | 철저한 감사 |
| W=e | make W=e | 모든 경고를 오류로 처리 | CI 강제 통과 |
# 커널 W= 레벨별 빌드 예시
$ make W=1 -j$(nproc) vmlinux # 추가 경고 활성화
$ make W=1 W=e -j$(nproc) vmlinux # 추가 경고 + 오류로 처리
$ make W=123 -j$(nproc) vmlinux # W=1 + W=2 + W=3 모두 활성화
# 특정 경고만 오류로 처리 (Werror=specific)
$ make KCFLAGS="-Werror=uninitialized -Werror=maybe-uninitialized" vmlinux
# 경고 수 카운트
$ make W=1 -j$(nproc) 2>&1 | grep -c 'warning:'
GCC 속성(Attributes)
커널에서 광범위하게 사용되는 GCC 속성의 정확한 의미와 동작을 분류별로 다룹니다.
섹션 배치 속성
/* __attribute__((section)) — 커널 섹션 매핑 */
/* 초기화 전용 코드 (.init.text) — 부팅 후 메모리 회수 */
#define __init __attribute__((section(".init.text"))) __cold
#define __initdata __attribute__((section(".init.data")))
#define __initconst __attribute__((section(".init.rodata")))
/* CPU hotplug 전용 */
#define __cpuinit __attribute__((section(".cpuinit.text")))
/* 읽기 빈번 데이터 (캐시 최적화) */
#define __read_mostly __attribute__((section(".data..read_mostly")))
static int cpu_count __read_mostly;
/* per-CPU 데이터 */
#define DEFINE_PER_CPU(type, name) \
__attribute__((section(".data..percpu"))) \
PER_CPU_DEF_ATTRIBUTES type name
/* 커널 모듈 정보 */
#define MODULE_INFO(tag, info) \
__attribute__((section(".modinfo"), used, aligned(1))) \
static const char __UNIQUE_ID(tag)[] = #tag "=" info
cleanup 속성 (자동 자원 해제)
/* __attribute__((cleanup)) — 스코프 종료 시 자동 함수 호출 */
/* 커널 6.4+에서 도입된 guard/scoped 락 패턴의 기반 */
static inline void __mutex_unlock(struct mutex **lock)
{
if (*lock)
mutex_unlock(*lock);
}
#define guard(mutex, lock) \
struct mutex *__guard_##lock \
__attribute__((cleanup(__mutex_unlock))) = lock; \
mutex_lock(__guard_##lock)
/* 사용 예 — 스코프 종료 시 자동 unlock */
void example(struct mutex *m)
{
guard(mutex, m);
/* ... 크리티컬 섹션 ... */
if (error)
return; /* cleanup이 자동으로 mutex_unlock() 호출 */
/* ... */
} /* 여기서도 자동 unlock */
GCC 내장 함수(Builtins)
커널에서 광범위하게 사용되는 GCC 내장 함수의 동작 원리와 코드 생성 결과를 분석합니다.
__builtin_expect 코드 생성 분석
/* likely/unlikely가 코드 생성에 미치는 영향 */
/* unlikely() 없이: */
if (error_code) {
handle_error(); /* 인라인 또는 가까운 위치 */
}
do_work();
/* unlikely() 사용: */
if (unlikely(error_code)) {
handle_error(); /* .text.cold 섹션으로 이동 */
}
do_work();
/* 코드 생성 차이 (x86-64 어셈블리):
*
* unlikely() 없이:
* test %edi, %edi
* jz .L_skip ← 에러 코드가 0이면 건너뜀
* call handle_error ← 에러 핸들러가 바로 여기
* .L_skip:
* call do_work
*
* unlikely() 사용:
* test %edi, %edi
* jnz .L_cold ← 에러 코드가 0이 아니면 cold로 점프
* call do_work ← 정상 경로가 폴스루 (캐시 친화)
* ...
* .section .text.cold
* .L_cold:
* call handle_error ← 에러 핸들러가 cold 섹션
* jmp .L_after_work
*/
__builtin_unreachable 활용
/* 커널에서 __builtin_unreachable()의 실전 활용 */
/* 1. switch 문에서 모든 case를 처리했음을 보장 */
switch (state) {
case STATE_A: return handle_a();
case STATE_B: return handle_b();
case STATE_C: return handle_c();
}
__builtin_unreachable(); /* 경고 제거 + 최적화 힌트 */
/* 2. BUG() 매크로 이후 (x86) */
#define BUG() do { \
asm volatile("ud2"); \
__builtin_unreachable(); \
} while (0)
/* ud2 이후 코드가 없음을 컴파일러에 알림
* → 이후 경고 제거, 불필요한 코드 생성 방지 */
/* 3. 무한 루프 최적화 */
for (;;) {
cpu_relax();
}
__builtin_unreachable(); /* "함수가 반환할 수 있다" 경고 방지 */
LTO (Link-Time Optimization)
LTO의 내부 동작, 커널에서의 장단점, GCC LTO와 Clang LTO의 차이를 상세히 비교합니다.
LTO 동작 원리
# LTO 빌드 파이프라인 상세
# 1단계: 컴파일 (GIMPLE IR을 .o에 포함)
$ gcc -flto -O2 -c a.c -o a.o # a.o = ELF + GIMPLE IR 섹션
$ gcc -flto -O2 -c b.c -o b.o # b.o = ELF + GIMPLE IR 섹션
# 각 .o 파일에는 기계어가 아닌 GIMPLE IR이 .gnu.lto_* 섹션에 포함
# 2단계: LTO 링크 (전체 프로그램 최적화)
$ gcc -flto=auto -O2 a.o b.o -o program
# 내부적으로:
# 1. lto1: 모든 .o에서 GIMPLE IR 수집
# 2. 전체 프로그램 GIMPLE 최적화 (IPA)
# - 크로스 파일 인라이닝
# - 전역 Dead Code Elimination
# - 절차간 상수 전파
# - 전역 별칭 분석
# 3. RTL 생성 → 레지스터 할당 → 기계어 생성
# 4. ld: 최종 ELF 링크
GCC LTO vs Clang LTO
| 항목 | GCC LTO (WHOPR) | Clang LTO (Full) | Clang ThinLTO |
|---|---|---|---|
| IR 형식 | GIMPLE (GCC 전용) | LLVM Bitcode | LLVM Bitcode (요약) |
| 병렬화 | -flto=auto (WHOPR 파티셔닝) | 제한적 | 높음 (파일 단위) |
| 메모리 사용 | 높음 | 매우 높음 | 낮음 |
| 빌드 시간 | 느림 | 매우 느림 | 비교적 빠름 |
| 최적화 품질 | 우수 | 우수 | 약간 낮음 |
| 커널 Kconfig | CONFIG_LTO_GCC=y | CONFIG_LTO_CLANG_FULL=y | CONFIG_LTO_CLANG_THIN=y |
| 커널 안정성 | 실험적 | 안정 (Android 기본) | 안정 (Android 기본) |
EXPORT_SYMBOL()로 내보낸 심볼이 LTO에 의해 인라인되거나 제거되면 모듈 로드 시 Unknown symbol 오류가 발생합니다.
코드 생성 옵션
-march, -mtune, 리타폴린(Retpoline) 등 아키텍처별 코드 생성 옵션을 다룹니다.
x86-64 마이크로아키텍처 레벨
| 레벨 | GCC 옵션 | 필수 확장 | 해당 CPU (예시) |
|---|---|---|---|
| x86-64 (baseline) | -march=x86-64 | SSE, SSE2 | Athlon 64, Pentium 4 |
| x86-64-v2 | -march=x86-64-v2 | + CMPXCHG16B, SSE3, SSE4.1/4.2, POPCNT | Nehalem, Westmere |
| x86-64-v3 | -march=x86-64-v3 | + AVX, AVX2, BMI1/2, F16C, FMA, LZCNT, MOVBE | Haswell, Zen2 |
| x86-64-v4 | -march=x86-64-v4 | + AVX-512F/BW/CD/DQ/VL | Skylake-SP, Zen4 |
리타폴린(Retpoline) 코드 생성
/* 일반 간접 호출 (Spectre v2 취약) */
call *%rax /* 투기 실행으로 임의 코드 실행 가능 */
/* 리타폴린 변환 (-mindirect-branch=thunk-extern) */
call __x86_indirect_thunk_rax /* thunk 함수로 대체 */
/* thunk 구현 (arch/x86/lib/retpoline.S) */
__x86_indirect_thunk_rax:
call .Lspec_trap /* RSB에 .Lspec_trap 주소 push */
.Lspec_trap:
pause /* 투기 실행이 여기서 루프 */
lfence
jmp .Lspec_trap /* 투기 실행 무한 루프 (무해) */
/* 실제 실행은 ret으로 %rax 주소로 점프 */
/* 결과: 투기 실행이 .Lspec_trap 루프에 갇히고,
* 실제 실행만 올바른 타깃으로 점프 → Spectre v2 방어 */
GCC vs Clang 비교
리눅스 커널 빌드에서 GCC와 Clang/LLVM의 차이점, 호환성, 장단점을 비교합니다.
커널 빌드 호환성 비교
| 항목 | GCC | Clang/LLVM |
|---|---|---|
| Kconfig 감지 | CONFIG_CC_IS_GCC=y | CONFIG_CC_IS_CLANG=y |
| 빌드 명령 | make (기본) | make LLVM=1 |
| 최소 버전 | GCC 5.1 (커널 5.15+) | Clang 14 (커널 6.7+) |
| 플러그인 | GCC 플러그인 API (structleak, randstruct) | 미지원 → Clang 자체 기능으로 대체 |
| CFI | 미지원 | -fsanitize=kcfi (kCFI) |
| LTO | WHOPR (-flto) | ThinLTO / Full LTO |
| 인라인 어셈블리 | GAS 문법 (기본) | LLVM 통합 어셈블러 (일부 차이) |
| 진단 품질 | 양호 | 우수 (컬러, fix-it 힌트) |
| 빌드 속도 | 보통 | 약간 빠름 |
| 아키텍처 지원 | 매우 넓음 | 넓음 (커널 지원은 x86/ARM64/RISC-V 중심) |
| Android 커널 | 레거시 | 공식 (GKI 기본) |
Clang 전용 기능 (커널)
# Clang으로 커널 빌드
$ make LLVM=1 ARCH=x86_64 defconfig
$ make LLVM=1 -j$(nproc) vmlinux
# LLVM=1 은 다음 도구를 LLVM 버전으로 교체:
# CC=clang, LD=ld.lld, AR=llvm-ar, NM=llvm-nm,
# STRIP=llvm-strip, OBJCOPY=llvm-objcopy, OBJDUMP=llvm-objdump
# Clang 전용 보안 기능
CONFIG_CFI_CLANG=y # kCFI (커널 Control Flow Integrity)
CONFIG_SHADOW_CALL_STACK=y # SCS (ARM64 전용, x18 레지스터 사용)
# Clang ThinLTO
CONFIG_LTO_CLANG_THIN=y # 빠른 LTO
# Clang 진단 차이점
# -Wno-gnu: GNU 확장 경고 억제
# -Wno-initializer-overrides: 구조체 지정 초기화 덮어쓰기 경고
CONFIG_CC_IS_GCC/CONFIG_CC_IS_CLANG 외에도 __has_attribute(), __has_builtin() 등 Clang 내장 매크로를 활용하여 컴파일러 중립적 코드를 작성합니다. GCC도 5.x부터 __has_attribute()를 지원하므로, 새 코드에서는 버전 매크로 대신 기능 탐지 매크로를 사용하는 것이 좋습니다.
크로스 컴파일
크로스 컴파일 툴체인의 구조, 설치 방법, 트러블슈팅을 상세히 다룹니다.
크로스 툴체인 구성 요소
| 도구 | 네이티브 | 크로스 (ARM64 예시) | 역할 |
|---|---|---|---|
| 컴파일러 | gcc | aarch64-linux-gnu-gcc | C/C++ 소스 → 어셈블리 |
| 어셈블러 | as | aarch64-linux-gnu-as | 어셈블리 → 오브젝트 |
| 링커 | ld | aarch64-linux-gnu-ld | 오브젝트 → 실행 파일 |
| 아카이버 | ar | aarch64-linux-gnu-ar | 정적 라이브러리 생성 |
| 심볼 덤프 | nm | aarch64-linux-gnu-nm | 심볼 테이블 출력 |
| 오브젝트 복사 | objcopy | aarch64-linux-gnu-objcopy | 바이너리 형식 변환 |
| ELF 분석 | readelf | aarch64-linux-gnu-readelf | ELF 헤더/섹션 분석 |
| 디스어셈블 | objdump | aarch64-linux-gnu-objdump | 디스어셈블리 출력 |
| 스트립 | strip | aarch64-linux-gnu-strip | 디버그 심볼 제거 |
크로스 툴체인 설치
# Ubuntu/Debian — 패키지 매니저 설치
$ sudo apt install gcc-aarch64-linux-gnu # ARM64
$ sudo apt install gcc-riscv64-linux-gnu # RISC-V 64
$ sudo apt install gcc-arm-linux-gnueabihf # ARM 32bit (hard float)
$ sudo apt install gcc-powerpc64le-linux-gnu # PowerPC 64 LE
# Fedora/RHEL
$ sudo dnf install gcc-aarch64-linux-gnu
# 다중 아키텍처 한 번에 설치
$ sudo apt install \
gcc-aarch64-linux-gnu \
gcc-riscv64-linux-gnu \
gcc-arm-linux-gnueabihf
# 설치 확인
$ aarch64-linux-gnu-gcc --version
$ riscv64-linux-gnu-gcc --version
# CROSS_COMPILE 환경 변수 설정
$ export CROSS_COMPILE=aarch64-linux-gnu-
$ export ARCH=arm64
$ make defconfig && make -j$(nproc) Image.gz
크로스 컴파일 트러블슈팅
| 문제 | 증상 | 해결 |
|---|---|---|
| 헤더 불일치 | fatal error: asm/types.h: No such file | ARCH= 변수 확인, make mrproper 후 재빌드 |
| 링커 미스매치 | ld: unrecognized option '--hash-style' | 크로스 링커 경로 확인: $(CROSS_COMPILE)ld --version |
| libgcc 누락 | undefined reference to '__divdi3' | 크로스 GCC의 libgcc 경로 확인: $(CC) -print-libgcc-file-name |
| 바이너리 형식 오류 | ELF file OS ABI invalid | file vmlinux로 아키텍처 확인, ARCH 변수 재확인 |
GCC 플러그인 시스템
GCC 플러그인이 커널 보안을 강화하는 구체적 메커니즘과 각 플러그인의 GIMPLE 변환 동작을 상세히 설명합니다.
structleak 플러그인 동작 원리
/* structleak 비활성화 시 (기본 GCC 동작) */
void copy_to_user_example(void __user *ubuf)
{
struct my_info info;
info.name = "test";
info.value = 42;
/* info.padding 필드는 미초기화 → 커널 스택 데이터 유출! */
copy_to_user(ubuf, &info, sizeof(info));
}
/* structleak 플러그인 활성화 시 (GIMPLE 변환 결과) */
void copy_to_user_example(void __user *ubuf)
{
struct my_info info;
memset(&info, 0, sizeof(info)); /* 플러그인이 자동 삽입! */
info.name = "test";
info.value = 42;
/* info.padding도 0으로 초기화됨 → 정보 누출 방지 */
copy_to_user(ubuf, &info, sizeof(info));
}
/* Kconfig 레벨별 동작 차이 */
/* CONFIG_GCC_PLUGIN_STRUCTLEAK_USER:
* copy_to_user()로 전달되는 구조체만 초기화 */
/* CONFIG_GCC_PLUGIN_STRUCTLEAK_BYREF:
* 주소가 참조(reference)로 전달되는 구조체 초기화 */
/* CONFIG_GCC_PLUGIN_STRUCTLEAK_BYREF_ALL:
* 모든 스택 구조체를 0 초기화 (가장 안전, 약간의 성능 비용) */
randstruct 플러그인 동작
/* __randomize_layout 태그가 있는 구조체의 필드 순서를 무작위화 */
/* 원본 정의 (소스 코드) */
struct cred {
atomic_long_t usage;
kuid_t uid;
kgid_t gid;
kuid_t euid;
/* ... */
} __randomize_layout;
/* 빌드 시 (randstruct 플러그인이 시드 기반으로 필드 재배치) */
struct cred {
kgid_t gid; /* 순서 변경됨! */
kuid_t euid; /* 공격자가 오프셋 예측 불가 */
atomic_long_t usage;
kuid_t uid;
/* ... */
};
/* 시드 파일: scripts/gcc-plugins/randomize_layout_seed.h
* 빌드마다 다른 시드 → 다른 레이아웃
* 커널 개발자는 필드를 이름으로만 접근 (오프셋 하드코딩 금지)
* → container_of, offsetof는 컴파일러가 올바르게 계산 */
latent_entropy 플러그인
/* 부팅 초기에 엔트로피가 부족한 문제를 완화 */
/* 플러그인이 __latent_entropy 태그 함수에 임의의 연산을 삽입 */
static int __latent_entropy init_subsystem(void)
{
/* 플러그인이 삽입하는 코드 (개념적) */
latent_entropy ^= 0xDEADBEEF12345678ULL;
latent_entropy = rol64(latent_entropy, 7);
/* 원래 함수 코드 */
return do_init();
}
/* 부팅 시 여러 __latent_entropy 함수가 실행되면서
* 타이밍 변동이 엔트로피에 기여
* → /dev/random 초기 엔트로피 풀 보강 */
GCC 코드 생성 분석 기법
GCC가 생성한 코드의 품질을 분석하고, 최적화가 올바르게 적용되었는지 확인하는 실전 기법을 다룹니다.
어셈블리 출력 분석
# 어셈블리 출력 + 소스 인터리브 (가장 유용한 분석 방법)
$ gcc -O2 -S -fverbose-asm -o example.s example.c
# -fverbose-asm: 각 어셈블리 명령어에 원본 C 변수명 주석 추가
# 출력 예시:
# addl %esi, %edi # b, a
# movl %edi, %eax # a,
# ret
# 특정 함수만 어셈블리 확인
$ gcc -O2 -S example.c -o - | awk '/^my_func:$/,/^\.size.*my_func/'
# 두 최적화 레벨 비교
$ gcc -O1 -S -o o1.s example.c
$ gcc -O2 -S -o o2.s example.c
$ diff -u o1.s o2.s | less
# objdump로 바이너리에서 역어셈블 (실제 기계어 크기 확인)
$ gcc -O2 -c example.c
$ objdump -d -M intel example.o
# Compiler Explorer와 동일한 분석 (로컬)
$ gcc -O2 -S -masm=intel -fno-asynchronous-unwind-tables \
-fno-exceptions -fno-rtti example.c -o -
최적화 리포트 활용
# 벡터화 결과 리포트
$ gcc -O3 -ftree-vectorize -fopt-info-vec-all=vec.log example.c -c
$ cat vec.log
# 출력 예시:
# example.c:15:3: optimized: loop vectorized using 32 byte vectors
# example.c:22:3: missed: not vectorized: not enough data-refs in basic block
# 인라이닝 결정 리포트
$ gcc -O2 -fopt-info-inline-all=inline.log example.c -c
$ cat inline.log
# 출력 예시:
# Inlining helper_func into main_func, 20 insns
# Not inlining: large_func, --param max-inline-insns-single limit reached
# 활성화된 최적화 패스 전체 목록
$ gcc -O2 -Q --help=optimizers 2>&1 | grep enabled | wc -l
# 특정 패스의 활성화 여부 확인
$ gcc -O2 -Q --help=optimizers 2>&1 | grep -i vectorize
# -ftree-vectorize [enabled]
# 커널 빌드에서 최적화 리포트 생성
$ make KCFLAGS="-fopt-info-vec-missed=vec-missed.log" drivers/net/ethernet/intel/
바이너리 크기 분석
# 함수별 크기 분석 (nm + sort)
$ nm --size-sort -r vmlinux | head -20
# 가장 큰 함수부터 정렬
# 섹션별 크기 분석
$ size vmlinux
# text data bss dec hex filename
# 21831472 8042760 1572864 31447096 1dfdc28 vmlinux
# 섹션 상세 분석
$ readelf -S vmlinux | awk 'NR>5{printf "%8s %s\n", $6, $2}' | sort -rn | head -15
# bloat-o-meter: 두 빌드 간 크기 변화 분석
$ scripts/bloat-o-meter vmlinux.old vmlinux.new
# 출력: 함수별 크기 증감 (양수=증가, 음수=감소)
# 특정 옵션의 크기 영향 측정
$ make -j$(nproc) vmlinux && cp vmlinux vmlinux.baseline
$ make KCFLAGS="-Os" -j$(nproc) vmlinux
$ scripts/bloat-o-meter vmlinux.baseline vmlinux
재현 가능 빌드 (Reproducible Builds)
동일 소스 코드로 비트 단위(bit-for-bit) 동일한 바이너리를 생성하는 기법입니다. 커널은 보안 감사와 배포 검증을 위해 재현 가능 빌드를 지원합니다.
재현 가능 빌드 관련 GCC 옵션
| 옵션 | 역할 | 비결정성 원인 |
|---|---|---|
-ffile-prefix-map=old=new | 파일 경로 정규화 | 빌드 디렉토리 경로가 바이너리에 포함 |
-fmacro-prefix-map=old=new | 매크로 내 경로 정규화 | __FILE__ 매크로 값 |
-fdebug-prefix-map=old=new | DWARF 내 경로 정규화 | 디버그 정보의 소스 경로 |
-frandom-seed=string | 내부 난수 시드 고정 | GCC 내부 해시, 심볼 이름 |
-fno-guess-branch-probability | 분기 확률 추정 비활성화 | 프로파일 없을 때 휴리스틱 차이 |
# 재현 가능 커널 빌드
$ make \
KBUILD_BUILD_TIMESTAMP="2024-01-01" \
KBUILD_BUILD_USER="builder" \
KBUILD_BUILD_HOST="buildhost" \
-j$(nproc) vmlinux
# 경로 정규화 (빌드 디렉토리 독립)
$ make KCFLAGS="-ffile-prefix-map=$(pwd)=." -j$(nproc) vmlinux
# 두 빌드 결과 비교
$ diffoscope vmlinux.1 vmlinux.2 # 상세 차이 분석
$ sha256sum vmlinux.1 vmlinux.2 # 해시 비교
GCC 사용 모범 사례 (커널 개발)
커널 개발에서 GCC를 효과적으로 활용하기 위한 모범 사례를 정리합니다.
핵심 모범 사례
| 영역 | 모범 사례 | 잘못된 사례 | 이유 |
|---|---|---|---|
| 경고 | make W=1으로 검증 | 경고 무시 | 경고는 잠재적 버그의 지표 |
| 최적화 | -O2 유지 | -O3 적용 | 커널 CI가 -O2 기준 테스트 |
| 디버깅 | KCFLAGS="-Og" (개발 시) | -O0 전역 적용 | -O0은 일부 커널 코드에서 오류 |
| FPU | -mno-sse 유지 | SSE/AVX 사용 | 커널 코어에서 FPU 상태 관리 부재 |
| 스택 | VLA 금지, kmalloc 사용 | 큰 로컬 배열 | 커널 스택 크기 제한 (8~16KB) |
| 타입 | __attribute__((packed)) | 패딩 의존 코드 | 아키텍처별 패딩 차이 |
| 컴파일러 호환 | __has_attribute() 사용 | GCC 버전 매크로만 | Clang 호환성 확보 |
| 인라인 | __always_inline | 일반 inline | GCC가 inline 힌트를 무시할 수 있음 |
GCC 출력의 성능 분석 워크플로
# 1. 프로파일 기반 핫스팟 식별
$ perf record -g -- ./workload
$ perf report --stdio
# 2. 핫 함수의 어셈블리 분석
$ objdump -d -j .text vmlinux | awk '/^.*:$/,/^$/' | less
# 3. 최적화 적용 여부 확인
$ gcc -O2 -fopt-info-inline -S hot_function.c -o hot_function.s
# 4. 대안 코드로 A/B 테스트
$ make KCFLAGS="-fprofile-generate" vmlinux # PGO 1단계
# (워크로드 실행)
$ make KCFLAGS="-fprofile-use" vmlinux # PGO 2단계
# 5. 바이너리 크기 영향 확인
$ scripts/bloat-o-meter vmlinux.before vmlinux.after
참고 자료
GCC 공식 문서
- GCC Online Documentation — 전체 매뉴얼 목록 (gcc.gnu.org)
- GCC Option Summary — 전체 옵션 요약 (gcc.gnu.org)
- GCC Optimization Options — -O0/-O1/-O2/-O3/-Os/-Og 상세 (gcc.gnu.org)
- GCC Warning Options — -Wall/-Wextra/-Werror 등 경고 옵션 (gcc.gnu.org)
- Extensions to the C Language Family — GNU C 확장 문법 (gcc.gnu.org)
- GCC Built-in Functions — __builtin_expect, __builtin_clz 등 (gcc.gnu.org)
- Attribute Syntax — __attribute__ 문법 규칙 (gcc.gnu.org)
- Common Function Attributes — noinline, always_inline, cold, hot 등 (gcc.gnu.org)
- Common Variable Attributes — aligned, packed, section, used 등 (gcc.gnu.org)
- Common Type Attributes — packed, aligned, designated_init 등 (gcc.gnu.org)
- Instrumentation Options — -fsanitize, -fprofile-generate 등 (gcc.gnu.org)
- Link Options — -flto, -fuse-linker-plugin 등 (gcc.gnu.org)
- Inline Functions — inline 키워드 동작 방식 (gcc.gnu.org)
GCC 내부 구조
- GCC Internals Manual — 컴파일러 내부 구조 전체 문서 (gcc.gnu.org)
- GCC Wiki — 개발자 위키, 최적화 패스 설명 (gcc.gnu.org)
- GCC Bugzilla — 버그 추적 시스템 (gcc.gnu.org)
링커 및 바이너리 도구
- Linker Scripts — 링커 스크립트 문법 전체 문서 (sourceware.org)
- GNU Assembler Manual — GAS 어셈블러 매뉴얼 (sourceware.org)
Sanitizer 및 분석 도구
- AddressSanitizer (ASan) — 메모리 오류 탐지 (gcc.gnu.org)
- UndefinedBehaviorSanitizer (UBSan) — 미정의 동작 탐지 (gcc.gnu.org)
- ThreadSanitizer (TSan) — 데이터 경쟁 탐지 (gcc.gnu.org)
- KASAN — 커널 AddressSanitizer 사용 가이드 (docs.kernel.org)
- KUBSAN — 커널 UndefinedBehaviorSanitizer (docs.kernel.org)
도구 및 비교
- Compiler Explorer — 다중 컴파일러 실시간 어셈블리 비교 (godbolt.org)
- Clang — GCC Compatibility (clang.llvm.org)
- Building Linux with Clang/LLVM — 커널 Clang 빌드 가이드 (docs.kernel.org)
관련 문서
- Git — mainline/linux-next 추적, 토픽 브랜치와 커밋 규약, format-patc
- GNU Assembler (as) 완전 가이드 — GAS 섹션·심볼·재배치 지시자, 매크로/조건 조립, CFI 언와인드 정보, 아키텍처별 문