SAL 이해

MICROSOFT SAL(소스 코드 주석 언어)은 함수가 매개 변수를 사용하는 방법, 매개 변수에 대해 가정하는 방법 및 완료 시 보장을 설명하는 데 사용할 수 있는 주석 집합을 제공합니다. 주석은 헤더 파일에 <sal.h>정의됩니다. C++용 Visual Studio 코드 분석은 SAL 주석을 사용하여 함수 분석을 수정합니다. Windows 드라이버 개발을 위한 SAL 2.0에 대한 자세한 내용은 Windows 드라이버용 SAL 2.0 주석을 참조 하세요.

기본적으로 C 및 C++는 개발자가 의도와 분산을 일관되게 표현할 수 있는 제한된 방법만 제공합니다. SAL 주석을 사용하면 함수를 사용하는 개발자가 함수를 사용하는 방법을 더 잘 이해할 수 있도록 함수를 더 자세히 설명할 수 있습니다.

SAL의 정의 및 이를 사용해야 하는 이유

간단히 말해서 SAL은 컴파일러가 코드를 검사 수 있도록 하는 저렴한 방법입니다.

코드의 품질을 높이는 SAL

SAL을 사용하면 사용자와 코드 분석 도구 모두에서 코드 디자인을 더 쉽게 이해할 수 있습니다. C 런타임 함수 memcpy를 보여 주는 이 예제를 고려합니다.

void * memcpy(
   void *dest,
   const void *src,
   size_t count
);

이 함수가 수행하는 작업을 알 수 있나요? 함수를 구현하거나 호출하는 경우 프로그램 정확성을 보장하기 위해 특정 속성을 기본 확인해야 합니다. 예제의 선언과 같은 선언을 보는 것만으로는 해당 선언이 무엇인지 알 수 없습니다. SAL 주석이 없으면 설명서 또는 코드 주석을 사용해야 합니다. 설명서의 설명서는 memcpy 다음과 같습니다.

" src에서 dest의 복사본 개수 바이트 수입니다wmemcpy.memcpy 복사본 개수 와이드 문자(2바이트). 소스와 대상이 겹치는 경우 memcpy의 동작이 정의되지 않습니다. memmove를 사용하면 겹치는 영역을 처리할 수 있습니다.
중요: 대상 버퍼의 크기가 원본 버퍼보다 크거나 같은지 확인합니다. 자세한 내용은 버퍼 오버런 방지를 참조하세요."

설명서에는 코드가 프로그램 정확성을 보장하기 위해 특정 속성을 기본 확인해야 함을 암시하는 몇 가지 정보가 포함되어 있습니다.

  • memcpycount 원본 버퍼에서 대상 버퍼로 바이트를 복사합니다.

  • 대상 버퍼는 원본 버퍼만큼 커야 합니다.

그러나 컴파일러는 설명서 또는 비공식 주석을 읽을 수 없습니다. 두 버퍼 count간에 관계가 있다는 것을 알지 못하며 관계에 대해 효과적으로 추측할 수도 없습니다. SAL은 다음과 같이 함수의 속성 및 구현에 대한 더 명확한 설명을 제공할 수 있습니다.

void * memcpy(
   _Out_writes_bytes_all_(count) void *dest,
   _In_reads_bytes_(count) const void *src,
   size_t count
);

이러한 주석은 설명서의 정보와 유사하지만 더 간결하며 의미 체계 패턴을 따릅니다. 이 코드를 읽을 때 이 함수의 속성과 버퍼 오버런 보안 문제를 방지하는 방법을 빠르게 이해할 수 있습니다. 또한 SAL에서 제공하는 의미 체계 패턴은 잠재적 버그를 조기에 검색할 때 자동화된 코드 분석 도구의 효율성과 효율성을 향상시킬 수 있습니다. 누군가가 다음 버그 wmemcpy구현을 작성한다고 상상해 보십시오.

wchar_t * wmemcpy(
   _Out_writes_all_(count) wchar_t *dest,
   _In_reads_(count) const wchar_t *src,
   size_t count)
{
   size_t i;
   for (i = 0; i <= count; i++) { // BUG: off-by-one error
      dest[i] = src[i];
   }
   return dest;
}

이 구현에는 일반적인 오프-바이-일 오류가 포함됩니다. 다행히 코드 작성자가 SAL 버퍼 크기 주석을 포함했습니다. 코드 분석 도구는 이 함수만 분석하여 버그를 catch할 수 있습니다.

SAL 기본

SAL은 사용 패턴으로 분류되는 네 가지 기본 종류의 매개 변수를 정의합니다.

