Repository navigation
Chapter 03
Understand what actually happens when an embedded C project is built.
In this chapter:
- Build the Ch02 program without relying on an IDE build button
- Run each GNU ARM toolchain stage manually
- Inspect
.i,.s,.o,.elf, and.hex - Understand compiler options for Cortex-M4
- Inspect generated machine code with
objdump - Understand where compile errors and link errors occur
STM32CubeIDE에서는 보통:
Build
버튼 한 번으로 프로그램이 만들어진다.
하지만 실제 내부에서는 여러 단계가 순서대로 실행된다.
main.c
│
▼
Preprocessor
│
▼
main.i
│
▼
Compiler
│
▼
main.s
│
▼
Assembler
│
▼
main.o
│
▼
Linker
│
▼
program.elf
│
▼
objcopy
│
├── program.hex
└── program.bin
이 과정을 알아야 다음과 같은 오류가 어느 단계에서 발생했는지 판단할 수 있다.
syntax error
undefined reference
multiple definition
region FLASH overflowed
section overlaps
예를 들어:
undefined reference to `foo`
는 C 문법 오류가 아니다.
각 .c 파일은 정상적으로 compile되었지만, link 단계에서 foo의 실제 정의를 찾지 못했다는 뜻이다.
STM32F439ZI는 ARM Cortex-M4 기반 MCU이므로 일반 PC용 GCC가 아니라 ARM target용 cross compiler를 사용한다.
대표 명령:
arm-none-eabi-gcc
arm-none-eabi-as
arm-none-eabi-ld
arm-none-eabi-objcopy
arm-none-eabi-objdump
arm-none-eabi-sizeToolchain prefix:
arm-none-eabi
를 분해하면 다음과 같이 이해할 수 있다.
arm
│
└── Target architecture
none
│
└── No operating-system dependency
eabi
│
└── Embedded Application Binary Interface
즉:
ARM 기반 bare-metal embedded system을 위한 toolchain
이라고 이해하면 된다.
PC에서 GCC를 실행하지만 생성되는 machine code는 x86이 아니라 ARM Cortex-M4용 코드다.
Development PC
x86/x64
│
│ arm-none-eabi-gcc
▼
ARM Machine Code
│
▼
STM32F439ZI
이것이 Cross Compilation이다.
이번 실습에서는 다음 5단계를 직접 실행한다.
1. Preprocessing
2. Compilation
3. Assembly
4. Linking
5. Binary/Image Generation
각 단계의 입력과 출력:
main.c
↓ preprocessing
main.i
↓ compilation
main.s
↓ assembly
main.o
↓ linking
ch03_build.elf
↓ objcopy
ch03_build.hex
Ch03에서는 Ch02에서 사용한 LED blink source를 그대로 사용한다.
#define PERIPH_BASE (0x40000000UL)
#define AHB1PERIPH_BASE (PERIPH_BASE + 0x00020000UL)
#define RCC_BASE (AHB1PERIPH_BASE + 0x3800UL)
#define GPIOB_BASE (AHB1PERIPH_BASE + 0x0400UL)
/* Register mapping */
#define RCC_AHB1ENR (*(volatile unsigned int *)(RCC_BASE + 0x30UL))
#define GPIOB_MODER (*(volatile unsigned int *)(GPIOB_BASE + 0x00UL))
#define GPIOB_ODR (*(volatile unsigned int *)(GPIOB_BASE + 0x14UL))
/* Bit definitions */
#define GPIOBEN (1UL << 1)
#define LD1_PIN (1UL << 0)
static void delay(volatile unsigned int count)
{
while (count--)
{
__asm__("nop");
}
}
int main(void)
{
/* Enable GPIOB clock */
RCC_AHB1ENR |= GPIOBEN;
/* PB0 -> General Purpose Output */
GPIOB_MODER &= ~(0x3UL << (0 * 2));
GPIOB_MODER |= (0x1UL << (0 * 2));
while (1)
{
GPIOB_ODR ^= LD1_PIN;
delay(800000);
}
}The hardware operation is unchanged from Chapter 02.
The focus of this chapter is:
How does main.c become machine code?
Command:
arm-none-eabi-gcc \
-mcpu=cortex-m4 \
-mthumb \
-mfloat-abi=soft \
-E main.c \
-o main.iInput:
main.c
Output:
main.i
Preprocessor는 실제 C compiler보다 먼저 동작한다.
주요 작업:
#include
#define
#if
#ifdef
#ifndef
등을 처리한다.
가장 중요한 점:
Preprocessor는 C 문법의 의미를 이해하는 compiler가 아니다.
주로 text substitution을 수행한다.
예를 들어:
#define GPIOBEN (1UL << 1)
RCC_AHB1ENR |= GPIOBEN;는 preprocessing 후 개념적으로:
RCC_AHB1ENR |= (1UL << 1);처럼 치환된다.
Ch02에서 사용한:
#define GPIOB_ODR \
(*(volatile unsigned int *)(GPIOB_BASE + 0x14UL))도 preprocessing 단계에서 치환된다.
그리고:
#define GPIOB_BASE \
(AHB1PERIPH_BASE + 0x0400UL)등도 연쇄적으로 치환된다.
결국 compiler가 보는 코드는 개념적으로 다음과 가까워진다.
(*(volatile unsigned int *)
((0x40000000UL + 0x00020000UL)
+ 0x0400UL
+ 0x14UL))
^= (1UL << 0);즉 #define은 새로운 변수나 함수를 만드는 것이 아니다.
Source Text
↓
Preprocessor
↓
Expanded Source Text
이다.
생성된 파일 확인:
less main.i또는:
head main.i.i 파일은 preprocessing이 끝난 C source다.
아직 ARM assembly도 아니고 machine code도 아니다.
main.c
│
│ #define / #include expanded
▼
main.i
Command:
arm-none-eabi-gcc \
-mcpu=cortex-m4 \
-mthumb \
-mfloat-abi=soft \
-S main.i \
-o main.sInput:
main.i
Output:
main.s
Compiler는 C 언어를 target processor의 assembly language로 변환한다.
C
↓
Compiler
↓
ARM / Thumb Assembly
예를 들어 C 코드:
count--;는 architecture와 optimization에 따라 assembly에서:
subs r3, r3, #1같은 instruction으로 바뀔 수 있다.
그리고:
while (count--)같은 구조는 일반적으로:
subtract
compare
branch
계열 instruction으로 구현된다.
C에서는:
while (count--)
{
__asm__("nop");
}라고 작성하지만 CPU는 while이라는 개념을 모른다.
CPU가 이해하는 것은:
Load
Subtract
Compare
Branch
같은 instruction뿐이다.
개념적으로:
while(count--)
│
▼
load count
│
▼
subtract
│
▼
compare with zero
│
▼
branch if not finished
이다.
이 부분이 C programming과 Computer Architecture가 직접 연결되는 지점이다.
less main.s또는:
grep -A30 main main.s실제 assembly를 확인하면:
C variable
C loop
C pointer access
가 Cortex-M4 instruction으로 어떻게 변환되는지 볼 수 있다.
Command:
arm-none-eabi-gcc \
-mcpu=cortex-m4 \
-mthumb \
-mfloat-abi=soft \
-c main.s \
-o main.oInput:
main.s
Output:
main.o
Assembler는 assembly instruction을 machine code로 변환한다.
ARM Assembly
↓
Assembler
↓
Binary Machine Instructions
예:
nop같은 assembly mnemonic이 실제 CPU가 실행할 binary instruction으로 encoding된다.
하지만 .o 파일은 아직 완전한 실행 이미지가 아니다.
main.o는 object file이다.
Object file에는 보통 다음 정보가 들어있다.
Machine Code
Sections
Symbols
Relocation Information
Debug Information
중요한 점:
.o에는 machine code가 존재하지만 아직 최종 주소가 확정되지 않은 symbol이 있을 수 있다.
예를 들어:
foo();를 호출한다고 하자.
Compiler와 assembler는:
foo라는 symbol을 호출한다
는 정보는 만들 수 있다.
그러나 foo()가 다른 object file에 정의되어 있다면 실제 주소는 아직 알 수 없다.
main.o
└── needs symbol: foo
foo.o
└── defines symbol: foo
이 둘을 연결하는 단계가 Linking이다.
.o는 바로 MCU Flash에 쓸 파일이 아니다.
main.o
startup.o
driver.o
...
각 object file은 아직 분리되어 있다.
따라서 다음 단계에서 linker가 합쳐야 한다.
Disassemble:
arm-none-eabi-objdump -d main.o이 명령은 object file 안의 machine code를 사람이 읽기 쉬운 assembly 형태로 다시 보여준다.
Machine Code
↓
objdump
↓
Assembly View
이 과정을 Disassembly라고 한다.
Notion 실습에서는 delay()의:
while (count--)가 실제로 저수준에서:
subtract
compare
conditional branch
형태의 instruction으로 바뀌는 것을 확인했다.
실제 instruction은 compiler option에 따라 달라질 수 있지만 대표적으로:
subs
cmp
bne같은 instruction을 볼 수 있다.
Command:
arm-none-eabi-gcc \
-mcpu=cortex-m4 \
-mthumb \
-mfloat-abi=soft \
-T linker.ld \
main.o \
startup.o \
-o ch03_build.elfInput:
main.o
startup.o
linker.ld
Output:
ch03_build.elf
Linker의 중요한 역할은 크게 세 가지다.
main.o
startup.o
other.o
│
▼
Linker
│
▼
One Executable Image
예:
main.o
└── references Reset_Handler
startup.o
└── defines Reset_Handler
혹은 반대로 startup code가:
main
symbol을 참조한다.
Linker가 실제 주소를 찾아 연결한다.
.text
.rodata
.data
.bss
.vector_table
등을 실제 FLASH / SRAM address에 배치한다.
이 위치를 지정하는 것이:
linker.ld
즉 Linker Script다.
PC 프로그램은 Operating System과 runtime environment가 많은 부분을 관리한다.
Bare-metal MCU는 그렇지 않다.
우리가 직접 알려줘야 한다.
예:
FLASH starts here
FLASH size is this
RAM starts here
RAM size is this
.text goes to FLASH
.data initial image goes to FLASH
.data runtime location goes to RAM
.bss goes to RAM
이를 linker에게 알려주는 파일이:
linker.ld
이다.
Chapter 04에서 직접 작성한다.
개념적으로:
FLASH
0x08000000
│
├── Vector Table
├── .text
├── .rodata
└── Initial values for .data
SRAM
0x20000000
│
├── .data
├── .bss
├── Heap
└── Stack
처럼 code와 data가 서로 다른 memory 영역에 배치된다.
정확한 section placement는 linker script가 결정한다.
Bare-metal MCU에서 processor는 power-on 직후 바로 main()을 실행하지 않는다.
대략:
RESET
↓
Vector Table
↓
Initial Stack Pointer
↓
Reset_Handler
↓
Runtime Initialization
↓
main()
따라서 Reset_Handler와 vector table을 가진 startup code가 필요하다.
Chapter 03에서는 기존 startup object를 사용하고, 자세한 구현은 Chapter 04에서 다룬다.
Suppose:
void foo(void);
int main(void)
{
foo();
}그런데 어디에도:
void foo(void)
{
}가 없다면 compiler는 prototype을 알고 있기 때문에 compile은 가능하다.
하지만 linker는 실제 foo implementation을 찾지 못한다.
결과:
undefined reference to `foo'
즉:
Compile
PASS
↓
Link
FAIL
이다.
Examples:
syntax error
undeclared identifier
wrong type
missing semicolon
보통:
.c → .o
과정에서 실패한다.
Examples:
undefined reference
multiple definition
region overflowed
보통:
.o files → .elf
과정에서 실패한다.
이 차이를 아는 것이 build debugging에서 매우 중요하다.
Linking이 성공하면:
ch03_build.elf
가 생성된다.
ELF:
Executable and Linkable Format
이다.
ELF에는 단순 machine code 외에도 여러 정보가 포함될 수 있다.
Executable Code
Data
Sections
Symbols
Debug Information
Address Information
따라서 debugger가:
main()
uart_init()
line 37
variable x
등을 찾을 수 있다.
Command:
arm-none-eabi-size ch03_build.elf실습 결과:
text data bss
252 0 0
Meaning:
Program code and read-only data.
주로 Flash에 위치한다.
초기값이 있는 global/static variable.
Example:
int count = 10;초기값은 Flash image 안에 저장되어 있고 runtime에는 RAM으로 복사된다.
초기값이 없거나 0으로 초기화되는 global/static variable.
Example:
int count;보통 Flash image에 실제 0 데이터를 전부 저장하는 대신:
이 영역을 startup에서 0으로 초기화하라
는 정보만 가진다.
이번 Ch02/Ch03 source는 global variable을 거의 사용하지 않는다.
Registers는:
#definemacro이고 실제 RAM variable이 아니다.
예:
#define GPIOB_ODR \
(*(volatile unsigned int *)0x40020414UL)는 RAM 공간을 차지하는 전역 변수가 아니다.
따라서:
data = 0
bss = 0
이 나오는 것이 자연스럽다.
Command:
arm-none-eabi-objcopy \
-O ihex \
ch03_build.elf \
ch03_build.hexInput:
ch03_build.elf
Output:
ch03_build.hex
ELF에는 flashing에 필요하지 않은 정보도 많이 포함되어 있다.
예:
debug symbols
section metadata
symbol names
source mapping
Flash programmer가 필요한 것은 실제 MCU memory에 기록할 content다.
따라서:
ELF
│
│ objcopy
▼
Flash Image
로 변환한다.
ch03_build.hex
Text-based format.
내용에는:
Address
Data
Record Type
Checksum
등이 포함된다.
예:
:1000000068030020650100006D0100006F01000021
사람이 text file로 열 수 있다.
Binary image를 만들려면:
arm-none-eabi-objcopy \
-O binary \
ch03_build.elf \
ch03_build.bin.bin은 순수 binary byte stream이다.
HEX
→ address information included
BIN
→ raw bytes only
Notion Ch03에서 사용한 핵심 build sequence:
arm-none-eabi-gcc \
-mcpu=cortex-m4 \
-mthumb \
-mfloat-abi=soft \
-E main.c \
-o main.i
arm-none-eabi-gcc \
-mcpu=cortex-m4 \
-mthumb \
-mfloat-abi=soft \
-S main.i \
-o main.s
arm-none-eabi-gcc \
-mcpu=cortex-m4 \
-mthumb \
-mfloat-abi=soft \
-c main.s \
-o main.o
arm-none-eabi-gcc \
-mcpu=cortex-m4 \
-mthumb \
-mfloat-abi=soft \
-T linker.ld \
main.o startup.o \
-o ch03_build.elf
arm-none-eabi-objcopy \
-O ihex \
ch03_build.elf \
ch03_build.hexThe same sequence can be written as:
#!/bin/bash
CPU="-mcpu=cortex-m4"
THUMB="-mthumb"
FLOAT="-mfloat-abi=soft"
echo "[1] Preprocess"
arm-none-eabi-gcc \
$CPU $THUMB $FLOAT \
-E main.c \
-o main.i
echo "[2] Compile"
arm-none-eabi-gcc \
$CPU $THUMB $FLOAT \
-S main.i \
-o main.s
echo "[3] Assemble"
arm-none-eabi-gcc \
$CPU $THUMB $FLOAT \
-c main.s \
-o main.o
echo "[4] Link"
arm-none-eabi-gcc \
$CPU $THUMB $FLOAT \
-T linker.ld \
main.o startup.o \
-o ch03_build.elf
echo "[5] Generate HEX"
arm-none-eabi-objcopy \
-O ihex \
ch03_build.elf \
ch03_build.hex
echo "[6] Size"
arm-none-eabi-size ch03_build.elfExecute:
chmod +x build.sh
./build.sh-mcpu=cortex-m4Compiler에게 target CPU architecture를 알려준다.
STM32F439ZI core:
ARM Cortex-M4
따라서 compiler는 Cortex-M4가 지원하는 instruction을 기준으로 code를 생성한다.
잘못된 CPU option을 선택하면:
- 지원하지 않는 instruction 생성
- 최적화 방식 차이
- architecture incompatibility
문제가 발생할 수 있다.
-mthumbThumb instruction set을 사용하도록 지정한다.
Cortex-M series는 Thumb instruction execution을 기반으로 한다.
C Source
↓
Thumb Instructions
↓
Cortex-M4
따라서 STM32 firmware build에서는 일반적으로 필요한 option이다.
Ch03 practice:
-mfloat-abi=soft를 사용했다.
현재 code는:
GPIO register access
integer operation
bit operation
만 사용하므로 hardware floating-point unit을 사용할 필요가 없다.
STM32F439ZI의 Cortex-M4 core는 hardware floating-point support를 제공한다.
Floating-point를 실제 hardware FPU로 처리하려면 일반적으로 다음과 같은 option 조합을 사용할 수 있다.
-mfloat-abi=hard
-mfpu=fpv4-sp-d16개념:
soft
└── Floating-point ABI does not require hardware FPU instructions
hard
└── Hardware FPU calling convention / instructions used
중요한 점은 object files와 libraries가 호환되는 ABI 설정을 사용해야 한다는 것이다.
서로 다른 floating-point ABI의 object/library를 섞으면 link 문제가 발생할 수 있다.
GNU toolchain에는 각각 독립된 프로그램이 존재한다.
gcc
as
ld
하지만 실습에서는 대부분:
arm-none-eabi-gccdriver를 사용했다.
gcc driver는 option에 따라 내부적으로 필요한 tool을 호출한다.
예:
gcc -c main.s는 내부적으로 assembler를 사용한다.
그리고:
gcc main.o startup.o -o app.elf는 linker를 호출한다.
따라서:
GCC는 단순히 C compiler 하나만 의미하는 것이 아니라 build toolchain의 driver 역할도 한다.
전체 과정을 hardware 관점까지 연결하면:
main.c
│
▼
Preprocessor
│
▼
Compiler
│
▼
ARM Assembly
│
▼
Assembler
│
▼
Machine Code
│
▼
Linker
│
▼
Absolute Memory Layout
│
▼
ELF
│
▼
HEX / BIN
│
▼
FLASH
│
▼
Cortex-M4 Instruction Fetch
│
▼
Execution
즉 우리가 작성한 C code가 최종적으로는 Flash memory에 저장된 machine instruction이 된다.
Notion에 기록된 Ch03 결과:
text : 252 bytes
data : 0 bytes
bss : 0 bytes
Ch02의 Makefile build 결과와 동일했다.
Ch02
Makefile
│
└── 252 / 0 / 0
Ch03
Manual Toolchain
│
└── 252 / 0 / 0
같은 source와 같은 build configuration을 사용했기 때문에 동일한 result가 나온다.
이 실습의 핵심은:
IDE나 Make가 특별한 binary를 만드는 것이 아니라, 결국 동일한 compiler / assembler / linker를 순서대로 실행한다.
는 것을 확인한 것이다.
less main.iless main.sarm-none-eabi-objdump -d main.oarm-none-eabi-objdump -d ch03_build.elfarm-none-eabi-objdump -h ch03_build.elfarm-none-eabi-nm ch03_build.elfarm-none-eabi-size ch03_build.elfhead ch03_build.hex이 명령들을 같이 익혀두면 build/debugging 때 매우 유용하다.
Build error를 보면 어느 단계를 먼저 의심해야 하는지 정리하면:
#include file not found
↓
Preprocessor
syntax error
type error
undeclared identifier
↓
Compiler
invalid instruction
↓
Assembler
undefined reference
multiple definition
FLASH overflow
RAM overflow
↓
Linker
HEX conversion failure
↓
objcopy
Example:
undefined reference to `MYX_LWIP_Init'
이 종류의 error는 대표적인 linker error다.
의미:
Function call exists
↓
Compiler accepts it
↓
Object file references symbol
↓
Linker searches definition
↓
Definition not found
↓
FAIL
따라서 이 오류에서는:
C syntax
보다:
source file included?
object file generated?
function actually defined?
library linked?
를 확인해야 한다.
두 object file이 같은 global symbol을 정의하면:
file1.o
└── foo
file2.o
└── foo
linker가 어느 것을 사용할지 결정할 수 없다.
따라서:
multiple definition of `foo'
가 발생할 수 있다.
Linker script가:
FLASH size
RAM size
를 정의한다.
Program section이 해당 memory region보다 커지면:
region `FLASH' overflowed
같은 link error가 발생한다.
따라서 이 오류는 compile 문제가 아니라 memory placement 문제다.
이번 실습에서는 직접 명령어를 실행했지만 실제 project에서는 보통:
Makefile
IDE
CMake
Build Script
등이 동일한 명령을 자동으로 관리한다.
Manual Commands
↓
Shell Script
↓
Makefile
↓
IDE Build System
추상화 수준만 다를 뿐 underlying build flow는 같다.
Preprocess
↓
Compile
↓
Assemble
↓
Link
↓
Generate Flash Image
Preprocessed C source.
#define expanded
#include expanded
Target assembly code generated by the compiler.
For STM32F439ZI:
ARM Cortex-M4 / Thumb assembly
Machine code plus section, symbol and relocation information.
It is not yet the final firmware image.
Linked executable information including:
- machine code
- section layout
- addresses
- symbols
- optional debug information
Because a programmer/debugger needs an image describing the bytes to write into MCU memory.
ELF
├── machine code
├── symbols
├── sections
├── addresses
└── debug information
HEX
├── memory addresses
├── data
└── checksum
Because the Cortex-M4 implementation in STM32F439ZI includes an FPU.
For hardware-FPU builds, compatible options such as:
-mfloat-abi=hard
-mfpu=fpv4-sp-d16can be used.
Linking
Because understanding the build stages makes errors much easier to diagnose.
Error Message
↓
Identify Build Stage
↓
Identify Responsible File / Tool
↓
Fix Root Cause
C Source
↓
Preprocessor
↓
Expanded C
↓
Compiler
↓
Assembly
↓
Assembler
↓
Object File
↓
Linker + Linker Script
↓
ELF
↓
objcopy
↓
HEX / BIN
↓
MCU Flash
The most important concepts are:
- A build is not one operation.
- Each intermediate file has a different purpose.
- Compilation and linking are separate processes.
- Object files can contain unresolved symbols.
- The linker resolves symbols and assigns addresses.
- The linker script defines the MCU memory layout.
- ELF is useful for debugging and analysis.
- HEX/BIN are used as programming images.
-
objdump,nm, andsizeare essential low-level debugging tools. - IDE, Makefile, and shell scripts eventually invoke the same toolchain.