strcpy ... 이런 함수 쓰면 절대 안된다. strncpy를 써야지...
wsprintf ... 이런 함수도 쓰면 안된다. _snprintf를 써야지... (리눅스 같으면 snprintf)
모든 문자열 함수에 경계체크 하는 타입과 하지 않는 타입이 있어서, 하지 않는 타입을 사용하면 문자열이 깨지기도 프로그램이 죽기도 하고 경우에 따라 해킹을 당할 수도 있다. 하도 보안 보안 하다보니 모 이런건 이제 기본이 되었다.

근데...  경계 체크를 하는 함수들도.. 경계 체크하는 방식이 조금씩 차이가 있어서...
lstrcpyn 사용하는 식으로 _snprintf를 쓰다 보면 문제가 생길 때가 있다.

이 이슈는... 예전부터 머리속에 새기고 필요할 때마다 인터넷을 뒤져보던 주제이지만,
사실 일도 많고 귀찮다 보니.. 꼼꼼하게 따지지 않고 그냥 적당~히 넘어가는 것이 사실이다.
에이 설마~ 하던 문제가 오늘 터졌다.. ㅋㅋ
그래서 이번엔 내가 직접 함 정리해보려 한다.

※ 아래의 샘플과 테스트 들은 모두 MBCS 환경을 가정한다.
    유니코드일 때도 달라질 일은 없다.
   - 그냥 바이트수와 글자수를 헷갈리지 말고,
   - T-Type 함수와 T-TYPE 데이터를 사용한다.
      (strXXX 대신 _tcsXXX를 쓴다. CHAR 대신 TCHAR를 사용한다. 등등)


샘플코드다.
#ifndef _countof
#define    _countof(X)    sizeof(X)/sizeof(X[0])
#endif

int main(int argc, char* argv[])
{
    CHAR    szBuf[4] = {0,};
   
    // 1. strncpy
    memset (szBuf, 'X', sizeof(szBuf));
    strncpy(szBuf, "AAAAAAAAA", _countof(szBuf) -1);
    printf("strncpy : %c%c%c%c\r\n", szBuf[0], szBuf[1], szBuf[2], szBuf[3]);

    // 2. lstrcpyn
    memset (szBuf, 'X', sizeof(szBuf));
    lstrcpyn(szBuf, "AAAAAAAAA", _countof(szBuf) -1);
    printf("lstrcpyn : %c%c%c%c\r\n", szBuf[0], szBuf[1], szBuf[2], szBuf[3]);

    // 3. _snprintf
    memset (szBuf, 'X', sizeof(szBuf));
    _snprintf(szBuf, _countof(szBuf) -1, "%s", "AAAAAAAAA");
    printf("_snprintf : %c%c%c%c\r\n", szBuf[0], szBuf[1], szBuf[2], szBuf[3]);

    // 4. strncat
    memset (szBuf, 'X', sizeof(szBuf));
    szBuf[0] = NULL;
    strncat(szBuf, "AAAAAAAAA", _countof(szBuf) -1);
    printf("strncat : %c%c%c%c\r\n", szBuf[0], szBuf[1], szBuf[2], szBuf[3]);

    return 0;
} 


4글자 짜리 버퍼에 네가지 문자열 함수로 문자열을 복사해넣되, 버퍼사이즈는 전부 3으로 준다.
그리고 버퍼 4바이트를 한바이트씩 프린트해 본다. (스트링으로 프린트하는게 아님)
어떤 일이 벌어질까?

실행 결과는 다음과 같다. (A는 복사된 문자열, X는 버퍼 초기화된 값이 남아있는 것이다.)
strncpy : AAAX
lstrcpyn : AA X
_snprintf : AAAX
strncat : AAA
Press any key to continue

결과를 하나씩 분석해 보자.

1. strncpy, _snprintf
이 두가지 함수에서 지정된 버퍼사이즈는 곧 복사할 최대 글자수를 의미한다. 버퍼사이즈를 넘어서는 길이의 문자열을 복사하려고 시도할 경우 지정된 글자수만큼만 복사된다.
말하자면... Null로 스트링을 Terminate 시켜주는 따위의 일은 하지 않는다는 것이다. 따라서 이 문자열 함수를 쓸때 글자수는 버퍼사이즈 - 1 만큼을 지정하고, 수동으로 스트링 종결 처리를 해주어야 한다.
다음의 두가지 중 한가지 패턴을 지켜주어야 한다.

