Repository navigation
Chapter 05
Ch03에서 직접 실행했던 GNU ARM Toolchain 명령을 Makefile로 자동화하고, 파일 간 의존성(dependency) 과 incremental build 원리를 이해한다.
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는 파일 간 관계를 기억하고:
변경된 파일과 그 영향을 받는 결과물만 다시 생성한다.
예를 들어 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라고 한다.
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하지 않는다.
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 생성
다음 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를 다시 실행한다.
명령어를 계속 반복해서 쓰면 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.oCompiler를 변경하거나 option을 수정할 때 한 곳만 바꾸면 된다.
나쁜 구조:
arm-none-eabi-gcc ...
arm-none-eabi-gcc ...
arm-none-eabi-gcc ...
arm-none-eabi-gcc ...좋은 구조:
CC := arm-none-eabi-gcc그리고:
$(CC)를 재사용한다.
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 활성화
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)
endifUSE_CMSIS=1일 때만 이 option들이 추가된다.
Make에서는 조건문을 사용할 수 있다.
ifeq ($(USE_CMSIS),1)
...
endif예:
USE_CMSIS := 1이면 CMSIS 관련 option을 사용한다.
반대로:
USE_CMSIS := 0이면 해당 block을 건너뛴다.
즉 하나의 build system을 여러 chapter에서 재사용할 수 있다.
각 .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이 적용된다.
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
Rule:
%.o: %.c
$(CC) $(CFLAGS) -c $< -o $@gpio.c를 compile할 때 실제 명령은:
arm-none-eabi-gcc \
$(CFLAGS) \
-c gpio.c \
-o gpio.o가 된다.
예:
$(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을 지정한다.
Link가 끝난 뒤:
$(TARGET).hex: $(TARGET).elf
$(OBJCOPY) -O ihex $< $@를 실행한다.
실제 흐름:
ch05_make.elf
↓
objcopy
↓
ch05_make.hex
Build 후:
$(SIZE) $@를 실행하면:
text
data
bss
크기를 확인할 수 있다.
Example:
text data bss
704 4 2564
이 값은 firmware의 memory usage를 확인할 때 중요하다.
18개 chapter에서 동일한 build logic을 반복 작성하는 대신 공용 file로 분리한다.
Structure:
common/
└── mk/
└── common.mk
chapter05/
└── Makefile
chapter06/
└── Makefile
chapter07/
└── Makefile
실습 구조의 핵심:
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 \
$< \
$@각 chapter에서는 필요한 정보만 작성한다.
Example:
TARGET := ch05_make
CH_SRCS := \
Src/main.c
USE_CMSIS := 1
include ../common/mk/common.mk즉:
Chapter Makefile
↓
"무엇을 빌드할 것인가?"
common.mk
↓
"어떻게 빌드할 것인가?"
로 역할을 분리한다.
include ../common/mk/common.mk를 사용하면 다른 Makefile의 내용을 현재 Makefile에 포함할 수 있다.
장점:
Compiler option
Link option
Pattern rule
objcopy rule
clean rule
등을 한 곳에서 관리할 수 있다.
예를 들어 compiler option을 변경해야 할 때:
18개 Makefile 수정
대신:
common.mk 1개 수정
만 하면 된다.
기본 build:
makeclean:
make clean다시 build:
make clean
make실습에서는 debug build에:
-O0
-g3
를 사용했다.
특징:
- optimization 최소화
- source code와 assembly 대응이 쉬움
- debugger 사용에 유리
- code size가 다소 커질 수 있음
기록된 결과:
text = 704
data = 4
bss = 2564
실습에서는:
make DEBUG=0으로 optimization build를 비교했다.
대표적으로:
-O2
를 사용한다.
기록된 결과:
text = 668
data = 4
bss = 2564
Debug와 비교:
704 bytes
↓
668 bytes
.text가:
36 bytes
줄었다.
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
가 될 수 있다.
중요한 개념:
C Source
↓
Compiler Options
↓
Optimization
↓
Assembly
↓
Machine Code
즉 MCU가 실제로 실행하는 것은 C source가 아니라 compiler가 생성한 machine code다.
그래서 embedded debugging에서는:
Compiler Option
Optimization
Linker Script
Startup
까지 같이 봐야 한다.
make clean은 기존 build artifact를 삭제한다.
예:
clean:
rm -f *.o *.elf *.hex왜 필요한가?
가끔 dependency 설정이 잘못되거나 build artifact가 stale 상태가 되면 기존 object file 때문에 예상하지 못한 결과가 나올 수 있다.
이때:
make clean
make으로 처음부터 다시 build하면 문제를 분리하기 쉽다.
Make는:
make -j로 여러 compile job을 동시에 실행할 수 있다.
예:
main.c → main.o
gpio.c → gpio.o
uart.c → uart.o
는 서로 독립적이므로 동시에 compile할 수 있다.
현재 학습용 구조에서는 일부 공용 object file을 여러 chapter가 공유할 수 있다.
예:
common/
└── startup.o
이 상태에서 여러 chapter를 동시에:
make -j하면 같은 object를 동시에 갱신할 가능성이 있다.
더 안전한 구조:
build/
├── ch05/
├── ch06/
├── ch07/
└── ...
처럼 chapter마다 독립 build directory를 두는 것이다.
BareMetal/
│
├── common/
│ ├── inc/
│ ├── src/
│ ├── startup/
│ └── mk/
│ └── common.mk
│
├── Chapter05/
│ ├── Src/
│ └── Makefile
│
├── Chapter06/
│ ├── Src/
│ └── Makefile
│
└── build/
공통 code와 chapter-specific code를 분리하면 유지보수가 쉬워진다.
전체 흐름:
make
│
▼
Read Makefile
│
▼
Read Dependencies
│
▼
Check File Timestamps
│
├── unchanged → reuse object
│
└── changed → recompile
│
▼
Object Files
│
▼
Link
│
▼
ELF
│
▼
objcopy
│
▼
HEX
헷갈리지 말아야 할 점:
Make
≠ Compiler
Make는 compiler가 아니다.
Make의 역할:
Build Orchestration
Dependency Management
Command Automation
실제 C compile:
arm-none-eabi-gcc
가 수행한다.
즉:
Make
↓
calls GCC
↓
calls assembler/linker
구조다.
STM32CubeIDE에서:
Project → Build
를 눌러도 내부적으로 비슷한 build system이 동작한다.
즉:
IDE Button
↓
Make / Build System
↓
GNU ARM Toolchain
↓
ELF / HEX
이다.
Ch03에서 직접 명령을 실행하고 Ch05에서 Make를 배우는 이유는 IDE 내부 동작을 이해하기 위해서다.
make clean
make확인:
Compile
Link
objcopy
size
가 순서대로 실행되는지 확인한다.
아무 file도 수정하지 않고 다시:
make실행한다.
정상 dependency라면 이미 최신인 target을 불필요하게 compile하지 않는다.
main.c에 작은 수정 후:
make실행한다.
확인:
main.c
↓
main.o rebuild
↓
ELF relink
↓
HEX regenerate
Debug:
make clean
makeOptimized:
make clean
make DEBUG=0비교:
Debug
text = 704
Optimized
text = 668
재실습할 때 다음 결과를 캡처한다.
images/ch05_debug_build.png
보여야 할 것:
-O0
Build PASS
text/data/bss
images/ch05_release_build.png
보여야 할 것:
-O2
Build PASS
text/data/bss
images/ch05_incremental_build.png
한 파일만 수정한 뒤 해당 object만 다시 compile되는 모습을 캡처하면 좋다.
Build dependency를 관리하고 필요한 command를 자동 실행하는 build tool.
No.
Make
→ decides what should be built
GCC
→ actually compiles code
변경된 source와 그 영향을 받는 target만 다시 build하는 방식.
현재 rule의 target.
첫 번째 dependency.
모든 dependency.
모든 .c file을 동일한 rule로 .o file로 compile하는 pattern rule.
여러 project/chapter에서 동일한 build rule과 compiler option을 재사용하기 위해.
Compiler가 C code를 더 효율적인 machine code로 변환하기 때문이다.
기존 object/binary를 삭제하고 완전히 새 build를 수행하기 위해.
Source Files
↓
Dependency Graph
↓
Make
↓
GNU ARM Toolchain
↓
Object Files
↓
Link
↓
ELF
↓
HEX
핵심은 다음과 같다.
- Make는 compiler가 아니라 build automation tool이다.
- Make는 file timestamp와 dependency를 이용한다.
- 변경된 부분만 다시 build하는 것이 incremental build다.
- Pattern rule을 이용하면 여러 source file을 하나의 rule로 처리할 수 있다.
-
$@,$<,$^는 Makefile 작성에서 자주 사용하는 automatic variable이다. -
common.mk를 사용하면 build logic을 재사용할 수 있다. - Compiler option에 따라 같은 C source에서도 다른 machine code가 생성된다.
- Embedded project에서는 source code뿐 아니라 build system까지 이해해야 debugging이 쉬워진다.