그라운드 임베디드 EN 유튜브 구독

노트 / C 언어

배열과 문자열: 버퍼 넘침의 원리

배열은 메모리에 연속으로 놓인 같은 자료형의 묶음이고, 문자열은 널 문자로 끝나는 char 배열입니다.

GROUND EMBEDDED · 정리 노트C 언어 05배열과 문자열 1char msg[16]; 2snprintf(msg, sizeof msg, "T=%d", temp);// 크기…

배열

uint8_t buf[8];은 8바이트가 연속으로 놓인 공간입니다. buf[i]는 "시작 주소에서 i칸 뒤"라는 뜻일 뿐이라 C는 범위를 검사하지 않습니다. buf[8]에 쓰면 배열 바로 뒤에 있던 다른 변수가 망가집니다. 이것이 버퍼 넘침입니다.

문자열

C의 문자열은 끝에 '\0'이 붙은 char 배열입니다. "OK"는 세 바이트를 차지합니다.

char msg[16];
snprintf(msg, sizeof msg, "T=%d", temp);   // 크기를 넘기지 않음

strcpy, sprintf처럼 길이를 확인하지 않는 함수 대신 strncpy, snprintf를 씁니다.

임베디드에서 배열이 쓰이는 곳

  • UART 수신 버퍼, 통신 패킷
  • ADC 샘플을 모아 평균 내는 버퍼
  • 룩업 테이블과 설정값 표

배열 크기는 매크로 하나로 정하고 루프 조건도 그 매크로를 쓰면, 크기를 바꿀 때 실수가 줄어듭니다.

그림으로 보기: 문자열의 끝은 0이다

name[0]'A' 0x41name[1]'B' 0x42name[2]'C' 0x43name[3]'D' 0x44name[4]'\0' 0x00끝 표시
char name[5] = "ABCD". 글자 네 개 뒤에 끝을 알리는 0이 한 칸을 차지합니다. 다섯 글자를 넣으면 0이 들어갈 자리가 없습니다.

C의 문자열은 길이를 따로 저장하지 않습니다. strlen, printf("%s"), strcpy는 모두 0을 만날 때까지 메모리를 읽어 나갑니다. 0이 없으면 배열을 지나 옆 변수까지 읽거나 씁니다.

버퍼 넘침이 실제로 일어나는 모습

char cmd[8];
uint8_t motor_enable = 0;

void on_uart_line(const char *line) {
    strcpy(cmd, line);             // line이 8글자 이상이면 cmd를 넘어 씀
}

cmd 바로 뒤에 motor_enable이 놓였다면, 긴 문자열 하나로 그 값이 바뀝니다. 통신으로 들어온 데이터는 길이를 믿을 수 없으므로 항상 크기를 넘겨 제한합니다.

size_t n = strnlen(line, sizeof(cmd) - 1);
memcpy(cmd, line, n);
cmd[n] = '\0';                     // 끝을 직접 채움
3D 실험실여기까지 읽은 내용을 직접 돌려 볼 수 있습니다→

안전한 함수로 바꾸기

위험 대신 쓸 것 주의
strcpy 길이를 재고 memcpy 끝의 0을 직접 넣기
sprintf snprintf(buf, sizeof(buf), ...) 반환값이 크기 이상이면 잘린 것
strncpy 위와 같음 꽉 차면 0을 넣지 않음
gets 쓰지 않음 길이 제한이 아예 없음

strncpy는 이름과 달리 안전하지 않습니다. 원본이 버퍼보다 길면 끝에 0을 넣지 않고 그대로 돌아옵니다.

배열을 함수에 넘기면 크기가 사라진다

void clear(uint8_t buf[]) {
    memset(buf, 0, sizeof(buf));   // 틀림: 포인터 크기(4)만큼만 지움
}
void clear2(uint8_t *buf, size_t len) { memset(buf, 0, len); }   // 길이를 함께 넘김

함수 인자로 받은 배열은 포인터입니다. sizeof는 배열 전체가 아니라 포인터 크기를 돌려줍니다. 배열을 받는 함수는 길이를 함께 받도록 만듭니다.

배열의 칸 수는 선언한 곳에서 이렇게 구합니다.

#define ARRAY_LEN(a)  (sizeof(a) / sizeof((a)[0]))

인덱스 검사는 쓰기 전에

if (idx < ARRAY_LEN(table)) table[idx] = value;

통신으로 받은 채널 번호, 센서 ID처럼 바깥에서 온 값을 인덱스로 쓸 때는 반드시 범위를 확인합니다. C는 범위를 벗어나도 오류를 내지 않고 그 주소에 그대로 씁니다.

스스로 확인

  • char s[] = "hello";에서 sizeof(s)와 strlen(s)는 각각 얼마일까요?
  • 2차원 배열 uint8_t m[3][4]에서 m[1][5]에 쓰면 어디에 쓰일까요?
3D 실험실에서 직접 돌려 보기값을 바꾸며 이 노트의 원리를 확인합니다→영상으로 이어 보기공부법과 진로 시리즈 15편2025 임베디드 프로그래밍 공부 순서 | 공부 방법 | 로드맵PROJECT COURSEC 문법을 실제 펌웨어 코드로 써 보고 싶다면밸런싱 로봇 하나를 요구사항, 회로와 PCB, 펌웨어, 앱까지 실무 순서대로 만드는 강의입니다. 10파트 165강.강의 살펴보기 →FREE PDF무료 전자책 3권 받기이 주제를 한 권으로 이어서 읽고 싶다면. BLDC 모터 제어, 직렬통신, 센서를 책 분량으로 묶었습니다. 이메일만 남기면 바로 받습니다.무료로 받기 →