Introdução
O software crítico para a segurança, como o utilizado em naves espaciais ou sistemas automóveis, requer um código extremamente fiável. Para resolver este problema, o Laboratório de Propulsão a Jato da NASA criou as regras de codificação
"Power of 10" em 2006. Estas diretrizes concisas eliminam construções complexas em C que são difíceis de analisar e garantem que o código permanece simples, verificável e fiável.
Hoje, ferramentas como o Aikido code quality podem ser configuradas com verificações personalizadas para aplicar todas as dez regras em cada novo pull request. Neste artigo, explicamos cada regra, por que ela é importante e fornecemos exemplos de código mostrando abordagens incorretas e corretas.
Porque é que estas regras são importantes
As regras da NASA centram-se na legibilidade, capacidade de análise e fiabilidade, que são essenciais para aplicações de missão crítica, como o controlo de naves espaciais e o software de voo. Ao não permitir construções C obscuras e ao impor verificações defensivas, as diretrizes facilitam a revisão do código e provam a sua correção. Complementam normas como a MISRA C, abordando padrões que os analisadores estáticos muitas vezes não detectam. Por exemplo, evitar a recursão e a memória dinâmica mantém a utilização de recursos previsível, enquanto a aplicação de verificações do valor de retorno ajuda a detetar muitos erros em tempo de compilação.
De facto, o estudo da NASA sobre um sistema incorporado no mercado de massas, como o firmware do acelerador eletrónico da Toyota, encontrou centenas de violações das regras. Isto mostra que os projectos do mundo real se deparam frequentemente com os mesmos problemas que estas regras foram concebidas para evitar. Cada regra da lista previne uma classe de erros comuns (loops não controlados, desreferências de ponteiro nulo, efeitos colaterais invisíveis, etc.). Ignorá-los pode levar a falhas subtis em tempo de execução, falhas de segurança ou comportamento não determinístico. Em contrapartida, a adesão a todas as dez regras torna a verificação estática muito mais fácil.
As ferramentas automatizadas são importantes. As plataformas de qualidade do código podem ser configuradas para detetar construções ou padrões proibidos. Estas regras são executadas automaticamente em cada pedido pull, detectando problemas antes de o código ser fundido.
Contexto de ligação às regras
Antes de nos debruçarmos sobre as regras individuais, é importante compreender o contexto:
- Linguagem de destino: As regras da "Potência de 10" da NASA foram escritas para C, uma linguagem com grande suporte de ferramentas (compiladores, analisadores, depuradores), mas também notória por comportamentos indefinidos. Não pressupõem recolha de lixo ou gestão avançada de memória. Usando apenas C simples e bem estruturado, é possível aproveitar a análise estática para provar as propriedades do programa.
- Análise estática: Existem muitas regras para facilitar as verificações automáticas. Por exemplo, proibir a recursão (Regra 1) e exigir limites de laços (Regra 2) permite que as ferramentas provem quantas iterações ou uso de pilha uma função pode ter. Da mesma forma, a proibição de macros complexas e a limitação de ponteiros (Regras 8-9) tornam os padrões de código explícitos em vez de escondidos na magia do pré-processador ou em múltiplas indirecções.
- Fluxo de trabalho de desenvolvimento: Nos pipelines DevSecOps modernos, estas regras tornam-se parte das verificações de CI. As ferramentas de qualidade de código podem ser integradas no GitHub, GitLab ou Bitbucket para rever cada pedido pull e detetar problemas simples e padrões mais complexos. Pode criar uma regra personalizada para cada diretriz NASA, como "assinalar qualquer utilização de goto ou chamadas de função recursivas" ou "garantir que cada ciclo tem um limite literal". Uma vez configuradas, estas regras são aplicadas automaticamente em todas as análises de código futuras, detectando violações antecipadamente e fornecendo orientações sobre como corrigi-las.
Em resumo, as 10 regras da NASA incorporam a programação C defensiva e analisável. Abaixo, listamos cada regra, mostramos como são os códigos bons e ruins e explicamos por que a regra existe e quais riscos ela mitiga.
As 10 regras de codificação da NASA
1. Evitar um fluxo de controlo complexo.
Não utilizar goto, setjmp, ou longjmp, evitar escrever funções recursivas em qualquer parte do código.
❌ Exemplo de não conformidade
// Non-compliant: recursive function call
int factorial(int n) {
if (n <= 1) return 1;
return n * factorial(n-1); // recursion (direct)
}Exemplo de conformidade (utiliza o ciclo)
// Compliant: uses an explicit loop instead of recursion
int factorial(int n) {
int result = 1;
for (int i = n; i > 1; --i) {
result *= i;
}
return result;
}Porque é que isto é importante: A recursão e os gotos criam um fluxo de controlo não linear que é difícil de compreender. Chamadas recursivas tornam o gráfico de chamadas cíclico e a profundidade da pilha ilimitada; o goto cria um código espaguete. Usando loops simples e código linear, um analisador estático pode verificar facilmente o uso da pilha e os caminhos do programa. A violação desta regra pode levar a transbordos de pilha inesperados ou caminhos lógicos que são difíceis de analisar manualmente.
2. Os loops devem ter limites superiores fixos.
Cada ciclo deve ter um limite verificável em tempo de compilação.
Exemplo não conforme (ciclo não limitado):
// Non-compliant: loop with dynamic or unknown bound
int i = 0;
while (array[i] != 0) {
doSomething(array[i]);
i++;
}Exemplo de conformidade (laço de limite fixo):
// Compliant: loop with explicit fixed upper bound and assert
#define MAX_LEN 100
for (int i = 0; i < MAX_LEN; i++) {
if (array[i] == 0) break;
doSomething(array[i]);
}Por que isso é importante: Os loops sem limites podem ser executados para sempre ou exceder os limites de recursos. Com um limite fixo, as ferramentas podem provar estaticamente as iterações máximas. Em sistemas de segurança crítica, a falta de um limite pode causar um loop descontrolado. Ao impor um limite explícito (ou um tamanho de matriz estático), garantimos que os loops terminem de forma previsível. Sem esta regra, um erro na lógica do ciclo pode não ser detectado até à sua implementação (por exemplo, um erro de um para um que provoca um ciclo infinito).
3. Sem memória dinâmica após a inicialização.
Evite malloc/free ou qualquer utilização de heap no código em execução; utilize apenas alocação fixa ou de pilha.
Exemplo não conforme (utiliza malloc)
// Non-compliant: dynamic allocation inside the code
void storeData(int size) {
int *buffer = malloc(size * sizeof(int));
if (buffer == NULL) return;
// ... use buffer ...
free(buffer);
}Exemplo de conformidade (atribuição estática)
// Compliant: fixed-size array on stack or global
#define MAX_SIZE 256
void storeData() {
int buffer[MAX_SIZE];
// ... use buffer without dynamic alloc ...
}Porque é que isto é importante: A atribuição dinâmica de memória durante o tempo de execução pode levar a um comportamento imprevisível, à fragmentação da memória ou a falhas de atribuição, especialmente em sistemas com recursos limitados, como naves espaciais ou controladores incorporados. Se malloc ou free falharem no meio da missão, o software pode travar ou se comportar de forma imprevisível. Usar apenas memória de tamanho fixo ou alocada em pilha garante um comportamento determinístico, simplifica a validação e evita vazamentos de memória em tempo de execução.
4. As funções cabem numa página (~60 linhas).
Manter cada função curta (aproximadamente ≤ 60 linhas).
Exemplo de não conformidade
// Non-compliant: hundreds of lines in one function (not shown)
void processAllData() {
// ... imagine 100+ lines of code doing many tasks ...
}Exemplo de conformidade (funções modulares)
// Compliant: break the task into clear sub-functions
void processAllData() {
preprocessData();
analyzeData();
postprocessData();
}
void preprocessData() { /* ... */ }
void analyzeData() { /* ... */ }
void postprocessData(){ /* ... */ }Porque é que isto é importante: Funções extremamente longas são difíceis de entender, testar e verificar como uma unidade. Ao manter cada função limitada a uma tarefa concetual (e dentro de uma página impressa), as revisões de código e as verificações estáticas tornam-se mais fáceis. Se uma função se estender por muitas linhas, erros lógicos ou condições de limite podem passar despercebidos. Dividir o código em funções mais pequenas melhora a clareza e facilita a aplicação de outras regras (como a densidade de asserções e as verificações de retorno por função).
5. Utiliza pelo menos duas declarações assert por função.
Cada função deve efetuar controlos de defesa.
Exemplo de não-conformidade (sem observações):
int get_element(int *array, size_t size, size_t index) {
return array[index];
}Exemplo de conformidade (com asserções):
int get_element(int *array, size_t size, size_t index) {
assert(array != NULL); // Assertion 1: pointer validity
assert(index < size); // Assertion 2: bounds check
if (array == NULL) return -1; // Recovery: return error
if (index >= size) return -1; // Recovery: return error
return array[index];
}Por que isso é importante: As asserções são a primeira linha de defesa contra condições inválidas. A NASA descobriu que uma maior densidade de asserções aumenta significativamente a possibilidade de detetar erros. Com pelo menos duas asserções por função (verificando pré-condições, limites, invariantes), o código auto-documenta os seus pressupostos e assinala imediatamente as anomalias durante os testes. Sem asserções, um valor inesperado pode propagar-se silenciosamente, causando uma falha longe da fonte do erro.
6. Declarar dados com um âmbito mínimo.
Mantenha as variáveis tão locais quanto possível; evite as globais.
Exemplo de não conformidade (dados globais):
// Non-compliant: global variable visible everywhere
int statusFlag;
void setStatus(int f) {
statusFlag = f;
}Exemplo de conformidade (âmbito local):
// Compliant: local variable inside function
void setStatus(int f) {
int statusFlag = f;
// ... use statusFlag only here ...
}Por que isso é importante: Minimizar o âmbito reduz o acoplamento e as interações não intencionais. Se uma variável só é necessária dentro de uma função, declará-la globalmente arrisca-se a que outro código a altere inesperadamente. Ao manter os dados locais, cada função torna-se mais autónoma e livre de efeitos secundários, o que simplifica a análise e os testes. As violações (como a reutilização do estado global) podem levar a erros difíceis de encontrar devido a aliasing ou modificações inesperadas.
7. Verificar todos os valores de retorno e parâmetros da função.
O chamador deve examinar cada valor de retorno não vazio; cada função deve validar os seus parâmetros de entrada.
Exemplo não conforme (ignora o valor de retorno)
int bad_mission_control(int velocity, int time) {
int distance;
calculate_trajectory(velocity, time, &distance); // Didn't check!
return distance; // Could be garbage if calculation failed
}Exemplo de conformidade
int good_mission_control(int velocity, int time) {
int distance;
int status = calculate_trajectory(velocity, time, &distance);
if (status != 0) { // Checked the return value
return -1; // Propagate error to caller
}
return distance; // Safe to use
}Porque é que isto é importante: Ignorar valores de retorno ou parâmetros inválidos é uma grande fonte de bugs. Por exemplo, não verificar o malloc pode levar a uma desreferência de ponteiro nulo. Da mesma forma, não validar entradas (por exemplo, índices de matriz ou strings de formato) pode causar estouro de buffer ou falhas. A NASA exige que cada retorno seja tratado (ou explicitamente convertido para void para sinalizar a intenção) e que cada argumento seja verificado. Esta abordagem "catch-all" garante que nenhum erro é silenciosamente ignorado.
8. Limitar o pré-processador a includes e macros simples.
Evite macros complexas ou truques de compilação condicional.
Exemplo de não conformidade (marco complexo):
#definir DECLARE_FUNC(name) void func_##name(void)
DECLARE_FUNC(init); // Expande para: void func_init(void)Exemplo de conformidade (macros simples / em linha):
// Compliant: use inline function or straightforward definitions
static inline int sqr(int x) { return x*x; }
#define MAX_BUFFER 256Por que isso é importante: As macros complexas (especialmente as macros de várias linhas ou do tipo função) podem esconder a lógica, confundir o fluxo de controlo e impedir a análise estática. Limitar o pré-processador a tarefas triviais (por exemplo, constantes e cabeçalhos) mantém o código explícito. Por exemplo, a substituição de macros por funções inline melhora a verificação de tipos e a depuração. Sem esta regra, erros subtis de expansão de macros ou erros de compilação condicional podem passar despercebidos nas revisões.
9. Limitar a utilização do ponteiro.
Limitar a indirecção a um único nível - evitar int** e ponteiros de função.
Exemplo de não conformidade (inderecção múltipla):
// Não conforme: ponteiro duplo e ponteiro de função
int **doublePtr;
int (*funcPtr)(int) = someFunction;Exemplo de conformidade (ponteiro único):
// Conformidade: ponteiro de nível único, sem ponteiros de função
int *singlePtr;
// Utilizar chamada explícita em vez de ponteiro de função
int result = someFunction(5);Porque é que isto é importante: Vários níveis de ponteiros e ponteiros de função complicam o fluxo de dados e dificultam o acompanhamento da memória ou do código que está a ser acedido. Os analisadores estáticos devem resolver cada indirecção, o que pode ser indecidível em geral. Ao restringir-se a referências de ponteiro único, o código fica mais simples e seguro. A violação desta regra pode levar a um aliasing pouco claro (um ponteiro que modifica dados através de outro) ou a um comportamento inesperado de retorno de chamada, ambos arriscados em contextos críticos para a segurança.
10. Compilar com todos os avisos activados e corrigi-los.
Ativar todos os avisos do compilador e resolvê-los antes do lançamento.
Exemplo de não conformidade (código com avisos)
// Non-compliant: code that generates warnings (uninitialized, suspicious assignment)
int x;
if (x = 5) { // bug: should be '==' or initialize x
// ...
}
printf("%d\n", x); // warning: 'x' is used uninitializedExemplo de conformidade (compilação limpa)
// Compliant: initialize variables and use '==' in condition
int x = 0;
if (x == 5) {
// ...
}
printf("%d\n", x);Por que isso é importante: Os avisos do compilador assinalam frequentemente erros genuínos (como variáveis não inicializadas, incompatibilidades de tipos ou atribuições não intencionais). A regra da NASA obriga a que nenhum aviso seja ignorado. Antes de qualquer lançamento, o código deve ser compilado sem avisos nas definições de máxima verbosidade. Esta prática detecta muitos erros triviais numa fase inicial. Se um aviso não puder ser resolvido, o código deve ser reestruturado ou documentado para que o aviso nunca ocorra.
Cada uma destas regras elimina uma categoria de erros ocultos. Quando seguidas em conjunto, tornam o código C muito mais previsível e verificável.
Conclusão
As 10 regras da NASA (o "Poder de 10") fornecem um padrão de codificação claro e eficaz para software C crítico. Ao evitar construções complexas e ao impor verificações, reduzem a possibilidade de bugs ocultos e tornam viável a análise estática. No desenvolvimento moderno, estas diretrizes podem ser automatizadas com ferramentas de qualidade de código. Podem ser definidas regras personalizadas para assinalar qualquer violação das diretrizes da NASA, e estas regras podem ser executadas em todos os pedidos de transferência, fornecendo feedback imediato aos programadores.
A adoção precoce destas verificações conduz a um código mais seguro e de maior qualidade, que é mais fácil de manter. Mesmo fora do sector aeroespacial, os princípios mantêm-se: funções pequenas e claras, loops explícitos, programação defensiva e nada de ginástica de ponteiros. Seguir e automatizar estas regras com uma ferramenta de qualidade de código ajuda a sua equipa a detetar erros atempadamente e a fornecer software mais fiável.
.avif)
