
C++의 특성
C++을 초기에 사용할 때 가장 먼저 마주치는 오류인듯 한데요, Java 기반의 언어를 주로 다뤄왔기 때문에 메모리를 수동으로 관리하는 C의 특성을 잘 이해하지 못 해서 해당 오류를 마주치게 되었습니다.
C++의 경우, C 언어와 마찬가지로 메모리를 수동으로 관리하는 특성을 가지고 있는데요, 다음과 같이 포인터(*)를 선언하는 행위를 통해 메모리에 직접 접근할 수 있습니다.
struct FreeNode {
FreeNode* next;
};
코드 예시
C++은 메모리를 수동으로 관리한다는 특성 때문에 많은 제약을 가지고 있는데요, 그 예시중 하나가 다음 메모리 할당 코드의 예시입니다.
// MemoryPool.cpp
class MemoryPool {
private:
struct FreeNode {
FreeNode* next;
};
alignas(alignof(T)) char buffer[sizeof(T) * PoolSize];
FreeNode* freeList;
public:
MemoryPool() {
freeList = reinterpret_cast<FreeNode*>(buffer);
FreeNode* current = freeList;
for (std::size_t i = 1; i < PoolSize; ++i) {
current->next = reinterpret_cast<FreeNode*>(buffer + i * sizeof(T));
current = current->next;
}
current->next = nullptr;
}
T* allocate() {
if (!freeList) throw std::bad_alloc();
T* ptr = reinterpret_cast<T*>(freeList);
freeList = freeList->next;
return ptr;
}
void deallocate(T* ptr) {
FreeNode* node = reinterpret_cast<FreeNode*>(ptr);
node->next = freeList;
freeList = node;
}
};
// main.cpp
struct MyObject {
int x;
MyObject(int x) : x(x) {
std::cout << "Constructed: " << x << std::endl;
}
~MyObject() {
std::cout << "Destructed: " << x << std::endl;
}
};
int main() {
MemoryPool<MyObject> pool;
MyObject* obj = pool.allocate();
new (obj) MyObject(42);
std::cout << "Value: " << obj->x << std::endl;
obj->~MyObject();
pool.deallocate(obj);
return 0;
}
코드 설명
해당 코드는 MemoryPool이라는 class로부터 특정 T 객체를 입력받아 메모리를 할당하는 예시인데요, main 메서드르를 보면 int 타입(4바이트)를 가진 MyObject 구조체를 MemoryPool에 넘겨서 메모리를 할당하도록 되어있습니다.
문제는, MemoryPool<MyObject> 가 선언이 될 때, T로 받은 타입의 크기(4바이트)를 buffer에 할당하게 되는데요, 이후 생성자 내부에서 reinterpret_cast<FreeNode*>(buffer)를 통해 명시적 캐스팅을 하는 시점에 FreeNode*(8바이트)를 할당하게 되면서 SIGSEGV이슈가 발생하게 되었습니다.
이슈 해결
해당 이슈의 경우, 할당받은 메모리(4바이트)보다 많은 메모리(8바이트)를 할당했기 때문에 발생한 SIGSEGV이슈인데요, 해결 방법은 다음과 같습니다.
- reinterpret_cast<FreeNode*>를 써서 next를 쓰면 → 8바이트 오버런 발생
FreeNode* current = reinterpret_cast<FreeNode*>(buffer + i * sizeof(T));
current->next = ...; // ❌ 잘못된 메모리 접근: T는 4바이트인데 8바이트 씀
- union 변경으로 충분한 공간 확보 + 정렬 보장
union Node {
Node* next; // 8바이트
alignas(T) char data[sizeof(T)]; // 최소 sizeof(T)
};
- 기존 구조: 위험한 reinterpret_cast
buffer = [T][T][T]... (각각 sizeof(T), 예: 4B)
↑
reinterpret_cast<FreeNode*> → 8B write → overflow!
- union 구조: 안전하게 공유
Node = union { next: 8B, data: T(4B) } → 전체 8B 이상 확보됨
nodes = [Node][Node][Node]... (각 슬롯이 최소 8B)
↑
safe to store next or T object
SIGSEGV가 발생할 수 있는 다른 상황 예시
1. 널 포인터 역참조
int* ptr = nullptr;
*ptr = 42; // ❌ SIGSEGV
// 동적 할당 실수
// 리턴 값 확인 안 함
2. 해제된 메모리 접근 (Use After Free)
int* ptr = new int(5);
delete ptr;
*ptr = 10; // ❌ 이미 해제된 메모리에 쓰기
// double delete, dangling pointer로 이어지기도 함
3. 배열 인덱스 초과 (Out of Bounds)
int arr[5];
arr[10] = 42; // ❌ 경계 벗어남
// C++에서는 배열 범위 체크 안 해줍니다 → 직접 조심해야 함
4. 잘못된 포인터 캐스팅 (reinterpret_cast)
struct A { int a; };
struct B { double b; };
A a;
B* b = reinterpret_cast<B*>(&a);
std::cout << b->b << std::endl; // ❌ alignment 문제로 SIGSEGV 가능
5. 스택 오버플로우 (무한 재귀 등)
void f() {
f(); // ❌ 스택 한계 초과 → SIGSEGV
}
6. nullptr로 delete
int* ptr = nullptr;
delete ptr; // ✅ delete nullptr은 안전하지만 이후에 다시 접근하면 ❌
// delete는 안전하지만, 그 후 *ptr 같은 접근은 위험
7. 스택 변수의 주소를 넘겼는데 이미 파괴된 경우 (Dangling reference)
int* getPtr() {
int local = 10;
return &local; // ❌ local은 함수 종료 시 사라짐
}
int* p = getPtr();
std::cout << *p << std::endl; // ❌ SIGSEGV 가능
8. misaligned access (정렬 위반)
alignas(8) struct A { double d; };
char buffer[sizeof(A)];
A* a = reinterpret_cast<A*>(buffer); // ❌ buffer가 8바이트 정렬이 아닐 수 있음
a->d = 3.14; // ❌ SIGSEGV on ARM or strict architectures
9. 연산된 포인터가 잘못된 메모리로 가는 경우
int* arr = new int[5];
int* p = arr + 1000;
*p = 10; // ❌ 접근 권한 없음 → SIGSEGV
10. 다중 스레드 환경에서의 데이터 레이스 → 잘못된 포인터 접근
- 스레드 A가 delete한 포인터를 스레드 B가 사용 → 흔한 멀티스레드 버그
- 디버깅 어렵고 간헐적으로 SIGSEGV 발생

