임베디드 면접 용어 사전
펌웨어 빌드

startup 코드

main 전에 실행되어 .data 복사, .bss 클리어, 클럭·스택 초기화를 수행하는 코드.

리셋 직후부터 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 복사 루프가 이상한 주소를 가리키는가 (링커 심볼 확인)

함께 보면 좋은 용어

노트에서 맥락과 함께 보기 — 펌웨어 빌드·부팅 — 빌드 과정·메모리 섹션(.data/.bss)·링커·startup·OTA