HW부터 네이티브 코드까지 — C/C++/Rust/Go 실행 구조
한 문장 요약
소스 코드 → 컴파일러 → 기계어 바이너리 → OS 로더 → CPU가 트랜지스터 수준에서 직접 실행
전체 실행 스택 (위→아래: 추상화 높음→낮음)
┌─────────────────────────────────────────────┐
│ 소스 코드 (C / C++ / Rust / Go) │
│ int main() { printf("hello\n"); return 0; } │
└────────────────────┬────────────────────────┘
│ 컴파일러
│ (gcc / clang / rustc / go build)
▼
┌─────────────────────────────────────────────┐
│ 전처리 (Preprocessor) │
│ #include, #define 매크로 치환 │
│ → .c → 확장된 .c │
└────────────────────┬────────────────────────┘
│
▼
┌─────────────────────────────────────────────┐
│ 컴파일 (Compiler) │
│ C 문법 파싱 → AST → IR → 최적화 │
│ → .s (어셈블리 코드) │
│ │
│ 예시 어셈블리: │
│ mov eax, 1 │
│ add eax, ebx │
│ ret │
└────────────────────┬────────────────────────┘
│
▼
┌─────────────────────────────────────────────┐
│ 어셈블 (Assembler) │
│ 어셈블리 → 기계어 오브젝트 파일 │
│ → .o / .obj │
│ (각 소스 파일마다 1개의 .o 생성) │
└────────────────────┬────────────────────────┘
│
▼
┌─────────────────────────────────────────────┐
│ 링킹 (Linker) │
│ 여러 .o + 라이브러리(.a / .so / .dll) 결합 │
│ → 실행 가능한 바이너리 (ELF / Mach-O / PE) │
│ │
│ 바이너리 내부 구조: │
│ ┌───────────┐ │
│ │ .text │ 기계어 명령어 │
│ │ .data │ 초기화된 전역변수 │
│ │ .bss │ 초기화 안 된 전역변수 │
│ │ .rodata │ 상수 (문자열 리터럴 등) │
│ └───────────┘ │
└────────────────────┬────────────────────────┘
│
▼
┌─────────────────────────────────────────────┐
│ OS 실행 요청 (./a.out 또는 클릭) │
└────────────────────┬────────────────────────┘
│
▼
┌─────────────────────────────────────────────┐
│ 커널 (Kernel) │
│ │
│ 1. execve() 시스템 콜 수신 │
│ 2. ELF 헤더 파싱 → 포맷 확인 │
│ 3. 새 프로세스 생성 (PCB 할당) │
│ 4. 가상 주소 공간 설정 │
│ ┌──────────────────────┐ │
│ │ Stack (↓ 성장) │ 지역변수, 콜스택│
│ │ ... │ │
│ │ Heap (↑ 성장) │ malloc/new │
│ │ .bss │ │
│ │ .data │ │
│ │ .text (기계어) │ │
│ └──────────────────────┘ │
│ 5. 동적 링커(/lib/ld.so) 실행 │
│ - libc.so 등 공유 라이브러리 매핑 │
└────────────────────┬────────────────────────┘
│
▼
┌─────────────────────────────────────────────┐
│ CPU 명령어 실행 사이클 │
│ │
│ PC(Program Counter) → 명령어 주소 가리킴 │
│ │
│ ┌─────────────────────────────────────┐ │
│ │ Fetch → Decode → Execute → WB │ │
│ │ (인출) (해석) (실행) (저장) │ │
│ └─────────────────────────────────────┘ │
│ │
│ 레지스터: rax, rbx, rsp, rip ... │
│ ALU: 덧셈/뺄셈/비교 연산 │
│ Cache: L1(~ns) → L2 → L3 → RAM(~100ns) │
└────────────────────┬────────────────────────┘
│
▼
┌─────────────────────────────────────────────┐
│ 트랜지스터 / 전기 신호 │
│ │
│ 0 = 0V (LOW) 1 = ~3.3V / 5V (HIGH) │
│ MOSFET 트랜지스터: 스위치 ON/OFF │
│ 수십억 개의 트랜지스터가 논리 게이트 구성 │
│ AND / OR / NOT → 반가산기 → 전가산기 → │
│ ALU → 레지스터 → 파이프라인 → CPU │
└─────────────────────────────────────────────┘
컴파일 언어별 비교
언어 컴파일러 중간 표현 최종 결과
─────────────────────────────────────────────────
C gcc/clang AST → LLVM IR ELF/Mach-O
C++ g++/clang++ AST → LLVM IR ELF/Mach-O
Rust rustc MIR → LLVM IR ELF/Mach-O
Go go build SSA ELF/Mach-O (정적 링크)
Go의 특이점: 정적 링크
일반 C 실행 파일:
my_app → 실행 시 libc.so, libpthread.so 동적 로드 필요
Go 실행 파일:
my_app → 런타임 + 표준 라이브러리 전부 포함 (self-contained)
Go 스케줄러(goroutine M:N 스레드) 내장
GC(가비지 컬렉터) 내장
Rust의 특이점: 소유권이 컴파일 타임에 처리됨
Rust 컴파일러 단계:
소스 → 파싱 → HIR → MIR(소유권/수명 검사) → LLVM IR → 기계어
MIR 단계에서:
- Borrow Checker: 메모리 안전성 검증
- 런타임 GC 없음 → 컴파일 타임에 drop() 삽입
- 결과: C/C++ 수준 성능 + 메모리 안전
메모리 레이아웃 (실행 중 프로세스)
가상 주소 공간 (64bit, 높은 주소 → 낮은 주소)
0xFFFF_FFFF_FFFF_FFFF ┌──────────────────┐
│ 커널 공간 │ (접근 불가)
0xFFFF_8000_0000_0000 ├──────────────────┤
│ Stack │ 함수 호출, 지역변수
│ ↓ │ (자동 할당/해제)
│ │
│ ↑ │
│ Heap │ malloc/new (수동 관리)
├──────────────────┤
│ .bss │ 미초기화 전역변수
│ .data │ 초기화 전역변수
│ .rodata │ 상수, 문자열
│ .text │ 기계어 명령어
0x0000_0000_0040_0000 ├──────────────────┤
│ NULL (미사용) │
0x0000_0000_0000_0000 └──────────────────┘
핵심 요약
소스코드(.c/.rs/.go)
↓ 컴파일러 (전처리 → 파싱 → IR → 최적화 → 어셈블리)
어셈블리(.s)
↓ 어셈블러
오브젝트(.o)
↓ 링커 (심볼 해결 + 라이브러리 결합)
바이너리 (ELF/Mach-O/PE)
↓ OS execve() → 가상 메모리 매핑
프로세스 (가상 주소 공간: text/data/heap/stack)
↓ CPU Fetch-Decode-Execute
트랜지스터 ON/OFF (전기 신호)