programing tip

재귀 호출이 다른 스택 깊이에서 StackOverflow를 발생시키는 이유는 무엇입니까?

itbloger 2020. 10. 25. 11:44
반응형

재귀 호출이 다른 스택 깊이에서 StackOverflow를 발생시키는 이유는 무엇입니까?


나는 C # 컴파일러가 테일 호출을 처리하는 방법을 실습하려고했습니다.

(답변 : 그렇지 않습니다. 그러나 64 비트 JIT 는 TCE (테일 콜 제거)를 수행합니다. 제한 사항이 적용됩니다 .)

그래서 StackOverflowException프로세스를 죽이기 전에 호출되는 횟수를 출력하는 재귀 호출을 사용하여 작은 테스트를 작성했습니다 .

class Program
{
    static void Main(string[] args)
    {
        Rec();
    }

    static int sz = 0;
    static Random r = new Random();
    static void Rec()
    {
        sz++;

        //uncomment for faster, more imprecise runs
        //if (sz % 100 == 0)
        {
            //some code to keep this method from being inlined
            var zz = r.Next();  
            Console.Write("{0} Random: {1}\r", sz, zz);
        }

        //uncommenting this stops TCE from happening
        //else
        //{
        //    Console.Write("{0}\r", sz);
        //}

        Rec();
    }

큐에서 프로그램은 다음 중 하나에서 SO 예외로 끝납니다.

  • '빌드 최적화'끄기 (디버그 또는 릴리스)
  • 대상 : x86
  • 대상 : AnyCPU + "Prefer 32 bit"(VS 2012의 새로운 기능이며 처음으로 보았습니다. 자세한 내용은 여기 )
  • 코드에서 겉보기에 무해한 분기 (주석 'else'분기 참조).

반대로 '빌드 최적화'ON + (대상 = x64 또는 '32 비트 선호 '가 OFF 인 AnyCPU (64 비트 CPU에서))를 사용하면 TCE가 발생하고 카운터가 계속 회전합니다 (좋아요, 값이 오버플 될 때마다 회전이 감소 합니다. ).

하지만 내가 설명 할 수없는 행동주의StackOverflowException(?)이 결코에서 발생하지 : 경우 정확히 같은 스택 깊이를. 다음은 몇 가지 32 비트 실행, 릴리스 빌드의 출력입니다.

51600 Random: 1778264579
Process is terminated due to StackOverflowException.

51599 Random: 1515673450
Process is terminated due to StackOverflowException.

51602 Random: 1567871768
Process is terminated due to StackOverflowException.

51535 Random: 2760045665
Process is terminated due to StackOverflowException.

그리고 디버그 빌드 :

28641 Random: 4435795885
Process is terminated due to StackOverflowException.

28641 Random: 4873901326  //never say never
Process is terminated due to StackOverflowException.

28623 Random: 7255802746
Process is terminated due to StackOverflowException.

28669 Random: 1613806023
Process is terminated due to StackOverflowException.

스택 크기는 일정합니다 ( 기본값은 1MB ). 스택 프레임의 크기는 일정합니다.

그렇다면 StackOverflowException히트가 발생했을 때 스택 깊이의 (때로는 사소하지 않은) 변화를 설명 할 수있는 것은 무엇입니까?

최신 정보

Hans Passant는 Console.WriteLineP / Invoke, interop 및 비 결정적 잠금 문제를 제기합니다 .

그래서 코드를 다음과 같이 단순화했습니다.

class Program
{
    static void Main(string[] args)
    {
        Rec();
    }
    static int sz = 0;
    static void Rec()
    {
        sz++;
        Rec();
    }
}

디버거없이 Release / 32bit / Optimization ON에서 실행했습니다. 프로그램이 충돌하면 디버거를 연결하고 카운터 값을 확인합니다.

그리고 여러 번 실행 해도 여전히 동일하지 않습니다. (또는 내 테스트에 결함이 있습니다.)

업데이트 : 폐쇄

fejesjoco가 제안한대로 ASLR (Address space layout randomization)을 조사했습니다.

이는 스택 위치 및 크기를 포함하여 프로세스 주소 공간에서 다양한 항목을 무작위로 지정하여 버퍼 오버플로 공격이 특정 시스템 호출의 정확한 위치를 찾기 어렵게하는 보안 기술입니다.

이론은 좋은 것 같습니다. 연습 해 봅시다!

이를 테스트하기 위해 EMET 또는 The Enhanced Mitigation Experience Toolkit 작업에 특정한 Microsoft 도구를 사용했습니다 . 시스템 또는 프로세스 수준에서 ASLR 플래그 (및 훨씬 더)를 설정할 수 있습니다.
( 내가 시도하지 않은 시스템 전체의 레지스트리 해킹 대안도 있습니다 )

EMET GUI

도구의 효율성을 확인하기 위해 Process Explorer프로세스 의 '속성'페이지에서 ASLR 플래그의 상태를 정식으로보고 한다는 사실도 발견했습니다 . 오늘까지 본 적이 없습니다 :)

여기에 이미지 설명 입력

이론적으로 EMET는 단일 프로세스에 대해 ASLR 플래그를 (재) 설정할 수 있습니다. 실제로는 아무것도 변경하지 않은 것 같습니다 (위 이미지 참조).

However, I disabled ASLR for the entire system and (one reboot later) I could finally verify that indeed, the SO exception now always happens at the same stack depth.

BONUS

ASLR-related, in older news: How Chrome got pwned


I think it may be ASLR at work. You can turn off DEP to test this theory.

See here for a C# utility class to check memory information: https://stackoverflow.com/a/8716410/552139

By the way, with this tool, I found that the difference between the maximum and minimum stack size is around 2 KiB, which is half a page. That's weird.

Update: OK, now I know I'm right. I followed up on the half-page theory, and found this doc that examines the ASLR implementation in Windows: http://www.symantec.com/avcenter/reference/Address_Space_Layout_Randomization.pdf

Quote:

Once the stack has been placed, the initial stack pointer is further randomized by a random decremental amount. The initial offset is selected to be up to half a page (2,048 bytes)

And this is the answer to your question. ASLR takes away between 0 and 2048 bytes of your initial stack randomly.


Change r.Next() to r.Next(10). StackOverflowExceptions should occur in the same depth.

Generated strings should consume the same memory because they have the same size. r.Next(10).ToString().Length == 1 always. r.Next().ToString().Length is variable.

The same applies if you use r.Next(100, 1000)

참고 URL : https://stackoverflow.com/questions/20245735/why-does-a-recursive-call-cause-stackoverflow-at-different-stack-depths

반응형