Microsoft MVP성태의 닷넷 이야기
글쓴 사람
정성태 (techsharer at outlook.com)
홈페이지
첨부 파일
(연관된 글이 1개 있습니다.)
(시리즈 글이 12개 있습니다.)
Windows: 103. 작업 관리자에서의 "Commit size"가 가리키는 메모리의 의미
; https://www.sysnet.pe.kr/2/0/1850

.NET Framework: 492. .NET CLR Memory 성능 카운터의 의미
; https://www.sysnet.pe.kr/2/0/1852

.NET Framework: 626. Commit 메모리가 낮은 상황에서도 메모리 부족(Out-of-memory) 예외 발생
; https://www.sysnet.pe.kr/2/0/11110

VC++: 107. VirtualAlloc, HeapAlloc, GlobalAlloc, LocalAlloc, malloc, new의 차이점
; https://www.sysnet.pe.kr/2/0/11152

Windows: 136. Memory-mapped File은 Private Bytes 크기에 포함될까요?
; https://www.sysnet.pe.kr/2/0/11159

.NET Framework: 845. C# - 윈도우 작업 관리자와 리소스 모니터의 메모리 값을 구하는 방법
; https://www.sysnet.pe.kr/2/0/11950

Windows: 211. Windows - (commit이 아닌) reserved 메모리 사용량 확인 방법
; https://www.sysnet.pe.kr/2/0/13133

.NET Framework: 2073. C# - VMMap처럼 스택 메모리의 reserve/guard/commit 상태 출력
; https://www.sysnet.pe.kr/2/0/13174

.NET Framework: 2074. C# - 스택 메모리에 대한 여유 공간 확인하는 방법
; https://www.sysnet.pe.kr/2/0/13180

Linux: 57. C# - 리눅스 프로세스 메모리 정보
; https://www.sysnet.pe.kr/2/0/13221

닷넷: 2322. C# - 프로세스 메모리 중 Private Working Set 크기를 구하는 방법(성능 카운터, WMI)
; https://www.sysnet.pe.kr/2/0/13889

닷넷: 2323. C# - 프로세스 메모리 중 Private Working Set 크기를 구하는 방법(Win32 API)
; https://www.sysnet.pe.kr/2/0/13890




C# - 스택 메모리에 대한 여유 공간 확인하는 방법

이에 대해서는 다음의 문서에서 힌트를 얻을 수 있습니다.

Determining approximately how much stack space is available, part 1
; https://devblogs.microsoft.com/oldnewthing/20200609-00/?p=103847

Determining approximately how much stack space is available, part 2
; https://devblogs.microsoft.com/oldnewthing/20200610-00/?p=103855

간단하게 정리해 볼까요? ^^

우선, part 1에서는 다음의 코드로 설명하고 있습니다.

__declspec(noinline)
bool is_stack_available(size_t amount)
{
  __try {
    _alloca(amount); // 스택 할당 시도 후,
    return true;
  } __except (
    GetExceptionCode() == EXCEPTION_STACK_OVERFLOW
              ? EXCEPTION_EXECUTE_HANDLER
              : EXCEPTION_CONTINUE_SEARCH) {
    _resetstkoflw(); // EXCEPTION_STACK_OVERFLOW가 발생하면 _resetstkoflw 처리
    return false;
  }
}

위의 방법은, 어쨌든 EXCEPTION_STACK_OVERFLOW에 대한 처리는 할 수 있지만 아쉽게도 스택의 reserve 메모리 영역이 모두 commit된다는 차이점이 있습니다. 실제로 다음과 같이 간단하게 코드를 만들어,

int main()
{
    printf("threadid: %d", GetCurrentThreadId());
    is_stack_available(1024 * 1024);
    getchar();
}

실행하면, VMMap으로 아래와 같이 확인할 수 있습니다.

thread_stack_valid_1.png

즉, 애써 Guard 페이지를 사용해가며 working set을 줄이려한 노력을 물거품으로 만들어 버리는 것입니다.




그다음 소개하는 part 2의 코드는 Win32 API의 도움을 받아 Guard 페이지를 보호하면서 계산하는 방법을 소개하고 있습니다.

__declspec(noinline)
bool is_stack_available(size_t amount)
{
  ULONG_PTR low, high;
  GetCurrentThreadStackLimits(&low, &high);
  auto remaining = reinterpret_cast<ULONG_PTR>(&low) - low;
  if (remaining > high - low) {
    __fastfail(FAST_FAIL_INCORRECT_STACK);
  }
  return remaining >= amount;
}

위의 함수는 스택에 할당된 로컬 변수 low의 주소를 기준으로 GetCurrentThreadStackLimits 내에 amount 만큼의 공간이 있는지 확인하고 있습니다. 물론, low 주소는 컴파일러가 별도의 스택 공간을 확보하거나, 아니면 home space 또는 심지어 red zone을 사용하는 것도 가능하므로 정확한 계산이라고 볼 수는 없습니다.

