Misra-C – Padrão para software em C

- por Sergio Prado

Categorias: Linguagem C Tags: , , , ,

MISRA-C é um padrão de desenvolvimento de software em linguagem C desenvolvida pela mesma que dá seu nome, a Motor Industry Software Reliability Association. Este padrão tem foco em sistemas embarcados automotivos, mas suas práticas podem ser extendidas também para outras areas, como equipamentos médicos e aeroespaciais.

Seu principal objetivo é promover um conjunto de melhores práticas para o desenvolvimento de software seguro e portável em linguagem C. Foi inicialmente publicado em 1998, e melhorado no decorrer dos anos. A segunda e mais atual edição saiu em 2004, e foi entitulada de "Guidelines for the use of the C language in critical systems", e desde então vem sendo utilizada por empresas de vários ramos que desenvolvem software embarcado.

Infelizmente o documento não está disponível de forma gratuita, e quem quiser conhecer a fundo a norma precisa fazer um investimento para adquirí-laMas bastam algumas pesquisas no Google para encontrar bastante informação relevante à respeito do assunto.

Padrões de desenvolvimento

Antes de falar do padrão, vamos comentar sobre as motivações para usá-lo.

A linguagem C é uma das linguagens mais poderosas e flexíveis que existem. E é por isso que ela é extremamente popular em sistemas embarcados. Se bem utilizada, tem ótima performance, pode gerar um código bem pequeno e facilita bastante o acesso ao hardware. Mas o problema esta exatamente em como começamos a frase anterior: "Se bem utilizada…".

A linguagem C nas mãos de programadores inexperientes pode ser um problema. Sua flexibilidade é uma faca de dois gumes, pois dá poderes extras ao desenvolvedor, que talvez ainda não esteja preparado para usá-los.

E é por isso que os padrões de software existem. Para nos ajudar a usar esta ferramenta de programação (no nosso caso a linguagem C) da forma mais correta possível, segundo a norma, através de determinadas regras.

O padrão MISRA-C:2004

Voltando ao padrão, o MISRA-C:2004 contém 141 regras divididas em 21 categorias. Destas 141 regras, 121 são mandatórias e 20 são recomendadas. E para o código ser "MISRA-C Compliance", precisa estar de acordo com todas as 141 regras, sem exceção.

Infelizmente não temos espaço aqui para apresentar e discutir cada uma das regras (e até seria bem cansativo fazer isso), então vamos ver algumas das mais importantes (e controversas) regras deste padrão.

Rule 1: All code shall conform to ISO 9899 standard C, with no extensions permitted.

Tradução: Todo código deve estar de acordo com o padrão ISO 9899, sem permissão para o uso de extensões.

A primeira regra já é uma das mais polêmicas do padrão MISRA-C. O padrão ISO, também conhecido como C99, não foi desenvolvido com foco em sistemas embarcados, então como vamos ter um código para um sistema embarcado automotivo em conformidade com o C99? Um software embarcado normalmente requer algumas extensões para poder lidar diretamente com o hardware, como por exemplo a palavra chave interrupt usada em alguns compiladores para definir uma rotina de tratamento de interrupção. E isso pode ser um problema para o padrão MISRA-C.

Neste caso, o padrão MISRA-C permite alguns desvios à regra para extensões à linguagem. Estas extensões devem estar bem documentadas e centralizadas, como por exemplo, todos os acessos ao hardware em um único arquivo.

Rule 2.2: Source code shall only use /* … */ style comments.

Tradução: O código-fonte deve usar apenas comentários no estilo /* … */ 

Essa regra é mais para garantir um padrão de estilo de codificação. Nada de usar os comentários de linha "//" herdados do C++.

Rule 6.3: Typedefs that indicate size and signedness should be used in place of the basic types.

Tradução: Typedefs que indicam o tamanho e o sinal da variável devem ser usados no lugar dos tipos básicos.

Esse padrão deveria ser seguido por todos que pretendem garantir a portabilidade do código entre diferentes arquiteturas de hardware. Por exemplo, o código abaixo tem o objetivo de setar o bit 17 da variavel reg, considerando-se que int é um inteiro é de 4 bytes.

1
2
3
int reg;
 
