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 < 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 순서)
 *   2. Reset_Handler:
 *      - .data 섹션을 FLASH -> RAM으로 복사 (초기값 있는 전역변수)
 *      - .bss 섹션을 0으로 채움 (초기값 없는 전역변수)
 *      - SystemInit() 호출 (Ch06 CMSIS 이후부터 실제 클럭 설정을 함;
 *        그 전까지는 약한 심볼로 비워둠)
 *      - main() 호출
 *   3. 각 인터럽트 핸들러는 전부 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

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를 나중에 교체할 수 있는 구조를 확인했다.

네비게이션: 이전: Ch03. 빌드 과정과 GNU ARM Toolchain · 전체 목차 · 다음: Ch05. Make Build System

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