펌웨어 빌드·부팅 — 임베디드 면접 (실제 기출 기반·심화)
".data와 .bss 차이", "전역변수는 누가·언제 초기화하나", "전원 켜고 main()까지 부팅 과정", "벡터 테이블", "링커 스크립트가 뭐냐", "크로스 컴파일이 뭐냐", "OTA 중 전원 꺼지면" — 컴파일러·메모리·부팅을 이해했는지 보는 단골. 메모리 섹션·부팅 과정·volatile은 신입·경력 불문 거의 빠지지 않는다.
함정 다발(★): ① 전역변수는 컴파일러가 자동 초기화? → 아님·startup 코드(C 런타임)가 main 직전에 ② .bss가 ROM을 차지? → 아님·SHT_NOBITS(0바이트)·startup이 RAM 0-fill ③ main()이 첫 진입점? → Reset_Handler가 먼저 ④ const는 RAM? → 보통 .rodata=ROM ⑤ 펌웨어=.elf를 그대로 굽는다? → objcopy로 .bin/.hex 변환.
1. 빌드 과정 — 전처리→컴파일→어셈블→링크→objcopy (★ 단골)
실제 질문: "소스가 실행파일(HEX/BIN)이 되기까지 과정을 설명하라" · "ELF를 그대로 안 굽고 .bin/.hex로 바꾸는 이유?" · "컴파일과 빌드 차이?"
| 단계 | 도구/플래그 | 산출물 |
|---|---|---|
| 전처리 | gcc -E(cpp) | 매크로·#include 전개된 소스 |
| 컴파일 | gcc -S | .s 어셈블리 |
| 어셈블 | gcc -c(as) | .o 오브젝트(기계어) |
| 링크 | ld | .elf(심볼·디버그·여러 섹션) |
| 변환 | objcopy -O binary/ihex | .bin / .hex |
- 링크는 여러
.o의 심볼 해석(symbol resolution) + 재배치(relocation) + 라이브러리 결합. - objcopy 변환이 필요한 이유(★): ELF엔 심볼·디버그·섹션헤더 메타데이터가 붙고 서로 다른 주소의 세그먼트가 섞임. 플래시 도구는 순수 .bin(주소 없음·시작주소 별도) 또는 .hex(주소+체크섬 포함 텍스트)를 원함 →
arm-none-eabi-objcopy -O binary main.elf main.bin.
꼬리질문: ".bin vs .hex?" → .bin은 주소 없는 순수 바이너리·.hex(Intel HEX)는 주소+체크섬. / "정적 vs 동적 링크?" → 임베디드는 보통 정적(OS·동적 로더 없음).
함정: ❌ "펌웨어=.elf를 그대로 굽는다" → objcopy로 .bin/.hex 변환. ❌ ".bin과 .hex가 같다" → 주소 정보 유무가 다름.
2. 메모리 섹션 — .text/.rodata/.data/.bss/heap/stack (★★★ 최다 출제)
실제 질문: ".data와 .bss의 차이는?"(최빈출) · "메모리 영역(code/data/heap/stack)을 설명하라" · "전역/static 변수는 어디 저장?" · "초기화된 전역변수는 ROM이냐 RAM이냐?" · "BSS 약자는?"
| 섹션 | 내용 | 위치 |
|---|---|---|
| .text | 코드·실행 명령 | ROM/Flash |
| .rodata | const·문자열 리터럴·룩업테이블 | ROM(읽기전용) |
| .data | 초기값 있는 전역/static | ROM(LMA)에 초기값 + RAM(VMA)에 배치·startup이 복사 |
| .bss | 0/미초기화 전역/static | ROM 0바이트(SHT_NOBITS)·startup이 RAM 0-fill |
| heap | malloc | RAM(낮은→높은 주소) |
| stack | 지역변수·복귀주소 | RAM(보통 높은→낮은·LIFO) |
- .data vs .bss 핵심(★): 초기값을 주면 .data(
int a=5;)·안 주거나 0이면 .bss(int b;). BSS = Block Started by Symbol. - 왜 .data는 ROM+RAM 둘 다?: RAM은 휘발성이라 초기값은 *비휘발 ROM(LMA)*에 저장하고, 실행 중 쓰기 위해 *RAM(VMA)*으로 복사. → "초기화된 전역변수는?" 정답은 둘 다.
- .bss가 ROM 0바이트인 이유: 어차피 0이라 저장이 무의미 → SHT_NOBITS(파일 공간 0)·startup이 런타임 RAM에서 0으로 채움 → 바이너리 크기 절감.
- 실증(면접 단골):
int a[100000]={1,};→ 실행파일 ~400kB(.data) vsint a[100000];→ 파일 거의 0(.bss). - stack vs heap: stack=컴파일타임 프레임·자동 소멸·LIFO / heap=런타임·수동 관리(malloc/free)·단편화. 임베디드는 RAM 양끝에서 마주보고 성장. (malloc은 단편화·예측불가로 임베디드에서 기피)
꼬리질문: "const는 어디?" → 보통 .rodata=ROM(포인터로 강제수정 시 HardFault). / "스택 오버플로가 .bss/heap 침범?" → MPU 없으면 silent corruption → 링커 배치·MPU 가드. / "volatile은 왜?"(★ must-know) → HW 레지스터·ISR 공유변수에서 컴파일러 최적화(레지스터 캐싱) 방지.
함정: ❌ ".bss가 ROM을 차지" → SHT_NOBITS·0바이트. ❌ ".bss를 startup이 ROM→RAM 복사" → 복사 아니라 0-fill(복사는 .data만). ❌ "문자열 리터럴은 stack/heap" → .rodata.
3. 링커 스크립트 (.ld) — VMA vs LMA (★★ 중상)
실제 질문: "링커 스크립트가 뭐고 왜 필요?" · "VMA와 LMA 차이?" · ".bss는 왜 RAM에만, 왜 나누나?"(ROM 절약)
- VMA(Virtual Memory Address) = 실행 시 주소 · LMA(Load Memory Address) = 저장/로드 위치. 대부분 같지만 .data는 다르다(LMA=ROM·VMA=RAM) → startup 복사의 근본 이유. GNU ld: "data section is loaded into ROM, and copied into RAM at startup".
- MEMORY: 영역 정의 —
ROM (rx): ORIGIN=0x8000000, LENGTH=512K/RAM (xrw): ORIGIN=0x20000000, LENGTH=128K. - SECTIONS: 입력 섹션을 출력 섹션·주소에 배치.
> region=VMA·AT> region=LMA.
_sidata = LOADADDR(.data);
.data : { _sdata = .; *(.data*) _edata = .; } >RAM AT> ROM /* VMA=RAM, LMA=ROM */
.bss (NOLOAD) : { _sbss = .; *(.bss*) *(COMMON) _ebss = .; } >RAM /* Flash 미점유 */
- 링커 심볼은 값이 아니라 주소:
_sidata(.data의 LMA)·_sdata/_edata(.data VMA)·_sbss/_ebss·_estack(RAM 끝·초기 SP). C에선&_sdata로 접근. - 베어메탈 필수 이유: PC용 ld는 default script 내장이라 없어도 빌드되지만, 베어메탈은 OS·기본 메모리맵이 없고 리셋 시 CPU가 flash origin에서 벡터테이블을 읽으므로
.isr_vector를 맨 앞에 강제 배치 → 필수.
꼬리질문: "왜 .data만 AT> 쓰나?" → 초기값 저장(ROM)과 실행(RAM) 위치가 달라서. / "_sidata가 뭐냐?" → .data의 LMA(si=start of init data). / "startup 복사 의사코드?" → for(d=&_sdata, s=&_sidata; d<&_edata;) *d++=*s++;.
함정: ❌ "VMA=LMA 항상 같다" → .data는 다름. ❌ ".data를 >ROM AT> RAM" → 반대·정답 >RAM AT> ROM(앞=VMA 실행=RAM). ❌ "_etext는 값" → 주소.
4. startup·부팅 — reset→main (★★★ 개념 단골·순서 정확히)
실제 질문: "전원 켜고 main()까지 부팅 과정?" · "전역변수는 누가·언제 초기화?" · "벡터 테이블이 뭐고 어디?" · "Cortex-M 리셋 후 가장 먼저?"
부팅 순서(★ ST CMSIS GCC startup 실코드로 검증):
- [HW] 리셋 직후(Cortex-M 고유): 0번지 명령어를 실행하지 않고 벡터테이블 첫 두 워드를 데이터로 읽음 — 0x0000_0000 → MSP(초기 스택), 0x0000_0004 → PC(Reset_Handler). 고전 ARM7과 다름. → "벡터테이블 0번지에 왜 SP?"의 답: 첫 C 코드 실행 전에 스택을 확보해야 하므로 HW가 자동 세팅.
- [SW] Reset_Handler: SP 설정 → SystemInit(클럭/PLL/FPU·VTOR) → .data 복사(
_sidata→_sdata.._edata) → .bss 0클리어(_sbss.._ebss) → __libc_init_array(C++ 전역 생성자·constructor속성) → main(). - 순서 주의(★ 심화): ST 표준은 SystemInit이 .data/.bss 초기화보다 먼저(외부 SDRAM에 .data/.bss가 있을 수 있어 클럭/FMC를 먼저 켜야) → SystemInit 안에선 전역변수 의존 금지. (toolchain마다 순서가 다를 수 있음 — 이 트레이드오프 인지가 포인트.)
- 누가 전역변수를 초기화?(★): 컴파일러도 OS도 아닌 startup 코드(C 런타임/crt0)가 main 직전에 .data 복사 + .bss 0-fill. 컴파일러/링커는 초기값을 Flash에 배치하고 심볼 주소만 정함.
꼬리질문: ".bss 0-fill 안 하면?" → static 변수가 RAM 잔류 쓰레기값(C 표준의 0 보장은 startup이 구현). / "main 직행하면?" → SP 미설정·전역 쓰레기값 → 즉시 크래시. / "VTOR 재설정?" → 부트로더→앱 점프 시 앱 벡터테이블로 바꿔야 인터럽트가 앱 핸들러로.
함정: ❌ "컴파일러가 전역변수 자동 초기화" → startup이. ❌ "main이 첫 진입점" → Reset_Handler 먼저. ❌ "리셋 시 0번지 명령어 실행" → 데이터로 읽어 SP에. ❌ "벡터테이블 0번지=reset handler" → 0번지=SP·4번지=reset.
5. 크로스 컴파일·부트로더·OTA (★★ 단골)
실제 질문: "크로스 컴파일이 뭐냐? 왜 필요?" · "부트로더 역할?" · "OTA 어떻게 동작? 중간에 전원 꺼지면?"
- 크로스 컴파일: 호스트(컴파일러 실행)≠타깃(코드 실행). 호스트 x86 gcc는 x86 명령어를 내므로 ARM에서 실행 불가 → arm-none-eabi-gcc 필요. triplet
arm-none-eabi= arm 아키텍처·none(vendor·OS 없음=베어메탈)·eabi. 대비arm-linux-gnueabihf=Linux 유저스페이스(glibc). - newlib-nano(
--specs=nano.specs): 작은 Flash/RAM·printf float 기본 비활성. map 파일(-Wl,-Map=out.map): 섹션 크기·심볼 주소·FLASH/RAM 사용량 → 크기 초과 디버깅. - 부트로더→앱 점프(Cortex-M): ① 앱 벡터테이블 첫 워드로 MSP 설정 ②
SCB->VTOR = APP_ADDR로 VTOR 재설정 ③ 둘째 워드(Reset_Handler)로 점프. VTOR 안 바꾸면 인터럽트 시 부트로더 핸들러로 → HardFault. - OTA·A/B(이중 뱅크): 비활성 슬롯에 업데이트 기록 → 검증 후 다음 부팅에 전환 → 실패 시 이전 슬롯 롤백. 전원 끊겨도 기존 이미지 무손상 = 벽돌 방지(MCUboot·ESP-IDF). 무결성(CRC/해시) ≠ 진위(서명 RSA/ECDSA=Secure Boot) — 둘 다 필요.
꼬리질문: "OTA 중 전원 끊기면?" → 비활성 슬롯만 기록 중이라 활성 무손상·재시도/롤백(단일뱅크 덮어쓰기만 위험). / "A/B 트레이드오프?" → Flash 2배지만 안전·무중단·롤백. / "CRC만으로 보안?" → CRC는 무결성만·변조 방지는 서명.
함정: ❌ "호스트 gcc로 임베디드 빌드" → x86 명령어라 불가. ❌ "OTA 중 전원=무조건 벽돌" → A/B면 복구. ❌ "VTOR 재설정 없이 점프" → 인터럽트 시 HardFault.
한국 임베디드 면접 단골 Q&A (답변 골격)
| 질문 | 핵심 답 |
|---|---|
| 빌드 과정 | 전처리→컴파일→어셈블→링크(.elf)→objcopy(.bin/.hex) |
| .elf를 왜 변환 | 메타데이터 제거·플래시 도구는 .bin/.hex |
| .data vs .bss | 초기값 有(ROM+RAM) vs 無/0(ROM 0바이트·RAM 0-fill) |
| 전역변수 초기화 | startup 코드(C런타임)가 main 직전에 |
| 초기화 전역변수 위치 | ROM(초기값)+RAM(실행) 둘 다 |
| const 위치 | .rodata=ROM |
| VMA vs LMA | 실행 주소 vs 로드 주소(.data만 다름) |
| 링커 스크립트 왜 | 메모리맵·벡터테이블 배치(베어메탈 필수) |
| 부팅 순서 | HW(MSP·PC)→Reset_Handler→SystemInit→.data복사→.bss클리어→libc_init→main |
| 벡터테이블 0번지 | 초기 SP·4번지=Reset_Handler |
| 크로스 컴파일 | 호스트≠타깃·arm-none-eabi(none=베어메탈) |
| OTA 안전 | A/B 슬롯·롤백·서명 |
꼬리질문 대비 (상 난이도)
- "왜 .data는 ROM·RAM 둘 다 필요?" → 초기값은 비휘발 ROM·실행은 쓰기가능 RAM.
- ".bss를 startup이 0으로 안 채우면?" → static 변수 쓰레기값·C 표준 위반 상태.
- "SystemInit이 .data 복사보다 먼저인 이유?" → 외부 SDRAM에 .data/.bss가 있을 수 있어 클럭/FMC 먼저(전역변수 의존 금지).
- "스택 오버플로가 .bss/heap 침범?" → MPU 없으면 silent corruption → 링커 배치·MPU 가드.
- "__libc_init_array 안 부르면 C++에서?" → 전역 객체 생성자 미호출.
- "VTOR 재설정 안 하면?" → 인터럽트가 부트로더 핸들러로 → HardFault.
한 줄 요약 — 빌드 = 전처리→컴파일→어셈블→링크(.elf)→objcopy(.bin/.hex). 메모리 섹션 — .text/.rodata(ROM)·.data(초기값 有·ROM+RAM)·.bss(0·ROM 0바이트 SHT_NOBITS·RAM 0-fill)·stack/heap. 링커 스크립트는 **VMA(실행)≠LMA(로드)**를 분리(.data는
>RAM AT> ROM)·베어메탈 필수. 부팅 = [HW] 0번지→MSP·4번지→PC → Reset_Handler → SystemInit → .data복사 → .bss클리어 → __libc_init_array → main·전역변수 초기화 주체는 컴파일러도 OS도 아닌 startup 코드. 크로스 컴파일(arm-none-eabi·none=베어메탈)·OTA는 A/B 슬롯+롤백+서명.
(출처 — 한국 면접 기출·1차자료 교차검증 2026-06: velog @embeddedjune(부팅·컴파일)·@ebing·@ws_jung·KLDP(.data/.bss 실증)·80000coding(BSS 약자) · 1차자료: System V gABI — ELF Sections(SHT_NOBITS)·GNU ld manual — VMA/LMA·SECTIONS·GCC Overall Options·ST startup_stm32f407xx.s·ARM Cortex-M4 Generic User Guide — Vector table·Memfault — zero-to-main·MCUboot design 교차검증.)