Resposta ao Desafio da Semana #10 [Hang/Crash com Loops em C# e VB.NET]

 

Por: Roberto Alexis Farah

Olá pessoal!

Eis a resposta ao desafio https://blogs.technet.com/latam/archive/2006/08/25/451782.aspx publicado semana passada.

SINTOMA

A execução do código vai ocasionar um crash de aplicação devido a estouro de pilha (StackOverflow).

Se você pensou em loop infinito infelizmente errou pois o loop será transitório até que a pilha estoure.

PROBLEMA

O código faz uma chamada a DoSomething() recursivamente, o que ocasiona o estouro de pilha (stack overflow).

SOLUÇÃO

Uma solução simples, que exige poucas mudanças seria…

Ao invés de:

          public void DoSomething()

              {

                   DoSomething();

                   _b1 = _a1;

              }

Use:

          public class B : A

          {

              public int _b1;

             

              public void DoSomething()

              {

                   base.DoSomething(); // Chamada a classe pai.

                   _b1 = _a1;

              }

          }

Desse modo o método “pai” é chamado do método “filho”, como indicado no comentário.

Agora, eis a demonstração do que ocorre ao se executar o código acima… Vamos aos porques…

Sempre que uma rotina é chamada é colocada na pilha de execução algumas informações relacionadas a chamada do método que após a execução são removidas.

Entretanto, em uma chamada recursiva a remoção do conteúdo da pilha ocorrerá apenas quando a recursão terminar!

No nosso cenário a recursão nunca vai acabar portanto, a pilha vai encher e ocorrerá uma exceção quando o limite for atingido.

Eis uma amostra da pilha, pois o número de frames é muito maior que isso:

OS Thread Id: 0xe8c (0)

ESP EIP

00033000 00ca0134 Test.Program+B.DoSomething()

    PARAMETERS:

        this = 0x01271be0

00033008 00ca013a Test.Program+B.DoSomething()

    PARAMETERS:

        this = 0x01271be0

00033010 00ca013a Test.Program+B.DoSomething()

    PARAMETERS:

        this = 0x01271be0

00033018 00ca013a Test.Program+B.DoSomething()

    PARAMETERS:

        this = 0x01271be0

00033020 00ca013a Test.Program+B.DoSomething()

    PARAMETERS:

        this = 0x01271be0

00033028 00ca013a Test.Program+B.DoSomething()

    PARAMETERS:

        this = 0x01271be0

00033030 00ca013a Test.Program+B.DoSomething()

    PARAMETERS:

        this = 0x01271be0

00033038 00ca013a Test.Program+B.DoSomething()

    PARAMETERS:

        this = 0x01271be0

00033040 00ca013a Test.Program+B.DoSomething()

    PARAMETERS:

        this = 0x01271be0

00033048 00ca013a Test.Program+B.DoSomething()

    PARAMETERS:

        this = 0x01271be0

00033050 00ca013a Test.Program+B.DoSomething()

  PARAMETERS:

        this = 0x01271be0

00033058 00ca013a Test.Program+B.DoSomething()

    PARAMETERS:

        this = 0x01271be0

00033060 00ca013a Test.Program+B.DoSomething()

    PARAMETERS:

        this = 0x01271be0

00033068 00ca013a Test.Program+B.DoSomething()

    PARAMETERS:

        this = 0x01271be0

00033070 00ca013a Test.Program+B.DoSomething()

    PARAMETERS:

        this = 0x01271be0

00033078 00ca013a Test.Program+B.DoSomething()

    PARAMETERS:

        this = 0x01271be0

00033080 00ca013a Test.Program+B.DoSomething()

    PARAMETERS:

        this = 0x01271be0

00033088 00ca013a Test.Program+B.DoSomething()

    PARAMETERS:

        this = 0x01271be0

00033090 00ca013a Test.Program+B.DoSomething()

    PARAMETERS:

        this = 0x01271be0