범주 매개 변수 주석 설명
호출된 함수에 대한 입력 _In_ 데이터는 호출된 함수에 전달되고 읽기 전용으로 처리됩니다.
호출된 함수에 대한 입력 및 호출자에 대한 출력 _Inout_ 사용 가능한 데이터가 함수에 전달되고 잠재적으로 수정됩니다.
호출자에게 출력 _Out_ 호출자는 호출된 함수가 쓸 공간만 제공합니다. 호출된 함수는 해당 공간에 데이터를 씁니다.
호출자에 대한 포인터 출력 _Outptr_ 호출자에 대한 출력과 같습니다. 호출된 함수에서 반환되는 값은 포인터입니다.

이러한 네 가지 기본 주석은 다양한 방법으로 보다 명시적으로 만들 수 있습니다. 기본적으로 주석이 추가된 포인터 매개 변수는 필수로 간주됩니다. 함수가 성공하려면 NULL이 아닌 매개 변수여야 합니다. 기본 주석의 가장 일반적으로 사용되는 변형은 포인터 매개 변수가 선택 사항임을 나타냅니다. NULL인 경우 함수는 여전히 해당 작업을 성공적으로 수행할 수 있습니다.

이 표에서는 필수 매개 변수와 선택적 매개 변수를 구분하는 방법을 보여 있습니다.

매개 변수가 필요합니다. 매개 변수는 선택 사항입니다.
호출된 함수에 대한 입력 _In_ _In_opt_
호출된 함수에 대한 입력 및 호출자에 대한 출력 _Inout_ _Inout_opt_
호출자에게 출력 _Out_ _Out_opt_
호출자에 대한 포인터 출력 _Outptr_ _Outptr_opt_

이러한 주석은 초기화되지 않은 가능한 값과 잘못된 null 포인터 사용을 공식적이고 정확한 방식으로 식별하는 데 도움이 됩니다. 필요한 매개 변수에 NULL을 전달하면 충돌이 발생하거나 "실패한" 오류 코드가 반환될 수 있습니다. 어느 쪽이든 함수는 해당 작업을 수행하는 데 성공할 수 없습니다.

SAL 예제

이 섹션에서는 기본 SAL 주석에 대한 코드 예제를 보여 줍니다.

Visual Studio 코드 분석 도구를 사용하여 오류 찾기

예제에서 Visual Studio Code 분석 도구는 SAL 주석과 함께 사용하여 코드 결함을 찾습니다. 방법은 다음과 같습니다.

Visual Studio 코드 분석 도구 및 SAL을 사용하려면

  1. Visual Studio에서 SAL 주석이 포함된 C++ 프로젝트를 엽니다.

  2. 메뉴 모음에서 솔루션에서 빌드, 코드 분석 실행을 선택합니다.

    이 섹션의 _In_ 예제를 생각해 보세요. 코드 분석을 실행하면 이 경고가 표시됩니다.

    C6387 잘못된 매개 변수 값 'pInt'는 '0'일 수 있습니다. 이 값은 'InCallee' 함수의 사양을 준수하지 않습니다.

예: _In_ 주석

주석은 _In_ 다음을 나타냅니다.

  • 매개 변수는 유효해야 하며 수정되지 않습니다.

  • 함수는 단일 요소 버퍼에서만 읽습니다.

  • 호출자는 버퍼를 제공하고 초기화해야 합니다.

  • _In_ 는 "읽기 전용"을 지정합니다. 일반적인 실수는 주석이 있어야 _Inout_ 하는 매개 변수에 적용 _In_ 하는 것입니다.

  • _In_ 가 허용되지만 포인터가 아닌 스칼라의 분석기는 무시합니다.

void InCallee(_In_ int *pInt)
{
   int i = *pInt;
}

void GoodInCaller()
{
   int *pInt = new int;
   *pInt = 5;

   InCallee(pInt);
   delete pInt;
}

void BadInCaller()
{
   int *pInt = NULL;
   InCallee(pInt); // pInt should not be NULL
}

이 예제에서 Visual Studio Code 분석을 사용하는 경우 호출자가 Null이 아닌 포인터를 초기화된 버퍼에 pInt전달하는지 확인합니다. 이 경우 포인터는 pInt NULL일 수 없습니다.

예: _In_opt_ 주석

_In_opt_은 입력 매개 변수가 NULL이 되도록 허용되므로 함수가 이에 대해 검사 것을 제외하고는 동일합니다_In_.

void GoodInOptCallee(_In_opt_ int *pInt)
{
   if(pInt != NULL) {
      int i = *pInt;
   }
}

void BadInOptCallee(_In_opt_ int *pInt)
{
   int i = *pInt; // Dereferencing NULL pointer 'pInt'
}

void InOptCaller()
{
   int *pInt = NULL;
   GoodInOptCallee(pInt);
   BadInOptCallee(pInt);
}

