Bare metal C Ch04 링커스크립트 스타트업 - Hongjayeong-02/Basic_SoC_implementation GitHub Wiki

Ch04. Linker Script와 Startup File 직접 작성

목표
STM32F439ZI가 Reset된 직후 main()까지 어떻게 도달하는지 이해하고,
Linker Script + Startup Assembly + Vector Table + .data/.bss 초기화를 직접 구성한다.

자료 기준
이 페이지는 Notion Ch04 정리와 첨부 ZIP의 실제
Ch04_linker_startup/ 프로젝트 파일을 함께 기준으로 작성했다.


1. 왜 Linker Script와 Startup File이 필요한가

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 직후 실제 초기화 코드를 수행한다.


2. STM32F439ZI Memory Map

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이 필요하다.


3. .text, .data, .bss

.text

Program code가 들어간다.

int main(void)

같은 함수의 machine code가 Flash에 배치된다.

.data

초기값이 있는 전역/정적 변수.

unsigned int g_blink_count = 5;

실행 중에는 RAM에 있어야 하지만 초기값 5는 firmware image에 저장되어야 한다.

따라서:

Flash
  │ initial value
  ▼
.data load image
  │ Reset_Handler가 copy
  ▼
RAM

.bss

초기값이 없는 전역/정적 변수.

unsigned int g_toggle_flag;

C 규칙상 시작 시 0이어야 하므로 Flash에 0을 저장할 필요 없이 Reset_Handler가 RAM 영역을 0으로 채운다.


4. VMA와 LMA

.data를 이해할 때 중요한 개념이다.

LMA = Load Memory Address
→ Firmware image에서 초기값이 저장된 위치 (Flash)

VMA = Runtime/Virtual Memory Address → 프로그램 실행 중 실제 변수가 존재하는 위치 (RAM)

Linker Script에서:

.data :
{
...
} >RAM AT> FLASH

이라고 정의하면:

Load : Flash
Run  : RAM

이라는 의미가 된다.


5. 실제 실습 코드 — main.c

첨부 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 &lt; 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;

g_blink_count

초기값 있음
→ .data
→ Flash에 5 저장
→ Reset_Handler가 RAM으로 copy

g_toggle_flag

초기값 없음
→ .bss
→ Reset_Handler가 0으로 초기화

따라서 이 두 변수가 정상적으로 동작하면 Linker Script와 Startup의 초기화 흐름을 확인할 수 있다.


6. 실제 Linker Script — Flash 실행용

첨부 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) } }


7. Linker Script 핵심 부분

Entry point

ENTRY(Reset_Handler)

ELF의 진입 symbol을 Reset_Handler로 지정한다.


Memory region

MEMORY
{
FLASH (rx)  : ORIGIN = 0x08000000, LENGTH = 2048K
RAM   (rwx) : ORIGIN = 0x20000000, LENGTH = 128K
}

여기서 Linker에게:

FLASH는 여기부터 2MB
RAM은 여기부터 128KB

라고 알려준다.


Stack top

_estack = ORIGIN(RAM) + LENGTH(RAM);

따라서:

0x20000000 + 128KB
= 0x20020000

이 주소가 초기 stack pointer 값이 된다.


Vector Table

.isr_vector :
{
KEEP((.isr_vector))
} >FLASH

Vector Table은 Flash의 앞쪽에 배치해야 한다.

KEEP()은 linker garbage collection 과정에서 vector table이 사용되지 않는 section으로 판단되어 제거되지 않도록 보호한다.


.data

_sidata = LOADADDR(.data);

.data : { _sdata = .; *(.data) (.data) _edata = .; } >RAM AT> FLASH

Startup code는 이 symbol들을 이용한다.

_sidata : Flash의 .data 초기값 시작
_sdata  : RAM의 .data 시작
_edata  : RAM의 .data 끝

.bss

.bss :
{
_sbss = .;
*(.bss)
(.bss)
(COMMON)
_ebss = .;
} >RAM

