CS 지식

[CS 지식19.] C/C++ 개발자도 다시 보는 메모리 구조

Somaz 2025. 7. 30. 10:54
728x90
반응형

Overview

 

프로그램을 작성하고 실행할 때, 내부적으로 메모리가 어떻게 배치되고 관리되는지를 이해하는 것은

성능 최적화는 물론, 버그 예방디버깅 능력 향상에도 직결된다.

 

특히 C/C++, Rust, 또는 저수준 시스템 프로그래밍을 다루는 개발자라면,

포인터 오류, 세그멘테이션 폴트, 메모리 누수와 같은 문제를 정확히 파악하고 원인을 추적할 수 있어야 한다.

 

이 글에서는 시스템의 관점에서 프로세스 메모리 구조를 살펴보고,

Stack과 Heap 메모리의 차이점, 그리고 실제로 자주 마주치는 버그 사례와 메모리 디버깅 도구들을 정리해본다.

 

 

 

 

 

 

 

📅 관련 글

2023.01.13 - [CS 지식] - [CS 지식1.] 웹 브라우저의 동작원리

2023.02.23 - [CS 지식] - [CS 지식2.] DNS의 동작원리(Domain Name System)

2023.03.06 - [CS 지식] - [CS 지식3.] HTTP / HTTPS 란?

2023.03.07 - [CS 지식] - [CS 지식4.] OSI 7계층 & TCP/IP 4계층이란?

2023.03.17 - [CS 지식] - [CS 지식5.] 가상화란?

2023.05.24 - [CS 지식] - [CS 지식6.] HTTP 메서드(Method)란? / HTTP Status Code

2023.12.05 - [CS 지식] - [CS 지식7.] Kubernetes 구성요소와 Pod 생성 방식이란?

2023.12.19 - [CS 지식] - [CS 지식8.] 프로세스(Process)와 스레드(Thread)란?

2023.12.30 - [CS 지식] - [CS 지식9.] 클라우드 컴퓨팅이란?(Public & Private Cloud / IaaS SaaS PaaS / Multitenancy)

2024.01.05 - [CS 지식] - [CS 지식10.] 웹1.0(Web1.0) vs 웹2.0(Web2.0) vs 웹3.0(Web3.0)

2024.02.02 - [CS 지식] - [CS 지식11.] NAT(Network Address Translation)란?

2024.05.22 - [CS 지식] - [CS 지식13.] 동기 및 비동기 처리란?

2024.05.23 - [CS 지식] - [CS 지식14.] 3tier 아키텍처란?

2024.08.28 - [CS 지식] - [CS 지식15.] SSR vs CSR vs ISR vs SSG

2024.11.09 - [CS 지식] - [CS 지식16.] stdin(표준입력) vs stdout(표준출력) vs stderr(표준에러)

2024.11.11 - [CS 지식] - [CS 지식17.] IPsec vs SSL/TLS

2024.11.22 - [CS 지식] - [CS 지식18.] Quantum Computing(양자 컴퓨팅)

2025.04.01 - [CS 지식] - [CS 지식19.] C/C++ 개발자도 다시 보는 메모리 구조

 

 

 

 

 

 


 

 

 

메모리 구조 (Memory Layout)

 

 

C/C++ 프로그램이 실행되면 운영체제는 프로세스마다 메모리를 다음과 같은 영역으로 나누어 할당한다.

 

  1. Text 영역 (코드 영역)
    • 실행할 프로그램 코드가 저장됨
    • 보통 읽기 전용이며, 실행 권한이 있음
  2. Data 영역
    • 초기화된 전역 변수, static 변수 저장
  3. BSS 영역
    • 초기화되지 않은 전역 변수, static 변수 저장
  4. Heap 영역
    • malloc, new 등 동적 메모리 할당 시 사용됨
    • 런타임 중 크기 조절 가능
  5. Stack 영역
    • 함수 호출 시 지역 변수, 매개변수, 복귀 주소 등을 저장
    • 후입선출(LIFO) 구조

 

 

 

 

Stack vs Heap

구분 Stack Heap
메모리 위치 낮은 주소에서 높은 주소로 자람 높은 주소에서 낮은 주소로 자람
할당 시점 컴파일 타임 또는 함수 호출 시 런타임 (실행 중)
할당 방식 자동 (함수 호출 시 자동 할당/해제) 수동 (개발자가 직접 해제 필요)
속도 빠름 느림
크기 제한 상대적으로 작음 상대적으로 큼
누수 가능성 거의 없음 메모리 누수 가능

 

 

 

 

실무에서의 사례

 

 

Stack Overflow

재귀 함수의 종료 조건을 잘못 설계하거나, 너무 큰 지역 배열을 선언할 때 발생