Visual Studio Code Analysis는 함수가 버퍼에 액세스하기 전에 NULL에 대해 검사 유효성을 검사합니다.

예: _Out_ 주석

_Out_ 는 요소 버퍼를 가리키는 NULL이 아닌 포인터가 전달되고 함수가 요소를 초기화하는 일반적인 시나리오를 지원합니다. 호출자는 호출 전에 버퍼를 초기화할 필요가 없습니다. 호출된 함수는 반환되기 전에 초기화할 것을 약속합니다.

void GoodOutCallee(_Out_ int *pInt)
{
   *pInt = 5;
}

void BadOutCallee(_Out_ int *pInt)
{
   // Did not initialize pInt buffer before returning!
}

void OutCaller()
{
   int *pInt = new int;
   GoodOutCallee(pInt);
   BadOutCallee(pInt);
   delete pInt;
}

Visual Studio Code 분석 도구는 호출자가 NULL이 아닌 포인터를 버퍼에 pInt 전달하고 버퍼가 반환되기 전에 함수에 의해 초기화되어 있는지 확인합니다.

예: _Out_opt_ 주석

_Out_opt_은 매개 변수가 NULL로 허용되므로 함수가 이에 대해 검사 것을 제외하고는 동일합니다_Out_.

void GoodOutOptCallee(_Out_opt_ int *pInt)
{
   if (pInt != NULL) {
      *pInt = 5;
   }
}

void BadOutOptCallee(_Out_opt_ int *pInt)
{
   *pInt = 5; // Dereferencing NULL pointer 'pInt'
}

void OutOptCaller()
{
   int *pInt = NULL;
   GoodOutOptCallee(pInt);
   BadOutOptCallee(pInt);
}

Visual Studio Code Analysis는 이 함수가 역참조되기 전에 pInt NULL에 대해 검사 있는지, NULL이 아닌 경우 pInt 버퍼가 반환되기 전에 함수에 의해 초기화되는지 확인합니다.

예: _Inout_ 주석

_Inout_ 는 함수에 의해 변경될 수 있는 포인터 매개 변수에 주석을 추가하는 데 사용됩니다. 포인터는 호출 전에 유효한 초기화된 데이터를 가리킬 수 있어야 하며, 변경되더라도 반환 시 유효한 값이 있어야 합니다. 주석은 함수가 원 요소 버퍼에서 자유롭게 읽고 쓸 수 있도록 지정합니다. 호출자는 버퍼를 제공하고 초기화해야 합니다.

참고 항목

_Inout_ 다음과 같이 _Out_수정 가능한 값에 적용해야 합니다.

void InOutCallee(_Inout_ int *pInt)
{
   int i = *pInt;
   *pInt = 6;
}

void InOutCaller()
{
   int *pInt = new int;
   *pInt = 5;
   InOutCallee(pInt);
   delete pInt;
}

void BadInOutCaller()
{
   int *pInt = NULL;
   InOutCallee(pInt); // 'pInt' should not be NULL
}

Visual Studio Code Analysis는 호출자가 NULL이 아닌 포인터를 초기화된 버퍼에 pInt전달하고 반환 pInt 하기 전에 여전히 NULL이 아니고 버퍼가 초기화되어 있는지 확인합니다.

예: _Inout_opt_ 주석

_Inout_opt_은 입력 매개 변수가 NULL이 되도록 허용되므로 함수가 이에 대해 검사 것을 제외하고는 동일합니다_Inout_.

void GoodInOutOptCallee(_Inout_opt_ int *pInt)
{
   if(pInt != NULL) {
      int i = *pInt;
      *pInt = 6;
   }
}

void BadInOutOptCallee(_Inout_opt_ int *pInt)
{
   int i = *pInt; // Dereferencing NULL pointer 'pInt'
   *pInt = 6;
}

void InOutOptCaller()
{
   int *pInt = NULL;
   GoodInOutOptCallee(pInt);
   BadInOutOptCallee(pInt);
}

Visual Studio Code Analysis는 이 함수가 버퍼에 액세스하기 전에 NULL에 대해 검사, NULL이 아닌 경우 pInt 반환하기 전에 함수에 의해 버퍼가 초기화되는지 확인합니다.

예: _Outptr_ 주석

_Outptr_ 는 포인터를 반환하기 위한 매개 변수에 주석을 추가하는 데 사용됩니다. 매개 변수 자체는 NULL이 아니어야 하며 호출된 함수는 NULL이 아닌 포인터를 반환하고 해당 포인터는 초기화된 데이터를 가리킵니다.

void GoodOutPtrCallee(_Outptr_ int **pInt)
{
   int *pInt2 = new int;
   *pInt2 = 5;

   *pInt = pInt2;
}

