Why should I know this?

C++ new 연산자 null 체크는 필요한가? 본문

Study

C++ new 연산자 null 체크는 필요한가?

die4taoam 2023. 2. 8. 23:49

최근에 면접을 보러 다니는데 제목과 같은 내용을 질문받아서 흥미롭다는 생각을 했습니다.

이 글은 해당 내용에 대해 다루고자 합니다.

 

1. LLVM libcxx 에 구현되어 있는 new

_LIBCPP_WEAK
void *
operator new(std::size_t size) _THROW_BAD_ALLOC
{
    if (size == 0)
        size = 1;
    void* p;
    while ((p = ::malloc(size)) == nullptr)
    {
        // If malloc fails and there is a new_handler,
        // call it to try free up memory.
        std::new_handler nh = std::get_new_handler();
        if (nh)
            nh();
        else
#ifndef _LIBCPP_NO_EXCEPTIONS
            throw std::bad_alloc();
#else
            break;
#endif
    }
    return p;
}

 

2. C++ 코드 + IR 로 보는 new 연산자의 호출

보시다시피 new 는 내부적으로 malloc을 호출합니다..!? 이런 구조가 어떻게 가능한지 생각해봅니다.

초간단 C++코드 입니다.

class cls {
int a;
};

int main()
{
 cls *c;
 c = new cls();
}

 

그리고 그 IR 입니다.

; Function Attrs: mustprogress noinline norecurse optnone uwtable
define dso_local noundef i32 @main() #0 {
  %1 = alloca ptr, align 8
  %2 = call noalias noundef nonnull ptr @_Znwm(i64 noundef 4) #3
  call void @llvm.memset.p0.i64(ptr align 4 %2, i8 0, i64 4, i1 false)
  store ptr %2, ptr %1, align 8
  ret i32 0
}

 

_Znwm = new 연산자의 mangled name 입니다.

new cls(); 의 호출은 말 그대로 new 라는 함수를 호출하게 됩니다.

이 함수가 바로 1에서 봤던 libcxx 에 포함된 operator new(std::size_t size) _THROW_BAD_ALLOC 입니다.

 

호출 과정을 디버거로 한번 보죠.

(gdb) disas main
Dump of assembler code for function main():
   0x00000000000007d4 <+0>:     sub     sp, sp, #0x20
   0x00000000000007d8 <+4>:     stp     x29, x30, [sp, #16]
   0x00000000000007dc <+8>:     add     x29, sp, #0x10
   0x00000000000007e0 <+12>:    mov     x0, #0x4                        // #4
   0x00000000000007e4 <+16>:    bl      0x670 <_Znwm@plt>

 

Breakpoint 1, main () at test.cpp:8
8        c = new cls();
Breakpoint 1, main () at test.cpp:9
9        c = new cls();
(gdb) si
0x0000aaaaaec60670 in operator new(unsigned long)@plt ()
(gdb) 
0x0000aaaaaec60674 in operator new(unsigned long)@plt ()
(gdb) 
0x0000aaaaaec60678 in operator new(unsigned long)@plt ()
(gdb) 
0x0000aaaaaec6067c in operator new(unsigned long)@plt ()
(gdb) 
0x0000ffff8d7232b0 in operator new(unsigned long) () from /lib/aarch64-linux-gnu/libstdc++.so.6
(gdb) 

보시는 것처럼 libstdc++.so.6 에 구현되어 있는 new 를 호출합니다.

 

우선 정리하자면 operator new는 단순히 지정된 class 의 사이즈를 malloc 하고 결과를 반환합니다.

단 메모리 할당에 문제가 생겼을 때는 "지정된 작업"을 하게 됩니다.

 

몇 번의 재할당 시도 후에 "일반적인 경우 = 예외를 사용하도록 설정된 경우" 에는  std::bad_alloc(); 을 throw하게 되어 있죠.

 

3. 모든 C++ 라이브러리가 같을까?

여기서 std::bad_alloc()의 throw는 구현하기 나름이라는 겁니다. 그래서 아마 gnu c++과 mscv c++의 동작은 미묘하지만 다를 수 있습니다. 다음은 안드로이드 올드 버전에 구현되어 있는 new 연산자입니다.

 

void __libc_fatal(const char* format, ...) {
  va_list args;
  va_start(args, format);
  __libc_fatal(format, args);
  va_end(args);
  abort();
}

 

void* operator new(std::size_t size) {
    void* p = malloc(size);
    if (p == NULL) {
        __libc_fatal("new failed to allocate %zu bytes", size);
    }
    return p;
}

 

보시다시피 new 연산자가 실패할 경우 abort() 시켜버립니다. (뭐지 이건?)

 

 

결론을 내자면 new 연산자는 OS에 패키지로 포함된 라이브러리 (libstdc++ 같은)의 new 함수를 호출하게 되어 있으며

우리가 사용하는 일반적인 범용 환경에서는 null 체크를 할 필요 없이 bad_alloc 에 대한 예외처리를 해야 합니다.

 

하지만, 이는 환경마다 차이가 있으므로 주의가 필요합니다.

또한 C++의 특성상 new와 같은 연산자는 오버로드 가능하죠.

 

ps1. 이제야 왜 android에서 작성된 C++코드 중에 일부에 항상 메모리 관리자가 함께 구현되어 있는지 의문이 풀렸네요.

ps2. 메모리 관리가 엄격한 환경 (특히 모바일/Embedded)에서는 자체 메모리 관리자를 함께 갖추는걸 고려하는게 좋은 방향일지도 모르겠습니다. (ps1을 생각했을때)

 

 

'Study' 카테고리의 다른 글

Program Analysis (??)  (0) 2023.05.18
백업 - Nodejs Uftrace  (0) 2023.03.13
Shuffler Fast and Deployable Continuous  (0) 2023.02.05
Control Flow Integrity for COTS Binaries  (0) 2023.02.04
Class linking 메커니즘 - jvm  (0) 2022.09.30
Comments