Skip to content

Chapter 05

Hongjayeong-02 edited this page Oct 4, 2026 · 2 revisions

05. Make Build System

Goal

Ch03에서 직접 실행했던 GNU ARM Toolchain 명령을 Makefile로 자동화하고, 파일 간 의존성(dependency) 과 incremental build 원리를 이해한다.


1. Why Make?

Ch03에서는 직접 다음 과정을 수행했다.

main.c
  ↓
Compiler
  ↓
main.o
  ↓
Linker
  ↓
firmware.elf
  ↓
objcopy
  ↓
firmware.hex

소스 파일이 하나라면 직접 명령어를 입력해도 큰 문제가 없다.

하지만 실제 embedded project는 보통:

main.c
gpio.c
uart.c
adc.c
timer.c
systick.c
...

처럼 여러 파일로 구성된다.

이때 매번 모든 파일을 다시 compile하면 불필요한 시간이 많이 든다.

Make는 파일 간 관계를 기억하고:

변경된 파일과 그 영향을 받는 결과물만 다시 생성한다.


2. Dependency

예를 들어 project가 다음과 같다고 하자.

main.c ─────→ main.o ─┐
gpio.c ─────→ gpio.o ─┼──→ firmware.elf ──→ firmware.hex
uart.c ─────→ uart.o ─┘

만약 gpio.c만 수정했다면:

gpio.c
  ↓
gpio.o
  ↓
firmware.elf
  ↓
firmware.hex

만 다시 생성하면 된다.

main.c, uart.c는 바뀌지 않았기 때문에:

main.o
uart.o

를 다시 compile할 필요가 없다.

이것을 Incremental Build라고 한다.


3. How Does Make Know What Changed?

Make는 기본적으로 file의 timestamp를 비교한다.

예:

gpio.c  : 12:10
gpio.o  : 12:05

source가 object file보다 더 최근에 수정되었으므로:

gpio.c is newer than gpio.o

Make는:

gpio.o must be rebuilt

라고 판단한다.

반대로:

gpio.c  : 12:05
gpio.o  : 12:10

이면 object가 더 최신이므로 다시 compile하지 않는다.


4. Basic Makefile Rule

Makefile의 기본 구조:

target: dependency
	command

예:

main.o: main.c
	arm-none-eabi-gcc -c main.c -o main.o

의미:

main.o가 없거나
main.c가 main.o보다 최신이면

↓
compiler 실행
↓
main.o 생성

5. Target and Dependency

다음 rule:

firmware.elf: main.o gpio.o uart.o
	arm-none-eabi-gcc main.o gpio.o uart.o -o firmware.elf

여기서:

Target
firmware.elf

Dependency:

main.o
gpio.o
uart.o

즉 object file 중 하나라도 ELF보다 최신이면 linker를 다시 실행한다.


6. Variables

명령어를 계속 반복해서 쓰면 Makefile이 길어진다.

따라서 변수로 정의한다.

CC      := arm-none-eabi-gcc
OBJCOPY := arm-none-eabi-objcopy
SIZE    := arm-none-eabi-size

이제:

$(CC)

는 실제로:

arm-none-eabi-gcc

를 의미한다.

Example:

$(CC) -c main.c -o main.o

↓

arm-none-eabi-gcc -c main.c -o main.o

7. Why Use Variables?

Compiler를 변경하거나 option을 수정할 때 한 곳만 바꾸면 된다.

나쁜 구조:

arm-none-eabi-gcc ...
arm-none-eabi-gcc ...
arm-none-eabi-gcc ...
arm-none-eabi-gcc ...

좋은 구조:

CC := arm-none-eabi-gcc

그리고:

$(CC)

를 재사용한다.


8. Compiler Flags

Bare-metal Cortex-M4 build에 사용되는 대표 option:

CFLAGS := \
	-mcpu=cortex-m4 \
	-mthumb \
	-O0 \
	-g3 \
	-Wall

각 의미:

-mcpu=cortex-m4
→ Cortex-M4 target code 생성

-mthumb
→ Thumb instruction set 사용

-O0
→ optimization 비활성화

-g3
→ debug information 생성

-Wall
→ 주요 compiler warning 활성화

9. CMSIS Build Options