void BadOutPtrCallee(_Outptr_ int **pInt)
{
   int *pInt2 = new int;
   // Did not initialize pInt buffer before returning!
   *pInt = pInt2;
}

void OutPtrCaller()
{
   int *pInt = NULL;
   GoodOutPtrCallee(&pInt);
   BadOutPtrCallee(&pInt);
}

Visual Studio Code Analysis는 호출자가 NULL이 아닌 포인터를 *pInt전달하고 버퍼가 반환되기 전에 함수에 의해 초기화되어 있는지 확인합니다.

예: _Outptr_opt_ 주석

_Outptr_opt_ 은 매개 변수가 선택 사항이라는 점을 제외하고와 동일합니다 _Outptr_. 호출자는 매개 변수에 대한 NULL 포인터를 전달할 수 있습니다.

void GoodOutPtrOptCallee(_Outptr_opt_ int **pInt)
{
   int *pInt2 = new int;
   *pInt2 = 6;

   if(pInt != NULL) {
      *pInt = pInt2;
   }
}

void BadOutPtrOptCallee(_Outptr_opt_ int **pInt)
{
   int *pInt2 = new int;
   *pInt2 = 6;
   *pInt = pInt2; // Dereferencing NULL pointer 'pInt'
}

void OutPtrOptCaller()
{
   int **ppInt = NULL;
   GoodOutPtrOptCallee(ppInt);
   BadOutPtrOptCallee(ppInt);
}

Visual Studio Code Analysis는 이 함수가 이전에 *pInt NULL에 대해 검사 역참조되고 버퍼가 반환되기 전에 함수에 의해 초기화되는지 확인합니다.

예: _Success_ Annotation in Combination with _Out_

주석은 대부분의 개체에 적용할 수 있습니다. 특히 전체 함수에 주석을 달 수 있습니다. 함수의 가장 명백한 특징 중 하나는 성공 또는 실패할 수 있다는 것입니다. 그러나 버퍼와 해당 크기 간의 연결과 마찬가지로 C/C++는 함수 성공 또는 실패를 표현할 수 없습니다. 주석을 _Success_ 사용하여 함수의 성공을 말할 수 있습니다. 주석에 대한 _Success_ 매개 변수는 true일 때 함수가 성공했음을 나타내는 식일 뿐입니다. 식은 주석 파서가 처리할 수 있는 모든 항목이 될 수 있습니다. 함수가 반환된 후 주석의 효과는 함수가 성공한 경우에만 적용할 수 있습니다. 이 예제에서는 옳은 _Out_ 일을 하기 위해 상호 작용하는 방법을 _Success_ 보여 줍니다. 키워드(keyword) return 사용하여 반환 값을 나타낼 수 있습니다.

_Success_(return != false) // Can also be stated as _Success_(return)
bool GetValue(_Out_ int *pInt, bool flag)
{
   if(flag) {
      *pInt = 5;
      return true;
   } else {
      return false;
   }
}

이 주석으로 _Out_ 인해 Visual Studio Code Analysis는 호출자가 NULL이 아닌 포인터를 버퍼에 pInt전달하고 버퍼가 반환되기 전에 함수에 의해 초기화되어 있는지 확인합니다.

SAL 유용한 정보

기존 코드에 주석 추가

SAL은 코드의 보안 및 안정성을 개선하는 데 도움이 되는 강력한 기술입니다. SAL을 학습한 후에는 새 기술을 일상 업무에 적용할 수 있습니다. 새 코드에서는 전체 디자인에서 SAL 기반 사양을 사용할 수 있습니다. 이전 코드에서는 주석을 증분 방식으로 추가하여 업데이트할 때마다 혜택을 늘릴 수 있습니다.

Microsoft 공용 헤더에 이미 주석이 추가되었습니다. 따라서 프로젝트에서 Win32 API를 호출하는 리프 노드 함수 및 함수에 먼저 주석을 추가하여 가장 많은 이점을 얻는 것이 좋습니다.

주석을 달아야 하는 경우

다음은 몇 가지 지침입니다.

  • 모든 포인터 매개 변수에 주석을 추가합니다.

  • 코드 분석에서 버퍼 및 포인터 안전을 보장할 수 있도록 값 범위 주석에 주석을 추가합니다.

  • 잠금 규칙 및 잠금 부작용에 주석을 추가합니다. 자세한 내용은 잠금 동작에 주석을 추가하세요.

  • 드라이버 속성 및 기타 do기본 관련 속성에 주석을 추가합니다.

또는 모든 매개 변수에 주석을 추가하여 의도를 명확히 하고 주석이 완료되었음을 쉽게 검사 수 있습니다.

참고 항목