reg |= 0x10000;

Quando portamos este código para uma arquitetura de 16 bits, este código não vai funcionar, já que um inteiro terá apenas 2 bytes. Então o que o padrão sugere é o código abaixo:

1
2
3
4
5
typedef int int32;
 
int32 reg;
 
reg |= 0x10000;

Neste caso, quando portamos o código, basta mudar a declaração do typedef para o tipo correto de um inteiro de 32 bits, por exemplo:

1
typedef long int32;

Rule 8.1: Functions shall have prototype declarations and the prototype shall be visible at both the function definition and call.

Tradução: Funções devem ter seus protótipos declarados e visíveis à sua definição e às suas chamadas.

Se a função é usada apenas no arquivo em que é declarada, o ideal é declarar seu protótipo no inicio do arquivo. Se a função é usada em outros arquivos/módulos, o ideal é declarar seu protótipo em um arquivo de header e incluí-lo em todos os modulos onde a função é declarada e usada. 

Rule 9.1: All automatic variables shall have been assigned a value before being used

Tradução: Todas as variáveis automáticas (locais) devem ter um valor atribuido antes de serem usadas.

Esta regra pode evitar muitos erros. Declarou uma variável local, inicialize-a. Declarou um buffer, use memset ou bzero para inicializá-lo. 

Rule 14.1: There shall be no unreachable code.

Tradução: Não deve existir código que nunca é executado.

A maioria dos compiladores hoje, quando configurado para otimizar por tamanho, acaba removendo do binário códigos que nunca são executados. Mas até para melhorar a leitura, é sempre bom fazer uma limpeza em códigos que nunca são executados, como o else do código abaixo:

1
2
3
4
5
6
7
void initBuffer(char *buffer, unsigned int size)
{
    if (size >= 0)
        bzero(buffer, size);
    else
        logError("Tamanho invalido!");
}

A função logError() nunca será executada já que size é uma variável sem sinal, portanto nunca será menor que zero.

Rule 16.2: Functions shall not call themselves, either directly or indirectly

Tradução: Funções não podem chamar elas mesmas direta ou indiretamente.

Você quer ver as consequências de um stack overflow? Use funções recursivas! Recursão pode ser às vezes uma solução elegante, mas consome muitos recursos de memória e processamento. Evite e busque soluções alternativas.

Rule 16.10: If a function returns error information, then that error information shall be tested.

Tradução: Se uma função retorna informações de erro, este erro deve ser checado.

Esta regra deve ser sempre seguida. Já vi muito código por aí chamando malloc() mas não verificando o resultado. Depois perdem horas para debugar o código.

Rule 20.4: Dynamic heap memory allocation shall not be used

Tradução: Alocação dinâmica de memória não deve ser usada.

OK, na minha opinião esta é a regra mais polêmica do padrão. Alocação dinâmica de memória pode ser a causa da maioria (e dos piores) bugs da sua aplicação. Fragmentação de memória e memory leak são os principais culpados. Mas às vezes precisamos do malloc() para resolver alguns problemas. Minha sugestão: use com muito cuidado e apenas como último recurso.

Rule 49: Tests of a value against zero should be made explicit, unless the operand is effectively Boolean.

Tradução: Verificar se uma variável está zerada deve ser feito de forma explicita, a não ser que o operando seja booleano.

Bom, logo de cara vou dizer que entendo os motivos, mas discordo desta regra. O que vocês acham mais legível, o primeiro if (padrão MISRA-C) ou o segundo?

1
2
3
4
5
6
7
if (bufferVazio == 0) { /* padrao MISRA-C */
    return;
}
 
if (!bufferVazio) {
    return;
}

Rule 104: Non-constant pointers to functions shall not be used.

Tradução: Ponteiros não constantes para funções não devem ser usados.

Discordo novamente! Em alguns casos, ponteiros para funções podem ser uma solução bem elegante, prática e sem muitos custos para o sistema. Por exemplo, dá para se implementar uma rotina de tratamento de uma máquina de estados com 3 ou 4 linhas de código usando ponteiro para funções (o meu artigo sobre máquina de estados fala sobre isso). 