00033098 00ca013a Test.Program+B.DoSomething()

    PARAMETERS:

        this = 0x01271be0

000330a0 00ca013a Test.Program+B.DoSomething()

    PARAMETERS:

        this = 0x01271be0

000330a8 00ca013a Test.Program+B.DoSomething()

    PARAMETERS:

        this = 0x01271be0

000330b0 00ca013a Test.Program+B.DoSomething()

    PARAMETERS:

        this = 0x01271be0

000330b8 00ca013a Test.Program+B.DoSomething()

    PARAMETERS:

        this = 0x01271be0

000330c0 00ca013a Test.Program+B.DoSomething()

    PARAMETERS:

     this = 0x01271be0

000330c8 00ca013a Test.Program+B.DoSomething()

    PARAMETERS:

        this = 0x01271be0

000330d0 00ca013a Test.Program+B.DoSomething()

    PARAMETERS:

        this = 0x01271be0

000330d8 00ca013a Test.Program+B.DoSomething()

    PARAMETERS:

        this = 0x01271be0

000330e0 00ca013a Test.Program+B.DoSomething()

    PARAMETERS:

        this = 0x01271be0

Notem como ESP constantemente muda para refletir o incremento na pilha.

O endereço vai do maior número (endereço de memória virtual) para o menor, que é como a pilha cresce.

A pilha, por default, tem aproximadamente 1 mb de tamanho para cada thread, logo, ao se tentar passar esse limite ocorre a exceção.

Para nossa thread em questão temos:

TEB at 7ffdf000

    ExceptionList: 0012f4ac

    StackBase: 00130000

    StackLimit: 00031000

    SubSystemTib: 00000000

    FiberData: 00001e00

    ArbitraryUserPointer: 00000000

    Self: 7ffdf000

    EnvironmentPointer: 00000000

    ClientId: 000013e8 . 00000e8c

    RpcHandle: 00000000

    Tls Storage: 00000000

    PEB Address: 7ffd8000

    LastErrorValue: 2

    LastStatusValue: c000000f

    Count Owned Locks: 0

   HardErrorsMode: 0

A diferença da base e limite é: 0n1044480 (decimal) ou 0x000ff000 (hexadecimal)

É possível ver que o limite foi atingido. De fato, temos a exceção:

ExceptionAddress: 00ca0134 (Test!Test.Program+B.DoSomething()+0x00000014)

   ExceptionCode: c00000fd (Stack overflow)

  ExceptionFlags: 00000000

NumberParameters: 2

   Parameter[0]: 00000001

   Parameter[1]: 00032ffc

E a parte do código que ocasionou a exceção:

Test!Test.Program+B.DoSomething()+14 [C:\Development\My Tools\BLOG Articles\Article #15\Test\Test\Program.cs @ 27]

00ca0134 ff1568319100 call dword ptr ds:[913168h]

    23: public int _b1;

    24:

    25: public void DoSomething()

    26: {

> 27: DoSomething();

    28: _b1 = _a1;

    29: }

  30: }

    31:

    32: static void Main(string[] args)

Eis as últimas chamadas na pilha até o momento do overflow. Aqui fica bem claro o preenchimento da pilha através das chamadas recursivas e finalizando em 00033000:

00032ff4 00000000

00032ff8 00000000

00032ffc 00000000

00033000 01271be0

