はじめに
宇宙船や自動車システムなどで使用されるような安全性が極めて重要なソフトウェアには、極めて信頼性の高いコードが求められます。これに対処するため、NASAのジェット推進研究所は2006年に
「Power of 10」コーディングルールを作成しました。これらの簡潔なガイドラインは、分析が困難な複雑なC言語の構造を取り除き、コードがシンプルで検証可能、かつ信頼性の高い状態を維持することを保証します。
今日、Aikidoのコード品質ツールのようなものは、すべての新しいプルリクエストで10のルールすべてを強制するために、カスタムチェックを設定できます。この記事では、各ルール、その重要性、および誤ったアプローチと正しいアプローチの両方を示すコード例を説明します。
これらの規則が重要な理由
NASAのルールは、可読性、解析可能性、信頼性に焦点を当てており、宇宙船制御やフライトソフトウェアなどのミッションクリティカルなアプリケーションにとって不可欠です。難解なC言語の構造を禁止し、防御的なチェックを強制することで、これらのガイドラインはコードのレビューと正確性の証明を容易にします。これらは、静的アナライザーが見落としがちなパターンに対処することで、MISRA Cのような標準を補完します。例えば、再帰と動的メモリを避けることでリソースの使用量を予測可能にし、戻り値のチェックを強制することでコンパイル時に多くのバグを捕捉するのに役立ちます。
実際、NASAがトヨタの電子スロットルファームウェアのような大量生産の組み込みシステムを研究したところ、何百ものルール違反が発見されました。これは、実際のプロジェクトが、これらのルールが防止するために設計されたのと同じ問題に頻繁に遭遇することを示しています。リストの各ルールは、一般的なエラー(無制御ループ、ヌルポインタの逆参照、目に見えない副作用など)のクラスを防止します。それらを無視すると、微妙なランタイム障害、セキュリティホール、または非決定的な動作につながる可能性があります。対照的に、10のルールすべてを遵守することで、静的検証ははるかに容易になります。
自動化されたツールは重要です。コード品質プラットフォームは、禁止された構造やパターンを検出するように設定できます。これらのルールは、すべてのプルリクエストで自動的に実行され、コードがマージされる前に問題を捕捉します。
コンテキストとルールを連携させる
個々のルールに入る前に、そのコンテキストを理解することが重要です。
- ターゲット言語: NASAの「Power of 10」ルールは、C言語向けに作成されました。C言語は、豊富なツールサポート(コンパイラ、アナライザ、デバッガ)がある一方で、未定義の動作で悪名高い言語です。これらはガベージコレクションや高度なメモリ管理がないことを前提としています。シンプルで構造化されたC言語のみを使用することで、静的解析を活用してプログラムの特性を証明できます。
- 静的解析: 自動チェックを容易にするための多くのルールが存在します。例えば、再帰の禁止(ルール1)やループ境界の要求(ルール2)により、ツールは任意の関数が持つイテレーション数やスタック使用量を証明できます。同様に、複雑なマクロの禁止やポインタの制限(ルール8~9)により、コードパターンがプリプロセッサの魔法や複数の間接参照に隠されることなく、明示的になります。
- 開発ワークフロー: 現代のDevSecOpsパイプラインでは、これらのルールはCIチェックの一部となります。コード品質ツールはGitHub、GitLab、またはBitbucketと統合し、各プルリクエストをレビューして、単純な問題とより複雑なパターンの両方を検出できます。NASAの各ガイドラインに対して、「gotoまたは再帰関数の使用をフラグ付けする」や「各ループにリテラル制限があることを確認する」などのカスタムルールを作成できます。一度設定すると、これらのルールは将来のすべてのコードスキャンで自動的に適用され、違反を早期に検出し、修正方法に関するガイダンスを提供します。
要するに、NASAの10のルールは防御的で分析可能なCプログラミングを体現しています。以下に各ルールをリストアップし、良いコードと悪いコードがどのようなものかを示し、そのルールが存在する理由と、それが軽減するリスクを説明します。
NASAの10のコーディングルール
1. 複雑な制御フローを避ける。
goto、setjmp、longjmpを使用せず、コードのいかなる部分でも再帰関数を作成することは避けてください。
❌ 非準拠の例
// Non-compliant: recursive function call
int factorial(int n) {
if (n <= 1) return 1;
return n * factorial(n-1); // recursion (direct)
}✅ 準拠例(ループを使用)
// 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;
}これが問題となる理由: 再帰とgotoは、推論が困難な非線形制御フローを作成します。再帰呼び出しはコールグラフを循環させ、スタック深度を無制限にし、gotoはスパゲッティコードを作成します。シンプルなループと直線的なコードを使用することで、静的アナライザーはスタック使用量とプログラムパスを容易に検証できます。このルールに違反すると、予期しないスタックオーバーフローや手動でのレビューが困難なロジックパスにつながる可能性があります。
2. ループには固定の上限を設ける必要があります。
すべてのループには、コンパイル時に検証可能な制限があるべきです。
❌ 非準拠例(無制限ループ):
// Non-compliant: loop with dynamic or unknown bound
int i = 0;
while (array[i] != 0) {
doSomething(array[i]);
i++;
}✅ 準拠例(固定境界ループ):
// 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]);
}これが問題となる理由: 境界のないループは、永久に実行されるか、リソース制限を超える可能性があります。固定された境界があれば、ツールは最大反復回数を静的に証明できます。安全性が重要なシステムでは、境界が欠けていると暴走ループを引き起こす可能性があります。明示的な制限(または静的配列サイズ)を強制することで、ループが予測可能に終了することを保証します。このルールがないと、ループロジックのエラーがデプロイまで検出されない可能性があります(例:無限ループを引き起こすオフバイワンエラー)。
3. 初期化後に動的メモリは使用しません。
実行中のコードでは、malloc/freeやヒープの使用を避け、固定割り当てまたはスタック割り当てのみを使用してください。
❌ 非準拠例(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);
}✅ 準拠例(静的割り当て)
// Compliant: fixed-size array on stack or global
#define MAX_SIZE 256
void storeData() {
int buffer[MAX_SIZE];
// ... use buffer without dynamic alloc ...
}重要な理由:実行時における動的メモリ割り当ては、特に宇宙船や組み込みコントローラのようなリソースが限られたシステムにおいて、予測不能な動作、メモリ断片化、または割り当て失敗を引き起こす可能性があります。ミッション中にmallocやfreeが失敗した場合、ソフトウェアがクラッシュしたり、予測不能な動作をしたりする可能性があります。固定サイズまたはスタック割り当てメモリのみを使用することで、決定論的な動作が保証され、検証が簡素化され、実行時のメモリリークが防止されます。
4. 関数は1ページ(約60行)に収まるようにします。
各関数は短く保ってください(目安として60行以内)。
❌ 非準拠例
// Non-compliant: hundreds of lines in one function (not shown)
void processAllData() {
// ... imagine 100+ lines of code doing many tasks ...
}✅ 準拠例(モジュール関数)
// Compliant: break the task into clear sub-functions
void processAllData() {
preprocessData();
analyzeData();
postprocessData();
}
void preprocessData() { /* ... */ }
void analyzeData() { /* ... */ }
void postprocessData(){ /* ... */ }重要な理由:非常に長い関数は、単一の単位として理解し、テストし、検証することが困難です。各関数を1つの概念的なタスクに限定し(かつ印刷された1ページ以内に収めることで)、コードレビューと静的チェックが扱いやすくなります。関数が多くの行にわたると、論理エラーや境界条件が見落とされる可能性があります。コードをより小さな関数に分割することで、明確さが向上し、他のルール(アサーション密度や関数ごとの戻り値チェックなど)を適用しやすくなります。
5. 関数ごとに少なくとも2つのassert文を使用します。
各関数は防御的なチェックを実行すべきです。
❌ 非準拠例(アサーションなし):
int get_element(int *array, size_t size, size_t index) {
return array[index];
}✅ 準拠例(アサーションあり):
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];
}重要な理由:アサーションは、無効な条件に対する最初の防御線です。NASAは、アサーション密度が高いほどバグを発見する可能性が大幅に増加することを発見しました。関数ごとに少なくとも2つのアサート(事前条件、制限、不変条件のチェック)を使用することで、コードはその前提条件を自己文書化し、テスト中に異常を即座に通知します。アサートがない場合、予期しない値が静かに伝播し、エラーの原因から遠く離れた場所で障害を引き起こす可能性があります。
6. データを最小限のスコープで宣言する。
変数は可能な限りローカルに保ち、グローバル変数は避けてください。
❌ 非準拠例(グローバルデータ):
// Non-compliant: global variable visible everywhere
int statusFlag;
void setStatus(int f) {
statusFlag = f;
}✅ 準拠例(ローカルスコープ):
// Compliant: local variable inside function
void setStatus(int f) {
int statusFlag = f;
// ... use statusFlag only here ...
}これが問題となる理由: スコープを最小限に抑えることは、結合度と意図しない相互作用を低減します。変数が関数内でのみ必要な場合、グローバルに宣言すると、他のコードが予期せず変更するリスクがあります。データをローカルに保つことで、各関数はより自己完結的で副作用がなくなり、分析とテストが簡素化されます。違反(グローバル状態の再利用など)は、エイリアシングや予期しない変更により、発見が困難なバグにつながる可能性があります。
7. すべての関数の戻り値とパラメータをチェックする。
呼び出し元は、void型以外のすべての戻り値を検査しなければなりません。また、すべての関数は、その入力パラメータを検証する必要があります。
❌ 非準拠の例(戻り値を無視)
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
}✅ 準拠例
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
}重要な理由:戻り値や無効なパラメータを無視することは、バグの主要な原因です。例えば、mallocのチェックを怠ると、ヌルポインタの逆参照につながる可能性があります。同様に、入力(配列インデックスやフォーマット文字列など)を検証しないと、バッファオーバーフローやクラッシュを引き起こす可能性があります。NASAは、すべての戻り値が処理されること(または意図を示すために明示的にvoidにキャストされること)、およびすべての引数が検証されることを要求しています。この包括的なアプローチにより、エラーが静かに無視されることがなくなります。
8. プリプロセッサはインクルードと単純なマクロに限定する。
複雑なマクロや条件付きコンパイルの技巧は避けてください。
❌ 非準拠例(複雑なマクロ):
#define DECLARE_FUNC(name) void func_##name(void)
DECLARE_FUNC(init); // 展開されると: void func_init(void)✅ 準拠例(シンプルなマクロ / インライン):
// Compliant: use inline function or straightforward definitions
static inline int sqr(int x) { return x*x; }
#define MAX_BUFFER 256重要な理由:複雑なマクロ(特に複数行または関数のようなマクロ)は、ロジックを隠蔽し、制御フローを混乱させ、静的解析を妨げる可能性があります。プリプロセッサを些細なタスク(定数やヘッダーなど)に限定することで、コードを明示的に保ちます。例えば、マクロをインライン関数に置き換えることで、型チェックとデバッグのしやすさが向上します。このルールがないと、微妙なマクロ展開バグや条件付きコンパイルエラーが見過ごされてしまう可能性があります。
9. ポインタの使用を制限する。
間接参照を単一レベルに制限し、int**や関数ポインタは避けてください。
❌ 非準拠例(多重間接参照):
// 非準拠: 二重ポインタと関数ポインタ
int **doublePtr;
int (*funcPtr)(int) = someFunction;✅ 準拠例(シングルポインター):
// 準拠: 単一レベルポインタ、関数ポインタなし
int *singlePtr;
// 関数ポインタの代わりに明示的な呼び出しを使用
int result = someFunction(5);これが問題となる理由: 複数のレベルのポインタと関数ポインタはデータフローを複雑にし、どのメモリやコードがアクセスされているかを追跡することを困難にします。静的アナライザーは各間接参照を解決する必要がありますが、これは一般的に決定不能である可能性があります。シングルポインタ参照に制限することで、コードはよりシンプルで安全になります。これに違反すると、不明瞭なエイリアシング(あるポインタが別のポインタを介してデータを変更する)や予期しないコールバック動作につながる可能性があり、これらはいずれも安全性が重要なコンテキストではリスクが高いです。
10. すべての警告を有効にしてコンパイルし、それらを修正してください。
すべてのコンパイラ警告を有効にし、リリース前にそれらに対処します。
❌ 非準拠例(警告を含むコード)
// 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 uninitialized✅ 準拠例(クリーンコンパイル)
// Compliant: initialize variables and use '==' in condition
int x = 0;
if (x == 5) {
// ...
}
printf("%d\n", x);重要な理由:コンパイラの警告は、未初期化変数、型不一致、意図しない代入などの真のバグをしばしば指摘します。NASAのルールでは、いかなる警告も無視してはならないと規定されています。リリース前に、コードは最大詳細度設定で警告なしにコンパイルされるべきです。この慣行により、多くの些細なミスを早期に発見できます。警告が解決できない場合は、警告がそもそも発生しないようにコードを再構築または文書化する必要があります。
これらの各ルールは、隠れたエラーのカテゴリを排除します。これらを一緒に遵守することで、Cコードははるかに予測可能で検証しやすくなります。
まとめ
NASAの10のルール(「Power of 10」)は、重要なCソフトウェア向けの明確で効果的なコーディング標準を提供します。複雑な構造を避け、チェックを強制することで、隠れたバグの可能性を減らし、静的解析を可能にします。現代の開発では、これらのガイドラインはコード品質ツールで自動化できます。NASAのガイドラインへの違反を検出するためにカスタムルールを定義でき、これらのルールはすべてのプルリクエストで実行され、開発者に即座のフィードバックを提供します。
これらのチェックを早期に採用することは、より安全で高品質かつ保守しやすいコードにつながります。航空宇宙分野以外でも、その原則は有効です:小さく明確な関数、明示的なループ、防御的プログラミング、そして恐ろしいポインタ操作はなし。コード品質ツールでこれらのルールに従い自動化することは、チームが早期にエラーを検出し、より信頼性の高いソフトウェアを出荷するのに役立ちます。

