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로 만듭니다.
- 이름(문자열), 나이(정수), 좋아하는 숫자(정수)를 차례로 입력받습니다.
- 입력받은 내용을 보기 좋게 출력합니다. 출력 형태의 예:
=== 자기소개 카드 ===
이름: Lee
나이: 20
좋아하는 숫자: 7
10년 뒤에는 30살이 되는군요!
- 일부러 세미콜론을 하나 지우고 컴파일해서, 오류 메시지의 줄 번호를 확인하고 고치는 연습을 합니다.
%d자리에 이름을 넣는 등 서식 지정자 실험을 한 가지 이상 하고, 결과를 노트에 적습니다.- 완성한 .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 완료입니다. 사이드바의 체크박스를 눌러 진도를 저장하세요.