초기값이 없거나 0인 전역·static 변수가 들어가는 섹션. 파일에는 크기 정보만 있고 실제 데이터는 없다.
int counter; // .bss
static uint8_t buffer[4096]; // .bss
int flags = 0; // .bss (0 초기화도 여기로)
왜 파일에 데이터가 없나
4KB 배열을 0으로 초기화한다면
→ .data 라면 플래시에 0을 4096개 저장해야 한다 ← 낭비
→ .bss 는 "4096바이트가 필요하고 전부 0" 이라는 정보만 저장
→ startup 코드가 런타임에 0으로 채운다
플래시 공간을 아끼는 최적화다. 이름은 옛 어셈블러의 "Block Started by Symbol" 의사 명령에서 유래했다.
startup 코드의 초기화
extern uint32_t _sbss, _ebss;
void Reset_Handler(void) {
…
uint32_t *p = &_sbss;
while (p < &_ebss) *p++ = 0;
…
}
C 표준이 "초기화하지 않은 정적 저장 기간 객체는 0" 이라고 규정하므로 이 초기화는 선택이 아니라 필수다.
지역 변수와의 차이 — 중요
void f(void) {
int a; // 스택. 쓰레기 값!
static int b; // .bss. 0 보장
}
int c; // .bss. 0 보장
전역·static은 0이 보장되지만 지역 변수는 아니다. 초심자가 자주 혼동하며, 실제로 "가끔 이상한 값이 나온다"의 원인이 된다.
RAM만 소모한다
플래시: 0 바이트
- RAM — : 선언한 크기 전부
큰 버퍼를 두기에 적합하다.
void process(void) {
uint8_t buf[4096]; // ✗ 스택 4KB — 오버플로 위험
}
void process(void) {
static uint8_t buf[4096]; // ✓ .bss — 링커가 크기를 계산해 준다
}
링커가 컴파일 시점에 RAM 사용량을 계산하므로, 빌드가 성공하면 그 부분에서 메모리 부족이 일어나지 않는다. 스택은 런타임에 결정되므로 이런 보장이 없다.
초기화를 건너뛰고 싶을 때 — .noinit
__attribute__((section(".noinit"))) uint32_t reset_count;
리셋되어도 값을 유지하고 싶은 경우에 쓴다.
워치독 리셋 횟수 마지막 오류 코드 부트로더와 앱 사이의 통신용 매직 값
소프트 리셋에서는 RAM 내용이 보존되므로 이 값이 살아남는다. 전원이 완전히 끊기면 사라지므로, 매직 값으로 유효성을 검사해야 한다.
if (magic == 0xDEADBEEF) { /* 값이 유효 */ }
else { magic = 0xDEADBEEF; reset_count = 0; }
크기 확인과 진단
arm-none-eabi-size firmware.elf
text data bss
45231 412 8192 ← bss 8KB
# 무엇이 큰지 찾기
arm-none-eabi-nm --print-size --size-sort -r firmware.elf | grep ' [bB] '
b(소문자)는 static, B(대문자)는 전역 심볼이다.
초기화 시간
.bss 가 크면 부팅이 느려진다 64KB를 0으로 채우려면 32비트 쓰기 16384번 ≈ 수백 μs
빠른 부팅이 필요하면 DMA로 채우거나, 정말 필요한 부분만 초기화하도록
.noinit을 활용하기도 한다. 다만 C 표준 위반이 되므로 신중해야 한다.
힙과의 관계
[.data][.bss][힙 ↓ ][빈 공간][ ↑ 스택]
.bss가 커지면 힙과 스택이 쓸 공간이 줄어든다.
링커 스크립트에서 최소 스택·힙 크기를 ASSERT로 검사하면
빌드 시점에 부족을 감지할 수 있다.