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 http://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.

Comments (1)

  1. Alexandre Albuquerque says:

    Parabéns mais uma vez Roberto. Os tópicos são bem feitos e não somente com teoria, mas exemplificando na prática (amostras das pilhas, etc).

    Creio que o crescimento técnico do pessoal que acompanha o blog tenha crescido bastante! Principalmente aos que se sentem desafiados e correm para tentar matar a charada. Já dizia o ditado "O que vem fácil, sai fácil!"… Valew! =P Inté o próximo!