CMSIS를 사용하는 chapter에서는 추가 option이 필요하다.

-mfloat-abi=hard
-mfpu=fpv4-sp-d16
-DSTM32F439xx

그리고 include path:

-I...

를 추가한다.

Example:

ifeq ($(USE_CMSIS),1)

CFLAGS += \
	-mfloat-abi=hard \
	-mfpu=fpv4-sp-d16 \
	-DSTM32F439xx \
	-I$(CMSIS_INC)

endif

USE_CMSIS=1일 때만 이 option들이 추가된다.


10. Conditional Build

Make에서는 조건문을 사용할 수 있다.

ifeq ($(USE_CMSIS),1)

...

endif

예:

USE_CMSIS := 1

이면 CMSIS 관련 option을 사용한다.

반대로:

USE_CMSIS := 0

이면 해당 block을 건너뛴다.

즉 하나의 build system을 여러 chapter에서 재사용할 수 있다.


11. Pattern Rule

각 .c 파일마다 다음을 전부 작성하는 것은 비효율적이다.

main.o: main.c
	...

gpio.o: gpio.c
	...

uart.o: uart.c
	...

Make의 pattern rule을 사용하면 하나로 처리할 수 있다.

%.o: %.c
	$(CC) $(CFLAGS) -c $< -o $@

%는 공통 file name 부분을 의미한다.

따라서:

main.c → main.o
gpio.c → gpio.o
uart.c → uart.o

모두 같은 rule이 적용된다.


12. Automatic Variables

Make에는 rule 내부에서 사용할 수 있는 자동 변수가 있다.

$@

현재 target.

main.o: main.c

이면:

$@ = main.o

$<

첫 번째 dependency.

$< = main.c

$^

모든 dependency.

예:

firmware.elf: main.o gpio.o uart.o

이면:

$^ =
main.o gpio.o uart.o

13. Pattern Rule Expansion

Rule:

%.o: %.c
	$(CC) $(CFLAGS) -c $< -o $@

gpio.c를 compile할 때 실제 명령은:

arm-none-eabi-gcc \
    $(CFLAGS) \
    -c gpio.c \
    -o gpio.o

가 된다.


14. Linking Rule

예:

$(TARGET).elf: $(OBJS)
	$(CC) $(CFLAGS) -T$(LDSCRIPT) $^ -o $@

만약:

TARGET := ch05_make

라면:

Target
→ ch05_make.elf

Dependencies:

main.o
startup.o
gpio.o
...

이다.

Linker script:

-T$(LDSCRIPT)

를 통해 memory layout을 지정한다.


15. ELF to HEX

Link가 끝난 뒤:

$(TARGET).hex: $(TARGET).elf
	$(OBJCOPY) -O ihex $< $@

를 실행한다.

실제 흐름:

ch05_make.elf
      ↓
objcopy
      ↓
ch05_make.hex

16. Program Size

Build 후:

$(SIZE) $@

를 실행하면:

text
data
bss

크기를 확인할 수 있다.

Example:

   text    data     bss
    704       4    2564

이 값은 firmware의 memory usage를 확인할 때 중요하다.


17. Shared common.mk

18개 chapter에서 동일한 build logic을 반복 작성하는 대신 공용 file로 분리한다.

Structure:

common/
└── mk/
    └── common.mk

chapter05/
└── Makefile

chapter06/
└── Makefile

chapter07/
└── Makefile

18. common.mk

실습 구조의 핵심:

CC      := arm-none-eabi-gcc
OBJCOPY := arm-none-eabi-objcopy
SIZE    := arm-none-eabi-size

CFLAGS  := \
	-mcpu=cortex-m4 \
	-mthumb \
	-O0 \
	-g3 \
	-Wall

ifeq ($(USE_CMSIS),1)

CFLAGS += \
	-mfloat-abi=hard \
	-mfpu=fpv4-sp-d16 \
	-I$(CMSIS_INC) \
	-DSTM32F439xx

endif

%.o: %.c
	$(CC) $(CFLAGS) -c $< -o $@

$(TARGET).elf: $(OBJS)
	$(CC) $(CFLAGS) \
	-T$(LDSCRIPT) \
	$^ \
	-o $@

	$(SIZE) $@

