Bare metal C Ch03 빌드과정과 GNU툴체인 - Hongjayeong-02/Basic_SoC_implementation GitHub Wiki

Ch03. 빌드 과정과 GNU ARM Toolchain

목표
STM32CubeIDE의 Build 버튼 뒤에서 실제로 일어나는 과정을
전처리 → 컴파일 → 어셈블 → 링킹 → 이미지 생성 순서로 직접 이해한다.

현재 자료 기준 안내
Notion에는 Ch03을 build.sh로 단계별 실행한 학습 기록이 정리되어 있다.
다만 현재 첨부한 bare-metal 실습코드.zip에는 별도의 Ch03 폴더나 build.sh 파일은 포함되어 있지 않다.
따라서 이 페이지의 명령어와 결과는 Notion 학습 기록을 기준으로 정리하고, 실습 소스는 Ch02의 실제 main.c를 사용한다.


1. 왜 Build 과정을 나눠서 봐야 하는가

IDE에서 Build 버튼을 누르면 한 번에 끝나는 것처럼 보이지만, 실제 내부 과정은 여러 단계로 나뉜다.

main.c
  │
  ├─ 1. Preprocessing
  ▼
main.i
  │
  ├─ 2. Compilation
  ▼
main.s
  │
  ├─ 3. Assembly
  ▼
main.o
  │
  ├─ 4. Linking
  ▼
firmware.elf
  │
  ├─ 5. Image conversion
  ▼
firmware.hex / firmware.bin

이 흐름을 알면 다음과 같은 오류가 어느 단계의 문제인지 구분하기 쉬워진다.

  • #define, #include 관련 문제 → 전처리 단계
  • C 문법/타입 오류 → 컴파일 단계
  • 어셈블리 문법 오류 → 어셈블 단계
  • undefined reference → 링킹 단계
  • region ... overflowed → 메모리 배치/링커스크립트 문제

2. 단계별 정리

단계 입력 출력 대표 명령 역할
Preprocessing .c .i gcc -E #include, #define 전개
Compilation .i .s gcc -S C → ARM Assembly
Assembly .s .o gcc -c Assembly → Object code
Linking .o .elf gcc -T linker.ld Object 파일 결합 + 주소 배치
Image conversion .elf .hex, .bin objcopy Flash에 쓸 이미지 추출

3. 전처리 — main.c → main.i

arm-none-eabi-gcc \
    -mcpu=cortex-m4 \
    -mthumb \
    -mfloat-abi=soft \
    -E main.c \
    -o main.i

-E 옵션은 전처리만 수행한다.

대표적으로 다음이 처리된다.

#define LD1_PIN (1UL << 0)

컴파일러가 C 문법으로 분석하기 전에 전처리기가 단순 치환한다.

즉 전처리기는:

#define
#include
#if / #ifdef

같은 전처리 지시문을 처리하지만, C 프로그램의 의미를 이해하는 단계는 아니다.


4. 컴파일 — main.i → main.s

arm-none-eabi-gcc \
    -mcpu=cortex-m4 \
    -mthumb \
    -mfloat-abi=soft \
    -S main.i \
    -o main.s

-S는 C 코드를 ARM Assembly로 변환하고 멈춘다.

예를 들어 C의:

while (count--)
{
    __asm("nop");
}

같은 구문은 실제로는 조건 비교, 감소, 분기 명령들로 바뀐다.

이 단계에서 중요한 점은:

C 언어의 if, while, 함수 호출도 CPU가 실행할 때는 결국 명령어들의 조합으로 바뀐다는 것.


5. 어셈블 — main.s → main.o

arm-none-eabi-gcc \
    -mcpu=cortex-m4 \
    -mthumb \
    -mfloat-abi=soft \
    -c main.s \
    -o main.o

Assembly를 기계어가 포함된 Object file로 변환한다.

main.o는 아직 완성된 firmware가 아니다.

이 상태에서는:

  • 함수/변수의 최종 주소가 확정되지 않을 수 있고
  • startup object와 아직 결합되지 않았고
  • Flash/RAM 배치도 최종 결정되지 않았다.

즉 .o는 링크 전 중간 산출물이다.


6. 링킹 — .o → .elf

Notion 학습 기록의 명령은 다음과 같다.

arm-none-eabi-gcc \
    -mcpu=cortex-m4 \
    -mthumb \
    -mfloat-abi=soft \
    -T linker.ld \
    main.o startup.o \
    -o ch03_build.elf

링커는 여러 Object file을 하나로 합친다.

main.o
startup.o
   │
   ├─ Linker Script
   ▼
ch03_build.elf

링커스크립트는 다음과 같은 질문에 답한다.

Vector Table은 어디에 둘 것인가?
.text는 Flash 어디에 둘 것인가?
.data는 Flash에 어떻게 저장하고 RAM 어디에서 실행할 것인가?
.bss는 RAM 어디에 둘 것인가?
Stack의 시작 주소는 어디인가?

이 내용은 Ch04에서 직접 다룬다.


7. ELF → HEX

arm-none-eabi-objcopy \
    -O ihex \
    ch03_build.elf \
    ch03_build.hex