C++의 특성
C++을 초기에 사용할 때 가장 먼저 마주치는 오류인듯 한데요, Java 기반의 언어를 주로 다뤄왔기 때문에 메모리를 수동으로 관리하는 C의 특성을 잘 이해하지 못 해서 해당 오류를 마주치게 되었습니다.
C++의 경우, C 언어와 마찬가지로 메모리를 수동으로 관리하는 특성을 가지고 있는데요, 다음과 같이 포인터(*)를 선언하는 행위를 통해 메모리에 직접 접근할 수 있습니다.
struct FreeNode {
FreeNode* next;
};
코드 예시
C++은 메모리를 수동으로 관리한다는 특성 때문에 많은 제약을 가지고 있는데요, 그 예시중 하나가 다음 메모리 할당 코드의 예시입니다.
// MemoryPool.cpp
class MemoryPool {
private:
struct FreeNode {
FreeNode* next;
};
alignas(alignof(T)) char buffer[sizeof(T) * PoolSize];
FreeNode* freeList;
public:
MemoryPool() {
freeList = reinterpret_cast<FreeNode*>(buffer);
FreeNode* current = freeList;
for (std::size_t i = 1; i < PoolSize; ++i) {
current->next = reinterpret_cast<FreeNode*>(buffer + i * sizeof(T));
current = current->next;
}
current->next = nullptr;
}
T* allocate() {
if (!freeList) throw std::bad_alloc();
T* ptr = reinterpret_cast<T*>(freeList);
freeList = freeList->next;
return ptr;
}
void deallocate(T* ptr) {
FreeNode* node = reinterpret_cast<FreeNode*>(ptr);
node->next = freeList;
freeList = node;
}
};
// main.cpp
struct MyObject {
int x;
MyObject(int x) : x(x) {
std::cout << "Constructed: " << x << std::endl;
}
~MyObject() {
std::cout << "Destructed: " << x << std::endl;
}
};
int main() {
MemoryPool<MyObject> pool;
MyObject* obj = pool.allocate();
new (obj) MyObject(42);
std::cout << "Value: " << obj->x << std::endl;
obj->~MyObject();
pool.deallocate(obj);
return 0;
}
코드 설명
해당 코드는 MemoryPool이라는 class로부터 특정 T 객체를 입력받아 메모리를 할당하는 예시인데요, main 메서드르를 보면 int 타입(4바이트)를 가진 MyObject 구조체를 MemoryPool에 넘겨서 메모리를 할당하도록 되어있습니다.
문제는, MemoryPool<MyObject> 가 선언이 될 때, T로 받은 타입의 크기(4바이트)를 buffer에 할당하게 되는데요, 이후 생성자 내부에서 reinterpret_cast<FreeNode*>(buffer)를 통해 명시적 캐스팅을 하는 시점에 FreeNode*(8바이트)를 할당하게 되면서 SIGSEGV이슈가 발생하게 되었습니다.
이슈 해결
해당 이슈의 경우, 할당받은 메모리(4바이트)보다 많은 메모리(8바이트)를 할당했기 때문에 발생한 SIGSEGV이슈인데요, 해결 방법은 다음과 같습니다.
- reinterpret_cast<FreeNode*>를 써서 next를 쓰면 → 8바이트 오버런 발생
FreeNode* current = reinterpret_cast<FreeNode*>(buffer + i * sizeof(T));
current->next = ...; // ❌ 잘못된 메모리 접근: T는 4바이트인데 8바이트 씀
- union 변경으로 충분한 공간 확보 + 정렬 보장
union Node {
Node* next; // 8바이트
alignas(T) char data[sizeof(T)]; // 최소 sizeof(T)
};
- 기존 구조: 위험한 reinterpret_cast
buffer = [T][T][T]... (각각 sizeof(T), 예: 4B)
↑
reinterpret_cast<FreeNode*> → 8B write → overflow!
- union 구조: 안전하게 공유
Node = union { next: 8B, data: T(4B) } → 전체 8B 이상 확보됨
nodes = [Node][Node][Node]... (각 슬롯이 최소 8B)
↑
safe to store next or T object
SIGSEGV가 발생할 수 있는 다른 상황 예시
1. 널 포인터 역참조
int* ptr = nullptr;
*ptr = 42; // ❌ SIGSEGV
// 동적 할당 실수
// 리턴 값 확인 안 함
2. 해제된 메모리 접근 (Use After Free)
int* ptr = new int(5);
delete ptr;
*ptr = 10; // ❌ 이미 해제된 메모리에 쓰기
// double delete, dangling pointer로 이어지기도 함
3. 배열 인덱스 초과 (Out of Bounds)
int arr[5];
arr[10] = 42; // ❌ 경계 벗어남
// C++에서는 배열 범위 체크 안 해줍니다 → 직접 조심해야 함
4. 잘못된 포인터 캐스팅 (reinterpret_cast)
struct A { int a; };
struct B { double b; };
A a;
B* b = reinterpret_cast<B*>(&a);
std::cout << b->b << std::endl; // ❌ alignment 문제로 SIGSEGV 가능
5. 스택 오버플로우 (무한 재귀 등)
void f() {
f(); // ❌ 스택 한계 초과 → SIGSEGV
}
6. nullptr로 delete
int* ptr = nullptr;
delete ptr; // ✅ delete nullptr은 안전하지만 이후에 다시 접근하면 ❌
// delete는 안전하지만, 그 후 *ptr 같은 접근은 위험
7. 스택 변수의 주소를 넘겼는데 이미 파괴된 경우 (Dangling reference)
int* getPtr() {
int local = 10;
return &local; // ❌ local은 함수 종료 시 사라짐
}
int* p = getPtr();
std::cout << *p << std::endl; // ❌ SIGSEGV 가능
8. misaligned access (정렬 위반)
alignas(8) struct A { double d; };
char buffer[sizeof(A)];
A* a = reinterpret_cast<A*>(buffer); // ❌ buffer가 8바이트 정렬이 아닐 수 있음
a->d = 3.14; // ❌ SIGSEGV on ARM or strict architectures
9. 연산된 포인터가 잘못된 메모리로 가는 경우
int* arr = new int[5];
int* p = arr + 1000;
*p = 10; // ❌ 접근 권한 없음 → SIGSEGV
10. 다중 스레드 환경에서의 데이터 레이스 → 잘못된 포인터 접근
- 스레드 A가 delete한 포인터를 스레드 B가 사용 → 흔한 멀티스레드 버그
- 디버깅 어렵고 간헐적으로 SIGSEGV 발생