Step 56. C 시작 — 기계가 직접 아는 언어로

Step 56. C 시작 — 기계가 직접 아는 언어로

Level 1 — 프로그래밍과 컴퓨터 내부 | 난이도 ★★★☆☆ | 예상 소요 시간 4시간

전제: Step 41~55를 마쳤다. 파이썬으로 프로그램을 만들 수 있다. 리눅스 터미널(WSL 또는 VirtualBox 우분투)을 열 수 있다.

  • 준비물: 리눅스 터미널, gcc(설치법은 3-1절에서 함께 합니다).
  • 주의: 오늘 실습은 내 컴퓨터 안에서 파일을 만들고 번역하고 실행하는 일뿐, 100% 안전합니다.

여기까지 파이썬으로 왔습니다. 그런데 보안의 깊은 곳 — 운영체제, 네트워크 장비, 유명한 취약점들 — 에는 다른 언어가 깔려 있습니다. C입니다. 운영체제 커널도, 파이썬 자체도, 대부분의 시스템 도구도 C로 만들어졌습니다. C는 입문자에게 어려운 파트입니다. 처음엔 헷갈립니다. 정상입니다.

왜 지금 C를 만나느냐면, C가 "메모리에서 무슨 일이 일어나는가"를 가장 날것으로 보여 주는 언어이기 때문입니다. 파이썬이 자동으로 처리해 준 것들을 C는 우리 손에 맡깁니다. 그 자유가 C의 힘이고, 동시에 버퍼 오버플로우 같은 취약점이 태어나는 자리입니다. 이 단원의 목적은 C 고수가 되는 것이 아닙니다. "메모리를 보는 눈"을 얻는 것, 그것이 전부이자 충분한 목표입니다.


1. 학습 목표

이 챕터를 끝내면 다음을 할 수 있습니다:

  • 해석 언어(파이썬)와 컴파일 언어(C)의 차이를 설명한다
  • 리눅스에서 gcc로 C 프로그램을 컴파일하고 ./로 실행한다
  • main 함수와 printf의 서식 지정자(%d, %s)를 쓴다
  • scanf로 입력을 받되, 변수 앞에 &를 붙이는 규칙을 안다
  • 컴파일 오류 메시지에서 줄 번호를 읽고 스스로 고친다

2. 배경 지식 — 오늘의 도구와 개념

오늘의 도구 한눈에 보기

구분 내용
언어·환경 C 언어, 리눅스 터미널(WSL 우분투), gcc 컴파일러 (실측: Ubuntu 13.3.0)
오늘의 문법 #include <stdio.h>, int main(void), printf와 서식 지정자(%d, %s), scanf&, 세미콜론(;)
필요한 개념 컴파일(번역), 실행 파일, 자료형(int, char 배열), 변수의 주소

2-1. 해석과 번역 — 파이썬과 C의 결정적 차이

파이썬은 한 줄씩 읽으며 바로 실행하는 해석 언어(인터프리터 언어)입니다. 통역사가 문장마다 동시통역하는 방식입니다. C는 다릅니다. 소스 코드 전체를 기계어(CPU가 직접 아는 명령)로 통째로 번역해 실행 파일을 만들고, 그 파일을 실행합니다.

이 번역을 컴파일(compile)이라 하고, 번역기를 컴파일러(compiler)라 합니다. 책 전체를 미리 번역해 두고 읽는 셈이라, 실행은 훨씬 빠릅니다. 대신 고칠 때마다 다시 번역해야 합니다.

2-2. gcc — 가장 유명한 번역기

리눅스 세계의 대표 C 컴파일러가 gcc입니다. gcc hello.c -o hello는 "hello.c를 번역해서 hello라는 실행 파일을 만들어라"입니다. 오늘 우리가 계속 치게 될 명령입니다.

2-3. main — 프로그램의 현관