.elf

ELF에는 실행 코드뿐 아니라 다음 정보도 포함될 수 있다.

  • Symbol
  • Section
  • 함수/변수 정보
  • Debugging information

따라서 GDB, nm, objdump, size 같은 도구가 분석하기 좋다.

.hex / .bin

실제 Flash programming에 사용하기 쉬운 메모리 이미지다.

ELF
 ├─ Code
 ├─ Data
 ├─ Symbols
 └─ Debug information

        ↓ objcopy

HEX / BIN
 └─ Flash에 기록할 실제 이미지

8. objdump로 Object file 확인

arm-none-eabi-objdump -d main.o

-d는 Object file의 기계어를 다시 Assembly 형태로 보여준다.

이를 통해:

C source
   ↓ compiler
Assembly
   ↓ assembler
Machine code

관계를 직접 확인할 수 있다.

Notion 학습 기록에서는 delay() 루프가 subs, cmp, bne 계열의 저수준 명령으로 변환되는 것을 확인했다.


9. Compiler option

-mcpu=cortex-m4

Target CPU를 Cortex-M4로 지정한다.

-mcpu=cortex-m4

STM32F439ZI는 Cortex-M4F core를 사용한다.

-mthumb

Cortex-M 계열이 사용하는 Thumb instruction set을 대상으로 코드를 생성한다.

-mthumb

-mfloat-abi=soft

이 Ch03 실습은 GPIO register를 조작하는 정수 연산만 사용하므로 Notion 기록에서는:

-mfloat-abi=soft

를 사용했다.

후속 CMSIS 실습에서는 F439ZI의 FPv4-SP FPU를 사용하기 위해:

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

옵션을 사용하는 흐름으로 정리되어 있다.


10. 실습에 사용한 실제 main.c

현재 첨부 ZIP에서 확인한 Ch02_register/Src/main.c이다.

#define PERIPH_BASE       (0x40000000UL)
#define AHB1PERIPH_BASE    (PERIPH_BASE + 0x00020000UL)

#define RCC_BASE           (AHB1PERIPH_BASE + 0x3800UL)
#define GPIOB_BASE          (AHB1PERIPH_BASE + 0x0400UL)

#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))

#define GPIOBEN       (1UL << 1)
#define LD1_PIN       (1UL << 0)

static void delay(volatile unsigned int count)
{
    while (count--) { __asm("nop"); }
}

int main(void)
{
    RCC_AHB1ENR |= GPIOBEN;

    GPIOB_MODER &= ~(0x3UL << (0 * 2));
    GPIOB_MODER |=  (0x1UL << (0 * 2));

    while (1)
    {
        GPIOB_ODR ^= LD1_PIN;
        delay(800000);
    }
}

Ch03에서는 이 소스의 기능 자체를 바꾸는 것이 목적이 아니라, 이 코드가 어떤 단계를 거쳐 ELF/HEX로 변환되는지 확인하는 것이 핵심이다.


11. Notion에 기록된 실습 결과

Notion 기록 기준:

text = 252 bytes
data = 0 bytes
bss  = 0 bytes

Ch02에서 동일한 소스와 동일한 링크 조건을 사용했기 때문에, Make를 사용한 결과와 직접 명령을 실행한 결과가 같았다고 기록되어 있다.

핵심은:

Build 도구가 달라도 같은 source + 같은 compiler option + 같은 linker script를 사용하면 결과는 동일해야 한다.


12. 자주 사용하는 GNU ARM 도구

arm-none-eabi-gcc
arm-none-eabi-objcopy
arm-none-eabi-objdump
arm-none-eabi-size
arm-none-eabi-nm
Tool 역할
gcc Compile / Assemble / Link driver
objcopy ELF → HEX/BIN 변환
objdump Object/ELF disassembly 및 정보 확인
size .text, .data, .bss 크기 확인
nm ELF symbol 확인

13. 핵심 정리

Build pipeline

.c
↓ Preprocess
.k
↓ Compile
.s
↓ Assemble
.o
↓ Link
.elf
↓ objcopy
.hex / .bin

파일별 의미

확장자 의미
.c C source
.i 전처리 완료 C source
.s Assembly source
.o Relocatable object
.elf 최종 linked executable
.hex/.bin Flash programming image

한 줄 요약

IDE의 Build 버튼은 하나의 마법 버튼이 아니라 GNU Toolchain의 여러 단계를 연속으로 실행한 결과다.


14. 이 챕터에서 배운 점

  • Build 과정이 단계별로 어떤 파일을 만드는지 이해했다.
  • C 코드가 Assembly와 machine code로 변환되는 흐름을 확인했다.
  • .o, .elf, .hex의 역할 차이를 구분했다.
  • Linker가 단순 파일 결합이 아니라 실제 메모리 주소 배치까지 담당한다는 것을 확인했다.
  • 다음 Ch04에서 Linker Script와 Startup code를 직접 작성하기 위한 기반을 만들었다.

네비게이션: 이전: Ch02. 메모리 주소로 레지스터 직접 정의 · 전체 목차 · 다음: Ch04. Linker Script와 Startup File