Startup code가 _sbss ~ _ebss를 0으로 채운다.


8. 실제 Startup Assembly 전체

첨부 ZIP:

Ch04_linker_startup/Startup/startup_stm32f439zitx.s

/
  • startup_stm32f439zi.s
  • Ch04에서 직접 작성한 STM32F439ZI(NUCLEO-F439ZI)용 스타트업 파일.
  • Ch05부터 이 저장소의 모든 챕터가 공통으로 이 파일을 사용한다.
  • 하는 일:
    1. 벡터 테이블(g_pfnVectors)을 .isr_vector 섹션에 배치
  •  - 0번: 초기 MSP(Main Stack Pointer) 값 (_estack, 링커스크립트가 정의)
    
  •  - 1번: Reset_Handler 주소
    
  •  - 이후: 시스템 예외 + STM32F439의 IRQ 90개 (RM0090 Table 62 순서)
    
    1. Reset_Handler:
  •  - .data 섹션을 FLASH -&gt; RAM으로 복사 (초기값 있는 전역변수)
    
  •  - .bss 섹션을 0으로 채움 (초기값 없는 전역변수)
    
  •  - SystemInit() 호출 (Ch06 CMSIS 이후부터 실제 클럭 설정을 함;
    
  •    그 전까지는 약한 심볼로 비워둠)
    
  •  - main() 호출
    
    1. 각 인터럽트 핸들러는 전부 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의 초기값 -&gt; 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


9. Reset_Handler 동작

실제 startup 파일의 핵심 흐름:

Reset_Handler
│
├─ SP = _estack
│
├─ _sidata → _sdata ... _edata copy
│
├─ _sbss ... _ebss = 0
│
├─ SystemInit()
│
├─ __libc_init_array()
│
└─ main()

Stack initialization

ldr sp, =_estack

.data copy

Flash의 initial value를 RAM으로 복사한다.

.bss clear

movs r3, #0

을 이용해서 .bss 영역을 0으로 채운다.


10. Vector Table

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 시 이 두 값을 하드웨어적으로 사용한다.


11. Weak Handler

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을 매번 수정하지 않아도 된다.


12. CubeIDE 생성 Support Code — syscalls.c

첨부 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; }


13. CubeIDE 생성 Support Code — sysmem.c

/

  • @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와도 연결될 수 있다.


14. RAM Debug Linker Script

첨부 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를 기준으로 한다.


15. Notion에 기록된 Symbol 검증

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

와 일치한다.


16. Notion에 기록된 Section size

.data = 4 bytes
.bss  = 2564 bytes

Notion에서는:

g_toggle_flag : 4B
heap/stack 예약 : 2560B

로 해석해 linker/startup 구성 검증 결과로 정리했다.


17. 프로젝트 구조

첨부 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 코드 본문에서는 제외한다.


18. 핵심 정리

Linker Script

"무엇을 어디에 놓을 것인가?"

를 결정한다.

Startup File

"Reset 직후 main 전에 무엇을 할 것인가?"

를 구현한다.

Boot 흐름

Reset
↓
Vector Table
↓
MSP / Reset_Handler
↓
.data copy
↓
.bss clear
↓
SystemInit
↓
C runtime init
↓
main

19. 이 챕터에서 배운 점

  • main() 이전에도 반드시 실행되는 코드가 있다는 것을 이해했다.

  • Linker Script로 Flash/RAM 배치를 직접 정의했다.

  • .data의 LMA/VMA 차이를 확인했다.

  • .bss 초기화가 Startup code 책임이라는 것을 확인했다.

  • Cortex-M Vector Table의 첫 두 entry가 MSP와 Reset Handler라는 것을 이해했다.

  • weak symbol을 이용해 interrupt handler를 나중에 교체할 수 있는 구조를 확인했다.

⚠️ **GitHub.com Fallback** ⚠️