Troubleshooting

[C언어 임베디드] 버퍼 오버플로우(Buffer Overflow) 에러 예방과 안전한 메모리 복사

임베디드 친구 2026. 6. 7. 20:15
반응형

[Quick Summary (TL;DR) - For Global Developers]

  • Symptom: Unexpected modification of adjacent variables, sudden system resets, or immediate jumps to HardFault_Handler during data parsing.
  • Cause: Writing data beyond the allocated array boundary, which overwrites the Stack Frame (return address) or adjacent global variables in RAM.
  • Solution: Implement strict input validation (Bounds Checking) before any memory write operation, and replace unsafe memory functions with size-limited alternatives.

임베디드 C언어 버퍼 오버플로우(Buffer Overflow) 발생 증상과 HardFault 하드웨어 오작동

임베디드 시스템에서 배열 인덱스 초과(Array Index Out of Bounds)로 인한 메모리 오염은 OS가 존재하는 PC 환경과 달리 Segmentation Fault를 발생시키지 않고 침묵 속에 동작하는 경우가 많습니다. 대개 다음과 같은 하드웨어 및 소프트웨어적 징후로 나타납니다.

  • 인접 변수 값의 이유 없는 변경: 특정 배열에 데이터를 채운 직후, 코드상에서 수정하지 않은 다른 전역 변수나 로컬 변수의 값이 무작위로 변경됩니다.
  • 비정상적인 분기 및 시스템 리셋: 함수가 종료되는 시점(return)에 시스템이 얼어버리거나 예기치 못한 번지로 점프하여 Watchdog Timer 리셋을 유발합니다.
  • 코어 예외(Core Exception) 발생: ARM Cortex-M 코어 기준으로, 오염된 메모리 주소에 접근하거나 잘못된 명령어 번지로 진입하면서 HardFault_Handler, MemManage_Handler, 또는 Alignment Fault가 즉각적으로 트리거됩니다.

배열 인덱스 초과(Array Index Out of Bounds)가 유발하는 메모리 오염의 컴파일러 레벨 원인 분석

임베디드 C 환경에서 컴파일된 바이너리는 메모리(RAM) 구조상 고정된 배치를 가집니다. 컴파일러와 MCU 레지스터 관점에서 원인은 두 가지 레이어로 나뉩니다.

스택 프레임(Stack Frame) 파괴

함수 내부의 로컬 버퍼(Local Buffer)는 스택(Stack) 영역에 할당됩니다. 이 버퍼의 인덱스를 초과하여 데이터를 쓰면, 스택 프레임 상위에 저장된 함수의 반환 주소(Return Address)인 Link Register (LR) 값이 오염됩니다. 함수 실행이 끝나고 Program Counter (PC) 레지스터가 오염된 LR 값을 복원하는 순간, CPU는 무효한 메모리 주소의 코드를 실행하려 시도하므로 HardFault가 발생합니다.

데이터 세그먼트(Data/BSS Segment) 오염

전역(Global) 또는 정적(Static) 배열의 경우 링커 스크립트(.ld)에 정의된 순서 및 컴파일러 최적화 정렬(Data Alignment) 규칙에 따라 RAM에 차례대로 배치됩니다.

[ 낮은 주소 ] ---> [ 높은 주소 ]
[    rx_buffer[64]    ] [ critical_flag (4-byte) ] [ target_address (4-byte) ]

위 구조에서 rx_buffer에 64바이트를 초과하는 데이터를 쓰면 내부 제어 플래그인 critical_flag나 포인터 변수인 target_address가 그대로 덮어써집니다.

임베디드 통신 패킷 수신 시 버퍼 오버플로우를 유발하는 잘못된 C 코드 예시 (Bad Case)

외부 인터페이스(UART, SPI, CAN 등)로부터 전달받은 패킷 내부의 길이 필드(Length Field)를 검증 없이 바이트 복사에 그대로 사용할 때 가장 흔하게 발생합니다.

#include <stdint.h>
#include <string.h>

#define MAX_BUFFER_SIZE 64

typedef struct {
    uint8_t payload_len;
    uint8_t payload_data[128]; 
} Packet_t;

uint8_t g_system_buffer[MAX_BUFFER_SIZE]; // Destination buffer allocated in RAM
uint32_t g_critical_system_state = 0x11223344; // Target variable susceptible to corruption

void process_received_packet(const Packet_t* packet) {
    /* 
     * BAD: No bounds checking performed on packet->payload_len.
     * If payload_len is greater than 64, g_critical_system_state will be overwritten.
     */
    memcpy(g_system_buffer, packet->payload_data, packet->payload_len);
}

경계 검증(Bounds Checking)을 적용한 안전한 메모리 복사 및 방어적 C 코드 (Good Case)

런타임 환경에서 입력 데이터의 크기를 타깃 버퍼의 물리적 크기와 비교하는 예외 처리 코드를 반드시 포함해야 합니다

#include <stdint.h>
#include <string.h>
#include <stdbool.h>

#define MAX_BUFFER_SIZE 64

typedef struct {
    uint8_t payload_len;
    uint8_t payload_data[128]; 
} Packet_t;

uint8_t g_system_buffer[MAX_BUFFER_SIZE];
uint32_t g_critical_system_state = 0x11223344;

bool safe_process_received_packet(const Packet_t* packet) {
    /* 
     * GOOD: Strict bounds checking against the destination buffer size.
     * Prevent memory corruption before execution.
     */
    if (packet->payload_len > MAX_BUFFER_SIZE) {
        // Log error or update communication status register here
        return false; 
    }

    /* 
     * Alternatively, use size-bounded copy functions, ensuring length validation.
     */
    memcpy(g_system_buffer, packet->payload_data, packet->payload_len);
    return true;
}

핵심 수정 포인트

  • 조건문 기반 크기 검증: 복사 연산을 수행하기 전, 소스(Source) 데이터의 크기(packet->payload_len)가 목적지(Destination) 버퍼의 최대 크기(MAX_BUFFER_SIZE)보다 작은지 비교하는 방어적 구문을 명시했습니다.
  • 에러 핸들링 리턴 플래그 추가: 유효하지 않은 패킷이 인입되었을 때 복사를 차단하고 상위 모듈로 에러 상태(false)를 즉시 반환하여 후속 예외 처리가 가능하도록 설계했습니다.

버퍼 오버플로우 디버깅 및 MAP 파일 활용 트러블슈팅 가이드

  • 링커 맵 파일(MAP File) 주소 분석: 빌드 결과물로 생성되는 .map 파일을 열어 오염된 전역 변수의 메모리 주소를 확인하십시오. 해당 변수 바로 앞에 배치된 배열이나 구조체가 버퍼 오버플로우를 일으킨 유력한 원인(Originator)입니다.
  • 데이터 와치포인트(Data Watchpoint) 설정: J-Link 또는 ST-Link 환경에서 값이 원치 않게 바뀌는 인접 변수에 GDB 명령어나 IDE 기능을 통해 Watchpoint를 등록하십시오. 해당 메모리 주소에 쓰기(Write) 작업이 발생하는 즉시 CPU 코어가 멈추므로 오버플로우를 유발한 원인 코드를 실시간으로 추적할 수 있습니다.
  • 스택 포인터(SP) 및 링크 레지스터(LR) 역추적: 함수 리턴 시점에 HardFault가 발생했다면, 예외 스택 프레임(Exception Stack Frame)에 저장된 PC와 LR 값을 확인하십시오. 스택 포인터(SP) 주변 메모리를 덤프하여 어떤 로컬 버퍼가 상위 레지스터 영역을 침범했는지 크기를 역산할 수 있습니다.
반응형