C 언어 시리즈 목차 | 기초부터 시스템 프로그래밍까지 10편 학습 경로
이 글의 핵심
C 문법은 알지만 왜 그렇게 동작하는지가 궁금한 개발자를 위한 10편 시리즈의 목차입니다. 편마다 다루는 핵심 질문과 읽는 순서, 세그폴트·링크 에러 같은 증상별로 먼저 펼칠 편, 실습 전에 켜 둘 컴파일러 옵션을 정리했습니다.
시리즈 개요
이 시리즈는 C 언어를 문법 요약이 아니라 실행 모델, ABI, 미정의 동작의 관점에서 정리합니다. 대상은 C 문법을 이미 아는 독자이며, C11/C17 표준을 기준으로 하고 필요한 곳에서 C23의 변경 사항과 GCC·Clang·MSVC의 차이를 짚습니다.
C가 어려운 이유는 문법이 많아서가 아니라, 같은 코드가 컴파일러, 최적화 수준, 플랫폼에 따라 다르게 동작할 수 있기 때문입니다. -O0에서는 멀쩡하던 루프가 -O2에서 끝나지 않거나, x86에서는 문제없던 비정렬 접근이 일부 ARM 환경이나 특정 명령에서는 SIGBUS로 죽기도 합니다. 대부분 원인은 표준이 보장하지 않는 동작에 코드가 기대고 있었다는 데 있습니다. 그래서 이 시리즈는 편마다 “표준은 무엇을 약속하는가, 컴파일러는 그 약속을 어떻게 이용하는가, 약속을 벗어나면 어떤 증상이 나오는가”라는 세 질문을 축으로 삼습니다.
읽기 전에 켜 둘 옵션
예제를 직접 돌려 볼 때는 경고와 Sanitizer를 켠 상태로 컴파일하는 것을 권합니다. 이 시리즈에서 다루는 버그의 상당수는 옵션 없이는 조용히 지나가고, 옵션을 켜면 실행 즉시 줄 번호와 함께 보고됩니다.
# GCC / Clang 공통
cc -std=c17 -Wall -Wextra -Wpedantic -O1 -g \
-fsanitize=address,undefined -fno-omit-frame-pointer main.c -o main
-fsanitize=undefined는 부호 있는 정수 오버플로, 잘못된 시프트, 정렬되지 않은 포인터 역참조 같은 미정의 동작을 실행 중에 잡아 runtime error: signed integer overflow 같은 메시지를 냅니다. MSVC는 Sanitizer 중 /fsanitize=address(AddressSanitizer)만 지원하므로, Windows에서는 WSL이나 Clang으로 UBSan까지 돌려 보는 편이 좋습니다. 각 편의 예제는 짧은 재현 코드라서, 최적화 수준(-O0, -O2)이나 Sanitizer 옵션을 바꿔 가며 결과가 어떻게 달라지는지 직접 확인해 볼 수 있습니다.
전체 목차
기초 개념과 실행 모델
#01 기초와 실행 모델: 객체 표현·정렬·번역 단위
C 표준이 메모리를 “객체”와 그 표현(바이트열)으로 다루는 방식, 정렬과 패딩이 생기는 이유, 번역 단위가 따로 컴파일된다는 사실이 링크 단계에서 갖는 의미를 다룹니다. 구현 정의 동작과 미정의 동작이 실무에서 어떻게 다른지도 여기서 정리합니다.
타입 시스템과 변환
#02 타입·승격(usual arithmetic)·정수 표현과 패딩
정수 승격과 통상 산술 변환 때문에 size_t와 int를 섞은 비교나 루프가 깨지는 이유, 고정폭 정수와 엔디안, 구조체 패딩을 직렬화할 때 _Static_assert로 검증하는 방법, 부동소수점 환경(fenv.h)과 관련 pragma의 상호작용을 다룹니다.
제어 흐름과 최적화
#03 제어 흐름: 분기·스위치 테이블·setjmp와 스택
분기 예측과 switch가 점프 테이블로 바뀌는 조건, Duff’s device의 유지보수 비용, 자원 정리를 위한 goto 패턴, setjmp/longjmp가 지역 변수와 VLA, 스택 정리와 충돌하는 이유를 다룹니다.
함수와 호출 규약
#04 함수·스택 프레임·호출 규약(ABI)과 가변 인자
System V AMD64와 Windows x64 호출 규약의 인자 레지스터, 스택 슬롯, 16바이트 정렬 규칙, 프레임 포인터를 생략했을 때 디버깅과 스택 언와인드에 미치는 영향, va_list로 가변 인자를 순회하는 방식과 그 타입 안전성 문제, 인라인과 심볼 가시성을 다룹니다.
포인터와 메모리 접근
#05 포인터 연산·엄격 별칭(strict aliasing)·유효성
포인터 덧셈과 배열 끝 다음(one-past-the-end) 규칙, 엄격 별칭 규칙 때문에 최적화 후 코드가 깨지는 경로, restrict 계약과 유효 타입(effective type), 타입 재해석에 memcpy를 쓰는 패턴, 포인터 출처(provenance) 개념, -fno-strict-aliasing과 Sanitizer로 대응하는 방법을 다룹니다.
배열과 문자열
#06 배열·디케이(decay)·문자열 리터럴·VLA
배열이 포인터로 변환(decay)되는 규칙과 sizeof의 예외, 함수 매개변수로 넘기면 길이 정보가 사라지는 문제, 문자열 리터럴이 읽기 전용 영역에 놓이는 이유, VLA가 스택 사용량을 예측하기 어렵게 만드는 방식과 대안, UTF-8 문자열에서 인덱스가 바이트 단위라는 점을 다룹니다.
구조체와 공용체
#07 구조체·공용체·비트필드·패딩과 ABI
ABI에 고정된 구조체 레이아웃을 offsetof와 _Static_assert로 검증하는 방법, 패딩과 캐시 라인 거짓 공유(false sharing), union의 활성 멤버와 유효 타입, 비트필드 배치가 구현 정의라서 하드웨어 레지스터 맵에 쓰기 위험한 이유, 네트워크 프로토콜에 enum을 그대로 쓰면 안 되는 이유를 다룹니다.
전처리와 컴파일 파이프라인
#08 전처리기 8단계·매크로·_Pragma·번역 단위 경계
ISO C가 정의한 번역 단계에서 전처리기가 맡는 부분, #include가 텍스트를 합성하는 방식과 번역 단위 경계, 함수형 매크로의 다중 평가와 괄호, #/## 연산자의 함정, 여러 문장을 담는 매크로, _Pragma와 include guard, 조건부 컴파일을 다룹니다.
동적 메모리 관리
#09 동적 메모리·단편화·할당기·Sanitizer·프로덕션 패턴
malloc/realloc 실패 처리와 누수 패턴, 힙 단편화와 할당기 메타데이터, 아레나·풀·스레드 로컬 캐시의 트레이드오프, calloc이 malloc + memset과 다른 점, ASan·Valgrind·malloc_stats로 메모리 문제를 검증하는 흐름을 다룹니다.
빌드와 링커
#10 컴파일 파이프라인·링커 심볼·정적·동적 링크
전처리기, 컴파일러, 어셈블러, 링커로 이어지는 파이프라인, 오브젝트 파일의 재배치 정보와 심볼 테이블, 링커가 내는 미해결 심볼과 중복 정의 에러, -fPIC와 PLT/GOT, rpath, nm/readelf를 이용한 진단, 정적·동적 링크의 차이와 RELRO 같은 보안 관련 링크 옵션을 다룹니다.
학습 경로
처음 읽는 경우
처음 읽는 경로에서 #03(제어 흐름)과 #07(구조체), #08(전처리기)을 뒤로 미룬 이유가 있습니다. #03의 setjmp/longjmp와 #07의 ABI 레이아웃은 #04의 스택 프레임과 호출 규약을 알아야 제대로 이해되고, #08의 매크로 함정은 #01의 번역 단위 개념 위에서 읽어야 “왜 헤더에 정의를 두면 링크 에러가 나는가”까지 연결됩니다. 반대로 #02의 산술 변환 규칙은 거의 모든 편의 예제에 등장하므로 초반에 읽어 두는 것이 좋습니다. int i = -1;일 때 if (i < sizeof(buf))가 거짓이 되는 버그가 대표적인데(i가 size_t로 변환되어 아주 큰 수가 됩니다), 이 동작을 모르면 이후 편의 예제를 읽다가 계속 걸리게 됩니다.
특정 주제만 필요한 경우
증상별로 먼저 볼 편
C에서는 증상과 원인이 멀리 떨어져 있는 경우가 흔합니다. 크래시는 free() 내부에서 났지만 실제 원인은 한참 전에 힙 버퍼를 넘겨 써서 할당기 메타데이터를 망가뜨린 코드일 수 있고, 헤더에 static 없이 inline 함수만 정의한 C99 코드에서 나는 undefined reference 링크 에러는 C의 inline 정의가 외부 정의를 만들지 않는다는 언어 규칙에서 비롯됩니다. 위 목록은 증상이 처음 드러나는 곳이 아니라 원인이 있을 가능성이 높은 곳을 기준으로 편을 골랐습니다.
같이 보면 좋은 시리즈
- C++ 시리즈 전체 목차: #0~#55 기초부터 고성능·분산 시스템까지 — C의 기초 위에 현대 C++ 기능 쌓기
- Rust 기초 — 메모리 안전성을 언어 차원에서 보장하는 접근