gcc와 같은 컴파일러 드라이버는 단순히 컴파일만 하는 게 아니라, 여러 단계를 자동으로 실행해주는 “드라이버(driver)” 역할을 한다.
gcc는 다음 단계를 순서대로 수행한다
*gcc -Og -o main main.c sum.c*
# 또는 (main.c만 컴파일 시)
*gcc main.c -o main*

- 전처리기 (cpp)
- #include <stdio.h> 같은
헤더포함 - #define, #ifdef 등 전처리 지시어(
매크로) 처리 - 결과: .i 파일 (순수 C 코드)
- 똑같은 C 코드지만,
모든 include가 펼쳐진 상태 // 예시 출력 int main() { printf("Hello, world\n"); }
- 똑같은 C 코드지만,
- #include <stdio.h> 같은
gcc -E main.c -o main.i- 컴파일러 (cc1)
- .i → .s : 어셈블리 코드로 변환
- 결과: .s 파일 (x86-64 어셈블리)
"예시 출력" main: pushq %rbp movq %rsp, %rbp ... call printf
# .s 파일이 나오는 결과자체는 같음 cc1 main.i -o main.s # .i -> .s gcc -S main.c # .c -> .s- 어셈블러 (as)
- .s → .o :
기계어 바이너리로 변환 - 결과: 리로케이션 가능한 오브젝트 파일
형식은 ELF지만, 단독 실행 불가능- 내부 심볼 테이블(.symtab), 코드 섹션(.txt) 등을 포함
- .s → .o :
as main.s -o main.o- 링커 (ld)
- 외부 라이브러리(libc, printf, …)까지 결합
- 주소 재배치, 심볼 해결, 실행 헤더 추가
- .o + .o + ... → 실행 가능한 ELF 파일(a.out 또는 지정한 이름)
- 커널이 읽을 수 있도록 헤더 정리
- 실행 시 .text, .data, .heap, .stack 등으로 메모리 매핑될 준비 완료
ld main.o -o main
💡 예시 흐름
# 내부적으로는 이런 일들이 순서대로 벌어짐
cpp main.c → main.i
cc1 main.i → main.s
as main.s → main.o
ld main.o sum.o → main (ELF 실행 파일)
| 확인하고 싶은 것 | 명령어 |
|---|---|
| 전처리 결과만 보기 | gcc -E main.c |
| 어셈블리 코드 보기 | gcc -S main.c |
| 오브젝트 파일 만들기 | gcc -c main.c |
| 디버깅용 ELF 만들기 | gcc -g main.c |
| ELF 구조 보기 | readelf -a main / objdump -d main |
ELF?
Executable and Linkable Format
유닉스 계열 시스템에서 실행파일, 오브젝트 파일, 공유 라이브러리, 코어 덤프 등을 표현하기 위한 표준 바이너리 포맷임
역할
C로 프로그램을 만들 때 .c → .o → 실행파일 순 생성,
그 중 .o (오브젝트 파일)이나 a.out, main 같은 실행파일이 ELF 포맷으로 저장
ELF는 다음을 포함한 바이너리 구조를 정의:
| 요소 | 설명 | ELF 섹션 예시 |
|---|---|---|
| 코드 | 실행할 기계어 명령어 | .text |
| 데이터 | 전역 변수/상수 등 | .data, .bss, .rodata |
| 심볼(Symbol) | 함수/변수 이름, 주소 등의 테이블 | .symtab, .strtab |
| 재배치(Relocation) | 함수 호출/주소 참조를 링크 시점에 수정할 정보 | .rel.text, .rela.data |
| 디버깅 정보 | gdb 같은 도구가 사용할 함수명/소스위치 | .debug_info, .line, .stab |
| ELF 섹션 구분 | 설명 |
|---|---|
| .text | 실제 실행되는 기계어 코드 |
| .data | 초기화된 전역 변수 |
| .bss | 초기화되지 않은 전역 변수 (공간만 잡음) |
| .rodata | 읽기 전용 상수 데이터 (ex. 문자열) |
| .symtab | 심볼 테이블 (함수 이름, 변수 이름 등) |
| .rel.text | 재배치 정보 (링커가 주소 수정할 때 사용) |
ELF 파일의 예
int g = 10; // .data
int h; // .bss
char *s = "hi"; // .rodata
int main() { // .text
return g;
}
이걸 gcc main.c -o main으로 컴파일하면,
main이라는 ELF 실행파일이 생성
확인해보기
1. 파일 포맷 확인
file main
main: ELF 64-bit LSB executable, x86-64, ...
2. ELF 내부 확인 (요약 정보)
readelf -h main
또는
objdump -x main
왜 중요한가?
| 이유 | 설명 |
|---|---|
| 리눅스 커널이 읽는 구조 | 실행할 때 커널은 ELF 헤더를 보고 어디에 어떤 섹션을 매핑할지 결정함 |
| 디버깅 가능 | gdb, strace 같은 도구는 ELF 심볼 테이블을 이용해 디버깅 |
| 동적 링킹 정보 | 어떤 .so(공유 라이브러리)를 사용하는지도 ELF에 저장 |
| 보안 분석에 필수 | 바이너리 분석, 리버스 엔지니어링, 펌웨어 해킹에서도 ELF를 직접 다룸 |
728x90
'CS:APP' 카테고리의 다른 글
| 실행 가능 목적파일의 로딩 (0) | 2025.08.09 |
|---|---|
| ELF, Executable and Linkable Format (재배치 가능 목적파일) (0) | 2025.08.08 |
| 함수는 추상화의 결과다 - CS:APP 3.7 프로시저 호출 (0) | 2025.04.08 |
| 스택은 사실 아래로 자란다(push,pop) - CS:APP 3.4.4 (0) | 2025.04.07 |
| [C, Assembly] movq 예제 (0) | 2025.04.07 |