C 프로그램은 언제나 main 함수에서 시작됩니다. 컴파일된 실행 파일이 깨어나면 가장 먼저 main을 찾습니다. 파이썬이 맨 윗줄부터 흘러가는 것과 달리, C는 이 현관이 있어야 문이 열립니다.

2-4. printf와 서식 지정자 — 빈칸 끼워 넣기 출력

C의 출력 함수 printf는 "문장 틀"과 "끼워 넣을 값"을 나눠서 받습니다. printf("나이: %d", age)에서 %d가 빈칸이고, age가 그 빈칸에 들어갈 값입니다. %d는 정수, %s는 문자열을 뜻하는 서식 지정자(빈칸의 형태 표시)입니다.

빈칸의 종류와 값의 종류가 맞아야 합니다. 어긋나면 어떻게 되는지는 오늘 직접 실험합니다.

2-5. 왜 리눅스에서 하는가

C는 윈도우에서도 할 수 있지만, 보안 공부의 표준 무대는 리눅스입니다. 운영체제와 도구들이 C로 되어 있는 그 세계에서 직접 만지는 것이 가장 자연스럽습니다. 앞으로의 실습도 리눅스(우분투) 기준입니다.


3. 따라 하기

오늘부터는 리눅스 터미널에서 합니다. 파워쉘에서 wsl을 입력해 들어간 WSL 우분투든, VirtualBox의 우분투든 어느 쪽이든 좋습니다.

3-1. gcc 설치와 확인

입력 (리눅스 터미널)

sudo apt install gcc -y
gcc --version
gcc (Ubuntu 13.3.0-6ubuntu2~24.04.1) 13.3.0
Copyright (C) 2023 Free Software Foundation, Inc.
This is free software; see the source for copying conditions.  There is NO
warranty; ...

(2026-09-09 실측. 버전 숫자는 환경에 따라 다를 수 있습니다.)

읽는 법: sudo는 "관리자 권한으로", apt는 우분투의 앱스토어, -y는 "묻지 말고 진행해"입니다. 버전이 보이면 번역기 준비 완료입니다. 이미 깔려 있으면 설치 단계는 "이미 최신 버전"이라고 나옵니다.

3-2. 첫 C 프로그램 — Hello, C

입력 (nano 에디터로 hello.c를 만듭니다)

nano hello.c

에디터가 열리면 다음을 입력합니다.

#include <stdio.h>

int main(void) {
    printf("Hello, C!\n");
    return 0;
}

저장하고 나오는 법: Ctrl+O(저장), Enter(확인), Ctrl+X(나가기).

읽는 법: 첫 줄 #include <stdio.h>는 "printf의 설명서(헤더 파일)를 가져와라"입니다. int main(void)가 현관이고, 중괄호 {} 사이가 할 일입니다. \n은 줄바꿈, return 0은 "잘 끝났다"는 퇴장 인사입니다. 그리고 문장 끝의 세미콜론(;) — 파이썬에 없던 이것이 C의 마침표입니다.

3-3. 컴파일과 실행

입력

gcc hello.c -o hello
./hello
Hello, C!

(2026-09-09 실측.)

읽는 법: gcc가 조용히 끝나면 번역 성공이고, hello라는 실행 파일이 생깁니다. ./hello./는 "현재 폴더에 있는"이라는 뜻입니다. 리눅스는 현재 폴더의 프로그램도 이 표시 없이는 실행해 주지 않습니다(직접 확인해 보면 hello: command not found가 뜹니다 — 벽 2에서 다룹니다).

예측: hello.c에서 세미콜론 하나를 지우고 다시 컴파일하면 어떻게 될까요? 일부러 지워 보세요. 오류 메시지에 줄 번호가 나옵니다. 실측 메시지는 6번 섹션 벽 1에 있습니다.

: "쓰기(nano) → 번역(gcc) → 실행(./)" 이 세 박자가 C의 일상 리듬입니다.

3-4. 변수와 서식 지정자