00033004 00ca013a Test!Test.Program+B.DoSomething()+0x1a [C:\Development\My Tools\BLOG Articles\Article #15\Test\Test\Program.cs @ 27]

00033008 01271be0

0003300c 00ca013a Test!Test.Program+B.DoSomething()+0x1a [C:\Development\My Tools\BLOG Articles\Article #15\Test\Test\Program.cs @ 27]

00033010 01271be0

00033014 00ca013a Test!Test.Program+B.DoSomething()+0x1a [C:\Development\My Tools\BLOG Articles\Article #15\Test\Test\Program.cs @ 27]

00033018 01271be0

0003301c 00ca013a Test!Test.Program+B.DoSomething()+0x1a [C:\Development\My Tools\BLOG Articles\Article #15\Test\Test\Program.cs @ 27]

00033020 01271be0

00033024 00ca013a Test!Test.Program+B.DoSomething()+0x1a [C:\Development\My Tools\BLOG Articles\Article #15\Test\Test\Program.cs @ 27]

00033028 01271be0

0003302c 00ca013a Test!Test.Program+B.DoSomething()+0x1a [C:\Development\My Tools\BLOG Articles\Article #15\Test\Test\Program.cs @ 27]

00033030 01271be0

00033034 00ca013a Test!Test.Program+B.DoSomething()+0x1a [C:\Development\My Tools\BLOG Articles\Article #15\Test\Test\Program.cs @ 27]

00033038 01271be0

0003303c 00ca013a Test!Test.Program+B.DoSomething()+0x1a [C:\Development\My Tools\BLOG Articles\Article #15\Test\Test\Program.cs @ 27]

00033040 01271be0

00033044 00ca013a Test!Test.Program+B.DoSomething()+0x1a [C:\Development\My Tools\BLOG Articles\Article #15\Test\Test\Program.cs @ 27]

00033048 01271be0

0003304c 00ca013a Test!Test.Program+B.DoSomething()+0x1a [C:\Development\My Tools\BLOG Articles\Article #15\Test\Test\Program.cs @ 27]

00033050 01271be0

O que é o endereço 01271be0 que aparece acima?

Name: Test.Program+B

MethodTable: 00913130

EEClass: 00911364

Size: 20(0x14) bytes

GC Generation: 0

 (C:\Development\My Tools\BLOG Articles\Article #15\Test\Test\bin\Debug\Test.exe)

Fields:

      MT Field Offset Type VT Attr Value Name

790fed1c 4000001 4 System.Int32 0 instance 0 _a1

790fed1c 4000002 8 System.Int32 0 instance 0 _b1

790fed1c 4000003 c System.Int32 0 instance 0 _b1

Portanto, a cada nova chamada do método informações são colocadas na pilha e nunca removidas. Após várias execuções, quando a pilha atinge seu limite de ~1mb temos a exceção por estouro de pilha!

Para aqueles que estiverem se perguntando quando temos um sintoma de loop infinito, eis um exemplo:

while(1)

{

        Console.WriteLine("X"); // Loop infinito sempre é acompanhado de alto consumo de CPU???

}

Por que nesse caso temos um loop infinito (com alto consumo de CPU) ao invés de um estouro de pilha?

A resposta é simples, nesse caso as informações colocadas na pilha quando a rotina dentro do loop é chamada são removidas quando a execução retorna para o loop, ou seja, o espaço necessário na pilha para permitir a chamada desse método é muito pequeno pois é constantemente limpa quando a execução de Console.WriteLine() finaliza. Não há uma serialização de chamadas na pilha que nunca são retornadas como no caso do desafio colocado.

Agora atente para um detalhe importante: No loop acima temos um cenário de alto consumo de CPU não por ser um loop infinito, mas por ser um loop sem uma pausa para o processador “respirar”, digo, sem por exemplo, uma chamada para Sleep(200). O que quero dizer com isso é que podem haver cenários de loops infinitos de alta CPU ou loops infinitos de baixa CPU, tudo depende de haver chamadas dentro do loop que possibilitem a CPU dedicar mais tempo de processamento para as outras threads ou não.

Acredito que após entender esse desafio você estará apto a examinar um código suspeito e identificar se é candidato a crash por stack overflow ou um loop infinito (hang, aplicação pendurada ) , e se poderia haver um consumo elevado de CPU ou não. J

Como havia dito ao postar o desafio, algumas semanas atrás usei-o em entrevistas e curiosamente ninguém acertou 100%.

O loop infinito foi a resposta mais usada.

Até o próximo desafio.