Bare metal C Ch04 링커스크립트 스타트업 - Hongjayeong-02/Basic_SoC_implementation GitHub Wiki
목표
STM32F439ZI가 Reset된 직후main()까지 어떻게 도달하는지 이해하고,
Linker Script + Startup Assembly + Vector Table +.data/.bss초기화를 직접 구성한다.
자료 기준
이 페이지는 Notion Ch04 정리와 첨부 ZIP의 실제Ch04_linker_startup/프로젝트 파일을 함께 기준으로 작성했다.
C 코드에서:
int main(void)
{
...
}
만 작성해도 프로그램이 바로 main()에서 시작하는 것처럼 보인다.
하지만 실제 Cortex-M4의 Reset 흐름은 다음과 같다.
Power On / Reset
│
▼
Vector Table[0]
→ Initial MSP
│
▼
Vector Table[1]
→ Reset_Handler
│
▼
.data : FLASH → RAM copy
│
▼
.bss : RAM 영역을 0으로 초기화
│
▼
SystemInit()
│
▼
C runtime initialization
│
▼
main()
이 과정에서:
Linker Script는 코드/데이터를 어느 메모리에 배치할지 결정하고
Startup File은 Reset 직후 실제 초기화 코드를 수행한다.
Notion 정리에서 사용한 메모리 구성:
Memory | 시작 주소 | 크기 | 용도 -- | -- | -- | -- Flash | 0x0800_0000 | 2048 KB | Program / constant / initial data SRAM | 0x2000_0000 | 128 KB | Runtime data / stack / heap CCM RAM | 0x1000_0000 | 64 KB | Core-coupled RAM, 이 실습에서는 미사용원서의 STM32F411과 비교하면 F439ZI는 Flash와 interrupt source가 더 많기 때문에 원서 파일을 그대로 사용할 수 없고 target에 맞는 memory definition과 vector table이 필요하다.
Program code가 들어간다.
int main(void)
같은 함수의 machine code가 Flash에 배치된다.
초기값이 있는 전역/정적 변수.
unsigned int g_blink_count = 5;
실행 중에는 RAM에 있어야 하지만 초기값 5는 firmware image에 저장되어야 한다.
따라서:
Flash
│ initial value
▼
.data load image
│ Reset_Handler가 copy
▼
RAM
초기값이 없는 전역/정적 변수.
unsigned int g_toggle_flag;
C 규칙상 시작 시 0이어야 하므로 Flash에 0을 저장할 필요 없이 Reset_Handler가 RAM 영역을 0으로 채운다.
.data를 이해할 때 중요한 개념이다.
LMA = Load Memory Address
→ Firmware image에서 초기값이 저장된 위치 (Flash)
VMA = Runtime/Virtual Memory Address
→ 프로그램 실행 중 실제 변수가 존재하는 위치 (RAM)
Linker Script에서:
.data :
{
...
} >RAM AT> FLASH
이라고 정의하면:
Load : Flash
Run : RAM
이라는 의미가 된다.
첨부 ZIP의 실제 파일:
Ch04_linker_startup/Src/main.c
/*
- Ch04 - Developing the Linker Script and Startup File
-
- 목적: 직접 작성한 stm32f439zi.ld / startup_stm32f439zi.s 가 실제로
-
.data / .bss 를 올바르게 초기화하는지 검증한다.
-
-
- g_blink_count : 초기값이 있는 전역변수 -> .data 섹션
-
(FLASH에 초기값 5가 저장되고, Reset_Handler가 RAM으로 복사)
-
- g_toggle_flag : 초기값이 없는 전역변수 -> .bss 섹션
-
(Reset_Handler가 0으로 채움. 만약 startup이 잘못됐다면
-
이 값이 쓰레기값이라 동작이 이상해진다)
*/
#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)
/* .data 섹션 검증용 (초기값 5) /
unsigned int g_blink_count = 5;
/ .bss 섹션 검증용 (초기값 없음 -> 0이어야 정상) */
unsigned int g_toggle_flag;
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));
/* g_toggle_flag가 .bss에서 제대로 0으로 초기화됐다면 이 if는 실행되지 않는다 */
if (g_toggle_flag != 0)
{
/* startup 파일의 .bss 초기화가 잘못된 경우: LED를 아주 빠르게 깜빡여
* 눈으로 바로 이상 상태를 알 수 있게 한다 */
while (1)
{
GPIOB_ODR ^= LD1_PIN;
delay(50000);
}
}
while (1)
{
for (unsigned int i = 0; i < g_blink_count; i++)
{
GPIOB_ODR ^= LD1_PIN;
delay(800000);
}
delay(3200000); /* g_blink_count(=5)번 깜빡이고 잠깐 쉼 */
g_toggle_flag++;
}
}
이 코드에서 중요한 것은 LED 자체가 아니라 두 변수다.
unsigned int g_blink_count = 5;
unsigned int g_toggle_flag;
초기값 있음
→ .data
→ Flash에 5 저장
→ Reset_Handler가 RAM으로 copy
초기값 없음
→ .bss
→ Reset_Handler가 0으로 초기화
따라서 이 두 변수가 정상적으로 동작하면 Linker Script와 Startup의 초기화 흐름을 확인할 수 있다.
첨부 ZIP:
Ch04_linker_startup/STM32F439ZITX_FLASH.ld
/*
- stm32f439zi.ld
-
- Ch04에서 직접 작성한 NUCLEO-F439ZI(STM32F439ZIT6)용 링커 스크립트.
- Ch05부터 이 저장소의 모든 챕터가 공통으로 이 파일을 사용한다.
-
- 메모리 맵 (RM0090 Reference manual 기준):
- FLASH : 0x0800 0000, 2048 KB (2 MB)
- RAM : 0x2000 0000, 128 KB (SRAM1 112KB + SRAM2 16KB, 연속)
- CCM : 0x1000 0000, 64 KB (Core Coupled Memory, DMA 접근 불가 -> 미사용)
- 백업 SRAM : 0x4002 4000, 4KB (RTC 챕터(Ch15)에서 별도로 다룸, 여기선 미사용)
*/
ENTRY(Reset_Handler)
/* 스택/힙 크기는 필요에 따라 각 챕터의 Makefile에서 -Wl,--defsym 로 덮어쓸 수 있다 */
_Min_Heap_Size = 0x200;
_Min_Stack_Size = 0x800;
MEMORY
{
FLASH (rx) : ORIGIN = 0x08000000, LENGTH = 2048K
RAM (rwx) : ORIGIN = 0x20000000, LENGTH = 128K
}
/* 스택은 RAM의 맨 꼭대기에서 아래로 자란다 (Full Descending Stack) */
_estack = ORIGIN(RAM) + LENGTH(RAM);
SECTIONS
{
/* 벡터 테이블은 반드시 FLASH 맨 앞(0x08000000)에 위치해야 한다.
- 리셋 후 코어가 이 주소에서 초기 SP/PC를 읽어오기 때문 /
.isr_vector :
{
. = ALIGN(4);
KEEP((.isr_vector))
. = ALIGN(4);
} >FLASH
/* 프로그램 코드 + 상수(rodata) */
.text :
{
. = ALIGN(4);
*(.text)
(.text)
*(.glue_7)
*(.glue_7t)
*(.eh_frame)
KEEP (*(.init))
KEEP (*(.fini))
. = ALIGN(4);
_etext = .;
} >FLASH
.rodata :
{
. = ALIGN(4);
*(.rodata)
(.rodata)
. = ALIGN(4);
} >FLASH
/* C++ 정적 생성자용 (사용 안 해도 표준 스크립트 관례상 유지) /
.ARM.extab : { (.ARM.extab .gnu.linkonce.armextab.) } >FLASH
.ARM : {
__exidx_start = .;
(.ARM.exidx)
__exidx_end = .;
} >FLASH
.preinit_array :
{
PROVIDE_HIDDEN (__preinit_array_start = .);
KEEP ((.preinit_array))
PROVIDE_HIDDEN (__preinit_array_end = .);
} >FLASH
.init_array :
{
PROVIDE_HIDDEN (__init_array_start = .);
KEEP ((SORT(.init_array.)))
KEEP ((.init_array))
PROVIDE_HIDDEN (__init_array_end = .);
} >FLASH
.fini_array :
{
PROVIDE_HIDDEN (__fini_array_start = .);
KEEP ((SORT(.fini_array.)))
KEEP ((.fini_array))
PROVIDE_HIDDEN (__fini_array_end = .);
} >FLASH
/* FLASH에서의 .data 적재 주소 (Reset_Handler가 이 주소에서 RAM으로 복사) */
_sidata = LOADADDR(.data);
.data :
{
. = ALIGN(4);
_sdata = .;
*(.data)
(.data)
. = ALIGN(4);
_edata = .;
} >RAM AT> FLASH
.bss :
{
. = ALIGN(4);
_sbss = .;
bss_start = _sbss;
*(.bss)
(.bss)
*(COMMON)
. = ALIGN(4);
_ebss = .;
bss_end = _ebss;
} >RAM
/* 힙/스택을 위한 최소 공간 확보 (부족하면 링크 단계에서 에러) */
._user_heap_stack :
{
. = ALIGN(8);
PROVIDE ( end = . );
PROVIDE ( _end = . );
. = . + _Min_Heap_Size;
. = . + _Min_Stack_Size;
. = ALIGN(8);
} >RAM
/* STM32CubeIDE 기본 링크는 newlib-nano(libc/libm/libgcc)를 그대로 쓰므로
- (syscalls.c/sysmem.c가 이걸 필요로 함) 여기서는 /DISCARD/ 하지 않는다.
- (참고: 이 저장소의 arm-none-eabi-gcc -nostdlib 단독 빌드용 버전에서는
- 이 라이브러리들을 의도적으로 제외했었다.) */
.ARM.attributes 0 : { (.ARM.attributes) }
}
ENTRY(Reset_Handler)
ELF의 진입 symbol을 Reset_Handler로 지정한다.
MEMORY
{
FLASH (rx) : ORIGIN = 0x08000000, LENGTH = 2048K
RAM (rwx) : ORIGIN = 0x20000000, LENGTH = 128K
}
여기서 Linker에게:
FLASH는 여기부터 2MB
RAM은 여기부터 128KB
라고 알려준다.
_estack = ORIGIN(RAM) + LENGTH(RAM);
따라서:
0x20000000 + 128KB
= 0x20020000
이 주소가 초기 stack pointer 값이 된다.
.isr_vector :
{
KEEP((.isr_vector))
} >FLASH
Vector Table은 Flash의 앞쪽에 배치해야 한다.
KEEP()은 linker garbage collection 과정에서 vector table이 사용되지 않는 section으로 판단되어 제거되지 않도록 보호한다.
_sidata = LOADADDR(.data);
.data :
{
_sdata = .;
*(.data)
(.data)
_edata = .;
} >RAM AT> FLASH
Startup code는 이 symbol들을 이용한다.
_sidata : Flash의 .data 초기값 시작
_sdata : RAM의 .data 시작
_edata : RAM의 .data 끝
.bss :
{
_sbss = .;
*(.bss)
(.bss)
(COMMON)
_ebss = .;
} >RAM
Startup code가 _sbss ~ _ebss를 0으로 채운다.
첨부 ZIP:
Ch04_linker_startup/Startup/startup_stm32f439zitx.s
/
- startup_stm32f439zi.s
-
- Ch04에서 직접 작성한 STM32F439ZI(NUCLEO-F439ZI)용 스타트업 파일.
- Ch05부터 이 저장소의 모든 챕터가 공통으로 이 파일을 사용한다.
-
- 하는 일:
-
- 벡터 테이블(g_pfnVectors)을 .isr_vector 섹션에 배치
-
- 0번: 초기 MSP(Main Stack Pointer) 값 (_estack, 링커스크립트가 정의)
-
- 1번: Reset_Handler 주소
-
- 이후: 시스템 예외 + STM32F439의 IRQ 90개 (RM0090 Table 62 순서)
-
- Reset_Handler:
-
- .data 섹션을 FLASH -> RAM으로 복사 (초기값 있는 전역변수)
-
- .bss 섹션을 0으로 채움 (초기값 없는 전역변수)
-
- SystemInit() 호출 (Ch06 CMSIS 이후부터 실제 클럭 설정을 함;
-
그 전까지는 약한 심볼로 비워둠)
-
- main() 호출
-
- 각 인터럽트 핸들러는 전부 Default_Handler를 가리키는 weak 심볼로
-
정의해서, 나중 챕터(Ch07 EXTI, Ch09 TIM, Ch10 USART, ...)에서
-
필요한 것만 같은 이름의 함수로 "재정의"하면 자동으로 연결되게 한다.
*/
.syntax unified
.cpu cortex-m4
.fpu softvfp
.thumb
.global Reset_Handler
.global g_pfnVectors
.global Default_Handler
.word _sidata
.word _sdata
.word _edata
.word _sbss
.word _ebss
.word _estack
/* ============================= Reset_Handler ============================= /
.section .text.Reset_Handler
.weak Reset_Handler
.type Reset_Handler, %function
Reset_Handler:
ldr sp, =_estack / 1: 스택 포인터 초기화 (링커스크립트의 _estack) */
/* 2: .data 섹션 복사 (FLASH의 초기값 -> RAM) */
movs r1, #0
b LoopCopyDataInit
CopyDataInit:
ldr r3, =_sidata
ldr r3, [r3, r1]
str r3, [r0, r1]
adds r1, r1, #4
LoopCopyDataInit:
ldr r0, =_sdata
ldr r3, =_edata
adds r2, r0, r1
cmp r2, r3
bcc CopyDataInit
/* 3: .bss 섹션 0으로 초기화
* (r2/r4는 루프 진입 전에 "한 번만" 설정 — 루프 안에서 다시 로드하면
* r2가 매번 _sbss로 리셋되어 영원히 끝나지 않는 버그가 됨) */
ldr r2, =_sbss
ldr r4, =_ebss
movs r3, #0
b LoopFillZerobss
FillZerobss:
str r3, [r2]
adds r2, r2, #4
LoopFillZerobss:
cmp r2, r4
bcc FillZerobss
/* 4: 시스템 초기화 (CMSIS system_stm32f4xx.c, Ch06 이후 실제 동작) */
bl SystemInit
/* 5: C 표준 라이브러리 초기화 후 main 진입
* (STM32CubeIDE 기본 링크 방식은 -nostdlib을 쓰지 않고 newlib-nano를
* 그대로 링크하므로, crt0/crti/crtn이 존재해 __libc_init_array 호출이
* 안전하다. CubeIDE가 자동 생성하는 스타트업 파일과 동일한 관례.) */
bl __libc_init_array
bl main
LoopForever:
b LoopForever
.size Reset_Handler, .-Reset_Handler
/* ============================= Default_Handler ============================= */
.section .text.Default_Handler,"ax",%progbits
Default_Handler:
Infinite_Loop:
b Infinite_Loop
.size Default_Handler, .-Default_Handler
/*
- SystemInit()은 CMSIS(Ch06)에서 실제로 제공된다. 그 전(Ch04~05)까지는
- 링크 에러가 나지 않도록 weak 심볼로 빈 구현을 하나 넣어둔다.
*/
.weak SystemInit
.type SystemInit, %function
SystemInit:
bx lr
.size SystemInit, .-SystemInit
/* ============================= Vector Table ============================= */
.section .isr_vector,"a",%progbits
.type g_pfnVectors, %object
.size g_pfnVectors, .-g_pfnVectors
g_pfnVectors:
.word _estack /* 0 : 초기 스택 포인터 /
.word Reset_Handler / 1 : Reset /
.word NMI_Handler / 2 : NMI /
.word HardFault_Handler / 3 : Hard Fault /
.word MemManage_Handler / 4 : MPU Fault /
.word BusFault_Handler / 5 : Bus Fault /
.word UsageFault_Handler / 6 : Usage Fault /
.word 0 / 7 : Reserved /
.word 0 / 8 : Reserved /
.word 0 / 9 : Reserved /
.word 0 / 10 : Reserved /
.word SVC_Handler / 11 : SVCall /
.word DebugMon_Handler / 12 : Debug Monitor /
.word 0 / 13 : Reserved /
.word PendSV_Handler / 14 : PendSV /
.word SysTick_Handler / 15 : SysTick */
/* ---- STM32F439xx device interrupts (RM0090 Table 62) ---- */
.word WWDG_IRQHandler /* 0 */
.word PVD_IRQHandler /* 1 */
.word TAMP_STAMP_IRQHandler /* 2 */
.word RTC_WKUP_IRQHandler /* 3 */
.word FLASH_IRQHandler /* 4 */
.word RCC_IRQHandler /* 5 */
.word EXTI0_IRQHandler /* 6 */
.word EXTI1_IRQHandler /* 7 */
.word EXTI2_IRQHandler /* 8 */
.word EXTI3_IRQHandler /* 9 */
.word EXTI4_IRQHandler /* 10 */
.word DMA1_Stream0_IRQHandler /* 11 */
.word DMA1_Stream1_IRQHandler /* 12 */
.word DMA1_Stream2_IRQHandler /* 13 */
.word DMA1_Stream3_IRQHandler /* 14 */
.word DMA1_Stream4_IRQHandler /* 15 */
.word DMA1_Stream5_IRQHandler /* 16 */
.word DMA1_Stream6_IRQHandler /* 17 */
.word ADC_IRQHandler /* 18 */
.word CAN1_TX_IRQHandler /* 19 */
.word CAN1_RX0_IRQHandler /* 20 */
.word CAN1_RX1_IRQHandler /* 21 */
.word CAN1_SCE_IRQHandler /* 22 */
.word EXTI9_5_IRQHandler /* 23 */
.word TIM1_BRK_TIM9_IRQHandler /* 24 */
.word TIM1_UP_TIM10_IRQHandler /* 25 */
.word TIM1_TRG_COM_TIM11_IRQHandler /* 26 */
.word TIM1_CC_IRQHandler /* 27 */
.word TIM2_IRQHandler /* 28 */
.word TIM3_IRQHandler /* 29 */
.word TIM4_IRQHandler /* 30 */
.word I2C1_EV_IRQHandler /* 31 */
.word I2C1_ER_IRQHandler /* 32 */
.word I2C2_EV_IRQHandler /* 33 */
.word I2C2_ER_IRQHandler /* 34 */
.word SPI1_IRQHandler /* 35 */
.word SPI2_IRQHandler /* 36 */
.word USART1_IRQHandler /* 37 */
.word USART2_IRQHandler /* 38 */
.word USART3_IRQHandler /* 39 */
.word EXTI15_10_IRQHandler /* 40 */
.word RTC_Alarm_IRQHandler /* 41 */
.word OTG_FS_WKUP_IRQHandler /* 42 */
.word TIM8_BRK_TIM12_IRQHandler /* 43 */
.word TIM8_UP_TIM13_IRQHandler /* 44 */
.word TIM8_TRG_COM_TIM14_IRQHandler /* 45 */
.word TIM8_CC_IRQHandler /* 46 */
.word DMA1_Stream7_IRQHandler /* 47 */
.word FMC_IRQHandler /* 48 */
.word SDIO_IRQHandler /* 49 */
.word TIM5_IRQHandler /* 50 */
.word SPI3_IRQHandler /* 51 */
.word UART4_IRQHandler /* 52 */
.word UART5_IRQHandler /* 53 */
.word TIM6_DAC_IRQHandler /* 54 */
.word TIM7_IRQHandler /* 55 */
.word DMA2_Stream0_IRQHandler /* 56 */
.word DMA2_Stream1_IRQHandler /* 57 */
.word DMA2_Stream2_IRQHandler /* 58 */
.word DMA2_Stream3_IRQHandler /* 59 */
.word DMA2_Stream4_IRQHandler /* 60 */
.word ETH_IRQHandler /* 61 */
.word ETH_WKUP_IRQHandler /* 62 */
.word CAN2_TX_IRQHandler /* 63 */
.word CAN2_RX0_IRQHandler /* 64 */
.word CAN2_RX1_IRQHandler /* 65 */
.word CAN2_SCE_IRQHandler /* 66 */
.word OTG_FS_IRQHandler /* 67 */
.word DMA2_Stream5_IRQHandler /* 68 */
.word DMA2_Stream6_IRQHandler /* 69 */
.word DMA2_Stream7_IRQHandler /* 70 */
.word USART6_IRQHandler /* 71 */
.word I2C3_EV_IRQHandler /* 72 */
.word I2C3_ER_IRQHandler /* 73 */
.word OTG_HS_EP1_OUT_IRQHandler /* 74 */
.word OTG_HS_EP1_IN_IRQHandler /* 75 */
.word OTG_HS_WKUP_IRQHandler /* 76 */
.word OTG_HS_IRQHandler /* 77 */
.word DCMI_IRQHandler /* 78 */
.word CRYP_IRQHandler /* 79 */
.word HASH_RNG_IRQHandler /* 80 */
.word FPU_IRQHandler /* 81 */
.word UART7_IRQHandler /* 82 */
.word UART8_IRQHandler /* 83 */
.word SPI4_IRQHandler /* 84 */
.word SPI5_IRQHandler /* 85 */
.word SPI6_IRQHandler /* 86 */
.word SAI1_IRQHandler /* 87 */
.word LTDC_IRQHandler /* 88 */
.word LTDC_ER_IRQHandler /* 89 */
.word DMA2D_IRQHandler /* 90 */
/* ============================= weak alias 정의 ============================= */
.macro DEFAULT_HANDLER name
.weak \name
.thumb_set \name, Default_Handler
.endm
DEFAULT_HANDLER NMI_Handler
DEFAULT_HANDLER HardFault_Handler
DEFAULT_HANDLER MemManage_Handler
DEFAULT_HANDLER BusFault_Handler
DEFAULT_HANDLER UsageFault_Handler
DEFAULT_HANDLER SVC_Handler
DEFAULT_HANDLER DebugMon_Handler
DEFAULT_HANDLER PendSV_Handler
DEFAULT_HANDLER SysTick_Handler
DEFAULT_HANDLER WWDG_IRQHandler
DEFAULT_HANDLER PVD_IRQHandler
DEFAULT_HANDLER TAMP_STAMP_IRQHandler
DEFAULT_HANDLER RTC_WKUP_IRQHandler
DEFAULT_HANDLER FLASH_IRQHandler
DEFAULT_HANDLER RCC_IRQHandler
DEFAULT_HANDLER EXTI0_IRQHandler
DEFAULT_HANDLER EXTI1_IRQHandler
DEFAULT_HANDLER EXTI2_IRQHandler
DEFAULT_HANDLER EXTI3_IRQHandler
DEFAULT_HANDLER EXTI4_IRQHandler
DEFAULT_HANDLER DMA1_Stream0_IRQHandler
DEFAULT_HANDLER DMA1_Stream1_IRQHandler
DEFAULT_HANDLER DMA1_Stream2_IRQHandler
DEFAULT_HANDLER DMA1_Stream3_IRQHandler
DEFAULT_HANDLER DMA1_Stream4_IRQHandler
DEFAULT_HANDLER DMA1_Stream5_IRQHandler
DEFAULT_HANDLER DMA1_Stream6_IRQHandler
DEFAULT_HANDLER ADC_IRQHandler
DEFAULT_HANDLER CAN1_TX_IRQHandler
DEFAULT_HANDLER CAN1_RX0_IRQHandler
DEFAULT_HANDLER CAN1_RX1_IRQHandler
DEFAULT_HANDLER CAN1_SCE_IRQHandler
DEFAULT_HANDLER EXTI9_5_IRQHandler
DEFAULT_HANDLER TIM1_BRK_TIM9_IRQHandler
DEFAULT_HANDLER TIM1_UP_TIM10_IRQHandler
DEFAULT_HANDLER TIM1_TRG_COM_TIM11_IRQHandler
DEFAULT_HANDLER TIM1_CC_IRQHandler
DEFAULT_HANDLER TIM2_IRQHandler
DEFAULT_HANDLER TIM3_IRQHandler
DEFAULT_HANDLER TIM4_IRQHandler
DEFAULT_HANDLER I2C1_EV_IRQHandler
DEFAULT_HANDLER I2C1_ER_IRQHandler
DEFAULT_HANDLER I2C2_EV_IRQHandler
DEFAULT_HANDLER I2C2_ER_IRQHandler
DEFAULT_HANDLER SPI1_IRQHandler
DEFAULT_HANDLER SPI2_IRQHandler
DEFAULT_HANDLER USART1_IRQHandler
DEFAULT_HANDLER USART2_IRQHandler
DEFAULT_HANDLER USART3_IRQHandler
DEFAULT_HANDLER EXTI15_10_IRQHandler
DEFAULT_HANDLER RTC_Alarm_IRQHandler
DEFAULT_HANDLER OTG_FS_WKUP_IRQHandler
DEFAULT_HANDLER TIM8_BRK_TIM12_IRQHandler
DEFAULT_HANDLER TIM8_UP_TIM13_IRQHandler
DEFAULT_HANDLER TIM8_TRG_COM_TIM14_IRQHandler
DEFAULT_HANDLER TIM8_CC_IRQHandler
DEFAULT_HANDLER DMA1_Stream7_IRQHandler
DEFAULT_HANDLER FMC_IRQHandler
DEFAULT_HANDLER SDIO_IRQHandler
DEFAULT_HANDLER TIM5_IRQHandler
DEFAULT_HANDLER SPI3_IRQHandler
DEFAULT_HANDLER UART4_IRQHandler
DEFAULT_HANDLER UART5_IRQHandler
DEFAULT_HANDLER TIM6_DAC_IRQHandler
DEFAULT_HANDLER TIM7_IRQHandler
DEFAULT_HANDLER DMA2_Stream0_IRQHandler
DEFAULT_HANDLER DMA2_Stream1_IRQHandler
DEFAULT_HANDLER DMA2_Stream2_IRQHandler
DEFAULT_HANDLER DMA2_Stream3_IRQHandler
DEFAULT_HANDLER DMA2_Stream4_IRQHandler
DEFAULT_HANDLER ETH_IRQHandler
DEFAULT_HANDLER ETH_WKUP_IRQHandler
DEFAULT_HANDLER CAN2_TX_IRQHandler
DEFAULT_HANDLER CAN2_RX0_IRQHandler
DEFAULT_HANDLER CAN2_RX1_IRQHandler
DEFAULT_HANDLER CAN2_SCE_IRQHandler
DEFAULT_HANDLER OTG_FS_IRQHandler
DEFAULT_HANDLER DMA2_Stream5_IRQHandler
DEFAULT_HANDLER DMA2_Stream6_IRQHandler
DEFAULT_HANDLER DMA2_Stream7_IRQHandler
DEFAULT_HANDLER USART6_IRQHandler
DEFAULT_HANDLER I2C3_EV_IRQHandler
DEFAULT_HANDLER I2C3_ER_IRQHandler
DEFAULT_HANDLER OTG_HS_EP1_OUT_IRQHandler
DEFAULT_HANDLER OTG_HS_EP1_IN_IRQHandler
DEFAULT_HANDLER OTG_HS_WKUP_IRQHandler
DEFAULT_HANDLER OTG_HS_IRQHandler
DEFAULT_HANDLER DCMI_IRQHandler
DEFAULT_HANDLER CRYP_IRQHandler
DEFAULT_HANDLER HASH_RNG_IRQHandler
DEFAULT_HANDLER FPU_IRQHandler
DEFAULT_HANDLER UART7_IRQHandler
DEFAULT_HANDLER UART8_IRQHandler
DEFAULT_HANDLER SPI4_IRQHandler
DEFAULT_HANDLER SPI5_IRQHandler
DEFAULT_HANDLER SPI6_IRQHandler
DEFAULT_HANDLER SAI1_IRQHandler
DEFAULT_HANDLER LTDC_IRQHandler
DEFAULT_HANDLER LTDC_ER_IRQHandler
DEFAULT_HANDLER DMA2D_IRQHandler
실제 startup 파일의 핵심 흐름:
Reset_Handler
│
├─ SP = _estack
│
├─ _sidata → _sdata ... _edata copy
│
├─ _sbss ... _ebss = 0
│
├─ SystemInit()
│
├─ __libc_init_array()
│
└─ main()
ldr sp, =_estack
Flash의 initial value를 RAM으로 복사한다.
movs r3, #0
을 이용해서 .bss 영역을 0으로 채운다.
Startup file에는:
g_pfnVectors:
.word _estack
.word Reset_Handler
.word NMI_Handler
.word HardFault_Handler
...
형태로 vector table이 정의되어 있다.
가장 중요한 첫 두 entry:
Vector[0] = 초기 Main Stack Pointer
Vector[1] = Reset_Handler 주소
Cortex-M core는 Reset 시 이 두 값을 하드웨어적으로 사용한다.
Startup file의:
.macro DEFAULT_HANDLER name
.weak \name
.thumb_set \name, Default_Handler
.endm
구조를 사용해서 모든 interrupt handler를 기본적으로 Default_Handler에 연결한다.
나중에 C 코드에서:
void USART3_IRQHandler(void)
{
...
}
처럼 같은 이름의 strong symbol을 정의하면 Linker가 weak symbol 대신 사용자 함수를 선택한다.
이 구조 덕분에 startup vector table을 매번 수정하지 않아도 된다.
첨부 ZIP에 함께 포함된 실제 파일이다.
이 파일은 Ch04 핵심 학습 대상이라기보다는 newlib C library와 MCU 환경을 연결하기 위해 STM32CubeIDE가 생성한 support code이다.
/**
- @file syscalls.c
- @author Auto-generated by STM32CubeIDE
- @brief STM32CubeIDE Minimal System calls file
-
-
For more information about which c-functions
-
need which of these lowlevel functions
-
please consult the Newlib libc-manual
- @attention
-
- Copyright (c) 2020-2025 STMicroelectronics.
- All rights reserved.
-
- This software is licensed under terms that can be found in the LICENSE file
- in the root directory of this software component.
- If no LICENSE file comes with this software, it is provided AS-IS.
-
*/
/* Includes */
#include <sys/stat.h>
#include <stdlib.h>
#include <errno.h>
#include <stdio.h>
#include <signal.h>
#include <time.h>
#include <sys/time.h>
#include <sys/times.h>
/* Variables */
extern int __io_putchar(int ch) attribute((weak));
extern int __io_getchar(void) attribute((weak));
char *__env[1] = { 0 };
char **environ = __env;
/* Functions */
void initialise_monitor_handles()
{
}
int _getpid(void)
{
return 1;
}
int _kill(int pid, int sig)
{
(void)pid;
(void)sig;
errno = EINVAL;
return -1;
}
void _exit (int status)
{
_kill(status, -1);
while (1) {} /* Make sure we hang here */
}
attribute((weak)) int _read(int file, char *ptr, int len)
{
(void)file;
int DataIdx;
for (DataIdx = 0; DataIdx < len; DataIdx++)
{
*ptr++ = __io_getchar();
}
return len;
}
attribute((weak)) int _write(int file, char *ptr, int len)
{
(void)file;
int DataIdx;
for (DataIdx = 0; DataIdx < len; DataIdx++)
{
__io_putchar(*ptr++);
}
return len;
}
int _close(int file)
{
(void)file;
return -1;
}
int _fstat(int file, struct stat *st)
{
(void)file;
st->st_mode = S_IFCHR;
return 0;
}
int _isatty(int file)
{
(void)file;
return 1;
}
int _lseek(int file, int ptr, int dir)
{
(void)file;
(void)ptr;
(void)dir;
return 0;
}
int _open(char path, int flags, ...)
{
(void)path;
(void)flags;
/ Pretend like we always fail */
return -1;
}
int _wait(int *status)
{
(void)status;
errno = ECHILD;
return -1;
}
int _unlink(char *name)
{
(void)name;
errno = ENOENT;
return -1;
}
int _times(struct tms *buf)
{
(void)buf;
return -1;
}
int _stat(char *file, struct stat *st)
{
(void)file;
st->st_mode = S_IFCHR;
return 0;
}
int _link(char *old, char *new)
{
(void)old;
(void)new;
errno = EMLINK;
return -1;
}
int _fork(void)
{
errno = EAGAIN;
return -1;
}
int _execve(char *name, char **argv, char env)
{
(void)name;
(void)argv;
(void)env;
errno = ENOMEM;
return -1;
}
/
- @file sysmem.c
- @author Generated by STM32CubeIDE
- @brief STM32CubeIDE System Memory calls file
-
-
For more information about which C functions
-
need which of these lowlevel functions
-
please consult the newlib libc manual
- @attention
-
- Copyright (c) 2025 STMicroelectronics.
- All rights reserved.
-
- This software is licensed under terms that can be found in the LICENSE file
- in the root directory of this software component.
- If no LICENSE file comes with this software, it is provided AS-IS.
-
*/
/* Includes */
#include <errno.h>
#include <stdint.h>
/**
- Pointer to the current high watermark of the heap usage
*/
static uint8_t *__sbrk_heap_end = NULL;
/**
- @brief _sbrk() allocates memory to the newlib heap and is used by malloc
-
and others from the C library
-
- @verbatim
- ############################################################################
-
.data # .bss # newlib heap # MSP stack
-
# # # Reserved by _Min_Stack_Size
- ############################################################################
- ^-- RAM start ^-- _end _estack, RAM end --^
- @endverbatim
-
- This implementation starts allocating at the '_end' linker symbol
- The '_Min_Stack_Size' linker symbol reserves a memory for the MSP stack
- The implementation considers '_estack' linker symbol to be RAM end
- NOTE: If the MSP stack, at any point during execution, grows larger than the
- reserved size, please increase the '_Min_Stack_Size'.
-
- @param incr Memory size
- @return Pointer to allocated memory
*/
void _sbrk(ptrdiff_t incr)
{
extern uint8_t _end; / Symbol defined in the linker script /
extern uint8_t _estack; / Symbol defined in the linker script /
extern uint32_t _Min_Stack_Size; / Symbol defined in the linker script */
const uint32_t stack_limit = (uint32_t)&_estack - (uint32_t)&_Min_Stack_Size;
const uint8_t *max_heap = (uint8_t *)stack_limit;
uint8_t *prev_heap_end;
/* Initialize heap end at first call */
if (NULL == __sbrk_heap_end)
{
__sbrk_heap_end = &_end;
}
/* Protect heap from growing into the reserved MSP stack */
if (__sbrk_heap_end + incr > max_heap)
{
errno = ENOMEM;
return (void *)-1;
}
prev_heap_end = __sbrk_heap_end;
__sbrk_heap_end += incr;
return (void )prev_heap_end;
}
여기서 _sbrk()가 Linker Script의:
_end
_estack
_Min_Stack_Size
symbol을 사용하는 점이 중요하다.
즉 Linker Script의 symbol은 startup code뿐 아니라 C library memory management와도 연결될 수 있다.
첨부 ZIP에는 Flash용 custom linker script 외에 CubeIDE가 생성한 RAM debug용 linker script도 존재한다.
파일:
STM32F439ZITX_RAM.ld
/
**
** @file : LinkerScript.ld (debug in RAM dedicated)
**
** @author : Auto-generated by STM32CubeIDE
**
** Abstract : Linker script for NUCLEO-F439ZI Board embedding STM32F439ZITx Device from stm32f4 series
** 2048KBytes FLASH
** 64KBytes CCMRAM
** 192KBytes RAM
**
** Set heap size, stack size and stack location according
** to application requirements.
**
** Set memory bank area and size if external memory is used
**
** Target : STMicroelectronics STM32
**
** Distribution: The file is distributed as is, without any warranty
** of any kind.
**
** @attention
**
** Copyright (c) 2026 STMicroelectronics.
** All rights reserved.
**
** This software is licensed under terms that can be found in the LICENSE file
** in the root directory of this software component.
** If no LICENSE file comes with this software, it is provided AS-IS.
**
*/
/* Entry Point */
ENTRY(Reset_Handler)
/* Highest address of the user mode stack /
_estack = ORIGIN(RAM) + LENGTH(RAM); / end of "RAM" Ram type memory */
_Min_Heap_Size = 0x200; /* required amount of heap /
_Min_Stack_Size = 0x400; / required amount of stack */
/* Memories definition */
MEMORY
{
CCMRAM (xrw) : ORIGIN = 0x10000000, LENGTH = 64K
RAM (xrw) : ORIGIN = 0x20000000, LENGTH = 192K
FLASH (rx) : ORIGIN = 0x8000000, LENGTH = 2048K
}
/* Sections /
SECTIONS
{
/ The startup code into "RAM" Ram type memory /
.isr_vector :
{
. = ALIGN(4);
KEEP((.isr_vector)) /* Startup code */
. = ALIGN(4);
} >RAM
/* The program code and other data into "RAM" Ram type memory /
.text :
{
. = ALIGN(4);
(.text) / .text sections (code) /
(.text) / .text sections (code) */
(.glue_7) / glue arm to thumb code */
(.glue_7t) / glue thumb to arm code */
(.eh_frame)
(.RamFunc) / .RamFunc sections /
(.RamFunc) / .RamFunc sections */
KEEP (*(.init))
KEEP (*(.fini))
. = ALIGN(4);
_etext = .; /* define a global symbols at end of code */
} >RAM
/* Constant data into "RAM" Ram type memory /
.rodata :
{
. = ALIGN(4);
(.rodata) / .rodata sections (constants, strings, etc.) /
(.rodata) / .rodata sections (constants, strings, etc.) */
. = ALIGN(4);
} >RAM
.ARM.extab (READONLY) : /* The "READONLY" keyword is only supported in GCC11 and later, remove it if using GCC10 or earlier. /
{
. = ALIGN(4);
(.ARM.extab .gnu.linkonce.armextab.)
. = ALIGN(4);
} >RAM
.ARM (READONLY) : /* The "READONLY" keyword is only supported in GCC11 and later, remove it if using GCC10 or earlier. */
{
. = ALIGN(4);
__exidx_start = .;
(.ARM.exidx)
__exidx_end = .;
. = ALIGN(4);
} >RAM
.preinit_array (READONLY) : /* The "READONLY" keyword is only supported in GCC11 and later, remove it if using GCC10 or earlier. /
{
. = ALIGN(4);
PROVIDE_HIDDEN (__preinit_array_start = .);
KEEP ((.preinit_array*))
PROVIDE_HIDDEN (__preinit_array_end = .);
. = ALIGN(4);
} >RAM
.init_array (READONLY) : /* The "READONLY" keyword is only supported in GCC11 and later, remove it if using GCC10 or earlier. /
{
. = ALIGN(4);
PROVIDE_HIDDEN (__init_array_start = .);
KEEP ((SORT(.init_array.)))
KEEP ((.init_array*))
PROVIDE_HIDDEN (__init_array_end = .);
. = ALIGN(4);
} >RAM
.fini_array (READONLY) : /* The "READONLY" keyword is only supported in GCC11 and later, remove it if using GCC10 or earlier. /
{
. = ALIGN(4);
PROVIDE_HIDDEN (__fini_array_start = .);
KEEP ((SORT(.fini_array.)))
KEEP ((.fini_array*))
PROVIDE_HIDDEN (__fini_array_end = .);
. = ALIGN(4);
} >RAM
/* Used by the startup to initialize data */
_sidata = LOADADDR(.data);
/* Initialized data sections into "RAM" Ram type memory /
.data :
{
. = ALIGN(4);
_sdata = .; / create a global symbol at data start /
(.data) / .data sections /
(.data) / .data sections */
. = ALIGN(4);
_edata = .; /* define a global symbol at data end */
} >RAM
_siccmram = LOADADDR(.ccmram);
/* CCM-RAM section
*
-
IMPORTANT NOTE!
-
If initialized variables will be placed in this section,
-
the startup code needs to be modified to copy the init-values.
/
.ccmram :
{
. = ALIGN(4);
_sccmram = .; / create a global symbol at ccmram start */
*(.ccmram)
(.ccmram)
. = ALIGN(4);
_eccmram = .; /* create a global symbol at ccmram end */
} >CCMRAM AT> RAM
/* Uninitialized data section into "RAM" Ram type memory /
. = ALIGN(4);
.bss :
{
/ This is used by the startup in order to initialize the .bss section /
_sbss = .; / define a global symbol at bss start */
bss_start = _sbss;
*(.bss)
(.bss)
*(COMMON)
. = ALIGN(4);
_ebss = .; /* define a global symbol at bss end */
__bss_end__ = _ebss;
} >RAM
/* User_heap_stack section, used to check that there is enough "RAM" Ram type memory left */
._user_heap_stack :
{
. = ALIGN(8);
PROVIDE ( end = . );
PROVIDE ( _end = . );
. = . + _Min_Heap_Size;
. = . + _Min_Stack_Size;
. = ALIGN(8);
} >RAM
/* Remove information from the compiler libraries */
/DISCARD/ :
{
libc.a ( * )
libm.a ( * )
libgcc.a ( * )
}
.ARM.attributes 0 : { *(.ARM.attributes) }
}
이 파일은 program section을 RAM에 두는 debug configuration용이며, Ch04의 주된 bare-metal Flash boot 설명은 STM32F439ZITX_FLASH.ld를 기준으로 한다.
Notion 기록:
arm-none-eabi-nm ch04_linker_startup.elf | grep -E "_estack|_sdata|_sbss"
기록된 결과:
20000008 B _ebss
20000004 D _edata
20020000 R _estack
20000004 B _sbss
20000000 D _sdata
특히:
_estack = 0x20020000
은:
RAM start 0x20000000
- 128KB
= 0x20020000
와 일치한다.
.data = 4 bytes
.bss = 2564 bytes
Notion에서는:
g_toggle_flag : 4B
heap/stack 예약 : 2560B
로 해석해 linker/startup 구성 검증 결과로 정리했다.
첨부 ZIP 기준:
Ch04_linker_startup/
├── Src/
│ ├── main.c
│ ├── syscalls.c
│ └── sysmem.c
├── Startup/
│ └── startup_stm32f439zitx.s
├── STM32F439ZITX_FLASH.ld
├── STM32F439ZITX_RAM.ld
└── manual_build/
└── Makefile
Debug/, .project, .cproject, .settings/ 등은 IDE metadata와 build output이므로 Wiki 코드 본문에서는 제외한다.
"무엇을 어디에 놓을 것인가?"
를 결정한다.
"Reset 직후 main 전에 무엇을 할 것인가?"
를 구현한다.
Reset
↓
Vector Table
↓
MSP / Reset_Handler
↓
.data copy
↓
.bss clear
↓
SystemInit
↓
C runtime init
↓
main
main()이전에도 반드시 실행되는 코드가 있다는 것을 이해했다.Linker Script로 Flash/RAM 배치를 직접 정의했다.
.data의 LMA/VMA 차이를 확인했다..bss초기화가 Startup code 책임이라는 것을 확인했다.Cortex-M Vector Table의 첫 두 entry가 MSP와 Reset Handler라는 것을 이해했다.
weak symbol을 이용해 interrupt handler를 나중에 교체할 수 있는 구조를 확인했다.