[임베디드기사 실기 20부작 15편] Makefile, Cross Compiler, Linking, ELF와 readelf — 소스코드가 실행파일이 되는 과정
임베디드기사 실기에서는 소스코드를 작성하는 것만큼이나, 그것이 어떻게 오브젝트 파일과 실행파일로 바뀌고, 다른 아키텍처용으로 Cross Compile되며, 최종 ELF 바이너리 내부를 어떻게 검증하는지를 묻는 문제가 반복적으로 등장합니다. 특히 2025년 기출에는 Dynamic Linking, Makefile 자동 변수 $^, readelf, Cross Compile 시 -c와 -o가 함께 출제되었습니다. 이번 편에서는 이 네 주제를 따로 외우지 않고 하나의 Build Pipeline으로 연결해 보겠습니다.
C 언어로 작성한 main.c는 CPU가 직접 실행할 수 없습니다. 보통 다음 과정을 거칩니다.
main.c ↓ Preprocess main.i ↓ Compile main.s ↓ Assemble main.o ↓ Link application (ELF) ↓ Load Memory ↓ CPU Execute
즉 전처리 → 컴파일 → 어셈블 → 링킹 → 로딩 → 실행의 흐름을 이해해야 합니다.
전처리기는 C 컴파일 이전에 #include, #define, 조건부 컴파일 등의 지시문을 처리합니다.
예를 들어 #include <stdio.h>가 있다면 Header의 선언 정보가 전처리 단계에서 반영됩니다. 매크로도 이때 치환됩니다.
전처리 결과는 아직 기계어가 아니라 확장된 C Source 형태입니다.
Compiler는 C Source를 분석하여 Target CPU에 맞는 Assembly Code를 생성합니다.
이 과정에서는 문법 분석, 타입 검사, 최적화, 명령어 선택이 일어납니다. 임베디드 환경에서는 Compiler가 어떤 CPU Architecture를 Target으로 하느냐가 매우 중요합니다.
Assembler는 Assembly 명령어를 실제 Machine Code로 변환하여 Object File(.o)을 생성합니다.
하지만 Object File은 아직 단독 실행파일이 아닙니다. 외부 함수 주소가 확정되지 않았을 수 있고, 여러 Object File을 하나로 묶어야 하기 때문입니다.
2025년 기출 자료에서 Cross Compile과 관련해 -c 옵션이 등장했습니다.
arm-linux-gnueabihf-gcc -c main.c -o main.o
-c는 Link를 수행하지 않고 Compile과 Assemble까지만 진행해 Object File을 생성합니다.
-o는 출력 파일명을 지정하는 옵션입니다. 즉 -o main.o는 결과 파일명을 main.o로 지정한다는 뜻입니다.
Linker는 여러 Object File과 Library를 결합하고, 각 Symbol의 최종 주소를 결정해 실행 가능한 이미지를 만듭니다.
예를 들어 main.o에서 printf()를 호출한다면 Linker는 printf가 어디에 정의되어 있는지 찾아 연결해야 합니다.
Linking에서 핵심 키워드는 Symbol Resolution과 Relocation입니다.
Symbol Resolution은 함수와 변수의 정의 위치를 찾는 과정이고, Relocation은 최종 메모리 배치에 맞춰 주소를 조정하는 과정입니다.
Static Linking은 필요한 Library Code를 실행파일에 포함시키는 방식입니다. 실행파일 크기는 커질 수 있지만 Runtime에 외부 공유 Library 의존성이 줄어듭니다.
Dynamic Linking은 실행파일 안에 모든 Library Code를 포함하지 않고, 실행 시점에 Shared Library와 연결하는 방식입니다. 2025년 기출 자료에서는 프로그램 실행 시 Library가 Runtime에 연결되는 방식의 답으로 Dynamic Linking이 제시되었습니다.
| 항목 | Static Linking | Dynamic Linking |
|---|---|---|
| Library 포함 | 실행파일 내부에 포함 | Runtime에 공유 Library 사용 |
| 실행파일 크기 | 상대적으로 큼 | 상대적으로 작음 |
| 배포 의존성 | 낮음 | Shared Library 필요 |
임베디드 개발의 대표적인 특징은 Host와 Target이 다르다는 점입니다.
예를 들어 x86-64 Linux PC에서 ARM Cortex-A용 프로그램을 빌드한다면 Host Compiler가 아니라 ARM용 Cross Compiler가 필요합니다.
Host: x86-64 Linux PC
↓ Cross Compiler
Target: ARM Linux Board
Cross Compiler는 Target의 Instruction Set, ABI, Endianness, Library 환경을 고려해 Binary를 생성합니다.
Native Compile은 Build하는 시스템과 실행하는 시스템의 Architecture가 같은 경우입니다.
Cross Compile은 Build하는 Host와 실행 Target Architecture가 다른 경우입니다.
임베디드 Linux에서는 Target Board 자체에서 큰 소스를 Compile하기보다 성능 좋은 PC에서 Cross Compile한 후 Binary를 Board로 전송하는 방식이 일반적입니다.
Source File이 하나라면 직접 GCC Command를 입력할 수 있지만, 수십·수백 개 파일이 있는 Project에서는 매번 직접 Build 명령을 입력하기 어렵습니다.
Makefile은 Target, Prerequisite, Recipe 관계를 기술해 필요한 부분만 다시 Build하도록 도와줍니다.
app: main.o uart.o gpio.o
$(CC) $^ -o $@
이 예제에서 app은 Target이고, main.o uart.o gpio.o는 Prerequisite입니다.
2025년 실기에서는 Prerequisite 전체 목록을 의미하는 자동 변수를 묻고 정답으로 $^가 제시되었습니다.
| 자동 변수 | 의미 |
|---|---|
$@ |
현재 Target 이름 |
$^ |
모든 Prerequisite 목록 |
$< |
첫 번째 Prerequisite |
암기법은 간단합니다. $@ = 결과물, $^ = 재료 전부, $< = 첫 재료라고 기억하면 좋습니다.
ELF(Executable and Linkable Format)는 Linux/Unix 계열에서 실행파일, Object File, Shared Library 등에 사용되는 대표적인 Binary Format입니다.
ELF 내부에는 Header, Section, Program Header, Symbol Table 등의 구조가 들어 있습니다.
실무에서는 Binary가 정상적으로 Cross Compile되었는지, Target Architecture가 맞는지, Section이 원하는 주소에 배치되었는지를 ELF 분석 Tool로 확인합니다.
Section은 Linking 관점의 논리적인 Binary 구성 요소입니다. 대표적으로 다음이 있습니다.
.text — 실행 코드.rodata — 읽기 전용 상수.data — 초기값이 있는 전역/정적 데이터.bss — 초기값이 없는 전역/정적 데이터
Segment는 Loader가 Runtime에 Memory에 올리는 관점의 단위입니다. 즉 Section은 Linker 중심, Segment는 Loader 중심으로 이해하면 좋습니다.
2025년 실기 기출에서 ELF 정보를 확인하는 Utility의 답으로 readelf가 제시되었습니다.
학습자료에서는 주요 옵션을 다음과 같이 정리하고 있습니다.
readelf -h file # ELF Header readelf -S file # Section Header readelf -l file # Program Header / Segment readelf -s file # Symbol Table readelf -d file # Dynamic Section
특히 Cross Compile 후에는 readelf -h로 Architecture, Entry Point, Endianness 등을 확인하는 것이 유용합니다.
| Tool | 주요 역할 |
|---|---|
| readelf | ELF Header / Section / Segment / Symbol 분석 |
| objdump | Disassembly, Section 내용 확인 |
| nm | Symbol 목록 및 Undefined Symbol 확인 |
| file | 파일 형식, Architecture, Bit 수 등을 빠르게 확인 |
임베디드 Firmware에서는 Code와 Data를 어느 Memory 주소에 배치할지 매우 중요합니다. 이때 Linker Script(.ld)가 Memory Layout을 정의합니다.
예를 들어 Flash에는 .text와 .rodata를 배치하고, RAM에는 .data와 .bss를 배치할 수 있습니다.
Startup Code에서 .data를 Flash에서 RAM으로 복사하고 .bss를 0으로 초기화했던 내용이 바로 이 Linker Script의 배치 정보와 연결됩니다. 즉 2편의 Startup Code와 이번 Build/ELF 편이 여기서 다시 이어집니다.
Compile은 성공했는데 Link 단계에서 undefined reference 오류가 발생하는 경우가 있습니다.
이는 선언은 알고 있지만 실제 함수나 변수의 정의를 Linker가 찾지 못했기 때문입니다.
주요 원인은 다음과 같습니다.
① 필요한 Object File이 Link 대상에서 빠짐
② Library가 Link되지 않음
③ Library Link 순서 문제
④ 함수 이름 또는 Symbol 불일치
⑤ C/C++ 혼용 시 Name Mangling 문제
Cross Compile된 프로그램이 Target Board에서 실행되지 않는다면 아래 순서로 점검하면 좋습니다.
① file application — Target Architecture 확인
② readelf -h application — Class, Endianness, Machine 확인
③ Dynamic Linking이라면 필요한 .so Library 존재 확인
④ ABI / libc 호환성 확인
⑤ Entry Point와 Program Header 확인
⑥ 실행 권한 확인
⑦ 필요한 Interpreter/Dynamic Loader 경로 확인
즉 Build 오류가 아니라 Architecture, ABI, Library, Loader 문제일 수도 있습니다.
문제 1. Makefile에서 모든 Prerequisite를 나타내는 자동 변수는?
답: $^
문제 2. Makefile에서 Target 이름을 나타내는 자동 변수는?
답: $@
문제 3. Cross Compile 시 Link를 수행하지 않고 Object File만 생성하는 GCC 옵션은?
답: -c
문제 4. ELF Header, Section, Segment, Symbol 정보를 확인하는 대표 Utility는?
답: readelf
문제 5. 실행 시 Shared Library와 연결되는 Linking 방식은?
답: Dynamic Linking
문제 6. Cross Compiler를 설명하시오.
모범답안: Cross Compiler는 Build를 수행하는 Host와 실행되는 Target의 CPU Architecture가 다를 때 Target용 Machine Code를 생성하는 Compiler이다. 임베디드 개발에서는 주로 PC에서 ARM 등 Target Board용 Binary를 생성할 때 사용한다.
Source → Object → ELF
-c = Link 없이 .o 생성
-o = Output 이름 지정
$@ = Target
$^ = 모든 Prerequisite
$< = 첫 번째 Prerequisite
Static Linking = Library Code 포함
Dynamic Linking = Runtime Shared Library 연결
ELF = Executable and Linkable Format
readelf -h = Header
readelf -S = Section
readelf -l = Segment
readelf -s = Symbol
이번 편의 핵심은 명령어를 따로 암기하지 않는 것입니다.
C Source → Compile → Object → Link → ELF → Loader → CPU Execute
그리고 임베디드 환경에서는 여기에 Cross Compiler가 들어가고, Build 자동화를 위해 Makefile이 사용되며, 생성 결과를 확인할 때 readelf가 사용됩니다.
이 흐름을 이해하면 Dynamic Linking, $^, -c, readelf가 각각 독립된 암기문제가 아니라 하나의 Build 과정 안에서 자연스럽게 연결됩니다.
16편 — TCP Socket, 3-Way/4-Way Handshake, UART와 SPI: 임베디드 통신의 핵심
#임베디드기사 #임베디드기사실기 #Makefile #CrossCompiler #CrossCompile #Linking #DynamicLinking #StaticLinking #ELF #readelf #objdump #GNUmake #GCC #LinkerScript #SymbolTable #EmbeddedLinux #BuildSystem #Toolchain #임베디드시스템 #자격증공부