$(TARGET).hex: $(TARGET).elf
	$(OBJCOPY) \
	-O ihex \
	$< \
	$@

19. Chapter Makefile

각 chapter에서는 필요한 정보만 작성한다.

Example:

TARGET := ch05_make

CH_SRCS := \
	Src/main.c

USE_CMSIS := 1

include ../common/mk/common.mk

즉:

Chapter Makefile
      ↓
"무엇을 빌드할 것인가?"

common.mk
      ↓
"어떻게 빌드할 것인가?"

로 역할을 분리한다.


20. Why Use include?

include ../common/mk/common.mk

를 사용하면 다른 Makefile의 내용을 현재 Makefile에 포함할 수 있다.

장점:

Compiler option
Link option
Pattern rule
objcopy rule
clean rule

등을 한 곳에서 관리할 수 있다.

예를 들어 compiler option을 변경해야 할 때:

18개 Makefile 수정

대신:

common.mk 1개 수정

만 하면 된다.


21. Build Command

기본 build:

make

clean:

make clean

다시 build:

make clean
make

22. Debug Build

실습에서는 debug build에:

-O0
-g3

를 사용했다.

특징:

  • optimization 최소화
  • source code와 assembly 대응이 쉬움
  • debugger 사용에 유리
  • code size가 다소 커질 수 있음

기록된 결과:

text = 704
data = 4
bss  = 2564

23. Release / Optimized Build

실습에서는:

make DEBUG=0

으로 optimization build를 비교했다.

대표적으로:

-O2

를 사용한다.

기록된 결과:

text = 668
data = 4
bss  = 2564

Debug와 비교:

704 bytes
   ↓
668 bytes

.text가:

36 bytes

줄었다.


24. Why Does Optimization Reduce Code Size?

Compiler가 다음 작업을 할 수 있기 때문이다.

Remove unnecessary instructions
Simplify expressions
Optimize branches
Optimize loops
Reuse registers
Inline or reorganize code

따라서 같은 C source라도:

Compiler Option
       ↓
Different Assembly
       ↓
Different Machine Code

가 될 수 있다.


25. Source Code Is Not the Final Program

중요한 개념:

C Source
   ↓
Compiler Options
   ↓
Optimization
   ↓
Assembly
   ↓
Machine Code

즉 MCU가 실제로 실행하는 것은 C source가 아니라 compiler가 생성한 machine code다.

그래서 embedded debugging에서는:

Compiler Option
Optimization
Linker Script
Startup

까지 같이 봐야 한다.


26. make clean

make clean은 기존 build artifact를 삭제한다.

예:

clean:
	rm -f *.o *.elf *.hex

왜 필요한가?

가끔 dependency 설정이 잘못되거나 build artifact가 stale 상태가 되면 기존 object file 때문에 예상하지 못한 결과가 나올 수 있다.

이때:

make clean
make

으로 처음부터 다시 build하면 문제를 분리하기 쉽다.


27. Parallel Build

Make는:

make -j

로 여러 compile job을 동시에 실행할 수 있다.

예:

main.c → main.o
gpio.c → gpio.o
uart.c → uart.o

는 서로 독립적이므로 동시에 compile할 수 있다.


28. Current Build-System Limitation

현재 학습용 구조에서는 일부 공용 object file을 여러 chapter가 공유할 수 있다.

예:

common/
└── startup.o

이 상태에서 여러 chapter를 동시에:

make -j

하면 같은 object를 동시에 갱신할 가능성이 있다.

더 안전한 구조:

build/
├── ch05/
├── ch06/
├── ch07/
└── ...

처럼 chapter마다 독립 build directory를 두는 것이다.


29. Recommended Project Structure

BareMetal/
│
├── common/
│   ├── inc/
│   ├── src/
│   ├── startup/
│   └── mk/
│       └── common.mk
│
├── Chapter05/
│   ├── Src/
│   └── Makefile
│
├── Chapter06/
│   ├── Src/
│   └── Makefile
│
└── build/

공통 code와 chapter-specific code를 분리하면 유지보수가 쉬워진다.


30. Build Flow

전체 흐름:

make
 │
 ▼
Read Makefile
 │
 ▼