입력 (vars.c)

#include <stdio.h>

int main(void) {
    int age = 20;
    char name[] = "Lee";
    printf("이름: %s\n", name);
    printf("나이: %d\n", age);
    printf("내년 나이: %d\n", age + 1);
    return 0;
}

컴파일과 실행

gcc vars.c -o vars
./vars
이름: Lee
나이: 20
내년 나이: 21

(2026-09-09 실측.)

읽는 법: C는 변수를 만들 때 자료형(type)을 먼저 선언합니다. int age는 "정수 상자 age", char name[]는 "문자 나열 상자 name"(C에서 문자열은 문자의 배열입니다)입니다. printf의 빈칸(%s, %d)에 값이 순서대로 끼워집니다.

예측: %d 자리에 문자열을 넣으면 어떻게 될까요? 컴파일은 될까요, 실행은 될까요? 직접 실험해 보세요. 실측 결과는 벽 4에 있습니다 — 이 실험이 "C는 우리를 믿는다"는 감각을 줍니다.

3-5. 입력받기 — scanf와 &의 첫 등장

입력 (ask.c)

#include <stdio.h>

int main(void) {
    int age;
    printf("나이를 입력하세요: ");
    scanf("%d", &age);
    printf("입력한 나이: %d\n", age);
    return 0;
}

컴파일과 실행

gcc ask.c -o ask
./ask
나이를 입력하세요: 20
입력한 나이: 20

(2026-09-09 실측. 첫 줄의 20은 제가 키보드로 친 입력입니다.)

읽는 법: scanf는 printf의 반대 방향입니다. 키보드 입력을 빈칸(%d)에 맞춰 읽어 변수에 담습니다. 그런데 변수 앞에 &가 붙었습니다. 이것은 "age의 주소"라는 뜻입니다. scanf에게 "값을 담을 상자의 주소"를 알려 주는 것입니다.

왜 주소를 주어야 하는지는 포인터 챕터에서 정면으로 다룹니다. 지금은 "scanf에는 &를 붙인다"로 외워 두세요. 이 &가 이 단원 전체의 씨앗입니다.

3-6. 이름까지 — 종합 입력 프로그램

입력 (profile.c)

#include <stdio.h>

int main(void) {
    char name[20];
    int age;
    printf("이름을 입력하세요: ");
    scanf("%19s", name);
    printf("나이를 입력하세요: ");
    scanf("%d", &age);
    printf("%s님은 %d살이군요.\n", name, age);
    return 0;
}

컴파일과 실행

gcc profile.c -o profile
./profile
이름을 입력하세요: Lee
나이를 입력하세요: 20
Lee님은 20살이군요.

(2026-09-09 실측.)

읽는 법: 주목할 것 둘. 첫째, name에는 &가 없습니다. 문자 나열(배열)은 이름 자체가 주소처럼 동작하기 때문입니다 — 지금은 규칙으로 받아들이면 됩니다. 둘째, %19s의 19는 "최대 19글자만"이라는 안전장치입니다. 상자(20칸)보다 긴 입력을 받지 않겠다는 뜻으로, 이 한 글자가 유명한 취약점 하나를 막는 예방 주사입니다.

3-7. 경고 켜기 — 엄격한 선생 고용하기

gcc에 -Wall 옵션을 붙이면 "모든 경고를 보여 줘"가 됩니다.

입력 (warn.c)

#include <stdio.h>

int main(void) {
    int x;
    printf("%d\n", x);
    return 0;
}

입력

gcc -Wall warn.c -o warn
warn.c: In function 'main':
warn.c:5:5: warning: 'x' is used uninitialized [-Wuninitialized]
    5 |     printf("%d\n", x);
      |     ^~~~~~~~~~~~~~~~~
warn.c:4:9: note: 'x' was declared here
    4 |     int x;
      |         ^

(2026-09-09 실측.)

