지금까지는 VSCode와 여러 IDE의 도움으로 내가 작성한 코드에 대해서 “빌드하기” 버튼을 누르면 마법과 같은 일이 일어나서 프로그램이 실행된다! 라고 생각해왔다고 볼 수 있다. 왜냐면 빌드하면 진짜 IDE가 알아서 컴파일, 어셈블링, 링킹을 거쳐 실행파일을 만드는걸 다 해줘왔거든. 그래서 이게 어떤 절차로 이루어지는지 크게 궁금하지도 않았었다. 학부 1학년때에나 간략하게 짚고 넘어가는 과정이여서 1학년때는 관심이 크게 없었고, 그 이후로도 쭉 관심을 크게 가지지 않고 지나왔던 부분인 것 같다.
ㅤ
그런데 이제 임베디드 세상으로 온 이상, 더 이상 마법같은 일이라고 부를 수는 없을 것 같다. 혹시나 내가 사용해야 하는 보드가 딱 적합한 IDE를 지원하지 않으면서, 특정 경로에 있는 헤더파일과 DLL을 링킹 과정에 포함시켜서 빌드해야 하는 상황처럼 빌드 과정에 대해서 커스텀을 해야하는 상황이라면 이게 단순히 마법과 같은 일이라고 생각하기 보다는 “그 아래에 사실은 과학이 있었다” 라고 여기고 어떤 원리로 돌아가는지 파악해야 하기 때문.
ㅤ
이러저러한 여러가지 필요성에 의해, 전체적인 빌드 과정과 빌드 부산물들에 대해서 관심을 가지고 그 절차를 낱낱히 파헤쳐보고자 한다. 혹시 글이 길어진다면 부득이하게 2개의 게시글로 분리하게 될 수도 있다. 절대 양치기가 아니다. 절대.
ㅤ
당신에게 컴파일러의 가호가 있기를.
ㅤ
전체적인 빌드 흐름
빌드는 작성된 소스코드를 실행이 가능한 파일(SW산출물)로 변환하는 전체 과정을 말한다.
작성한 .c 파일과 .h 파일들
↓
[전처리기]
↓
.i 파일 (빌드 부산물)
↓
[컴파일러]
↓
.s 파일 (빌드 부산물)
↓
[어셈블러]
↓
.o 파일 (이게 오브젝트 파일)
↓
[링커]
↓
.elf 파일 (실행 가능한 바이너리 파일)
↓
[후처리]
↓
.bin (플래시를 위한 바이너리 파일)
ㅤ
위 흐름이 STM Cube IDE에서는 어떤 과정으로 보여지는지 확인해보자. 우선 프로젝트를 빌드했을 때 나오는 로그를 간단하게 살펴보면 아래와 같다.

ㅤ
여기에서 명령어 구조를 살펴보면, 전체 프로젝트 내 소스파일에 대해서 전처리 + 컴파일 + 어셈블을 수행하는 단계 → 만들어진 파일들에 대해서 링킹을 수행하는 단계로 나뉜다. 여기에 대해서 스텝 별로 어떤 일들이 일어나는지 한 번 살펴보자.
ㅤ
들어가기에 앞서, STM Cube IDE에서는 ‘개별 컴파일 + 증분 빌드’를 적용하였다.
- 개별 컴파일
- 한 명령어로 모든 파일을 컴파일 + 링킹 하는 것이 아니라, 각각의 파일에 대해서 개별적으로 컴파일을 적용한 이후에 한 번에 링킹으로 실행 파일을 만드는 것을 말한다.
- 호스트 PC의 멀티코어를 이용해서 병렬로 컴파일을 수행해 더 빠르게 빌드를 처리할 수 있음!
- 증분 빌드
- 프로젝트 내에서 수정된 부분이 있다면 해당 부분만 따로 빌드 + 수정X 파일들에 링킹 을 수행해서 실행 파일을 만들어내는 방식을 말한다.
- 수정된 부분만 따로 적용해 더 빠르고 효율적으로 빌드를 처리할 수 있음!