// 방법 1 : 일단 전체 버퍼를 초기화시켜 놓고 최대 "버퍼 사이즈 - 1" 글자 만큼만 복사하는 방법
CHAR  szBuf[1024] = {0,}; // 혹은 memcpy(szBuf, NULL, sizeof(szBuf)); 해준다.
strncpy(szBuf, lpSrcStr, _countof(szBuf) - 1);

// 방법 2 : 문자열을 복사한 후 마지막 글자를 NULL로 덮어써서 수동으로 문자열을 Terminate 시켜준다.
CHAR  szBuf[1024] = {0,};
strncpy(szBuf, lpSrcStr, _countof(szBuf));
szBuf[_countof(szBuf) - 1] = NULL;

만약 스트링 종결처리를 안해주었을 땐...버퍼사이즈를 넘어서는 문자열이 입력되었을 때 스트링이 종결되지 않고, 뒤에 쓰레기 값이 붙게 된다. (상기 실행결과를 살펴보라.)

물론.. 입력 문자열이 버퍼를 넘어서지 않을 때는 문제될 게 없다. 하지만, 항상 좋은 입력만 들어올 거라고 누가 장담하겠는가? 심지어 GUI에서 입력문자열 제한을 하는 경우에도 문자열 핸들링시 버퍼체크는 반드시 해주어야 한다.


2. lstrcpyn
가장 쓰기 편한 함수이다. 이 함수에서 지정된 버퍼사이즈에는 말 그대로 버퍼사이즈를 주면 된다.
원본 문자열이 버퍼 사이즈를 초과할 경우, 자동으로 "버퍼사이즈 - 1" 글자 만큼만 복사한 후 나머지 한글자를 NULL로 Terminate 시켜준다. 상기 실행결과를 보면, 버퍼사이즈를 3글자를 주었는데, 결과는 2글자만 복사되고 3번째에는 NULL이 들어가서 문자열이 종결된 것을 알 수 있다.


3. strncat
이 함수는... 좀 특이하다. "n"은 버퍼사이즈가 아니라 복사할 최대 글자수를 의미한다. Src문자열의 길이가 지정된 최대 글자수를 넘어설 경우, 지정된 글자수만큼을 복사한 후 그 뒤에 NULL을 append해준다... 말하자면... 최대 글자수를 N으로 지정했을 경우, 실제로는 N+1 바이트가 복사될 수도 있다는 의미이다. 상기 실행결과를 보면 3글자를 복사했는데, 4번째 글자가 NULL로 덮어씌워졌음을 알 수 있다. 따라서 그냥 생각없이 버퍼 사이즈만큼을 지정해주게 되면... 1바이트 오버플로우가 발생할 수 있다.
게다가 문자열 Concat 함수이기 때문에 다른 함수들 처럼 버퍼사이즈를 생각없이 써주는 게 아니라... 버퍼 중 현재 사용된 사이즈를 제외하고, 실제로 더 쓸 수 있는 바이트 수를 계산하여 다음과 같이 해주어야 한다.

    CHAR    szBigBuf[10] = {'a','b','c','d'};
    CHAR*    lpTest = "TESTSTRING";
    strncat (szBigBuf, lpTest, _countof(szBigBuf) - strlen(szBigBuf) - 1);
         // 덧붙여질 NULL을 고려하여 마지막에 -1을 해준다.
    printf ("%s\r\n", szBigBuf);


... 하여간 복잡한 함수다.


C에서 문자열을 다룬다는 것은... 정말 쉽지 않은 주제인 것 같다.
처음엔 멋도 모르고 쓰다가... 포인터를 이해하게 되고... 그다음엔 버퍼 체크의 개념을 알게 되고, (그땐 내가 문자열을 정복한 줄 알았지 ^^) 그 다음엔 유니코드를 알게 되고... 유니코드를 알고 나면 거꾸로 MBCS 다루기가 얼마나 어려운지를 알게 된다...

아 정말 개발이란 심오해.




