드라이버에서 문자열을 처리하다 보면 C 표준 문자열 처리 함수인 strcpy()sprintf() 같은 함수를 사용할 때가 있습니다. 하지만 DDK에서는 안전한 문자열 함수(Safe String Functions)를 사용하라고 적극 권장하고 있습니다.

이 안전한 문자열 함수는 ntstrsafe.h에 정의되어 있습니다. 모두 RtlString으로 시작하는데, 이 함수들은 Cb와 Cch 두 계열로 나뉘어져 있습니다.

Cb는 byte-counted라는 뜻이며, 바이트 수를 기준으로 문자열을 처리합니다. Cch는 character-counted라는 뜻이며, 문자 수를 기준으로 문자열을 처리합니다.

그런데 이 함수들도 자세히 살펴 보면 A와 W 계열로 또다시 나뉘어 집니다. A는 ANSI 함수이고 W는 Wide character 즉 유니코드 함수를 뜻합니다. 이렇게 해서 Cb A, Cb W 그리고 Cch A, Cch W 이렇게 4종류가 있는 것입니다.

이렇게 복잡하게 나누어 놓은 이유는 바로 유니코드(Wide character) 때문입니다. 윈도우에서는 유니코드를 UTF-16 인코딩으로 저장합니다. UTF-16은 16비트 즉 2바이트로 글자 하나를 처리합니다. 그래서 영문자든 한글이든 한자든 일본어든 모두 2바이트로 처리합니다(UTF-16이라도 유니코드 테이블 65535자를 넘어간 희귀한 한자(漢字)의 경우 2바이트로 처리하지 못하지만, 이런 경우는 거의 찾아보기 힘들기 때문에 무시합니다).

참고로 인터넷에서 많이 쓰이는 유니코드의 UTF-8 인코딩은 가변 바이트 방식입니다. 영문자의 경우 1바이트, 유럽에서 쓰이는 문자는 2바이트, 한글, 한자, 일본어 등은 3바이트 이고, 유니코드 테이블에 따라 4바이트 5바이트 6바이트로 글자 하나를 표현하고 있습니다.

그러면 이제 코드를 통해서 Cb와 Cch 함수의 차이점을 살펴보겠습니다.

VOID
RtlStringA()
{
    CHAR str[10] = "Hello";
    CHAR str2[10] = "안녕";
    ULONG length;

    RtlStringCchLengthA(str, 100, &length); // 5 글자
    RtlStringCbLengthA(str, 100, &length); // 5 바이트(1바이트 문자이므로)

    RtlStringCchLengthA(str2, 100, &length); // 4 글자
    RtlStringCbLengthA(str2, 100, &length); // 4 바이트

    Length = sizeof(str); // 10

    RtlStringCchPrintfA(str, 4, "World");
    // Wor\0 널문자 포함 4글자

    RtlStringCbPrintfA(str, 4, "World");
    // Wor\0 널문자 포함 4바이트
    // 1바이트 문자이므로 W, o, r 각 1바이트, 널문자 1바이트

    RtlStringCchPrintfA(str2, 5, "안녕");
    // 안녕\0 널문자 포함 5글자

    RtlStringCbPrintfA(str2, 5, "안녕");
    // 안녕\0 널문자 포함 5바이트
    // 2바이트 문자이므로 안, 녕 각 2바이트, 널문자 1바이트
}

먼저 ANSI 계열 함수부터 살펴보도록 하겠습니다.

RtlStringCchLengthA()RtlStringCbLengthA()로 Hello의 길이를 구하면 모두 5가 나옵니다. 영문자 1글자는 1바이트이므로 Cch로 5글자, Cb로 5바이트가 나온 것입니다. 이렇게 길이를 구할 때에는 \0(널문자)는 포함하지 않습니다.

이번에는 RtlStringCchLengthA()RtlStringCbLengthA()로 안녕의 길이를 구하면 모두 4가 나옵니다. 한글은 MBCS(Multi-byte character set) 방식으로 처리되는데 한글 1글자를 2바이트로 처리합니다. 그런데 우리 눈에는 안녕이 두글자로 보이지만 ANSI 계열 함수는 영문자를 기준으로 처리하기 때문에 Cch로 4글자, Cb로 4바이트가 나옵니다.

RtlStringCchPrintfA(str, 4, "World")로 World에서 4글자를 str로 복사해 보면 str에는 Wor\0이 들어가게 됩니다. Wor 3글자에 \0(널문자) 1글자 포함헤서 4글자를 복사하게 됩니다. 마찬가지로 RtlStringCbPrintfA(str, 4, "World")는 World에서 4바이트를 str에 복사했으므로 str에는 Wor\0이 들어갑니다. 1바이트에 1글자니 당연하겠죠.

RtlStringCchPrintfA(str2, 5, "안녕")는 안녕에서 5글자를 str2로 복사했는데, 한글은 글자 하나가 2바이트로 처리되지만 앞서 말했듯이 ANSI 함수에서는 영문자 기준으로 처리해서 안 2글자, 녕 2글자, \0(널문자) 1글자 총 5글자를 복사하게 됩니다.