ㅤ
우선 각각의 파일에 일괄적으로 적용되는 컴파일 과정에 대해 알아보자.
arm-none-eabi-gcc "../Drivers/STM32F4xx_HAL_Driver/Src/stm32f4xx_hal.c" -mcpu=cortex-m4 -std=gnu11 -g3 -DDEBUG -DUSE_HAL_DRIVER -DSTM32F429xx -c -I../Core/Inc -I../Drivers/STM32F4xx_HAL_Driver/Inc -I../Drivers/STM32F4xx_HAL_Driver/Inc/Legacy -I../Drivers/CMSIS/Device/ST/STM32F4xx/Include -I../Drivers/CMSIS/Include -O0 -ffunction-sections -fdata-sections -Wall -save-temps -fstack-usage -fcyclomatic-complexity -MMD -MP -MF"Drivers/STM32F4xx_HAL_Driver/Src/stm32f4xx_hal.d" -MT"Drivers/STM32F4xx_HAL_Driver/Src/stm32f4xx_hal.o" --specs=nano.specs -mfpu=fpv4-sp-d16 -mfloat-abi=hard -mthumb -o "Drivers/STM32F4xx_HAL_Driver/Src/stm32f4xx_hal.o"
ㅤ
명령어에 적용된 옵션들은 각각 아래의 내용들을 가진다. 사실 뭐 이건 타겟 시스템이나 IDE가 바뀌면 변경될 수 있는 내용들이라 크게 달달 외우거나 해야할 부분은 없지만, 이렇게 신경쓰는 곳이 많다! 정도로만 캐치해두면 될 듯 하다.
arm-none-eabi-gcc
*** 소스파일 ***
"../Drivers/STM32F4xx_HAL_Driver/Src/stm32f4xx_hal.c"
*** 빌드 단계 지정 ***
[전처리 + 컴파일 + 어셈블 수행 // 링킹 제외]
-c
*** CPU 옵션 ***
[Cortex-M4를 타겟으로 하겠다 -> ARMv7E-M + Thumb2 + 3 파이프라인 등의 특성]
-mcpu=cortex-m4
-mthumb
[부동 소수점 연산에 대한 설정]
-mfpu=fpv4-sp-d16
-mfloat-abi=hard
*** C언어 표준 지정 ***
-std=gnu11
[디버그 정보 : 최대 (0~3)]
-g3
*** 의존성 ***
[의존성 파일 생성 및 출력 경로 지정]
-MMD
-MP
-MF"Drivers/STM32F4xx_HAL_Driver/Src/stm32f4xx_hal.d"
-MT"Drivers/STM32F4xx_HAL_Driver/Src/stm32f4xx_hal.o"
*** 링커의 스펙 ***
--specs=nano.specs
*** 출력 ***
[중간 단계 파일 저장하기 -> .i .s .o 확인할 수 있도록]
-save-temps
[오브젝트 파일 생성]
-o "Drivers/STM32F4xx_HAL_Driver/Src/stm32f4xx_hal.o"
***** 전처리를 위한 옵션 *****
*** 전처리기 매크로 ***
[-D : 매크로 정의]
[디버그모델 사용 + HAL 라이브러리 사용 + STM32F429xx 사용 선언]
-DDEBUG
-DUSE_HAL_DRIVER
-DSTM32F429xx
*** 인클루드 ***
[-I : 인클루드 경로 설정]
-I../Core/Inc
-I../Drivers/STM32F4xx_HAL_Driver/Inc
-I../Drivers/STM32F4xx_HAL_Driver/Inc/Legacy
-I../Drivers/CMSIS/Device/ST/STM32F4xx/Include
-I../Drivers/CMSIS/Include
***** 컴파일을 위한 옵션 *****
*** 최적화 ***
[-O0 : 최적화 X]
-O0
[각 함수를 별도의 섹션에 배채]
-ffunction-sections
[각 전역 변수를 별도의 섹션에 배치]
-fdata-sections
*** 경고와 분석 ***
[모든 경고 띄우기 (Warning all)]
-Wall
[각 함수의 스택 사용량 분석 -> .su]
-fstack-usage
[함수의 복잡도 분석 -> .cc]
-fcyclomatic-complexity
ㅤ
예시를 위해 main.c 에 있는 아래 소스코드가 어떻게 변경되는지를 추적해보겠다.
while (1)
{
// new_input = HAL_GPIO_ReadPin(GPIOC, GPIO_PIN_13);
new_input = read_user_button();
if(new_input != old_input && new_input == GPIO_PIN_SET) {
HAL_GPIO_TogglePin(GPIOB, GPIO_PIN_7);
}
old_input = new_input;
}
ㅤ
1. 전처리기 (preprocessor)
전처리기는 컴파일러가 코드를 분석하기 전에 소스코드의 전처리문의 치환, 조작 등을 통해 분석하기 편한 상태로 만든다. 사실상 사람의 편의를 위해 만든 단계로 생각된다. 전처리기를 통과한 파일의 확장자는 .i.
ㅤ
- 전처리 구분에 대한 처리
#include으로 헤더 파일을 복사해 소스코드 파일 내에 다시 작성함. 이 경로는 명령어에서-I명령어를 통해 지정된 공간에서 찾는다.#define으로 작성된 매크로를 실제 값으로 치환.-D옵션으로 전달된 매크로에 따라서#ifdef,#ifndef전처리로 코드 잘라내기 수행.
- 주석 제거
ㅤ
빌드 명령어에서는 다음의 옵션들이 전처리와 직접 관련있다.
***** 전처리를 위한 옵션 *****
*** 전처리기 매크로 ***
[-D : 매크로 정의]
[디버그모델 사용 + HAL 라이브러리 사용 + STM32F429xx 사용 선언]
-DDEBUG
-DUSE_HAL_DRIVER
-DSTM32F429xx
*** 인클루드 ***
[-I : 인클루드 경로 설정]
-I../Core/Inc
-I../Drivers/STM32F4xx_HAL_Driver/Inc
-I../Drivers/STM32F4xx_HAL_Driver/Inc/Legacy
-I../Drivers/CMSIS/Device/ST/STM32F4xx/Include
-I../Drivers/CMSIS/Include
ㅤ
여기에서 -DDEBUG 라고 되어있으면 시스템 전체적으로 #define DEBUG 1 를 작성한 것과 동일하다. 이를 통해서 #ifdef DEBUG 으로 분기 처리한 전처리 구문들을 활성화시킬 수 있다. 나머지도 마찬가지.
ㅤ
원본 main.c 파일은 꼴랑 243줄이였는데, main.i 파일은 무려 29626줄이나 된다. 단순히 전처리기만 통과했다고 하나의 파일이 거의 100줄이 늘어나는 매직. main.i 파일에서 while 문은 아래처럼 변경되어있다. 잘 보면, define으로 정의되어있던 값들이 상수로 대치되고, 주석이 제거된 것을 확인할 수 있다.
while (1)
{
# 115 "../Core/Src/main.c"
new_input = read_user_button();
if(new_input != old_input && new_input == GPIO_PIN_SET) {
HAL_GPIO_TogglePin(((GPIO_TypeDef *) ((0x40000000UL + 0x00020000UL) + 0x0400UL)), ((uint16_t)0x0080));
}
old_input = new_input;
}
}
ㅤ
2. 컴파일러 (Compiler)
컴파일러는 전처리기를 통과한 c코드 (.i 파일)에 대해서 타겟 시스템의 어셈블리 언어로 변환하는 과정을 담당한다. 이 과정에서 최적화 를 수행하게 된다. 물론 어셈블리 언어로 변환하는게 가장 주요한 책무이지만, 컴파일러가 똑똑하다는건 최적화를 얼마나 잘 수행하느냐가 관건이다.
ㅤ
- 구문 분석
- C언어 문법이 잘 맞는지에 대해서 확인하고, 추상 구문 트리를 생성한다. (← 컴파일러 시간에 배웠던 그 녀석들)
- 괄호가 잘 열고 닫히는지 + 연산자 좌우에 개수가 올바른지 등 구조에 대해서 검증하는 단계
- 의미 분석
- 타입과 함수 시그니처, 범위에 대해서 확인한다.
- 구조 내 변수와 값들의 타입이 잘 맞는지 확인하는 단계
- 중간 표현 생성
- 하나의 문장이 최대 한 개의 연산을 담는 CPU와 독립적인 언어 (중간 언어 - IR)로 코드를 변환
- 최적화
- 안쓰는 코드 제거
- 상수는 컴파일 시간에 미리 계산 + 함수 인라인 등 실행 시간을 줄일 수 있도록 최적화 수행
- 컴파일러의 성능 + 옵션에 따라 최적화 성능은 달라질 수 있다!
- 레지스터 할당
- 타겟 시스템의 환경에 맞게 변수에 레지스터를 매핑
- 명령어 선택
- 중간 언어를 타겟 CPU의 명령어로 변환
- 명령어 스케줄링
- 파이프라인 스톨, 레이턴시 최소화 등을 위해 명령어의 순서를 재배치
- ARM 어셈블리 코드 생성 (
.s)
ㅤ
컴파일러가 작업을 수행하면서 참고할 수 있는 옵션들은 이렇게 구성된다.
*** CPU 옵션 ***
[Cortex-M4를 타겟으로 하겠다 -> ARMv7E-M + Thumb2 + 3 파이프라인 등의 특성]
-mcpu=cortex-m4
-mthumb
[부동 소수점 연산에 대한 설정]
-mfpu=fpv4-sp-d16
-mfloat-abi=hard
***** 컴파일을 위한 옵션 *****
*** 최적화 ***
[-O0 : 최적화 X]
-O0
[각 함수를 별도의 섹션에 배채]
-ffunction-sections
[각 전역 변수를 별도의 섹션에 배치]
-fdata-sections
*** 경고와 분석 ***
[모든 경고 띄우기 (Warning all)]
-Wall
[각 함수의 스택 사용량 분석 -> .su]
-fstack-usage
[함수의 복잡도 분석 -> .cc]
-fcyclomatic-complexity
ㅤ
컴파일러를 거쳐 생성된 main.s 파일은 아래처럼 ARM Assembly 코드이다.
// main.s
.L3:
.loc 1 115 16
bl read_user_button
str r0, [r7, #8]
.loc 1 117 6
ldr r2, [r7, #8]
ldr r3, [r7, #12]
cmp r2, r3
beq .L2
.loc 1 117 30 discriminator 1
ldr r3, [r7, #8]
cmp r3, #1
bne .L2
.loc 1 119 5
movs r1, #128
ldr r0, .L4+8
bl HAL_GPIO_TogglePin
.L2:
.loc 1 122 14
ldr r3, [r7, #8]
str r3, [r7, #12]
.loc 1 115 14
b .L3
ㅤ
옵션으로 -fstack-usage, -fcyclomatic-complexity 을 전달하면서 만들어진 main.su , main.cyclo 파일에는 아래와 같은 내용들이 담겨있다. 빌드 과정에서 튀어나온 이 파일들을 보면서 어떤 함수가 현재 스택을 얼마나 차지하는지, 함수가 얼마나 복잡하게 작성되어있는지를 살펴보고 최적화를 시도해볼 수 있겠다. (→ 이걸 수치적으로 제시하면서 내가 어떻게 코드를 효율화시켰는지 말 할 수도 있을 듯!)
// main.su
// 스택 사용량 분석
../Core/Src/main.c:66:5:main 24 static
../Core/Src/main.c:135:6:SystemClock_Config 88 static
../Core/Src/main.c:177:13:MX_GPIO_Init 40 static
../Core/Src/main.c:217:6:Error_Handler 4 static,ignoring_inline_asm
// main.cyclo
// 함수 복잡도 분석
../Core/Src/main.c:66:5:main 3
../Core/Src/main.c:135:6:SystemClock_Config 3
../Core/Src/main.c:177:13:MX_GPIO_Init 1
../Core/Src/main.c:217:6:Error_Handler 1
ㅤ
3. 어셈블러 (Assembler)
어셈블러는 어셈블리 코드(.s)를 입력으로 받아서 기계어(.o)로 변환하는 역할을 수행한다.
ㅤ
- 명령어 인코딩
- 명령어, 레지스터 번호, 숫자를 2진수로 변환
MOVS r2, #32→00100 010 00100000
- 의사 명령어 처리
.text,.data,.word,.global같은 키워드들을 처리- 보통
.으로 시작하는 키워드들이 ‘pseudo 명령어’ 인 듯 하다. 이게 어셈블리 명령어가 아니라 어셈블러가 기계어로 변환을 할 때 필요한 메타데이터들을 생성하거나 설정하는 작업에 활용되는 명령어로 생각된다.
- 심볼 테이블 생성
- 함수명, 변수명, 레이블 등을 주소와 매핑하는 작업
- 재배치 정보 생성
- 컴파일 타임에 주소를 알 수 없는 경우에 링커를 통해 이후에 실제 주소를 채울 수 있도록 정보를 만들어두기
- 다른 파일에 필요한 주소가 있는 경우!
- 섹션 생성
- 메모리를 효율적으로 사용하기 위한 섹션 분리
- ELF 파일 (
.o파일) 생성- ELF : Executable and Linkable File — 지금 단계에서는 link를 위한 파일이라고 봐도 된다!
ㅤ
이 시점에서부터는 완전히 바이너리 파일로 작성된다.

ㅤ
4. 링커 (linker)
링킹은 생성된 여러개의 오브젝트 파일(.o)들과 라이브러리 파일 (.a)들을 모아서 실행 가능한 하나의 단일 파일(.elf) 를 만든 작업을 수행한다. 이때 링커 스크립트 파일 (.ld)를 기반으로 링킹을 수행한다.
ㅤ
링커 스크립트 파일은 다음의 작업을 위한 내용들을 설명한다.
- 메모리 맵 + 섹션을 배치하는 규칙을 담고있다.
- Flash, RAM의 시작 주소와 크기를 정의
- 각 섹션을 어디에 배치할지 정의
- 심볼(내가 생각하는 변수들)에 대해 정의
- 진입점을 지정
ㅤ
링커의 역할
- 심볼 해석 및 재배치
a.o파일의 전역 변수 →b.o파일의 extern 변수와 주소를 매칭시키는 역할. 함수도 동일.- 다른 파일에 있는 내용들에 대해서 “Link” 하는 역할을 수행하는게 이거라고 보면 됨.
- 결국 명령어의 실행은 PC (절대 메모리주소) 기반이니깐.
- 섹션 병합
- 여러
*.o파일에 흩어져있는 .text, .bss, .data 등을 각각 섹션에 맞게 모으기. .text섹션 =main.o의 .text +uart.o의 .text + …
- 여러
- 메모리 배치
- 링커 스크립트 파일의 메모리 정의를 보고 순서대로 메모리 주소에 배치한다. (여기에 시작 위치 + 크기 가 적혀있음)
- .isr_vector → .text → .rodata → .data → …
- 미사용 코드 제거
- 링커는 의존성 분석을 통해 현재 사용되지 않는 (불필요한) 함수나 변수 등은 미리 제거할 수 있다.
.data.A_function이렇게 .data 영역 뒤에 내용이 적혀있는 경우에는 세부 데이터 섹션이라고 생각하면 된다.- 만약 이 섹션이 프로그램의 실행에 있어서 필요하지 않다면 제거해줄 수 있다.
- ELF 파일 생성
- 링킹의 결과로는 실행가능한 파일인 ELF 파일을 생성할 수 있다. ELF 파일에 대해서는 뒤에서 다시 한 번 자세히 다루자!
ㅤ
링킹 명령어는 아래처럼 생겼다. 각 옵션이 어떤 의미를 가지는지 한 번 슬쩍 살펴보자.
arm-none-eabi-gcc -o "TC_1024.elf" @"objects.list" -mcpu=cortex-m4 -T"C:\workspace\stm\TC_1024\STM32F429ZITX_FLASH.ld" --specs=nosys.specs -Wl,-Map="TC_1024.map" -Wl,--gc-sections -static --specs=nano.specs -mfpu=fpv4-sp-d16 -mfloat-abi=hard -mthumb -Wl,--start-group -lc -lm -Wl,--end-group
arm-none-eabi-gcc
*** 입출력 ***
[출력파일은 elf 파일]
-o "TC_1024.elf"
[입력파일은 objects.list 파일에서 따로 관리 (너무 길어지니깐)]
@"objects.list"
-mcpu=cortex-m4
*** 링커 스크립트 파일 지정 ***
-T"C:\workspace\stm\TC_1024\STM32F429ZITX_FLASH.ld"
*** C 라이브버리 스펙 ***
[OS 없는 환경에서의 표준 C 스펙 + 경량 라이브러리 사용]
--specs=nosys.specs
--specs=nano.specs
*** 링커 옵션***
[메모리 배치 정보를 TC_1024.map 파일에 저장]
-Wl,-Map="TC_1024.map"
[미사용 코드는 제거하기 (garbage collection)]
-Wl,--gc-sections
*** 기타 옵션 ***
[정적 링킹 (라이브러리 코드를 elf에 포함시키기 -> dll X)]
-static
[FPU 연산 관련]
-mfpu=fpv4-sp-d16
-mfloat-abi=hard
[Thumb 모드]
-mthumb
[라이브러리 그룹 ... lc : 표준 C 라이브러리 + lm : 수학 라이브러리]
-Wl,--start-group -lc -lm -Wl,--end-group
ㅤ
명령어 중간에 있는 원본 오브젝트 파일들 (.o) 는 objects.list에 담겨있다. 여기에 있는 오브젝트 파일들이 내 실행파일을 위해 실제로 포함되는 파일들이라고 생각하면 되겠다! 오호라!
// objects.list
"./Core/Src/main.o"
"./Core/Src/my_driver.o"
"./Core/Src/stm32f4xx_hal_msp.o"
"./Core/Src/stm32f4xx_it.o"
"./Core/Src/syscalls.o"
"./Core/Src/sysmem.o"
"./Core/Src/system_stm32f4xx.o"
"./Core/Startup/startup_stm32f429zitx.o"
"./Drivers/STM32F4xx_HAL_Driver/Src/stm32f4xx_hal.o"
"./Drivers/STM32F4xx_HAL_Driver/Src/stm32f4xx_hal_cortex.o"
"./Drivers/STM32F4xx_HAL_Driver/Src/stm32f4xx_hal_dma.o"
"./Drivers/STM32F4xx_HAL_Driver/Src/stm32f4xx_hal_dma_ex.o"
"./Drivers/STM32F4xx_HAL_Driver/Src/stm32f4xx_hal_exti.o"
"./Drivers/STM32F4xx_HAL_Driver/Src/stm32f4xx_hal_flash.o"
"./Drivers/STM32F4xx_HAL_Driver/Src/stm32f4xx_hal_flash_ex.o"
"./Drivers/STM32F4xx_HAL_Driver/Src/stm32f4xx_hal_flash_ramfunc.o"
"./Drivers/STM32F4xx_HAL_Driver/Src/stm32f4xx_hal_gpio.o"
"./Drivers/STM32F4xx_HAL_Driver/Src/stm32f4xx_hal_pwr.o"
"./Drivers/STM32F4xx_HAL_Driver/Src/stm32f4xx_hal_pwr_ex.o"
"./Drivers/STM32F4xx_HAL_Driver/Src/stm32f4xx_hal_rcc.o"
"./Drivers/STM32F4xx_HAL_Driver/Src/stm32f4xx_hal_rcc_ex.o"
ㅤ
ㅤ
이때 잠깐, Startup 파일
빌드 과정에서 잘 보면, 빌드 명령어 사이로 갑자기 startup 파일이 띨롱 컴파일되는 것을 볼 수 있다. 요시기가 모시기냐? 간략히 또 한 번 알아보자.

arm-none-eabi-gcc -mcpu=cortex-m4 -DDEBUG -c -x assembler-with-cpp -MMD -MP -MF"Core/Startup/startup_stm32f429zitx.d" -MT"Core/Startup/startup_stm32f429zitx.o" --specs=nano.specs -mfpu=fpv4-sp-d16 -mfloat-abi=hard -mthumb -o "Core/Startup/startup_stm32f429zitx.o" "../Core/Startup/startup_stm32f429zitx.s"
ㅤ
우선, 스타트업 코드는 .c 파일로 작성되어 있는게 아니라 .s 파일로 작성되어있다. Startup 코드는 내가 작성하는 파일에 따라 변경되는 것이 아니라, STM32에서 지정한 기본적인 파일이 들어간다. ← 이것은 Startup이 HW와 의존성을 가지는 코드이기 때문임. 그래서 굳이 c언어, c++언어로 작성되어있지 않고 어셈블리 파일인 .s 로 작성되어있다. (그리고 이게 이 명령어 순서 자체가 의미가 있기 때문에 함부로 건드리지 않는게 좋아보인다.)
ㅤ
스타트업 코드에서는 시스템을 시작하기 위한 대부분의 코드가 모여있다.
- 현 Target System에 대한 정보
- 메모리의 각 영역에 대한 정보 (Linker 로부터 알아낸다)
- 프로그램 (시스템) 시작 시 가장 처음에 시작될 Reset_Handler 함수의 내용
- isr_vector(각종 Handler)의 메모리 상 위치
ㅤ
// startup_stm32f429xx.s
.syntax unified
.cpu cortex-m4
.fpu softvfp
.thumb
.global g_pfnVectors
.global Default_Handler
/* start address for the initialization values of the .data section.
defined in linker script */
.word _sidata
/* start address for the .data section. defined in linker script */
.word _sdata
/* end address for the .data section. defined in linker script */
.word _edata
/* start address for the .bss section. defined in linker script */
.word _sbss
/* end address for the .bss section. defined in linker script */
.word _ebss
/* stack used for SystemInit_ExtMemCtl; always internal RAM used */
.section .text.Reset_Handler
.weak Reset_Handler
.type Reset_Handler, %function
Reset_Handler:
ldr sp, =_estack /* set stack pointer */
/* Call the clock system initialization function.*/
bl SystemInit
/* Copy the data segment initializers from flash to SRAM */
ldr r0, =_sdata
ldr r1, =_edata
ldr r2, =_sidata
movs r3, #0
b LoopCopyDataInit
... 중간생략 ...
/* Call static constructors */
bl __libc_init_array
/* Call the application's entry point.*/
bl main
bx lr
.size Reset_Handler, .-Reset_Handler
.section .text.Default_Handler,"ax",%progbits
Default_Handler:
Infinite_Loop:
b Infinite_Loop
.size Default_Handler, .-Default_Handler
/******************************************************************************
*
* The minimal vector table for a Cortex M3. Note that the proper constructs
* must be placed on this to ensure that it ends up at physical address
* 0x0000.0000.
*
*******************************************************************************/
.section .isr_vector,"a",%progbits
.type g_pfnVectors, %object
g_pfnVectors:
.word _estack
.word Reset_Handler
.word NMI_Handler
.word HardFault_Handler
.word MemManage_Handler
.word BusFault_Handler
.word UsageFault_Handler
.word 0
.word 0
.word 0
.word 0
.word SVC_Handler
.word DebugMon_Handler
.word 0
.word PendSV_Handler
.word SysTick_Handler
/* External Interrupts */
.word WWDG_IRQHandler /* Window WatchDog */
.word PVD_IRQHandler /* PVD through EXTI Line detection */
.word TAMP_STAMP_IRQHandler /* Tamper and TimeStamps through the EXTI line */
.word RTC_WKUP_IRQHandler /* RTC Wakeup through the EXTI line */
.word FLASH_IRQHandler /* FLASH */
... 중간생략 ...
.size g_pfnVectors, .-g_pfnVectors
/*******************************************************************************
*
* Provide weak aliases for each Exception handler to the Default_Handler.
* As they are weak aliases, any function with the same name will override
* this definition.
*
*******************************************************************************/
.weak NMI_Handler
.thumb_set NMI_Handler,Default_Handler
.weak HardFault_Handler
.thumb_set HardFault_Handler,Default_Handler
.weak MemManage_Handler
.thumb_set MemManage_Handler,Default_Handler
.weak BusFault_Handler
.thumb_set BusFault_Handler,Default_Handler
.weak UsageFault_Handler
.thumb_set UsageFault_Handler,Default_Handler
.weak SVC_Handler
.thumb_set SVC_Handler,Default_Handler
.weak DebugMon_Handler
.thumb_set DebugMon_Handler,Default_Handler
.weak PendSV_Handler
.thumb_set PendSV_Handler,Default_Handler
.weak SysTick_Handler
.thumb_set SysTick_Handler,Default_Handler
... 중간생략 ...
ㅤ
Linker Description (~~Flash.ld)파일을 보면, 플래시메모리에 어떻게 데이터를 쌓아나갈지에 대해서 작성이 되어있다. 아래 그림은 Linker Description 파일에 작성된 순서대로 메모리에 섹션들을 배치한 결과.

여기에서 플래시메모리의 가장 낮은 주소에는 .isr_vector 가 담겨있는 것을 볼 수 있다. 그리고 이 내용은 Startup 파일에 작성되어 있으며, .isr_vector 의 가장 앞에는 _estack, 그 다음 줄에는 Reset_Handler의 실제 코드가 어디에 있는지가 담겨있다 (함수 포인터).
ㅤ
Cortex-M4 에서는 시스템이 시작하면 0x0800_0000 의 주소에 있는 내용을 SP, 그 다음 주소인 0x0800_0004의 데이터를 PC에 담아준다. (→ 이건 HW적으로 동작)
ㅤ
0x0800_0004 에는 .text 영역에 있는 Reset_Handler의 코드 주소가 담겨있다. 처음에 시스템을 부팅(리셋)할 때 Reset_Handler의 주소가 PC에 담긴 뒤, 시스템의 초기화를 차례대로 실행하기 시작한다.
ㅤ
Reset_Handler의 Task
Reset_Handler는 처음에 프로그램이 실행될 환경을 만드는 함수라고 생각해도 된다. 보드 위에 있는 검은색 버튼을 눌렀을 때에 호출되는 Interrupt 에 해당(된다고 생각된다!) Reset_Handler는 다음의 역할을 한다.
SP레지스터에 Linker Discription으로부터 가져온_estack값 담아주기- 시스템 초기화 함수 호출
.data섹션을 RAM으로 옮기기.bss섹션을 RAM으로 옮기기 + 0으로 채우기- 힙 영역의 시작을 표시하는
_end값 담아주기 - main 함수 호출
ㅤ
위 FLASH, RAM, CCM을 표현한 그림에서 RAM 메모리를 그림처럼 구축하는 역할을 담당한다. (저런 구조가 되는게 기본적인 컴구조의 프로그램 단위라고 배웠던 것 같은데, 펌웨어에서는 보드 위에 프로그램이 하나만 돌게 되니깐 요런 구조가 시스템의 전체 메모리 구조가 된다.)
ㅤ
SystemInit 함수
Reset_Handler 에 보면 시스템 초기화 함수인 SystemInit 함수를 브랜치로 호출하고 있다. 그런데 타고 넘어가보니, 이 함수는 system_stm32f4xx.c 에서 정의해주고 있다. 심지어 주석에서는 CLK System Initialization 이라고 적혀 있었는데, 실제로 들어와서 확인해보니 하는 일은 Co-processor, External Memory Control, Custom Vector Table Location 에 대한 설정들이였다. (왜 그럼 주석을 CLK system init function이라고 해논거냐?;;) System control block (SCB) 을 이용하고 있기는 하다만… 이 중에서 내 시스템에서는 첫 번째 항목인 FPU와 관련된 활성화가 되어있고, ExtMemTrl, User_Vect_Tab 에 대한 설정은 전처리기에 의해 비활성화 되어있었다.
// system_stm32f4xx.c
void SystemInit(void)
{
/* FPU settings ------------------------------------------------------------*/
#if (__FPU_PRESENT == 1) && (__FPU_USED == 1)
SCB->CPACR |= ((3UL << 10*2)|(3UL << 11*2)); /* set CP10 and CP11 Full Access */
#endif
/* External Memory Control에 대한 설정 -------------------------------------*/
#if defined (DATA_IN_ExtSRAM) || defined (DATA_IN_ExtSDRAM)
SystemInit_ExtMemCtl();
#endif /* DATA_IN_ExtSRAM || DATA_IN_ExtSDRAM */
/* Configure the Vector Table location -------------------------------------*/
#if defined(USER_VECT_TAB_ADDRESS)
SCB->VTOR = VECT_TAB_BASE_ADDRESS | VECT_TAB_OFFSET; /* Vector Table Relocation in Internal SRAM */
#endif /* USER_VECT_TAB_ADDRESS */
}
ㅤ
ㅤ
.elf와 .bin의 차이
ELF 파일
ELF 파일 : Executable and Linkable Format
포함하고 있는 내용
- 실제 코드와 데이터
- 메타데이터
- Debugger, Profiler 등 개발 도구가 읽을 수 있는 형태
┌─────────────────────────────────┐
│ ELF Header (52 bytes) │ ← 파일 타입, 아키텍처, 엔트리 포인트
├─────────────────────────────────┤
│ Program Headers (N×32 bytes) │ ← 로드 정보 (어디에 적재할지)
├─────────────────────────────────┤
│ .isr_vector (0x08000000) │ ← 인터럽트 벡터 테이블
├─────────────────────────────────┤
│ .text (0x08000188) │ ← 실행 코드
├─────────────────────────────────┤
│ .rodata (0x08003000) │ ← 읽기전용 데이터
├─────────────────────────────────┤
│ .data 초기값 (0x08004000) │ ← RAM에 복사될 데이터
├─────────────────────────────────┤
│ .ARM.attributes │ ← 컴파일러 속성
├─────────────────────────────────┤
│ .debug_info │ ← 디버그 정보 (소스 라인, 변수 타입)
├─────────────────────────────────┤
│ .debug_line │ ← 소스 라인 번호 매핑
├─────────────────────────────────┤
│ .debug_frame │ ← 스택 프레임 정보
├─────────────────────────────────┤
│ .symtab │ ← 심볼 테이블 (함수명, 변수명, 주소)
├─────────────────────────────────┤
│ .strtab │ ← 문자열 테이블 (심볼 이름들)
├─────────────────────────────────┤
│ Section Headers (N×40 bytes) │ ← 각 섹션의 메타데이터
└─────────────────────────────────┘
ㅤ
다양한 디버깅용 심볼과 테이블, 메타데이터를 포함하고 있어서 elf파일의 용량은 000 KB ~ 0 MB 까지 커질 수 있다.
ㅤ
elf 파일에는 각 명령어가 원래 어떤 명령어와 매핑되는지에 대한 정보를 모두 가지고 있기 때문에, Fault가 발생했을 때 소스 코드의 어떤 파일의 몇 번째 줄에서 문제가 발생했는지를 쉽게 찾을 수 있다. 또 GDB가 브레이크 포인트를 걸거나 변수에 대한 값 읽기 (expression) 등을 처리할 수 있다.
ㅤ
그렇다고 elf 파일로 실행을 할 때 모든 elf 파일이 MCU 위에 올라가는 것은 아니다. MCU의 Flash 메모리에 bin 영역의 데이터만 저장된다. 메타 데이터는 HOST(컴퓨터)쪽에 남아있고, 이걸로 디버깅을 할 때 사용한다.
ㅤ
BIN 파일
BIN 파일 : Binary (bin파일 / hex파일)
포함하고 있는 내용
- 순수 바이너리 데이터 파일
- MCU의 Flash 메모리에 복사될 원시 그 자체 형태의 데이터
ㅤ
앞서 봤던 그림에서 FLASH 메모리에 올라가는 형태가 요것!
ㅤ
bin 파일은 원래 소스코드로 복원하기 매우 어렵기 때문에, 특정 명령어에서 문제가 발생했을 때 소스코드의 어떤 파일의 몇 번째 줄에서 문제가 발생했는지 찾기 쉽지않다. (→ 디버깅에는 용이하지 않음) 대신 파일의 용량이 elf 파일과 비교해 매우 가볍기 때문에 실제 제품에 들어갈 코드는 bin으로 사용한다.
ㅤ
'Embedded System > MCU' 카테고리의 다른 글
| [MCU] UART 구조와 이해 (0) | 2025.10.30 |
|---|---|
| [MCU] GPIO 실습 : USER Button과 LED 켜기 (0) | 2025.10.28 |
| [MCU] GPIO의 하드웨어 구조와 데이터시트, 침침한 눈을 곁들인 (1) | 2025.10.24 |
| [STM32] HAL 드라이버란? (0) | 2025.10.19 |
| [STM32] STM32에서 DHT11 로 온습도 측정하기 (0) | 2025.09.29 |