읽는 법: 컴파일은 됐는데 경고가 떴습니다. "x에 값을 안 넣고 썼는데, 의도한 거니?"라는 조언입니다. 이 프로그램은 실행할 때마다 쓰레기 값을 뿜습니다. 오류(error)는 번역 실패, 경고(warning)는 "번역은 했지만 수상함"입니다. C에서는 경고도 오류처럼 대접하는 것이 실력자의 습관입니다. 앞으로 컴파일은 언제나 gcc -Wall 파일.c -o 실행파일로 합시다.

: C는 우리를 믿는 언어라, 믿음을 점검해 주는 장치를 스스로 켜야 합니다.


4. 미션과 연습문제

미션 — 자기소개 카드 프로그램

오늘의 작품: 자기소개 카드 프로그램을 C로 만듭니다.

  1. 이름(문자열), 나이(정수), 좋아하는 숫자(정수)를 차례로 입력받습니다.
  2. 입력받은 내용을 보기 좋게 출력합니다. 출력 형태의 예:
=== 자기소개 카드 ===
이름: Lee
나이: 20
좋아하는 숫자: 7
10년 뒤에는 30살이 되는군요!
  1. 일부러 세미콜론을 하나 지우고 컴파일해서, 오류 메시지의 줄 번호를 확인하고 고치는 연습을 합니다.
  2. %d 자리에 이름을 넣는 등 서식 지정자 실험을 한 가지 이상 하고, 결과를 노트에 적습니다.
  3. 완성한 .c 파일과 실행 파일을 구분해 보관합니다. 소스는 .c, 산출물은 실행 파일입니다.

연습문제

문제 1. 파이썬은 작성 즉시 실행할 수 있고 C는 컴파일이라는 단계가 필요합니다. 이 차이가 "실행 속도"와 "실행 파일의 존재"에 각각 어떤 결과를 만드는지 말해 보세요.

문제 2. 리눅스에서 방금 만든 프로그램을 hello라고만 치면 왜 "command not found"가 뜰까요? ./가 해결하는 이유와, 이것이 보안상 갖는 이점을 함께 말해 보세요.

문제 3. scanf에는 변수의 값이 아니라 주소(&age)를 줍니다. 왜 값으로는 부족할까요? "상자를 찾아가야 넣을 수 있다"는 관점에서 설명해 보세요.

문제 4. scanf("%19s", name)의 19는 무엇이며, 왜 20이 아닐까요? 그리고 이 숫자를 생략한 scanf("%s", name)이 왜 위험한지, "상자 크기"의 관점에서 말해 보세요.


5. 모범 답안과 완료 기준

미션 모범 답안

#include <stdio.h>

int main(void) {
    char name[20];
    int age, fav;
    printf("이름: ");
    scanf("%19s", name);
    printf("나이: ");
    scanf("%d", &age);
    printf("좋아하는 숫자: ");
    scanf("%d", &fav);
    printf("=== 자기소개 카드 ===\n");
    printf("이름: %s\n", name);
    printf("나이: %d\n", age);
    printf("좋아하는 숫자: %d\n", fav);
    printf("10년 뒤에는 %d살이 되는군요!\n", age + 10);
    return 0;
}

컴파일과 실행 (2026-09-09 실측)

gcc -Wall card.c -o card
./card
이름: Lee
나이: 20
좋아하는 숫자: 7
=== 자기소개 카드 ===
이름: Lee
나이: 20
좋아하는 숫자: 7
10년 뒤에는 30살이 되는군요!

검증하는 법: ① -Wall로 컴파일했을 때 경고가 하나도 없어야 합니다. ② 세미콜론을 지웠을 때 error: expected ';'가 뜨고, 줄 번호를 보고 고칠 수 있으면 됩니다. ③ 서식 실험에서 이상한 숫자가 나오는 것을 눈으로 확인했으면 충분합니다 — "C는 틀려도 실행해 버린다"가 그 실험의 수확입니다.