Read Dependencies
 │
 ▼
Check File Timestamps
 │
 ├── unchanged → reuse object
 │
 └── changed   → recompile
                 │
                 ▼
              Object Files
                 │
                 ▼
               Link
                 │
                 ▼
                ELF
                 │
                 ▼
              objcopy
                 │
                 ▼
                HEX

31. Make vs Compiler

헷갈리지 말아야 할 점:

Make
≠ Compiler

Make는 compiler가 아니다.

Make의 역할:

Build Orchestration
Dependency Management
Command Automation

실제 C compile:

arm-none-eabi-gcc

가 수행한다.

즉:

Make
 ↓
calls GCC
 ↓
calls assembler/linker

구조다.


32. IDE Build System

STM32CubeIDE에서:

Project → Build

를 눌러도 내부적으로 비슷한 build system이 동작한다.

즉:

IDE Button
    ↓
Make / Build System
    ↓
GNU ARM Toolchain
    ↓
ELF / HEX

이다.

Ch03에서 직접 명령을 실행하고 Ch05에서 Make를 배우는 이유는 IDE 내부 동작을 이해하기 위해서다.


33. Verification

Build

make clean
make

확인:

Compile
Link
objcopy
size

가 순서대로 실행되는지 확인한다.


Rebuild Test

아무 file도 수정하지 않고 다시:

make

실행한다.

정상 dependency라면 이미 최신인 target을 불필요하게 compile하지 않는다.


Incremental Build Test

main.c에 작은 수정 후:

make

실행한다.

확인:

main.c
 ↓
main.o rebuild
 ↓
ELF relink
 ↓
HEX regenerate

Optimization Comparison

Debug:

make clean
make

Optimized:

make clean
make DEBUG=0

비교:

Debug
text = 704

Optimized
text = 668

34. Screenshot to Add

재실습할 때 다음 결과를 캡처한다.

Debug Build

images/ch05_debug_build.png

보여야 할 것:

-O0
Build PASS
text/data/bss

Optimized Build

images/ch05_release_build.png

보여야 할 것:

-O2
Build PASS
text/data/bss

Incremental Build

images/ch05_incremental_build.png

한 파일만 수정한 뒤 해당 object만 다시 compile되는 모습을 캡처하면 좋다.


35. Key Questions

Q1. What is Make?

Build dependency를 관리하고 필요한 command를 자동 실행하는 build tool.


Q2. Is Make a compiler?

No.

Make
→ decides what should be built

GCC
→ actually compiles code

Q3. What is incremental build?

변경된 source와 그 영향을 받는 target만 다시 build하는 방식.


Q4. What does $@ mean?

현재 rule의 target.


Q5. What does $< mean?

첫 번째 dependency.


Q6. What does $^ mean?

모든 dependency.


Q7. What does %.o: %.c mean?

모든 .c file을 동일한 rule로 .o file로 compile하는 pattern rule.


Q8. Why use common.mk?

여러 project/chapter에서 동일한 build rule과 compiler option을 재사용하기 위해.


Q9. Why does -O2 change the binary?

Compiler가 C code를 더 효율적인 machine code로 변환하기 때문이다.


Q10. Why use make clean?

기존 object/binary를 삭제하고 완전히 새 build를 수행하기 위해.


36. Key Takeaways

Source Files
     ↓
Dependency Graph
     ↓
Make
     ↓
GNU ARM Toolchain
     ↓
Object Files
     ↓
Link
     ↓
ELF
     ↓
HEX

핵심은 다음과 같다.

  1. Make는 compiler가 아니라 build automation tool이다.
  2. Make는 file timestamp와 dependency를 이용한다.
  3. 변경된 부분만 다시 build하는 것이 incremental build다.
  4. Pattern rule을 이용하면 여러 source file을 하나의 rule로 처리할 수 있다.
  5. $@, $<, $^는 Makefile 작성에서 자주 사용하는 automatic variable이다.
  6. common.mk를 사용하면 build logic을 재사용할 수 있다.
  7. Compiler option에 따라 같은 C source에서도 다른 machine code가 생성된다.
  8. Embedded project에서는 source code뿐 아니라 build system까지 이해해야 debugging이 쉬워진다.

Clone this wiki locally