void recurse() {
    int arr[1000000]; // 스택 공간 부족
    recurse();
}

 

 

 

 

Use After Free

이미 `free()` 된 메모리를 사용하는 실수

int* p = malloc(sizeof(int));
free(p);
*p = 10; // 정의되지 않은 동작

 

 

 

Memory Leak

`malloc` 등으로 할당 후 `free` 하지 않아 계속 메모리를 잡아먹는 상황

char* str = malloc(100);
// str 사용 후 free 생략 → 누수 발생

 

 

 

Dangling Pointer

해제된 메모리를 참조하는 포인터를 잘못 사용하는 경우

 

 

 

 

 

메모리 디버깅 팁

  • `valgrind` : 런타임 메모리 오류, 누수 탐지
  • `gdb` : 스택 프레임, 포인터 주소 확인
  • `ASAN (AddressSanitizer)` : 클랭/LLVM 기반 메모리 오류 감지 도구

 

 

 

동적 메모리 재할당 (realloc)

`realloc()` 은 이미 할당된 메모리 블록의 크기를 동적으로 변경할 때 사용된다.

하지만 `realloc()` 호출 후 반환된 포인터는 원래 포인터와 다를 수 있으므로, 원본 포인터를 무조건 대체해야 한다:

char *data = malloc(10);
data = realloc(data, 100);  // 반드시 반환값으로 덮어쓰기

 

 

 

함수 내 Stack 메모리의 주의점

로컬 배열의 주소를 반환하면 Dangling Pointer 문제가 발생할 수 있다.

char* dangerous() {
    char temp[100];
    return temp;  // 함수 종료와 함께 temp는 소멸
}

 

 

 

 

Heap Fragmentation (조각화)

많은 `malloc()/free()` 호출이 반복되면 힙 메모리에 빈 공간이 쪼개져 남아, 할당 가능한 총 공간은 충분해도 큰 블록은 할당하지 못하는 현상이 발생할 수 있다.

해결법: 메모리 풀, 가비지 컬렉션, slab allocator 사용

 

 

 

 

 

Modern C++의 RAII 패턴

`new/delete` 기반 메모리 관리를 직접 하지 않고, 스마트 포인터(`std::unique_ptr, std::shared_ptr`)를 사용하는 것이 현대 C++ 스타일이다.

std::unique_ptr<int> ptr = std::make_unique<int>(10);  // 자동 해제

 

 

 

 

 

메모리 누수 방지 패턴

// RAII 스타일의 C 구현
typedef struct {
    char* data;
    size_t size;
} Buffer;

Buffer* buffer_create(size_t size) {
    Buffer* buf = malloc(sizeof(Buffer));
    if (!buf) return NULL;
    
    buf->data = malloc(size);
    if (!buf->data) {
        free(buf);
        return NULL;
    }
    buf->size = size;
    return buf;
}

void buffer_destroy(Buffer** buf) {
    if (buf && *buf) {
        free((*buf)->data);
        free(*buf);
        *buf = NULL;  // 댕글링 포인터 방지
    }
}

 

 

 

 

큰 데이터 처리 시 고려사항

// 스트리밍 방식으로 큰 파일 처리
FILE* fp = fopen("large_file.txt", "r");
char buffer[4096];  // 작은 버퍼로 청크 단위 처리
while (fgets(buffer, sizeof(buffer), fp)) {
    // 청크별 처리
}
fclose(fp);

 

 

 

 

 

 

 


 

 

 

 

 

 

 

메모리 정렬(Memory Alignment)과 패딩

 

 

구조체 패딩의 영향

struct BadExample {
    char a;     // 1바이트
    int b;      // 4바이트 (3바이트 패딩 발생)
    char c;     // 1바이트 (3바이트 패딩 발생)
};  // 총 12바이트

struct GoodExample {
    int b;      // 4바이트
    char a;     // 1바이트
    char c;     // 1바이트 (2바이트 패딩)
};  // 총 8바이트

 

 

최적화 팁

  • 큰 타입부터 작은 타입 순으로 배치
  • `__attribute__((packed))` 사용 시 성능 트레이드오프 고려
  • 캐시 라인 크기(보통 64바이트)를 의식한 구조체 설계

 

 

 

 

가상 메모리와 페이지 시스템

 

 

페이지 폴트 처리

// 큰 배열 선언 시 실제 메모리는 접근할 때 할당됨
char huge_array[1000000];  // 가상 메모리만 예약
huge_array[0] = 'A';       // 이 시점에서 실제 페이지 할당

 

 

 

메모리 매핑 활용

#include <sys/mman.h>
void* addr = mmap(NULL, size, PROT_READ|PROT_WRITE, 
                  MAP_PRIVATE|MAP_ANONYMOUS, -1, 0);