연습문제 해답

문제 1 해답. 파이썬은 실행할 때마다 통역하므로 시작이 빠르지만 매 줄 통역 비용이 듭니다. C는 번역을 한 번 해 두면 그 뒤 실행은 기계어 그대로 달리므로 훨씬 빠릅니다. 그리고 컴파일의 산출물로 실행 파일이라는 실체가 남습니다 — 소스 없이도 돌릴 수 있는, CPU가 직접 아는 언어로 된 물건입니다.

문제 2 해답. 리눅스는 명령어를 PATH(검색 경로)에 등록된 폴더에서만 찾고, 현재 폴더는 거기 포함되지 않기 때문입니다. ./hello는 "현재 폴더의 hello"라고 명시하는 것입니다. 보안상 이점: 낯선 폴더에 같은 이름의 가짜 명령어를 심어 두는 함정에 걸리지 않습니다.

문제 3 해답. scanf는 입력값을 "어느 칸에" 넣을지 알아야 합니다. 값(예: 20)을 건네면 그것은 상자의 번지가 아니라 그냥 숫자일 뿐이라, 상자를 찾아갈 수 없습니다. 주소(&age)를 줘야 scanf가 그 칸으로 찾아가 값을 담을 수 있습니다.

문제 4 해답. 19는 "최대 19글자만 읽어라"는 제한입니다. 상자가 20칸인데 19까지만 받는 이유는, C 문자열의 끝에 항상 보이지 않는 종료 표시(널 문자) 한 칸이 더 필요하기 때문입니다. 이 숫자를 생략하면 상자보다 긴 입력이 그대로 넘쳐 옆 메모리를 덮습니다 — 버퍼 오버플로우라는 취약점의 기본 형태입니다.

완료 기준 체크리스트

  • [ ] 해석 언어와 컴파일 언어의 차이를 설명할 수 있다
  • [ ] gcc로 컴파일하고 ./로 실행할 수 있다
  • [ ] printf의 서식 지정자 %d와 %s를 쓸 수 있다
  • [ ] scanf에 &가 필요한 이유를 "주소"로 설명할 수 있다
  • [ ] 컴파일 오류 메시지에서 줄 번호를 찾아 고칠 수 있다
  • [ ] 자기소개 카드 프로그램을 컴파일·실행했다

6. 흔한 실수와 해결

벽 1. 세미콜론 누락 — 초보 에러 1위

증상: 컴파일할 때 이런 오류가 뜹니다 (2026-09-09 실측):

hello_err.c:4:26: error: expected ';' before 'return'
    4 |     printf("Hello, C!\n")
      |                          ^
      |                          ;

원인: 문장 끝의 세미콜론을 빠뜨렸습니다. C는 마침표 없는 문장을 용납하지 않습니다.
해결: 오류 메시지의 줄 번호(여기서 4번)를 보고, 그 줄 끝 또는 바로 윗줄 끝을 확인하세요. 친절하게 ; 표시까지 찍어 줍니다.

벽 2. ./ 없이 실행한다

증상 (2026-09-09 실측):

bash: line 1: hello: command not found

원인: 리눅스는 현재 폴더를 명령어 검색 경로(PATH)에서 보지 않습니다. 낯선 폴더의 가짜 명령어 함정을 막는 보안 설계이기도 합니다.
해결: ./hello처럼 "현재 폴더의 것"이라고 명시합니다. 이 실수를 계기로 PATH(Step 51의 환경 변수)의 존재 이유가 체감됩니다.

벽 3. scanf의 &를 빼먹는다

증상: -Wall로 컴파일하면 이런 경고가 뜨고, 실행하면 프로그램이 죽거나 이상하게 동작합니다 (2026-09-09 실측):

noamp.c:5:13: warning: format '%d' expects argument of type 'int *', but argument 2 has type 'int' [-Wformat=]
    5 |     scanf("%d", age);
      |            ~^   ~~~

