10 Regras Essenciais de Codificação da NASA para Desenvolver Software Crítico de Segurança
Projetos de software grandes e complexos usam vários padrões e diretrizes de codificação. Essas diretrizes estabelecem as regras básicas que devem ser seguidas ao escrever software. Geralmente, elas determinam:
a) Como o código deve ser estruturado?
b) Qual recurso da linguagem deve ou não deve ser usado?
Para ser eficaz, o conjunto de regras tem que ser pequeno e deve ser específico o suficiente para ser facilmente entendido e lembrado.
Os melhores programadores do mundo trabalhando na NASA seguem um conjunto de diretrizes para desenvolver código crítico de segurança. Na verdade, muitas agências, incluindo o Laboratório de Propulsão a Jato (JPL) da NASA, focam em código escrito em linguagem de programação C. Isso porque há suporte extensivo de ferramentas para esta linguagem, como extratores de modelo lógico, debuggers, compilador estável, analisadores fortes de código-fonte e ferramentas de métricas.
Em casos críticos, torna-se necessário aplicar estas regras, especialmente onde a vida humana pode depender de sua correção e eficiência. Por exemplo, programas de software usados para controlar aviões, espaçonaves ou usinas nucleares.
Mas você sabe quais padrões agências espaciais usam para operar suas máquinas? Abaixo, listamos as 10 regras de codificação da NASA estabelecidas pelo cientista líder do JPL, Gerard J. Holzmann. Todas elas focam principalmente em parâmetros de segurança, e você poderia aplicá-las a outras linguagens de programação também.
Regra 1 — Fluxo de Controle Simples
Escreva programas com construtores de fluxo de controle muito simples — Não use construtores setjmp ou longjmp, declarações goto e recursão direta ou indireta.
Razão: Fluxo de controle simples resulta em clareza de código melhorada e capacidades mais fortes para verificação. Sem recursão, não haverá gráfico de chamada de função cíclico. Assim, todas as execuções que devem ser limitadas permanecem de fato limitadas.
Regra 2 — Limite Superior Fixo para Loops
Todos os loops devem ter um limite superior fixo. Deve ser possível para uma ferramenta de verificação provar estaticamente que um limite superior predefinido no número de iterações de um loop não pode ser excedido.
A regra é considerada violada se o limite do loop não puder ser provado estaticamente.
Razão: A presença de limites de loop e a ausência de recursão previnem código descontrolado. No entanto, a regra não se aplica a iterações que são destinadas a ser não-terminantes (por exemplo, escalonador de processos). Em tais casos, a regra reversa é aplicada — Deve ser provável estaticamente que a iteração não pode terminar.
Regra 3 — Nenhuma Alocação Dinâmica de Memória
Não use alocação dinâmica de memória após inicialização.
Razão: Alocadores de memória como malloc, e coletores de lixo frequentemente têm comportamento imprevisível que pode impactar excepcionalmente o desempenho. Além disso, erros de memória também podem ocorrer por erro do programador, que inclui:
- Tentar alocar mais memória do que fisicamente disponível
- Esquecer de liberar memória
- Continuar a usar memória após ela ter sido liberada
- Ultrapassar limites na memória alocada
Forçar todos os módulos a viverem dentro de uma área de armazenamento pré-alocada e fixa pode eliminar esses problemas e tornar mais fácil verificar o uso de memória.
Uma maneira de reivindicar memória dinamicamente na ausência de alocação de memória do heap é usar memória de pilha (stack).
Regra 4 — Nenhuma Função Grande
Nenhuma função deve ser mais longa do que o que poderia ser impresso em uma única folha de papel em formato de referência padrão com uma linha por declaração e uma linha por instrução. Isso significa que uma função não deve ter mais de 60 linhas de código.
Razão: Funções excessivamente longas são frequentemente um sinal de estrutura ruim. Cada função deve ser uma unidade lógica que seja compreensível e verificável. É muito mais difícil entender uma unidade lógica que abrange múltiplas telas em um display de computador.
Regra 5 — Baixa Densidade de Asserções
A densidade de asserções do programa deve ter média de no mínimo duas asserções por função. Asserções são usadas para verificar condições anormais que nunca devem acontecer em execuções reais. Devem ser definidas como testes booleanos. Quando uma asserção falha, uma ação de recuperação explícita deve ser tomada.
Se uma ferramenta de verificação estática provar que a asserção nunca pode falhar ou nunca valer, a regra é considerada violada.
Razão: Segundo estatísticas de esforço de codificação da indústria, testes de unidade capturam pelo menos um defeito a cada 10 a 100 linhas de código. As chances de interceptar defeitos aumentam com a densidade de asserções.
O uso de asserção também é importante pois fazem parte de uma forte estratégia de codificação defensiva. São usadas para verificar pré e pós-condições de uma função, parâmetro e valor de retorno de uma função e invariantes de loop. Asserções podem ser desabilitadas seletivamente após testar código crítico de desempenho.
Regra 6 — Declarar Objetos de Dados no Menor Nível de Escopo
Esta apoia o princípio básico de ocultação de dados. Todos os objetos de dados devem ser declarados no menor nível possível de escopo.
Razão: Se um objeto não está no escopo, seu valor não pode ser referenciado ou corrompido. Esta regra desencoraja a reutilização de variáveis para múltiplos propósitos incompatíveis que podem complicar diagnóstico de falhas.
Regra 7 — Verificar Parâmetros e Valor de Retorno
O(s) valor(es) de retorno de funções não-void devem ser verificados por cada função chamadora, e a validade dos parâmetros deve ser verificada dentro de cada função.
Em sua forma mais estrita, esta regra significa que até o valor de retorno de declarações printf e close de arquivo devem ser verificados.
Razão: Se a resposta a um erro é legitimamente não diferente da resposta ao sucesso, deve-se verificar explicitamente um valor de retorno. Este é geralmente o caso com chamadas para close e printf. É aceitável fazer cast explícito do valor de retorno da função para void — indicando que o programador explicitamente (não acidentalmente) decide ignorar um valor de retorno.
Regra 8 — Uso Limitado do Pré-processador
O uso do pré-processador deve ser limitado à inclusão de arquivos de cabeçalho e definições de macro. Chamadas de macro recursivas, colagem de tokens e listas de argumentos variáveis não são permitidas.
Deve haver justificativa para mais de uma ou duas diretivas de compilação condicional mesmo em grandes esforços de desenvolvimento de aplicação, além do boilerplate padrão, que evita inclusão múltipla do mesmo arquivo de cabeçalho. Cada uso deve ser sinalizado por um verificador baseado em ferramenta e justificado no código.
Razão: O pré-processador C é uma ferramenta poderosa e ambígua que pode destruir a clareza do código e confundir muitos verificadores baseados em texto. O efeito de construções em código de pré-processador não limitado pode ser excepcionalmente difícil de decifrar, mesmo com uma definição de linguagem formal em mãos.
A cautela contra compilação condicional é igualmente importante — com apenas 10 diretivas de compilação condicional, poderiam haver 1024 versões possíveis (2^10) do código, o que aumentaria o esforço de teste necessário.
Regra 9 — Uso Limitado de Ponteiros
O uso de ponteiros deve ser restrito. Não mais que um nível de desreferenciação é permitido. Operações de desreferenciação de ponteiro não devem ser escondidas dentro de declaração typedef ou definições de macro.
Ponteiros de função também não são permitidos.
Razão: Ponteiros são facilmente mal utilizados, mesmo por especialistas. Eles tornam difícil seguir ou analisar o fluxo de dados em um programa, especialmente por analisadores estáticos baseados em ferramentas.
Ponteiros de função também restringem o tipo de verificações realizadas por analisadores estáticos. Assim, só devem ser usados se houver forte justificativa para sua implementação. Se ponteiros de função são usados, torna-se quase impossível para uma ferramenta provar a ausência de recursão, então métodos alternativos devem ser fornecidos para compensar esta perda em capacidades analíticas.
Regra 10 — Compilar Todo o Código
Todo código deve ser compilado desde o primeiro dia de desenvolvimento. O aviso do compilador deve ser ativado na configuração mais rigorosa do compilador. O código deve compilar com estas configurações sem nenhum aviso.
Todo código deve ser verificado diariamente com pelo menos um (preferencialmente mais de um) analisador estático de código-fonte de ponta e deve passar no processo de análise com zero avisos.
Razão: Existem muitos analisadores de código-fonte eficazes disponíveis no mercado; alguns são ferramentas gratuitas. Não há absolutamente desculpa para qualquer programador não fazer uso desta tecnologia prontamente disponível. Se o compilador ou analisador estático fica confuso, o código causando a confusão/erro deve ser reescrito para que se torne mais trivialmente válido.
O Que a NASA Diz Sobre Estas Regras?
Tecnologia industrial
- Temperaturas Críticas de Supercondutores Elementares e de Alta Temperatura (Kelvin)
- Regras Essenciais de Derivação: Referência Rápida
- Regras Fundamentais de Antiderivação: Constantes, Somas e Diferenças
- BigStitcher: Imagem 3D de Tecidos com Precisão Simplificada
- Construindo um Futuro Mais Seguro: Avançando Soluções Inteligentes de Segurança Alimentar
- Por que Modernizar sua Frota Reduz Custos e Aumenta a Sustentabilidade
- Além da Codificação: Habilidades-Chave para Projetos de Automação Bem-Sucedidos
- Domínio da Conformidade UID: 3 Regras Essenciais que Todo Fornecedor DoD Deve Seguir
- 4 Estratégias Comprovadas para Criar Checklists de Manutenção Eficazes
- Desenvolva Protocolos de Segurança Abrangentes para Trabalhadores e Técnicos