리셋 직후부터 main() 진입까지 실행되는 초기화 코드.
보통 startup_*.s 또는 .c 파일로 제공된다.
하는 일
void Reset_Handler(void) {
SystemInit(); // ① 클럭, FPU, VTOR
copy_data(); // ② .data 를 플래시 → RAM
zero_bss(); // ③ .bss 를 0으로
__libc_init_array(); // ④ C++ 생성자, atexit 등록
main(); // ⑤ 진입
while (1) { } // ⑥ main이 반환하면 여기 갇힌다
}
① SystemInit — 무엇을 하고 무엇을 하지 않나
void SystemInit(void) {
// FPU 활성화 (컴파일 옵션에 따라)
SCB->CPACR |= (0xF << 20);
// 벡터 테이블 위치
SCB->VTOR = FLASH_BASE | VECT_TAB_OFFSET;
}
주의 — SystemInit은 .data 복사 이전에 실행된다.
uint32_t g_clock = 168000000; // .data
void SystemInit(void) {
use(g_clock); // ✗ 아직 복사 전! 쓰레기 값
}
전역 변수 초기값을 신뢰할 수 없는 유일한 구간이며, 찾기 매우 어려운 버그를 만든다.
STM32 HAL에서 클럭 설정(SystemClock_Config)은 SystemInit이 아니라
main() 안에서 하도록 되어 있는 것도 이런 이유가 있다.
② .data 복사
extern uint32_t _sidata, _sdata, _edata;
uint32_t *src = &_sidata; // 플래시 (LMA)
uint32_t *dst = &_sdata; // RAM (VMA)
while (dst < &_edata) *dst++ = *src++;
4바이트 단위로 복사하므로 링커 스크립트에서 ALIGN(4)가 필요하다.
③ .bss 클리어
extern uint32_t _sbss, _ebss;
for (uint32_t *p = &_sbss; p < &_ebss; p++) *p = 0;
C 표준의 "정적 저장 기간 객체는 0으로 초기화" 요구를 만족시킨다.
④ __libc_init_array — C++과 생성자
extern void (*__init_array_start[])(void);
extern void (*__init_array_end[])(void);
for (int i = 0; i < __init_array_end - __init_array_start; i++)
__init_array_start[i]();
전역 C++ 객체의 생성자가 여기서 호출된다.
Sensor g_sensor(0x48); // 이 생성자가 main() 전에 실행된다
주의점
- 생성자 실행 순서가 파일 간에 정의되지 않는다 (static initialization order fiasco)
- 생성자에서 하드웨어를 건드리면 아직 클럭이 설정 전일 수 있다
- 예외를 던지면 처리할 곳이 없다
임베디드 C++에서 전역 객체를 피하고 명시적 init() 을 쓰는 것이 권장되는 이유다.
⑤ main 이후 — 반환하면 안 된다
int main(void) {
…
return 0; // ✗ 임베디드에서 main은 반환하지 않는다
}
반환하면 startup의 while(1)에 갇히거나, 구현에 따라
exit()이 호출되어 예측 불가능한 동작을 한다.
main은 무한 루프로 끝나야 한다.
어셈블리로 작성되는 이유
Reset_Handler:
ldr sp, =_estack /* 스택 포인터 설정 (Cortex-M은 자동이지만 명시) */
bl SystemInit
...
C 코드는 스택이 있어야 실행된다. 스택 설정 자체를 C로 쓸 수는 없다. Cortex-M은 하드웨어가 벡터 테이블에서 SP를 로드해 주므로 사실 C로 작성해도 되지만, 관례적으로 어셈블리를 쓴다.
커스터마이징이 필요한 경우
- ① 외부 SDRAM 초기화 — .data 복사 전에 SDRAM 컨트롤러를 켜야 한다
- ② .ramfunc 복사 추가
- ③ 여러 RAM 영역의 .data 복사
- ④ ECC 메모리 초기화 (읽기 전에 써야 하는 경우)
- ⑤ 워치독 조기 활성화
- ⑥ 리셋 원인 저장 (다른 코드가 플래그를 지우기 전에)
⑥이 실무에서 유용하다 — 리셋 원인을 가장 먼저 읽어
.noinit 영역에 저장해 두면 나중에 분석할 수 있다.
최소 스택으로 동작해야 한다
startup 코드가 실행될 때는 스택 사용량을 계산할 방법이 없다. 깊은 함수 호출이나 큰 지역 변수를 두면 안 된다.
디버깅
- main()에 도달하지 못한다면
- ① Reset_Handler 에 브레이크포인트 → 진입은 하는가
- ② 스택 포인터(SP)가 RAM 범위인가
- ③ 클럭 설정에서 무한 대기에 걸렸는가 (HSE 크리스털)
- ④ .data 복사 루프가 이상한 주소를 가리키는가 (링커 심볼 확인)