원인: scanf에게 변수의 주소(int *)가 아니라 값(int)을 줬습니다. "상자의 번지" 대신 "상자 안의 숫자"를 건넨 셈입니다.
해결: "scanf에는 &(단, 문자 배열은 제외)"라는 주문을 외우세요. 경고 문구가 번역은 됐지만 종류가 어긋났음을 정확히 알려 줍니다.

벽 4. 서식 지정자와 값의 종류가 어긋난다

증상: %d 자리에 문자열을 넣었더니 컴파일 경고 후 이런 출력이 나옵니다 (2026-09-09 실측):

fmtmix.c:6:14: warning: format '%d' expects argument of type 'int', but argument 2 has type 'char *' [-Wformat=]
(실행 출력) -1509063356

원인: 빈칸의 종류(%d는 정수)와 끼워 넣은 값(문자열)이 다릅니다. C는 "믿고" 실행해 버려서, 문자열이 놓인 자리의 번지를 정수처럼 읽은 엉뚱한 숫자가 나옵니다.
해결: 빈칸과 값의 종류를 맞추세요. 그리고 이 화면을 기억하세요 — "에러 없이 실행되는 것"과 "올바른 것"은 다릅니다.

벽 5. nano에서 못 나온다

증상: 에디터에 갇혀 우왕좌왕한다.
원인: nano의 단축키가 화면 아래에 적혀 있는데 처음엔 안 보입니다. ^ 기호는 Ctrl을 뜻합니다.
해결: Ctrl+O → Enter(저장), Ctrl+X(나가기). 저장 안 하고 나가면 "저장할까요?"라고 물으니 Y로 답하면 됩니다.


7. 정리

오늘의 개념

개념 한 줄 설명
컴파일 소스 전체를 기계어로 통째로 번역하는 것
실행 파일 컴파일의 산출물 — CPU가 직접 아는 언어로 된 프로그램
main 함수 C 프로그램의 현관 — 실행은 언제나 여기서 시작
서식 지정자 printf/scanf의 빈칸 표시 (%d 정수, %s 문자열)
오류(error) vs 경고(warning) 번역 실패 vs 번역은 됐지만 수상함 — 둘 다 고친다

오늘의 문법과 명령

문법·명령 하는 일
gcc 파일.c -o 이름 C 소스를 번역해 실행 파일 생성
./이름 현재 폴더의 프로그램 실행
gcc -Wall ... 모든 경고를 켜고 컴파일 (기본 습관)
printf("...%d...\n", 값) 빈칸에 값을 끼워 출력
scanf("%d", &변수) 입력을 변수의 주소로 받아 담기
char name[20] + %19s 20칸 상자에 19글자까지만 — 넘침 예방

문법보다 중요한 감각

"쓰기, 번역, 실행" 세 박자. 파이썬으로만 살아온 세계와 오늘 이후 세계의 경계선은 이 "번역" 단계입니다. 여러분은 오늘 번역된 프로그램을 처음 만들었고, 그 작은 실행 파일은 CPU가 직접 아는 언어로 된 산출물입니다.

하나 더. C는 "프로그래머를 전적으로 믿는" 언어라, 실수한 만큼 그대로 사고가 납니다. 오늘 %19s로 본 것처럼 "상자 크기를 아는 코드"와 "모르는 코드"의 차이가 곧 취약점 유무의 차이입니다. 지금은 문법이지만, 이 문법이 뒤의 보안 이야기 전체의 뿌리입니다. 그리고 컴파일 에러는 적이 아니라 실행 전에 문제를 잡아 주는 엄격한 선생입니다 — 오류 메시지를 두려워하지 말고 줄 번호부터 읽는 습관을 들이세요.


전부 체크되면 Step 56 완료입니다. 사이드바의 체크박스를 눌러 진도를 저장하세요.