C언어

C has the power of assembly language and the convenience of ... assembly language.

— C언어의 극단적인 하드웨어 친화적 제어 특성을 풍자하는 전산학자들의 역사적인 명언

오늘날 찬란하게 빛나는 Java, JavaScript, Python도 모두 껍데기를 벗겨보면 밑바닥은 결국 C언어로 짜인 가상 머신과 인터프리터가 돌려주고 있는, 명실상부한 전산학의 바이블이자 태초의 빛. 컴파일러는 에러를 뱉지 않는데 프로그램은 멋대로 뻗어버리고, 디버거를 켜면 주소값 '0x7ffee3b3a' 같은 외계어와 함께 터지는 메모리 오염 버그를 마주할 때 인류 문명에 대한 회의감이 밀려온다.(...)

1. 개요

1972년 벨 연구소의 데니스 리치가 유닉스(UNIX) 운영체제를 개발하기 위해 고안한 역사적인 하이레벨 컴파일 프로그래밍 언어. 기계어 및 어셈블리어와 완벽히 호환되는 미친 수준의 하드웨어 제어력을 지니고 있으면서도, 사람이 읽기 쉬운 문법 체계를 확립해 현대의 거의 모든 명령형 언어들의 직계 조상이 되었다. 하드웨어 리소스가 극도로 제한된 임베디드, 고도의 연산력이 요구되는 게임 엔진, 그리고 컴퓨터 시스템의 정수인 OS 커널 코딩 영역에서 반세기 넘게 대체 불가능한 권력을 누리고 있다.

2. 포인터와 메모리 제어라는 절대 반지

C언어의 가장 큰 기술적 특징이자 초보자들의 최대 통곡의 벽은 바로 포인터(Pointer)수동 메모리 관리이다. 변수가 위치한 물리적인 하드웨어 RAM 메모리의 실제 주소값을 직접 조작할 수 있는 강력한 포인터 제어 기능을 지원한다. 가비지 컬렉터(Garbage Collection)가 탑재된 모던 언어들과 달리, 개발자가 직접 malloc() 함수를 호출해 메모리를 운영체제로부터 빌려오고 작업이 끝나면 반드시 free() 함수로 반환해야 한다.1 이 수동 제어 덕분에 쓰레기 수집기가 가동될 때 발생하는 시스템 정지(Stop-the-world) 현상이 전혀 없어 제로 오버헤드(Zero Overhead)에 근접한 속도를 자랑하지만, 단 하나만 실수해도 다른 메모리 영역을 오염시키는 세그멘테이션 폴트(Segmentation Fault)나 메모리 누수(Memory Leak)가 야기되는 가혹한 환경을 안고 있다.

3. 타협 없는 성능과 시스템의 지배자

그럼에도 불구하고 현대 전산 진영이 C언어를 완전히 버릴 수 없는 이유는 바로 절대적인 성능과 이식성 때문이다. 거의 모든 하드웨어 칩셋 제조사들은 자사 아키텍처에 맞춤형으로 튜닝된 C 컴파일러를 가장 먼저 배포하며, C로 작성된 코드는 아주 단순한 컴파일 파이프라인을 거쳐 순수 바이너리 기계어로 변환되므로 다른 가상머신 기반 언어들과는 차원이 다른 연산 효율성을 제공한다.이식성이 너무 훌륭해서 토스터기에서도 돌아간다는 소문이 사실이다. 또한, 전 세계 서버 생태계를 완벽하게 집어삼킨 리눅스 커널과 마이크로소프트의 Windows 운영체제 소스 코드의 90% 이상이 C언어와 그 직계인 C++로 구축되어 있다.2 하드웨어의 미세한 트랜지스터 하나부터 거대한 클라우드 가상화 영역까지, 사실상 현대 모든 정보 기술 인프라는 C언어가 깔아둔 무대 위에서 춤을 추고 있는 셈이다.

4. 포인터 통곡의 벽과 널 포인터 역참조

4.1. Segmentation fault (core dumped)

C언어를 전공 필수로 공부하는 전산학과 학부생 및 현업 주니어들의 수명을 깎아 먹는 가장 사악하고도 무미건조한 에러 메시지다. 소스 코드가 논리적으로 잘 구성되어 컴파일에 성공하고 호기롭게 실행 파일을 돌리는 순간, 어떠한 세부적인 설명도 없이 저 한 줄의 차가운 시스템 메시지만을 남기고 OS가 강제로 프로그램을 영구 중단시킨다. 범인은 십중팔구 존재하지 않는 주소를 찌른 '널 포인터 역참조(Null Pointer Dereference)'이거나, 선언된 배열 크기를 초과해 메모리를 덮어써 버린 버퍼 오버플로우(Buffer Overflow) 공격이다. 기계와 1대1로 대화해야 하는 언어 특성상, 버그를 잡으려면 메모리 헥사 덤프를 직접 열어 주소값을 한 바이트씩 추적해야 하는 참담한 삽질이 동반되기도 한다.(...)

5. 여담

  • 벨 연구소의 장난감에서 세계의 중심으로: 데니스 리치는 원래 유닉스를 어셈블리어로 작성하기 너무 귀찮아서, 켄 톰슨이 만든 B 언어를 발전시켜 C언어를 설계했다. 그저 벨 연구소 내부의 파일 공유와 원활한 시스템 업그레이드를 위한 자작 프로젝트 수준이었으나, 이 가볍고 강력한 이식성에 홀딱 반한 전 세계 대학과 연구소들이 너도나도 유닉스를 설치하며 IT 혁명의 주역이 되었다.
  • C++과 Rust의 끝없는 왕위 계승 도전: C언어의 생산성과 메모리 안전성을 극대화하기 위해 역사적으로 수많은 언어들이 도전장을 던졌다. C언어의 문법 구조를 거의 그대로 계승하며 객체 지향을 덧씌운 C++이 탄생했고, 최근에는 메모리 소유권(Ownership) 개념을 도입해 빌드 시점에 포인터 에러를 완벽히 예방하겠다는 Rust가 무섭게 치고 올라오고 있지만, C언어 고유의 가볍고 직관적인 컴파일 최적화 속도를 완전히 꺾지는 못했다.
  • 이름에 얽힌 소박한 비밀: B 언어의 뒤를 이었다고 해서 알파벳 다음 글자인 'C'를 이름으로 채택했다. 만약 C언어의 후속 프로젝트들이 이름을 문자 순서대로 지었다면 현대 개발자들은 'D언어'나 'E언어'를 배우며 코딩하고 있었을지도 모른다.

6. 관련 문서

각주

  1. 포인터 연산 시 단 1바이트만 어긋나도 운영체제는 해당 프로그램이 임의로 보호 영역을 침범했다고 판단해 그 즉시 사살(Process Termination) 명령을 내린다.

  2. 윈도우, 맥OS, 안드로이드, iOS, 리눅스를 가리지 않고 현존하는 거의 모든 소비자용 운영체제의 최하단 하드웨어 추상화 계층(HAL)과 마이크로커널은 여전히 C언어 소스코드로 수십 년째 단단히 고정되어 지탱되고 있다.