C언어 문자열을 가능한한 피해야하는 이유

C++에서  C 언어 스타일로 — 메모리 할당만 빼고 — 문자열을 복사하는 코드다. 여기에는 버그가 숨어있다.

void some_function(const char* str) {
    int len = ::strlen(str); // str은 안전한 문자열임
    char* pBuffer = new char[len];
    ::memset(pBuffer, 0, len);
    ::strcpy_s(pBuffer, len, str); // 원본 코드는 ::memcpy_s 였음
    // blah blah blah. uses pBuffer

어제 코드베이스에 같이 일하는 팀과 연동하기 위해 받아온 리소스 로더 코드를 넣었다. 위의 코드는 거기에 있던 유틸리티 함수 격인 코드에 있던 일부분 — 을 버그있는 부분만 축약한 — 이다. (앞 뒤로 훨씬 많은 코드가 있다) 코드 베이스에 통합하고나서 리소스를 읽어오는 코드를 부르는 유닛 테스트를 넣었다1 . 그러고나니 유닛 테스트를 하다가 랜덤하게 프로세스가 죽는다. 처음에는 읽어들인 데이터를 잘못 처리하고 있나 의심하고, 죽는 테스트만 남겨봤는데 또 문제가 없다(…).

반복해본 결과 fixture2 부분의 오류라는 것을 알게 되었다 — 거기서 리소스를 읽고/지우고를 수행했다.

일단 리소스 로더 없이 커밋하고, 오늘 오후에 저 부분을 테스트하기 시작했는데, 디버거를 붙이면 정상 동작하고3 없으면 죽어서 일단 재연에 중점을 두고 재연에는 성공. 리소스를 읽고/지우고를 수십~수천번 반복하다보면 죽더라.

그래서 버그가 있다고 의심되는 부분에서 빼면 버그가 안나는 코드를 차근차근 줄여서 — 소위 델타 디버깅;delta debugging — 결국 위 코드가 포함된 리소스 길이 파서의 유틸리티 함수에서 오류가 나는 것을 확인.

실제로 저 코드는 “\0″를 복사하지 않는 버그가 있다. strcpy_s는 주어진 버퍼 길이 이상으로는 복사하지 않는 MS 확장함수인데4 \0를 복사 안하는 만행(?)을 저지르게 된다…  그리고 \0 이 없는 C 문자열을 각종 C 문자열 함수를 써서 조작해댔으니 힙 공간이 어떻게 오염될지는 Orz 게다가 이 동작은 이전 힙 공간의 메모리 상태에 완전히 의존적이기 때문에 거의 종잡을 수 없는 동작을(…).

여담으로, 위 코드가 strcpy를 썼다면,  흔히 “끝을 지나서 쓰는 문제” 인데, native code 언어라고 불리우는 언어에서 가장 악명높은 문제를 일으켰을 것이다5. C 언어 문자열의 가장 큰 문제 중 하나가 “문자열이 NULL로 끝난다"라는 전제 조건이 있기 때문에 길이단위 k인 문자열을 저장하기 위해서는 실제로 k+1 단위길이의 공간이 필요하다. 저 위에서는 k 만큼만 할당하게 되고, 마지막 문자(=널문자)가 할당되지 않아야할 힙공간을 덮어 쓰게된다.

여튼, 힙 공간은 보통 약간의 패딩/태깅이 있고, 그 덕분에 위 버그가 잘 안드러나다가(……..), 결국엔 펑하고 터져서 “왜 여기서 죽냐” 싶은 버그가 발생; 사실 DevPartner나 valgrind가 있으면 쉽게 잡힐 문제였는데 64bit OS에서 DevPartner는 동작하지 않고 / Valgrind는 linux 용 소프트웨어라서 Orz

위에서 생긴 버그처럼 “직관적으로 인식되지 않는 버그"를 쉽게 만들 수 있기 때문에, 정말 번개 같은 속도가 필요한게 아니면 C 문자열은 제발 안 써줬으면 좋겠다. std::string / std::wstring 같이 귀여운(?) 녀석들을 놔두고 이게 무슨 삽질이야.[^6]

최종적으론 len = strlen(str) 대신에 len = strlen(str) + 1로 고쳐놓고 퇴근했다.

결론: 문자열 처리 성능이 말도 안되게 중요한게 아니면 C++의 std::string / std::wstring을 쓰자.

+ 성능이 중요하다면 정말 중요한지 다시 생각하고, 가능하면 C++의 문자열을 쓰자(…).


  1. 이 간단한 추가(?)로 버그를 찾을 수 있게 된건 기쁘긴하다. ↩︎

  2. 테스트 묶음(suite)을 실행될 때 개별 테스트 시작 전에 초기화 되고 / 개별 테스트 완료 후에 폐기되는 데이터들 ↩︎

  3. : MS VisualStudio는 디버거가 있을 때와 없을 대 heap 할당 방식이 다르다. ↩︎

  4. 개인적으로는 그냥 strncpy를 쓰던가 std::string을 쓰는걸 추천. ↩︎

  5. 그래서 C#, Java, Python 등의 언어가 각광을 받는 것이기도 하고. ↩︎