// 큰 파일 처리 시 read/write보다 효율적

 

 

 

 

멀티스레딩 환경에서의 메모리 관리

 

스레드별 스택 공간

// 각 스레드마다 독립적인 스택 공간
void* thread_func(void* arg) {
    char local_buffer[1024];  // 각 스레드별로 별도 할당
    // ...
}

 

 

주의사항

  • 스레드 간 스택 영역 공유 불가
  • 힙 영역은 모든 스레드가 공유
  • 동기화 없이 힙 메모리 접근 시 레이스 컨디션 발생 가능

 

 

 

메모리 접근 패턴과 성능

 

캐시 친화적 코드

// 나쁜 예: 열 우선 접근
for (int j = 0; j < COLS; j++) {
    for (int i = 0; i < ROWS; i++) {
        matrix[i][j] = 0;  // 캐시 미스 빈발
    }
}

// 좋은 예: 행 우선 접근
for (int i = 0; i < ROWS; i++) {
    for (int j = 0; j < COLS; j++) {
        matrix[i][j] = 0;  // 캐시 히트율 높음
    }
}

 

 

메모리 지역성 원칙

  • 시간적 지역성: 최근 접근한 데이터를 다시 접근할 가능성
  • 공간적 지역성: 인접한 메모리 위치를 연속으로 접근

 

 

 

 

보안 관련 메모리 보호

 

 

스택 카나리(Stack Canary)

# 컴파일 시 스택 보호 활성화
gcc -fstack-protector-all program.c

 

 

 

버퍼 오버플로우 방지

// 위험한 함수 대신 안전한 함수 사용
char buffer[100];
// strcpy(buffer, user_input);     // 위험
strncpy(buffer, user_input, sizeof(buffer) - 1);  // 안전
buffer[sizeof(buffer) - 1] = '\0';

 

 

 

 

메모리 풀링 패턴

 

 

고정 크기 메모리 풀

ctypedef struct {
    char data[BLOCK_SIZE];
    int in_use;
} MemoryBlock;

MemoryBlock pool[POOL_SIZE];

void* pool_alloc() {
    for (int i = 0; i < POOL_SIZE; i++) {
        if (!pool[i].in_use) {
            pool[i].in_use = 1;
            return pool[i].data;
        }
    }
    return NULL;  // 풀이 가득 참
}

 

 

 

 

장점

  • 할당/해제 시간 예측 가능
  • 메모리 단편화 방지
  • 실시간 시스템에 적합

 

 

 

 

 

추가 디버깅 도구와 기법

 

메모리 사용량 모니터링

# 실행 중 메모리 사용량 확인
ps aux | grep process_name
top -p process_id

# 메모리 맵 확인
cat /proc/[pid]/maps

 

 

 

 

정적 분석 도구

# 정적 분석으로 메모리 이슈 미리 발견
cppcheck --enable=all program.c
scan-build gcc program.c

 

 

 

 

 

성능 측정과 프로파일링

 

 

메모리 할당 성능 측정

#include <time.h>

clock_t start = clock();
for (int i = 0; i < 1000000; i++) {
    void* ptr = malloc(100);
    free(ptr);
}
clock_t end = clock();
double time_spent = ((double)(end - start)) / CLOCKS_PER_SEC;
printf("Time spent: %f seconds\n", time_spent);

 

 

 

메모리 사용량 프로파일링

# 힙 사용량 상세 분석
valgrind --tool=massif ./program
ms_print massif.out.xxx

 

 

 

 

 

 


 

 

 

 

 

 

마무리

Stack과 Heap은 단순한 저장 위치의 차이를 넘어서, 프로그램의 실행 구조와 자원 관리 전략에 깊이 연결된 개념이다.

 

특히 C/C++과 같이 메모리 수동 관리 언어를 다루는 개발자라면,

Stack Overflow, 메모리 누수, Use After Free와 같은 메모리 이슈를 방지하고 디버깅하는 능력이 필수적이다.

 

운영체제와 컴파일러는 점점 더 많은 보호 장치를 제공하고 있지만, 여전히 많은 버그와 보안 취약점은 메모리에서 발생한다.

 


따라서 아래와 같은 태도를 갖는 것이 중요하다.

  • 항상 free()나 delete를 빠뜨리지 않도록 주의하고,
  • 메모리 사용 전 초기화하고, 사용 후 해제하며,
  • 디버깅 도구(valgrind, gdb, ASAN 등)를 적극적으로 활용하자.

 

안정적인 시스템은, 보이지 않는 메모리 위에 세워진다.

 

 

 

 

 

 

 


 

Reference

 

728x90
반응형