또한, 스택이 최대로 자랐을 때 마지막 Guard 공간으로 남을 영역까지 모두 포함하고 있기 때문에 이에 대한 보정도 필요합니다. 재미있게도 이에 대한 크기를 알 수 있는 방법이 있는데,

SetThreadStackGuarantee
; https://learn.microsoft.com/en-us/windows/win32/api/processthreadsapi/nf-processthreadsapi-setthreadstackguarantee

Windows XP/Server 2008부터 제공하던 것이니 GetCurrentThreadStackLimits를 쓸 수 있는 환경이면 문제될 것이 없습니다. 그래서 이에 대한 계산까지 C#으로 포팅해 보면,

[DllImport("kernel32.dll")]
static extern void GetCurrentThreadStackLimits(out ulong LowLimit, out ulong HightLimit);

[DllImport("kernel32.dll")]
[return: MarshalAs(UnmanagedType.Bool)]
static extern bool SetThreadStackGuarantee(ref ulong StackSizeInBytes);

[MethodImpl(MethodImplOptions.NoInlining)]
static unsafe bool IsStackAvailable(ulong amount)
{
    ulong low, high;
    nuint lowAddr = new nuint(&low);

    GetCurrentThreadStackLimits(out low, out high);
    var remaining = (ulong)lowAddr - low;
    if (remaining > high - low)
    {
        Environment.FailFast("FAST_FAIL_INCORRECT_STACK");
    }

    ulong guarantee = 0;
    SetThreadStackGuarantee(ref guarantee);

    return remaining >= (amount + guarantee);
}

그런대로 매끄럽게 작성됐습니다. 간단하게 테스트를 해볼까요? ^^

static unsafe void Main(string[] args)
{
    ulong max = 0;

    for (ulong i = 0x1000; i < (1024 * 1024 * 2); i++)
    {
        if (IsStackAvailable(i) == false)
        {
            Console.WriteLine(i);
            break;
        }

        max = i;
    }

    byte* buffer = stackalloc byte[(int)max];
}

실행해 보면, 아쉽게도 이런 식으로 예외가 발생합니다.

1550649
Stack overflow.
   at Program.Main(System.String[])

아무래도 Managed 환경의 특성상 약간의 보정이 필요해 보이는데요, 간단하게 guarantee를 2배로 잡는 식으로 조정하고,

static unsafe bool IsStackAvailable(ulong amount)
{
    // ...[생략]...

    return remaining >= (amount + guarantee * 2);
}

다시 실행하면 이제는 "Stack overflow" 예외가 발생하지 않습니다. 하지만, stackalloc 하위에 다음과 같이 간단한 Console.WriteLine이라도 포함하게 되면,

// ...[생략]...

byte* buffer = stackalloc byte[(int)max];
Console.WriteLine(buffer[0]);

이제는 다시 stack overflow 예외가 발생합니다.

1533817
Stack overflow.
   at System.Number.UInt32ToDecStr(UInt32)
   at System.IO.TextWriter.Write(Int32)
   at System.IO.TextWriter.WriteLine(Int32)
   at System.IO.TextWriter+SyncTextWriter.WriteLine(Int32)
   at System.Console.WriteLine(Int32)
   at Program.Main(System.String[])

가용한 스택 공간까지 할당한 다음, 이후의 Console.WriteLine을 실행하는 중 callstack을 따라 할당되는 스택 공간을 감당하지 못한 것입니다. 따라서, 상황에 따라 스택에 대한 여유 공간 확인은 넉넉하게 해야 합니다.




위의 코드를 현실적으로 사용할 만한 시나리오라면... 재귀 호출이 있는 경우 디버깅 용으로 넣어두는 것도 좋을 듯합니다. 예를 들어 이렇게 코드를 넣어두면,

static unsafe void Main(string[] args)
{
    Console.WriteLine(CallRecursive(0));
}

// 재귀 함수 중에서도 tail call 최적화가 이뤄지면 스택 체크가 무의미
unsafe static int CallRecursive(int count)
{
    if (IsStackAvailable(0x1000) == false) // 안전하게 4KB는 남도록 체크
    {
        return count;
    }

    Console.WriteLine(count);
    CallRecursive(count + 1);

    return count;
}

stack overflow까지 가기 전에 CallRecursive 재귀 호출을 안전하게 탈출할 수 있습니다. 물론, 0x1000의 체크와 IsStackAvailable 호출로 인한 간섭으로 실제 호출할 수 있는 재귀의 깊이보다 낮아질 수 있다는 점은 염두에 두어야 합니다.

(첨부 파일은 이 글의 예제 코드를 포합합니다.)




part 2 글을 보면 재미있는 설명이 하나 더 있습니다. C/C++의 경우 is_stack_available 함수가 인라인되는 것을 막아야 하는 사례로 coroutine을 이야기 합니다. 왜냐하면 coroutine의 경우 C/C++ 컴파일러가 로컬 변수의 사용 범위에 따라 stack이 아닌 heap에도 놓일 수 있도록 최적화하는 것이 가능하기 때문입니다. 이로 인해 "low" 로컬 변수도 컴파일러의 재량에 따라 heap에 놓이게 되면 GetCurrentThreadStackLimits 범위에 속하지 않아 결국 is_stack_available 함수는 오동작하게 됩니다.