Posted by kuaaan

댓글을 달아 주세요

  1. 헐...감동 2010.07.31 23:18  댓글주소  수정/삭제  댓글쓰기

    으헝허허헝허허헣허허헣 정말 감사해요 ㅜㅜ.. 이거찾느라 한참고생했는데 떡하고 나오넹 ㅜㅜ 으헝ㅎ어헣어허ㅓㅎ 정말 감사해요

  2. ranma 2010.11.24 17:26  댓글주소  수정/삭제  댓글쓰기

    vs6에서 snprintf를 아무리 쓰려해도 안되더군요.
    _snprintf랑은 틀리죠.
    그런 기능을 하는 함수가 있을거 같아서 헤매다가
    여기까지 왔네요. lstrcpyn 좋은거 알고갑니다.
    감사합니다.

  3. ranma 2010.11.24 22:26  댓글주소  수정/삭제  댓글쓰기

    테스트해보니..
    가변인자 처리를 제외하면 snprintf는 lstrcpyn과 같습니다.
    역시 가변인자 처리를 제외하면 _snprintf는 strncpy와 같구요.
    윈도우에 lstrcpyn기능을 하면서 가변인자 처리까지 해주는 함수는 없을까요?

    • kuaaan 2010.11.25 09:12 신고  댓글주소  수정/삭제

      _snprintf (_snwprintf)는 가변인자를 사용할 수 있습니다. NULL터미네이트 처리도 해주고요.

      아래 MSDN의 샘플코드 중 _snprintf가 사용된 부분을 참고하세요. ^^
      http://msdn.microsoft.com/en-us/library/2ts7cx93(VS.80).aspx

  4. ranma 2010.11.26 09:00  댓글주소  수정/삭제  댓글쓰기

    NULL터미네이트 처리는 안해주는듯합니다.
    다음 테스트의 결과는 ***def입니다.

    TCHAR szTest[4] = _T("***");
    TCHAR szRet[7] = _T("abcdef");

    _snwprintf(szRet, 3, szTest);

    • kuaaan 2010.11.26 09:35 신고  댓글주소  수정/삭제

      1. strncpy, _snprintf
      이 두가지 함수에서 지정된 버퍼사이즈는 곧 복사할 최대 글자수를 의미한다. 버퍼사이즈를 넘어서는 길이의 문자열을 복사하려고 시도할 경우 지정된 글자수만큼만 복사된다.
      말하자면... Null로 스트링을 Terminate 시켜주는 따위의 일은 하지 않는다는 것이다. 따라서 이 문자열 함수를 쓸때 글자수는 버퍼사이즈 - 1 만큼을 지정하고, 수동으로 스트링 종결 처리를 해주어야 한다.
      ==> 그렇군요. 제가 NULL터미네이트 처리 안해준다고 포스팅 해놓고 까먹었네요. 제가 헷갈린 것 같습니다.
      전 요즘엔 StringCchPrintf 류의 함수들을 주로 쓰기 때문에... CRL 계열의 문자열 함수는 써본지가 오래 되었습니다. ^^

  5. ranma 2010.11.26 15:16  댓글주소  수정/삭제  댓글쓰기

    _tcs* 함수들보다 StringCch* 함수들이 더 최근에 나온거죠?
    MSDN을 보니 XP SP2이상부터 지원하는거 같군요.
    제가 관리하는 고객들의 PC는 98은 몰라도 아직 2000쓰는데가 많을거 같네요.

    • kuaaan 2010.11.26 15:42 신고  댓글주소  수정/삭제

      음.. MSDN엔 정말로 그렇게 나와 있네요.
      제가 개발하는 제품은 StringCchXXX 류의 함수로 도배되어 있는데, Win2000에서 잘 동작합니다.
      어쩌면 제가 Side-By-Side 문제때문에 CRL을 Static으로 포함해서 빌드하기 때문에 별 문제없이 동작하는지도 모르겠네요.

  6. ranma 2010.11.26 16:21  댓글주소  수정/삭제  댓글쓰기

    가상PC로 실행해보니 98에서도 잘 되네요.
    MSDN에 언급된 최소사양이 뭘 말하는건지 모르겟군요.



사랑합니다. 편안히 잠드소서