Comportamento indefinido em C (Undefined behavior)

A linguagem C, desenvolvida em 1978 por Dennis Ritchie, é a  linguagem mais usada para a programação nos últimos anos. O site tiobe possui um índice das linguagens mais usadas desde 2001. O índice do mês de setembro de 2013 mostra o seguinte:

 

index

 

Ela também é talvez a linguagem mais popular em sistemas embarcados. Mas por que ela é tão popular? Dentre várias características positivas podemos destacar:

  • Possui poucos comandos (44 no padrão C11) e é relativamente simples de entender e aprender
  • Existem compiladores C portados para a maioria dos processadores
  • É uma linguagem poderosa que gera códigos eficiêntes

 

Sabe-se que arquiteturas diferentes não possuem o mesmo conjunto de instruções. Sendo assim, para que seja possível otimizar e gerar códigos mais eficientes para cada arquitetura, o padrão C, dá uma certa liberdade para os compiladores decidirem o que implementar em casos em que não temos um comportamento único para as arquiteturas.

 

Um exemplo simples é a divisão por zero.

 

 

Na arquitetura x86, teremos uma exceção por harware se o valor de b é zero, porém na arquitetura PowerPC, essa instrução é apenas ignorada. Esse exemplo é chamado de comportamento indefinido, em inglês (undefined behavior) uma vez que o compilador C tem mais de uma escolha para implementar. Por isso o padrão deixa aberto ao compilador gerar o código mais eficiente para cada arquitetura.
Neste mesmo exemplo, o compilador poderia implementar a função retornando zero, qualquer outro valor ou ainda na arquitetura PowerPC não fazer nada.
 
Um outro exemplo, agora real, usado no banco de dados PostgreeSQL: 
 

 

Neste exemplo, a mensagem "division by zero" nunca irá aparecer mesmo se arg2 = 0. Isso acontece porque o compilador conclui que se arg2 é igual a zero, a divisão em 

 

PG_RETURN_INT32((int32) arg1 / arg2)

 

sempre resultará num comportamento indefinido. Então neste caso o compilador decidiu por si só optimizar o código de forma que essa parte só execute quando arg2 é diferente de zero e não gerar o código para a chamada da função ereport.

 
A verdade é que a função ereport nunca retorna, e o programador concluiu (erroneamente) que se arg2 é igual a zero, a função seria chamada e nunca mais retornaria e a divisão por zero seria evitado. Porém esse detalhe de implementação não foi passado ao compilador. Uma solução para esse exemplo seria colocar um else.
 
Logo o programador deve evitar de concluir o que o compilador vai gerar como código e sempre que possível explicitar o que se deseja que o programa faça. Também deve-se prestar muita atenção quando troca-se de compilador e a otimização está habilitada principalmente quando um código é portado de uma arquitetura à outra.
 
Fontes:
Engenheiro eletrônico com 10 anos de experiência em sistemas embarcados, pós graduado em redes de computadores e atualmente cursando mestrado em sistemas de visão por computador na universidade Laval no Canadá. Compartilha seu conhecimento neste portal quando tem tempo livre e quando não está curtindo a vida com sua mulher e os 3 filhos.
  • Matheus Quick

    linguagem C ainda é muito usada, muito boa, mesmo sendo antiga.