RtlStringCbPrintfA(str2, 5, "안녕")은 5바이트를 복사하므로 str2에는 안 2바이트, 녕 2바이트 널문자 1바이트, 총 5바이트 즉 안녕\0이 들어갑니다.

여기서 주의해야 할 점은 RtlStringCchPrintfA(str2, 4, "안녕")을 해보면 영문자 기준 안 2글자, 녕의 앞부분 절반 1글자, \0(널문자) 1글자 이렇게 4글자가 되어 녕은 앞 부분만 가져오기 때문에 글자가 깨지게 됩니다. 물론 RtlStringCbPrintfA(str2, 4, "안녕")를 하더라도 녕이 짤리는 것은 똑같겠죠.

VOID
RtlStringW()
{
    WCHAR wStr[10] = L"Hello";
    WCHAR wStr2[10] = L"안녕";
    ULONG length;

    RtlStringCchLengthW(wStr, 100, &length); // 5 글자
    RtlStringCbLengthW(wStr, 100, &length); // 10 바이트(2바이트 문자이므로)

    RtlStringCchLengthW(wStr2, 100, &length); // 2 글자
    RtlStringCbLengthW(wStr2, 100, &length); // 4 바이트(2바이트 문자이므로)

    Length = sizeof(wStr); // 20

    RtlStringCchPrintfW(wStr, 4, L"World");
    // Wor\0 널문자 포함 4글자

    RtlStringCbPrintfW(wStr, 4, L"World");
    // W\0 널문자 포함 4바이트
    // 2바이트 문자이므로 W도 2바이트 널문자도 2바이트이기 때문

    RtlStringCchPrintfW(wStr2, 4, L"안녕");
    // 안녕\0 \0 널문자 포함 4글자

    RtlStringCbPrintfW(wStr2, 4, L"안녕");
    // 안\0 널문자 포함 4바이트
    // 2바이트 문자이므로 '안'도 2바이트 널문자도 2바이트이기 때문
}

이번에는 유니코드 함수를 살펴보겠습니다.

Hello의 길이를 조사해 보면 Cch 함수는 5글자, Cb 함수는 10바이트가 나왔습니다. 앞서 설명했듯이 영문자든 한글이든 한자든 유니코드는 2바이트로 처리하기 때문에 Hello는 문자개수로는 5글자 바이트 수로는 10바이트가 나오는 것입니다.

안녕의 길이를 조사해보면 Cch 함수는 2글자 Cb 함수는 4바이트가 나왔습니다. 2바이트씩 2글자가 들어가 있으므로 2글자가 나왔고, 바이트 수는 4바이트가 나옵니다.

WCHAR 형은 unsigned short를 재정의 한 것인데 short는 2바이트(16비트)를 저장할 수 있습니다. 그래서 이 변수 1개에 유니코드 영문자든 한글이든 1글자씩 들어가는 것입니다.

RtlStringCchPrintfW(wStr, 4, L"World")는 널문자 포함 4글자 Wor\0이 됩니다. RtlStringCbPrintfW(wStr, 4, L"World")는 W\0이 되는데 W 2바이트 \0(널문자) 2바이트 해서 4바이트가 됩니다. 유니코드(UTF-16)에서는 당연히 널문자도 2바이트입니다.

RtlStringCchPrintfW(wStr2, 4, L"안녕")는 안, 녕, \0, \0 이렇게 4글자인데, 마지막 널문자는 메모리상에 0이 있으면 저렇게 나올 것이고 만약 다른 값이 있으면 다른 값이 복사될 것입니다. 그래도 안녕 뒤에 널문자가 하나 있으므로 문제는 없습니다.

RtlStringCbPrintfW(wStr2, 4, L"안녕")는 안\0인데, 안 2바이트 그리고 널문자 2바이트 해서 4바이트입니다.

참고로 RtlStringXXXLengthX 함수의 두번째 매개변수를 설명하자면, Cch 함수의 경우 STRSAFE_MAX_CCH (2147483647)가 최대 값이며 이 값을 넘을 수는 없습니다. Cb 함수는 A와 W 함수가 각각 차이점이 있는데, A 함수는 STRSAFE_MAX_CCH * sizeof(char)sizeof(char)는 1이므로 STRSAFE_MAX_CCH 그대로이고, W 함수는 STRSAFE_MAX_CCH * sizeof(WCHAR) 즉 sizeof(WCHAR)가 2이므로 STRSAFE_MAX_CCH * 2가 최대값이 됩니다.

그리고 이 안전한 문자열 함수들은 PASSIVE_LEVEL에서만 사용할 수 있습니다.

이렇게 안전한 문자열 함수에서 Cb와 Cch의 차이점에 대해서 알아보았습니다. 드라이버를 만들때 용도에 맞게 골라쓰면 되겠습니다.


저작권 안내

이 웹사이트에 게시된 모든 글의 무단 복제 및 도용을 금지합니다.
  • 블로그, 게시판 등에 퍼가는 것을 금지합니다.
  • 비공개 포스트에 퍼가는 것을 금지합니다.
  • 글 내용, 그림을 발췌 및 요약하는 것을 금지합니다.
  • 링크 및 SNS 공유는 허용합니다.

Published

29 September 2007