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
,


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