마지막으로 아래의 글은 stack overflow가 발생한 사례 분석을 보여줍니다.

The case of the stack overflow exception when the stack is nowhere near overflowing
; https://devblogs.microsoft.com/oldnewthing/20220204-00/?p=106219

위의 경우는 스택 메모리가 아직 남아 있는데도 불구하고 stack overflow가 발생할 수 있음을 보여줍니다. Job에 소속돼 제한적인 메모리를 할당받은 상태에서 Job에 속해 있는 다른 프로세스의 메모리 과다 사용으로 인해 reserve 상태의 스택 메모리를 commit할 수 없어 stack overflow가 발생한 것입니다.

아마도... 이 정도만 알아두시면 향후 stack overflow 예외가 발생했을 때 적절한 대응을 할 수 있을 것입니다. ^^




[이 글에 대해서 여러분들과 의견을 공유하고 싶습니다. 틀리거나 미흡한 부분 또는 의문 사항이 있으시면 언제든 댓글 남겨주십시오.]

[연관 글]






[최초 등록일: ]
[최종 수정일: 12/3/2022]

Creative Commons License
이 저작물은 크리에이티브 커먼즈 코리아 저작자표시-비영리-변경금지 2.0 대한민국 라이센스에 따라 이용하실 수 있습니다.
by SeongTae Jeong, mailto:techsharer at outlook.com

비밀번호

댓글 작성자
 




... [181]  182  183  184  185  186  187  188  189  190  191  192  193  194  195  ...
NoWriterDateCnt.TitleFile(s)
458정성태2/12/200721880.NET Framework: 81. LINQ 개발 환경 설정 [1]
457정성태2/8/200726334.NET Framework: 80. LINQ 관련 용어 정리 및 리소스 소개 [2]
456정성태2/6/200724587Windows: 22. 가상화에 대해서.
455정성태2/4/200719484오류 유형: 25. 원격 데스크톱 환경에서의 Virtual Server 관리 환경 제어
454정성태2/4/200716921오류 유형: 24. VPC에서의 Vista 네트워킹 문제
509손대성6/24/200719016    답변글 오류 유형: 24.1. [답변]: 오류 유형 : 23. VPC 에서의 Vista 네트워킹 문제
453정성태2/4/200725085개발 환경 구성: 21. 서버 측 SoapExtension을 클라이언트에 알리고 싶다
452정성태1/31/200724801VC++: 31. 비스타에서 VS.NET 2005로 COM 프로젝트 빌드시 오류 [2]
451정성태1/31/200723967Windows: 21. Preview Handler 소개
450정성태1/30/200732260VS.NET IDE: 43. .NET에서의 필수 무결성 제어 조절하는 방법 - Manifest 파일 이용파일 다운로드2
449정성태2/4/200729094Windows: 20. UAC 이모저모 [2]
448정성태1/28/200724746Windows: 19. 3가지 유형의 가젯 프로그램
447정성태1/27/200721957Windows: 18. 비스타 도구 - 사양 정보 및 도구(Performance Information and Tools)
446정성태1/27/200730880VC++: 30. 필수 무결성 제어를 조절하는 방법(2) - 직접 코딩파일 다운로드1
445정성태2/8/200729473VC++: 29. 필수 무결성 제어를 조절하는 방법(1) - Manifest 파일 이용파일 다운로드2
444정성태1/27/200722887VC++: 28. 비스타 응용 프로그램 개발을 위한 VS.NET 2005 환경 설정
443정성태1/26/200721383VC++: 27. COM 개체로 인해 IE 7 비스타 버전이 종료될 때 오류 화면이 뜬다면?파일 다운로드1
442정성태1/24/200724341.NET Framework: 79. 새로운 암호화 클래스 (ECDsaCng, ECDiffieHellmanCng) 소개 [1]
441정성태1/23/200729077Windows: 17. 보안 데스크톱에서 활성화되지 않은 UAC 창이 안전할까?
440정성태1/24/200723251.NET Framework: 78. C# 3.0 - Anonymous types [1]
439정성태1/25/200724209.NET Framework: 77. C# 3.0 - Lambda 표현식 [1]
438정성태1/24/200723634.NET Framework: 76. C# 3.0 - 확장 함수
437정성태1/23/200731138Windows: 16. 개발자를 위한 UAC 환경 설정 [3]
436정성태1/17/200720289VS.NET IDE: 42. Orcas 2007년 1월 CTP 버전 설치 [5]
435정성태1/14/200719933기타: 17. 베타 제품과 최종 제품은 다르다 [2]
434정성태2/4/200723554Windows: 15. MIC 환경 구성 - Windows XP와 유사한 보안 설정 [4]
... [181]  182  183  184  185  186  187  188  189  190  191  192  193  194  195  ...