Para finalizar, existem muitas ferramentas de análise estática de código que podem ser usadas para verificar se um código é compatível com o padrão MISRA-C como PC-LINT, CodeCheck e QA-C.

Ter um código compatível com o padrão não significa apenas que você vai desenvolver um código mais seguro e portável. Significa também que você vai estar mudando sua forma de programar, nem sempre para melhor. Interprete, critique e veja o que é melhor para a solução que esta buscando. Você não precisa seguir à risca todas as regras, apenas aquelas que se aplicam à seu projeto. E pode não estar 100% compativel com o padrão, mas vai com certeza ter melhorado a qualidade do seu código. De qualquer forma, vale a pena pelo menos dar uma olhada. O único risco que você vai correr é aprender um pouco mais com isso.

Um abraço,

Sergio Prado

  • Fernando Barboza

    Oi Sergio!
        Parabéns pelos artigos e por dedicar parte do seu tempo em escreve-los.
        Uma pequena correção na comparação de códigos que fez, talvez até para deixar mais polêmica a comparação entre os estilos.

    if (bufferVazio == 0) { /* padrao MISRA-C */
    return;
    }
     
    if (!bufferVazio) {
    return;
    }
        Para tornar os códigos equivalentes, faltou o detalhe do !.
     
    p.s. Tenho o costume de usar o segundo estilo.

  • Olá Fernando!

    Você tem razão. Post corrigido. Obrigado pelo toque.

    E continue acompanhando o blog!

    Um abraço!

  • Alberto Fabiano

    Olá Sérgio!
    Legal o posto, este é um dos meus assuntos recorrentes.
    Vale lembrar que hoje há também o MISRA-C++ que também tem o mesmo foco, porém para a linguagem do tio Stroustrup! 
    [ ]s++;
    ./alberto -fabiano

  • Pingback: Tweets that mention Misra-C - Padrão para software em C -- Topsy.com()

  • Alexandre Rodrigues

    Quanto à Rule 49, concordo que !bufferVazio é mais legível e coerente, considerando que esta variável é booleana ou está sendo usada como tal.  Afinal "VERDADEIRO" ou "NOT VERDADEIRO" é mais que intuitivo para qualquer desenvolvedor.
    Quando se trata de uma variável que contenha um número, como tamBuffer, vejo muitos problemas quando usada desta forma. Neste caso prefiro o padrão MISRA (tamBuffer == 0).
    Um caso que vejo causar muita confusão é o uso do !strcmp ou !memcmp. Estas funções foram criadas com três retornos exatamente para termos as três situações possíveis str1 < str2, str1 = str2 e str1 > str2. Acho que não cabe usarmos o NOT neste caso, principalmente por !strcmp significar exatamente str1 = str2. É só pensarmos nos resultados que podemos experimentar ao termos um programador iniciante trabalhando no nosso projeto tentando entender o que o programador anterior quis dizer com esta comparação. Afinal, "NOT strcmp" significa que as strings são iguais?
    No mais, parabéns Sergio, grande amigo. Continua competente, interessado e disposto a ensinar o que sabe. Nossa área anda um tanto carente de profissionais assim.

  • Poloni

    Alexandre, a vantagem de se voltar um valor igual a zero tem vantagens na otimização, já que você tem instruções em Assembly que resolve as comparações em apenas um ciclo.
    If (tamanho == 0) equivale à if (!tamanho). Se o compilador for "espertinho" ele notará que é a mesma coisa. Portanto, colocar explicitamente é interessante. Isso vale inclusive para booleano.
    if (bool_var == false) é melhor e mais rápido do que if (bool_var == true). A comparação por zero normalmente é resolvida em um ciclo.

    Com relação a Rule 20.4 alguns sistemas operacionais em tempo real resolve esse problema de uma forma bem interessante. Quem tiver um tempo, dêem uma lida nesse artigo http://www.embedded.com/design/222300428;jsessionid=50P5LPREJXGYTQE1GHPSKHWATMY32JVN?pgno=1. Quem quiser ir direto ao assunto, vá a página 3.

  • alex

    Parabens pelo site.
    acho que você deveria apontar as fontes pois já havia lido exatamente o que você publicou
    em outros sites especializados. abraço

  • Olá Alex,

    Você tem toda razão. As regras do padrão MISRA-C que usei neste post tirei dos links abaixo:

    http://www.embedded.com/columns/beginerscorner/9900659

    http://www.embedded.com/columns/technicalinsights/172301672

    Se você conhecer mais alguma fonte interessante sobre o assunto deixe um comentário aqui.

    Um abraço!

  • Sérgio, antes de mais nada, parabéns pelo post :)

    A regra 49 cita que "unless the operand is effectively Boolean". Como a variável BufferVazio dos seus exemplos é booleana, ambos exemplos estariam corretos segundo esta regra, não? Pelo que entendi, o que esta regra evita é algo como "if(!Contador) …"
     
    Ainda no mesmo assunto, na minha opinião, se o compilador gera um código diferente para (!xxx) e (xxx!=0), já passou da hora de trocar de compilador ;)
     
    Abraços,
    Rudolf

    • Olá Rudolf,

      Ótima observação. A minha interpretação é que a regra quis dizer que você não precisa fazer uma comparação explicita apenas se sua variável for do tipo boolean. O exemplo abaixo é permitido pelo padrão MISRA-C:

      bool bufferVazio;

      if (bufferVazio) {
      return;
      }

      Veja que a variável “bufferVazio” é do tipo bool. O tipo bool está especificado no padrão ANSI C99.

      Um abraço,

      Sergio Prado

  • Oi Sergio,
     
    Dei uma pesquisada e vi que as regras 49 e 104 que você citou é do MISRA-C:1998. Não achei o equivalente destas regras na versão de 2004.
     
    Abraços,
    Rudolf

  • Hugo Sobreira

    Caro Sergio,

    Parabens pela iniciativa, de fato sinto falta de artigos relacionados ao tema na nossa língua portuguesa.
    Venho trabalhando com desenvolvimento de sistemas críticos já há alguns anos e hoje utilizo a MISRA-C como padrão de codificação. Vale à pena ressaltar a diferença entre padrão de codificação e padrão de desenvolvimento (ou design). Para desenvolver sistemas críticos é necesssario ter estes padrões definidos. Interessante notar também que adoptar uma padrão de codificação não é apenas boa prática quando se trata de sistemas críticos (SC): é mesmo uma obrigação. Padrões de codificação são requeridos por diferentes normas que defendem desenvolvimento de SC (veja a DO-178B, IEC-61508, EN-50128).
    Quanto ao padrão MISRA-C, os justificativas para cada regra são muito bem explicadas na própria norma, por isso recomendo sua leitura a qualquer um que pense seriamente em aplicá-la. Lá vc vai ver também que nem todas as regras são normativas, há também regras opcionais.

    O que tenho visto como experiência geral é que MISRA-C não tem por propósito somente "educar" desenvolvedores inexperientes. Desenvolver software para SC pode ser uma tarefa extremamente árdua pelo simples fato de construções mais elaboradas da linguagem não serem permitidas.Tomando como exemplo a regra comentada em seu artigo sobre alocação dinâmica de memória (regra 20.4, uma das requeridas pela norma). Num sistema crítico, é imprescindível saber a priori quantos e quais são os recursos utilizados pela aplicação. Mais que isso, é preciso demonstrar, que a quantidade de memória utilizada nunca ultrapassará o limite disponível (suponha que o programa falhe de uma maneira catastrófica caso não haja memória disponível). Quando se usa alocação dinâmica de memória é extremamente difícil provar esta 'propriedade'. Portanto MISRA-C fala muito também à comunidade de programadores experientes em C, mas que podem não estar devidamente contextualizados as necessidades da natureza deste tipo de aplicação.

    Abraços,
    Hugo Sobreira

    • Olá Hugo!

      Seu comentário é quase um novo post…:)

      Obrigado pelas dicas valiosas e continue aparecendo por aqui.

      Um abraço!

  • Sérgio MacGyver

    Caro xará,   :-)
     
    Pretendia comentar a regra 49, sobre os testes de variável zerada, mas vi que outros já teceram seus comentários. No entanto, após você acrescentar um comentário reconhecendo que o if (!bufferVazio) é permitido pelo MISRA-C, não consigo entender em quê reside sua discordância do padrão.
     
    Quanto à regra 20.4, sobre o dynamic heap memory allocation, creio que o padrão condena não a alocação dinâmica em si, mas o método utilizado (heap memory).
    Existem outros esquemas de alocação dinâmica em que a região alocável é previamente particionada em blocos de tamanho fixo, sendo alguns menores e outros maiores. Quando um código solicita memória, tenta-se entregar um bloco que possua tamanho igual ou pouco maior que o solicitado, caso disponível. Há perda de memória por entregar-se mais do que se pediu, mas não há o famigerado problema de fragmentação de memória, em que há memória suficiente, mas toda "espalhada" pelo heap.
    Desconfio que seja justamente o problema de fragmentação que esteja na causa das falhas comumente associadas à alocação de memória. Além da falta de teste de retorno do malloc, claro.
     
    Por fim, concordo com sua discordância (pun intended) da regra 104. Sou adepto da programação orientada a eventos (+maquinas de estados) em sistemas embarcados, e essa é a melhor forma de se implementar eventos (com funções de callback).
     
    Desconfio que o colega Hugo Sobreira é um conhecido meu que trabalhou na Motorola e talvez conheça o esquema de alocação que citei acima. Se for você mesmo, bom te ver por aqui, Hugo! (eu trabalhava no projeto CESAR-Motorola).
     
    Sérgio, por favor, encaminhe esso meu comentário pro email do Hugo. Obrigado!

    • Olá xará! :)

      Com relação à regra 49, quando uma variável representa um valor boolean, ou seja, que pode assumir apenas true (1) ou false (0), eu acho muito mais fácil de ler o código sem ter que comparar com 0 ou 1, como exige o padrão. Com relação à regra 20.4, você fez uma ótima observação. Ainda não tinha considerado esta hipótese. E o Hugo participa bastante do grupo sis_embarcados, do qual agora você também faz parte…:) Estou te enviando o email dele em PVT. Um abraço e continue acompanhando o blog!

  • Lucas Fernando Amorim

    Parabéns Sergio,

    Ficou muito bom o seu artigo, padronização de código é um assunto muito importante que merece ser discutido. Toda iniciativa de estilização é benéfica, levando a uma maior manutenabilidade do software.

    Como sempre seu blog é uma excelente fonte de informações úteis e profissionais.

    Abraço.

  • Esse exemplo da regra 49.
    bufferVazio não é um booleano, mesmo que declarado como um int?
    Quero dizer, acho que essa regra diz para que não façamos algo do tipo:
    if (!buffer_len)
    e que façamos
    if (buffer_len == 0) 
    o que é muito mais legível, o significaria "se o tamanho do buffer for igual a 0" ao invés de "se não tamanho do buffer"
    Agora para o exemplo que você disse, apesar de bufferVazio ser provavelmente um int, ele assume valores booleanos então !bufferVazio faz mais sentido "se o buffer não estiver vazio".
    Um abraço,
    Murilo

  • Olá Murilo, tudo bem?

    Semanticamente, "bufferVazio" é um booleano, mas sintaticamente ele continua sendo um int. O que o padrão quer nos dizer é que devemos fazer as comparações explicitas, ou seja, se desejamos verificar se o buffer esta vazio, esta implementação está fora do padrão:

    if (bufferVazio) { … };

    E esta esta dentro do padrão:

    if (bufferVazio == 1) { … };

    Eu concordo com você, e acho a primeira forma mais fácil de ler.

    Um abraço,

    Sergio Prado

  • Sérgio MacGyver

    Caros Sérgio e Murilo,
    Pelo que entendi da regra 49, "Tests of a value against zero should be made explicit, unless the operand is effec tively Boolean", no caso de bufferLen temos que fazer comparações explícitas, pois é um contador e não efetivamente um booleano. Já no caso de bufferVazio, por se tratar efetivamente de um booleano, ainda que implicitamente um int, as comparações if (bufferVazio) e if (!bufferVazio) são aceitas pelo MISRA-C.
    []s
    Sérgio

Navegue
Creative Commons Este trabalho de Sergio Prado é licenciado pelo
Creative Commons BY-NC-SA 3.0.