第10章:ソフトウェア品質の科学 - エラー処理、並行性、セキュリティの理論

10.1 エラー処理の理論と歴史

10.1.1 例外処理の発明

CLUとBarbara Liskov

例外処理の概念は、1970年代にMITのBarbara Liskovによって開発されたCLU言語で初めて体系化された。CLUは「抽象データ型」の概念を導入した言語であり、データの不変条件が破られた場合の処理として「シグナル(signal)」メカニズムを持っていた。

【CLUにおけるシグナル】

proc sqrt(x: real) returns (real)
     signals (imaginary)
    if x < 0 then
        signal imaginary
    end
    ... 実際の計算 ...
end sqrt

使用側:
result := sqrt(value)
   except when imaginary:
       ... エラー処理 ...
   end

CLUの設計原則:
1. エラーは通常の戻り値とは別の経路で報告
2. エラー処理は呼び出し側で明示的に記述
3. シグナルは「checked exception」の先駆け

C++とDynamic Exceptions

1980年代後半、Bjarne StroustrupはC++に例外機構を追加した。これはAda言語やCLUからの影響を受けていた。

【例外処理の進化】

1960年代: GOTO と エラーコード
          if (error) goto error_handler;
          問題: スパゲッティコード、リソースリーク

1970年代: CLU シグナル
          構造化されたエラー伝播
          問題: 限定的な普及

1980年代: C++ 例外
          try-catch-throw モデル
          stack unwinding によるリソース解放

1990年代: Java checked exceptions
          コンパイル時にエラー処理を強制
          問題: ボイラープレートの増加

2000年代: 関数型アプローチ
          Option/Maybe, Either/Result型
          モナディックなエラー処理

10.1.2 エラー処理パラダイム

1. 戻り値によるエラー通知(Cスタイル)

// UNIX/C の伝統的パターン

/*
 * 規約:
 * - 成功: 0 または正の値
 * - 失敗: -1(詳細はerrnoに設定)
 *
 * 利点:
 * - シンプル、低オーバーヘッド
 * - 明示的なエラーチェック
 *
 * 欠点:
 * - エラーチェックが省略されやすい
 * - 戻り値とエラーの区別が困難な場合がある
 * - エラー情報の伝播が面倒
 */

int fd = open("/path/to/file", O_RDONLY);
if (fd < 0) {
    // errno を確認
    switch (errno) {
        case ENOENT: /* ファイルが存在しない */ break;
        case EACCES: /* アクセス権限なし */ break;
        default:     /* その他のエラー */ break;
    }
}

2. 例外機構(C++/Java/Pythonスタイル)

【例外の理論的基礎】

例外は「非局所的制御フロー」の一形態:
- 通常の制御フロー: 順次実行、条件分岐、ループ
- 非局所的制御: goto, longjmp, 例外

スタック巻き戻し(Stack Unwinding):
┌─────────────────────────────────────────────────┐
│ main()                                          │
│   ├── function_a()                              │
│   │     ├── function_b()                        │
│   │     │     ├── function_c()                  │
│   │     │     │     └── throw exception!        │
│   │     │     │           ↓                     │
│   │     │     │     デストラクタ呼び出し          │
│   │     │     │           ↓                     │
│   │     │     └── スタックフレーム解放            │
│   │     │           ↓                           │
│   │     └── catch でキャッチ                   │
│   │                                             │
└─────────────────────────────────────────────────┘

mermaid sequenceDiagram participant Main as main() participant FA as function_a() participant FB as function_b() participant FC as function_c()

Main->>FA: call activate FA FA->>FB: call activate FB FB->>FC: call activate FC Note over FC: throw exception! FC-->>FB: Unwind (Destructor) deactivate FC FB-->>FA: Unwind (Destructor) deactivate FB FA-->>Main: Catch Exception deactivate FA

RAII (Resource Acquisition Is Initialization): 例外と組み合わせることで、リソースの確実な解放を保証


**3. 代数的データ型(関数型スタイル)**

【Result/Either型の理論】

Result = Ok(T) | Err(E)

数学的には「直和型」(Sum Type): Result は「IntまたはString」を表現

鉄道指向プログラミング(Railway Oriented Programming):

成功の線路: ┌─────┐ ┌─────┐ ┌─────┐ ───┤ f1 ├─────┤ f2 ├─────┤ f3 ├───→ Ok(result) └──┬──┘ └──┬──┘ └──┬──┘ │ │ │ 失敗の線路:  │ │ └───────────┴───────────┴─────────→ Err(error)

各関数は成功時は次の関数へ、 失敗時は失敗線路に移行

モナドとしてのResult:

  • bind/flatMap: 成功時のみ次の関数を適用
  • 失敗はそのまま伝播

### 10.1.3 Tony Hoareの「10億ドルの過ち」

2009年、Tony Hoareは1965年にnull参照を発明したことを「10億ドルの過ち」と呼んだ。

【Null参照の問題】

Hoareの発言: "I call it my billion-dollar mistake. It was the invention of the null reference in 1965... This has led to innumerable errors, vulnerabilities, and system crashes..."

問題点:

  • 型安全性の破壊
- 任意の参照型がnullになりうる - コンパイラは検出できない

  • 意味の曖昧さ
- null = 「未初期化」? - null = 「存在しない」? - null = 「不明」?

  • NullPointerExceptionの頻発
- 実行時エラーの最大の原因

解決策の進化:

  • Option/Maybe型(ML, Haskell, Rust, Swift)
  • Nullable annotations(Java, Kotlin)
  • Optional types(C++17, TypeScript)

### 10.1.4 C言語でのエラー処理設計

**エラー処理の設計原則**

c // error.h - 体系的なエラー処理システム

#ifndef ERROR_H

define ERROR_H

include

/ エラー処理の設計原則 1. 失敗の可能性を型で表現 - 成功/失敗が明確な戻り値 - 詳細情報は別途取得 2. 早期リターン - エラーチェックは関数冒頭で - ネストを減らす 3. リソースの確実な解放 - goto cleanup パターン - または、単一return point 4. エラーの伝播 - 下位関数のエラーを適切に上位へ - 文脈の追加 /

// エラーコード定義 typedef enum e_error_code { E_SUCCESS = 0, E_NOMEM = -1, // メモリ不足 E_INVAL = -2, // 無効な引数 E_RANGE = -3, // 範囲外 E_IO = -4, // I/Oエラー E_PERM = -5, // 権限なし E_EXIST = -6, // 既に存在 E_NOENT = -7, // 存在しない E_BUSY = -8, // ビジー状態 E_TIMEOUT = -9, // タイムアウト E_OVERFLOW = -10, // オーバーフロー E_UNDERFLOW = -11, // アンダーフロー E_UNKNOWN = -999 // 不明 } t_error_code;

// エラー詳細情報 typedef struct s_error_info { t_error_code code; const char message; const char file; const char function; int line; int sys_errno; // システムerrno void context; // 追加コンテキスト } t_error_info;

// スレッドローカルなエラー情報 // C11の_Thread_local または GCC の__thread #if __STDC_VERSION__ >= 201112L _Thread_local extern t_error_info g_last_error; #else extern __thread t_error_info g_last_error; #endif

// エラー操作関数 void set_error(t_error_code code, const char message, const char file, const char func, int line); t_error_info get_last_error(void); void clear_error(void); const char error_code_to_string(t_error_code code);

// マクロ #define SET_ERROR(code, msg) \ set_error(code, msg, __FILE__, __func__, __LINE__)

#define RETURN_ERROR(code, msg) do { \ SET_ERROR(code, msg); \ return code; \ } while(0)

#define RETURN_NULL_ERROR(code, msg) do { \ SET_ERROR(code, msg); \ return NULL; \ } while(0)

// エラーチェックマクロ #define CHECK_NULL(ptr, retval) do { \ if (!(ptr)) { \ SET_ERROR(E_INVAL, "Null pointer"); \ return retval; \ } \ } while(0)

#define CHECK_ALLOC(ptr) do { \ if (!(ptr)) { \ SET_ERROR(E_NOMEM, "Allocation failed"); \ return NULL; \ } \ } while(0)

#endif


**Result型の実装**

c // result.h - C言語でのResult型

#ifndef RESULT_H

define RESULT_H

include "error.h"

/ Result型(Rust風) 代数的データ型の模倣: Result = Ok(T) | Err(E) Cでは以下の方法で模倣: 1. 構造体 + タグ 2. ユニオン型 /

// ジェネリックなResult型はマクロで生成 #define DEFINE_RESULT(name, ok_type) \ typedef struct s_result_##name { \ int is_ok; \ union { \ ok_type value; \ t_error_code error; \ }; \ } t_result_##name

// 基本的なResult型 DEFINE_RESULT(int, int); DEFINE_RESULT(size, size_t); DEFINE_RESULT(ptr, void); DEFINE_RESULT(str, char);

// Result操作マクロ #define OK(result_ptr, val) do { \ (result_ptr)->is_ok = 1; \ (result_ptr)->value = (val); \ } while(0)

#define ERR(result_ptr, err_code) do { \ (result_ptr)->is_ok = 0; \ (result_ptr)->error = (err_code); \ } while(0)

#define IS_OK(result) ((result).is_ok) #define IS_ERR(result) (!(result).is_ok) #define UNWRAP(result) ((result).value) #define UNWRAP_ERR(result) ((result).error)

// 条件付きマクロ #define TRY(result) do { \ if (IS_ERR(result)) { \ return result; \ } \ } while(0)

#define UNWRAP_OR(result, default_val) \ (IS_OK(result) ? UNWRAP(result) : (default_val))

// 使用例 / t_result_str safe_strdup(const char s) { t_result_str result;

if (!s) { ERR(&result, E_INVAL); return result; }

char copy = ft_strdup(s); if (!copy) { ERR(&result, E_NOMEM); return result; }

OK(&result, copy); return result; }

void example(void) { t_result_str result = safe_strdup("hello");

if (IS_OK(result)) { printf("Success: %s\n", UNWRAP(result)); free(UNWRAP(result)); } else { printf("Error: %d\n", UNWRAP_ERR(result)); } } /

#endif


**gotoによるクリーンアップパターン**

c // cleanup.c - リソース管理の正しいパターン

#include "libft.h" #include

/ goto cleanup パターン 歴史: Linuxカーネルで広く使われるパターン。 エラー発生時に確実にリソースを解放するため。 構造: 1. リソースをNULL/無効値で初期化 2. リソースを順次獲得 3. エラー時はcleanupへジャンプ 4. cleanupで逆順に解放 /

typedef struct s_config { char name; char path; int values; size_t value_count; int fd; } t_config;

t_config load_config(const char filename) { t_config config = NULL; char buffer = NULL; int fd = -1;

// リソース1: 構造体割り当て config = ft_calloc(1, sizeof(t_config)); if (!config) goto cleanup;

config->fd = -1; // 無効値で初期化

// リソース2: ファイルオープン fd = open(filename, O_RDONLY); if (fd < 0) goto cleanup; config->fd = fd;

// リソース3: バッファ割り当て buffer = malloc(4096); if (!buffer) goto cleanup;

// リソース4: 名前の割り当て config->name = ft_strdup("default"); if (!config->name) goto cleanup;

// リソース5: パスの割り当て config->path = ft_strdup(filename); if (!config->path) goto cleanup;

// リソース6: 値配列の割り当て config->values = malloc(sizeof(int) 100); if (!config->values) goto cleanup; config->value_count = 100;

// 処理... // ...

// 成功:一時バッファのみ解放 free(buffer); return config;

cleanup: // 逆順に解放 if (config) { free(config->values); free(config->path); free(config->name); if (config->fd >= 0) close(config->fd); free(config); } free(buffer); return NULL; }

void free_config(t_config config) { if (!config) return;

free(config->values); free(config->path); free(config->name); if (config->fd >= 0) close(config->fd); free(config); }


---

## 10.2 並行計算の理論

### 10.2.1 並行性の歴史

**Edsger Dijkstraと相互排除問題**

1965年、Dijkstraは「相互排除問題(Mutual Exclusion Problem)」を定式化した。複数のプロセスが共有リソースに同時にアクセスする際の問題を解決するため、彼はセマフォを発明した。

【相互排除問題の定義】

複数のプロセス P1, P2, ..., Pn が 臨界領域(Critical Section)にアクセスする際:

要件:

  • 相互排除(Mutual Exclusion)
- 同時に臨界領域に入れるのは1プロセスのみ

  • 進行(Progress)
- 臨界領域が空いているなら、待機プロセスは入れる

  • 限定待ち(Bounded Waiting)
- 無限に待たされるプロセスはいない

  • 無関与(No assumptions)
- プロセスの速度について仮定しない

【Dijkstraのセマフォ】

P(s): // wait / down while (s <= 0) do skip; s := s - 1;

V(s): // signal / up s := s + 1;

特性:

  • アトミック操作として実装
  • カウンティングセマフォ: s >= 0
  • バイナリセマフォ: s ∈ {0, 1}

**Tony Hoareとモニタ**

1974年、Tony Hoareは「モニタ」という抽象化を提案した。これは条件変数とミューテックスをカプセル化したものである。

【モニタの概念】

monitor BoundedBuffer { item buffer[N]; int count = 0; condition notFull, notEmpty;

procedure deposit(item x) { while (count == N) wait(notFull); buffer[count++] = x; signal(notEmpty); }

procedure fetch() returns item { while (count == 0) wait(notEmpty); item x = buffer[--count]; signal(notFull); return x; } }

モニタの特性:

  • 暗黙的な相互排除
- モニタ内の手続きは自動的に排他実行
  • 条件変数
- wait: 条件が満たされるまでブロック - signal: 待機中のプロセスを起こす

Java の synchronized と wait/notify はモニタの実装


### 10.2.2 メモリ一貫性モデル

**逐次一貫性(Sequential Consistency)**

Leslie Lamportが1979年に定義した、最も直感的なメモリモデル。

【逐次一貫性の定義】

"A multiprocessor system is sequentially consistent if the result of any execution is the same as if the operations of all the processors were executed in some sequential order, and the operations of each individual processor appear in this sequence in the order specified by its program."

つまり:

  • 全操作に全順序が存在
  • 各プロセッサの操作は元の順序を保持
  • 読み取りは最後の書き込み値を返す

【問題:逐次一貫性は高価】

CPU1: CPU2: x = 1; y = 1; r1 = y; r2 = x;

逐次一貫性では、r1 = r2 = 0 は不可能

しかし現代のCPUは:

  • ストアバッファによる遅延書き込み
  • 命令の並べ替え
  • キャッシュの非同期更新

により、r1 = r2 = 0 が発生しうる


**リラックスメモリモデル**

【メモリモデルの階層】

Strong Weak │ │ ▼ ▼ 逐次一貫性 > TSO > PSO > Weak ordering > Release-Acquire

TSO (Total Store Order): - x86/x86-64 のモデル - ストアはストアの後に並べ替えられない - ロードは他のロード/ストアと並べ替え可能

Release-Acquire: - C11/C++11 の基本モデル - acquire操作: その後の操作は前に移動しない - release操作: その前の操作は後に移動しない

【メモリバリア】

__asm__ __volatile__("mfence" ::: "memory"); // 全てのロード/ストアが完了するまで待機

atomic_thread_fence(memory_order_seq_cst); // C11のメモリフェンス


### 10.2.3 ロックフリーアルゴリズム

**Compare-and-Swap (CAS)**

ロックフリーアルゴリズムの基礎となるアトミック命令。

c // CASの理論

/ Compare-and-Swap (CAS) 擬似コード(アトミック操作として実行): bool CAS(address p, value expected, value desired) { if (p == expected) { p = desired; return true; } return false; } 特性: - ハードウェアでアトミック性を保証 - x86: CMPXCHG命令 - ARM: LDREX/STREX命令ペア 使用パターン: do { old_value = ptr; new_value = compute(old_value); } while (!CAS(ptr, old_value, new_value)); /

#include

// C11のアトミック操作 int atomic_increment(_Atomic int ptr) { int old, new; do { old = atomic_load(ptr); new = old + 1; } while (!atomic_compare_exchange_weak(ptr, &old, new)); return new; }


**ABA問題**

【ABA問題】

CASの重要な落とし穴:

時刻 スレッド1 スレッド2 共有変数 t1 old = A を読む ptr = A t2 (中断) ptr を B に変更 ptr = B t3 (中断) ptr を A に戻す ptr = A t4 CAS(ptr, A, C) (完了) ptr = C 成功!しかし...

問題:

  • スレッド1は A → C への変更に成功
  • しかし、ptr は A → B → A と変化していた
  • 間の状態変化を検知できない

解決策:

  • タグ付きポインタ(Tagged Pointer)
- ポインタにカウンタを付加 - カウンタは単調増加

  • ハザードポインタ(Hazard Pointer)
- 使用中のポインタを登録 - 登録されている間は解放しない

  • Epoch-based Reclamation
- グローバルなエポックカウンタ - 安全な時期にまとめて解放

### 10.2.4 Amdahlの法則

**並列化の限界**

【Amdahlの法則】

Gene Amdahl (1967) が提唱した並列化の限界を示す法則:

スピードアップ S(n) = 1 / ((1 - P) + P/n)

P = 並列化可能な部分の割合 n = プロセッサ数

例: プログラムの80%が並列化可能

n=2: S = 1 / (0.2 + 0.4) = 1.67倍 n=4: S = 1 / (0.2 + 0.2) = 2.50倍 n=8: S = 1 / (0.2 + 0.1) = 3.33倍 n=∞: S = 1 / 0.2 = 5.00倍(理論上限)

重要な洞察:

  • 20%のシリアル部分が5倍の上限を決定
  • プロセッサを増やしても収穫逓減
  • シリアル部分の最適化が重要

【Gustafsonの法則】

John Gustafson (1988) による反論:

問題のサイズを固定するのではなく、 プロセッサ数に応じて問題を拡大すれば、 効率的な並列化が可能

S(n) = (1 - P) + n P

より大きな問題を同じ時間で解く観点


### 10.2.5 スレッド同期の実装

c // thread_sync.h - スレッド同期プリミティブ

#ifndef THREAD_SYNC_H

define THREAD_SYNC_H

include

include

/ スピンロック 理論: - ビジーウェイト(CPU時間を消費) - 短い臨界領域に適する - コンテキストスイッチのオーバーヘッドなし 実装: CASを使用したTest-and-Set /

typedef struct s_spinlock { atomic_flag flag; } t_spinlock;

static inline void spinlock_init(t_spinlock lock) { atomic_flag_clear(&lock->flag); }

static inline void spinlock_lock(t_spinlock lock) { while (atomic_flag_test_and_set_explicit( &lock->flag, memory_order_acquire)) { // スピン // 必要に応じてpauseヒントを挿入 __asm__ __volatile__("pause" ::: "memory"); } }

static inline void spinlock_unlock(t_spinlock lock) { atomic_flag_clear_explicit(&lock->flag, memory_order_release); }

/ 読み書きロック(Reader-Writer Lock) 理論: - 複数リーダーの同時読み取りを許可 - ライターは排他アクセス - 読み取りが多い場合に効率的 公平性のバリエーション: - リーダー優先: ライターが飢餓になりうる - ライター優先: リーダーが飢餓になりうる - 公平: 到着順 /

typedef struct s_rwlock { pthread_rwlock_t lock; } t_rwlock;

static inline void rwlock_init(t_rwlock lock) { pthread_rwlock_init(&lock->lock, NULL); }

static inline void rwlock_read_lock(t_rwlock lock) { pthread_rwlock_rdlock(&lock->lock); }

static inline void rwlock_write_lock(t_rwlock lock) { pthread_rwlock_wrlock(&lock->lock); }

static inline void rwlock_unlock(t_rwlock lock) { pthread_rwlock_unlock(&lock->lock); }

/ セマフォ 理論: - カウンタを持つ同期プリミティブ - wait: カウンタ > 0 なら減らして進行、 そうでなければブロック - post: カウンタを増やし、待機者を起こす 用途: - リソースプール管理 - 生産者-消費者問題 /

include

typedef struct s_semaphore { sem_t sem; } t_semaphore;

static inline void semaphore_init(t_semaphore sem, int value) { sem_init(&sem->sem, 0, value); }

static inline void semaphore_wait(t_semaphore sem) { sem_wait(&sem->sem); }

static inline void semaphore_post(t_semaphore sem) { sem_post(&sem->sem); }

/ 条件変数(Condition Variable) 理論: - 条件が満たされるまで待機 - ミューテックスと組み合わせて使用 - Hoareモニタの条件変数に対応 注意: spurious wakeup が発生しうる 常にwhileループで条件をチェック /

typedef struct s_condition { pthread_cond_t cond; pthread_mutex_t mutex; } t_condition;

static inline void condition_init(t_condition cv) { pthread_cond_init(&cv->cond, NULL); pthread_mutex_init(&cv->mutex, NULL); }

static inline void condition_wait(t_condition cv, int (pred)(void), void arg) { pthread_mutex_lock(&cv->mutex); while (!pred(arg)) { // spurious wakeup対策 pthread_cond_wait(&cv->cond, &cv->mutex); } pthread_mutex_unlock(&cv->mutex); }

static inline void condition_signal(t_condition cv) { pthread_mutex_lock(&cv->mutex); pthread_cond_signal(&cv->cond); pthread_mutex_unlock(&cv->mutex); }

static inline void condition_broadcast(t_condition cv) { pthread_mutex_lock(&cv->mutex); pthread_cond_broadcast(&cv->cond); pthread_mutex_unlock(&cv->mutex); }

#endif


---

## 10.3 コンピュータセキュリティの基礎

### 10.3.1 セキュリティの歴史

**Morris Wormと目覚め**

1988年、Robert Tappan MorrisによるMorris Wormは、インターネットセキュリティの重要性を世界に示した最初の大規模事件であった。

【Morris Worm の技術】

脆弱性の利用:

  • sendmail の DEBUG コマンド
  • fingerd のバッファオーバーフロー
  • rsh/rexec の弱い認証

バッファオーバーフローの原理:

graph TD
    subgraph Normal_Stack ["Normal Stack"]
        direction TB
        N1["Return Address"]
        N2["Saved EBP"]
        N3["Local Buffer [64 bytes]"]
        N1 --- N2 --- N3
    end
    subgraph Overflow_Stack ["Stack After Overflow"]
        direction TB
        O1["Shellcode Address (Overwritten Return)"]
        O2["AAAA... (Overwritten EBP)"]
        O3["Shellcode (Malicious Code)"]
        O1 --- O2 --- O3
    end
    style O1 fill:#f9f,stroke:#333,stroke-width:2px
    style O3 fill:#f9f,stroke:#333,stroke-width:2px

### 10.3.2 攻撃の分類

**STRIDE脅威モデル**

Microsoftが開発した脅威の分類法:

【STRIDE脅威モデル】

S - Spoofing Identity(なりすまし) 例: 偽のログインページ、セッションハイジャック

T - Tampering with Data(データ改ざん) 例: man-in-the-middle攻撃、SQLインジェクション

R - Repudiation(否認) 例: ログの削除、否認攻撃

I - Information Disclosure(情報漏洩) 例: 機密データの露出、サイドチャネル攻撃

D - Denial of Service(サービス妨害) 例: リソース枯渇、クラッシュ誘発

E - Elevation of Privilege(権限昇格) 例: バッファオーバーフロー、シェルコード実行


**OWASP Top 10(2021)**

【Webアプリケーションの脅威】

A01: Broken Access Control(アクセス制御の不備) A02: Cryptographic Failures(暗号化の失敗) A03: Injection(インジェクション) A04: Insecure Design(安全でない設計) A05: Security Misconfiguration(セキュリティ設定ミス) A06: Vulnerable and Outdated Components(脆弱なコンポーネント) A07: Identification and Authentication Failures(認証の失敗) A08: Software and Data Integrity Failures(整合性の失敗) A09: Security Logging and Monitoring Failures(ログ・監視の失敗) A10: Server-Side Request Forgery(SSRF)


### 10.3.3 防御的プログラミング

**安全なCプログラミングのルール**

c // secure_coding.h - セキュアコーディングのプラクティス

#ifndef SECURE_CODING_H

define SECURE_CODING_H

include "libft.h"

include

/ セキュアコーディングの原則 1. 入力を信頼しない - すべての外部入力を検証 - 境界チェック 2. 最小権限の原則 - 必要最小限の権限で動作 3. 深層防御 - 複数のセキュリティ層 4. デフォルトで安全 - 明示的に許可されていないものは拒否 5. シンプルさを保つ - 複雑さはセキュリティの敵 /

// ====================================== // 1. 整数オーバーフロー対策 // ======================================

/ 整数オーバーフローの危険性: size_t size = user_input; // 巨大な値 size_t total = size sizeof(int); // オーバーフロー! char buf = malloc(total); // 小さなバッファが確保される memcpy(buf, data, size sizeof(int)); // バッファオーバーフロー /

// 安全な加算 static inline int safe_add_size(size_t a, size_t b, size_t result) { if (a > SIZE_MAX - b) return -1; // オーバーフロー result = a + b; return 0; }

// 安全な乗算 static inline int safe_mul_size(size_t a, size_t b, size_t result) { if (a != 0 && b > SIZE_MAX / a) return -1; // オーバーフロー result = a b; return 0; }

// 安全なcalloc static inline void safe_calloc(size_t nmemb, size_t size) { size_t total; if (safe_mul_size(nmemb, size, &total) < 0) return NULL; return calloc(1, total); }

// ====================================== // 2. バッファオーバーフロー対策 // ======================================

/ 危険な関数とその代替: gets() → fgets() または getline() strcpy() → strlcpy() または strncpy() + 終端 strcat() → strlcat() sprintf() → snprintf() scanf("%s") → scanf("%Ns") で幅指定 /

// strlcpy の安全な実装 static inline size_t safe_strlcpy(char dst, const char src, size_t dsize) { const char osrc = src; size_t nleft = dsize;

// NULLチェック if (!dst || !src || dsize == 0) return 0;

// コピー while (--nleft != 0 && src != '\0') dst++ = src++;

// 終端 if (nleft == 0) { if (dsize != 0) dst = '\0'; while (src++) ; }

return (size_t)(src - osrc); }

// snprintf のラッパー static inline int safe_snprintf(char buf, size_t size, const char fmt, ...) { va_list ap; int ret;

if (!buf || size == 0) return -1;

va_start(ap, fmt); ret = vsnprintf(buf, size, fmt, ap); va_end(ap);

if (ret < 0 || (size_t)ret >= size) return -1; // 切り詰めが発生

return ret; }

// ====================================== // 3. フォーマット文字列攻撃対策 // ======================================

/ 危険なパターン: printf(user_input); // 攻撃者が %x, %n 等を注入可能 安全なパターン: printf("%s", user_input); // 文字列として扱う /

// printfのラッパー(フォーマット文字列を固定) #define SAFE_PRINTF(fmt, ...) printf(fmt, ##__VA_ARGS__) // ユーザー入力をフォーマット文字列として使わない

// ====================================== // 4. タイミング攻撃対策 // ======================================

/ 問題: strcmp(password, input) は最初の不一致で終了 → 比較時間から正しい文字数が推測可能 解決: 定数時間比較を使用 /

// 定数時間比較(タイミング攻撃対策) static inline int constant_time_cmp(const void a, const void b, size_t n) { const volatile unsigned char pa = a; const volatile unsigned char pb = b; volatile unsigned char result = 0;

for (size_t i = 0; i < n; i++) result |= pa[i] ^ pb[i];

return result == 0; // 全バイトが一致した場合のみ true }

// ====================================== // 5. メモリの安全なクリア // ======================================

/ 問題: memset(password, 0, len); コンパイラがデッドストアとして削除する可能性 解決: volatile を使用して最適化を防ぐ /

// 安全なメモリクリア(最適化で削除されない) static inline void secure_zero(void ptr, size_t size) { volatile unsigned char p = ptr; while (size--) p++ = 0; // メモリバリアで確実性を高める __asm__ __volatile__("" ::: "memory"); }

// ====================================== // 6. 入力検証 // ======================================

// パストラバーサル対策 static inline int validate_path(const char path) { if (!path || !path) return 0;

// ".." を含むパスを拒否 if (strstr(path, "..")) return 0;

// 絶対パスを拒否(必要に応じて) if (path[0] == '/') return 0;

// NULバイトを含むパスを拒否 size_t len = strlen(path); for (size_t i = 0; i < len; i++) { if (path[i] == '\0') return 0; }

return 1; }

// UTF-8 検証 static inline int validate_utf8(const unsigned char str, size_t len) { size_t i = 0;

while (i < len) { if (str[i] <= 0x7F) { // ASCII if (str[i] == 0 && i < len - 1) return 0; // 途中のNUL i++; } else if ((str[i] & 0xE0) == 0xC0) { // 2バイト if (i + 1 >= len) return 0; if ((str[i + 1] & 0xC0) != 0x80) return 0; i += 2; } else if ((str[i] & 0xF0) == 0xE0) { // 3バイト if (i + 2 >= len) return 0; if ((str[i + 1] & 0xC0) != 0x80 || (str[i + 2] & 0xC0) != 0x80) return 0; i += 3; } else if ((str[i] & 0xF8) == 0xF0) { // 4バイト if (i + 3 >= len) return 0; if ((str[i + 1] & 0xC0) != 0x80 || (str[i + 2] & 0xC0) != 0x80 || (str[i + 3] & 0xC0) != 0x80) return 0; i += 4; } else { return 0; // 不正なバイト } }

return 1; }

#endif


---

## 10.4 形式手法とプログラム検証

### 10.4.1 形式手法の歴史

**Hoare論理**

1969年、Tony Hoareは「An Axiomatic Basis for Computer Programming」を発表。プログラムの正しさを数学的に証明する基盤を確立した。

【Hoare三つ組(Hoare Triple)】

{P} S {Q}

P: 事前条件(Precondition) S: プログラム Q: 事後条件(Postcondition)

意味: 「Pが成り立つ状態でSを実行すると、 終了した場合はQが成り立つ」

【推論規則の例】

代入公理: {P[x/E]} x := E {P}

例: {x + 1 > 0} x := x + 1 {x > 0}

順次構成: {P} S1 {Q} {Q} S2 {R} ───────────────────────── {P} S1; S2 {R}

条件分岐: {P ∧ B} S1 {Q} {P ∧ ¬B} S2 {Q} ─────────────────────────────────── {P} if B then S1 else S2 {Q}

ループ不変条件: {P ∧ B} S {P} ───────────────────── {P} while B do S {P ∧ ¬B}


### 10.4.2 静的解析

**抽象解釈(Abstract Interpretation)**

Patrick CousetとRahdia Cousetが1977年に発表。プログラムの性質を近似的に解析する理論的フレームワーク。

【抽象解釈の考え方】

具体ドメイン → 抽象ドメイン

例: 整数変数の符号解析

具体値: -5, 0, 3, 1000, ...

抽象値: ⊥, -, 0, +, ⊤ ⊥: 値なし(到達不能) -: 負 0: ゼロ +: 正 ⊤: 任意(不明)

抽象演算: + × + = + + × - = - + × 0 = 0

  • × - = +
+ + - = ⊤ (不明)

実用例:

  • 0除算の検出
  • NULLポインタ参照
  • 配列境界チェック
  • 整数オーバーフロー

### 10.4.3 デバッグの科学

**科学的デバッグ法**

【科学的方法のデバッグへの適用】

  • 観察
- バグの症状を正確に記録 - 再現手順を確立

  • 仮説
- 症状を説明する原因を推測 - 複数の仮説を立てる

  • 予測
- 仮説が正しければ何が起こるか - 検証可能な予測を立てる

  • 実験
- 最小限の変更で検証 - 一度に一つの変数だけ変更

  • 結論
- 仮説を支持/棄却 - 次の仮説へ

【バグの分類(Boris Beizer)】

  • 要件の問題 (9%)
  • 設計の問題 (26%)
  • コーディングの問題 (55%)
- 構文エラー - 論理エラー - 境界条件 - 初期化漏れ
  • テストの問題 (10%)

**デバッグツールの理論**

c // debug.h - デバッグ支援ツール

#ifndef DEBUG_H

define DEBUG_H

include

include

include

/ デバッグ出力のレベル /

typedef enum e_debug_level { DEBUG_TRACE = 0, // 詳細なトレース DEBUG_DEBUG = 1, // デバッグ情報 DEBUG_INFO = 2, // 一般情報 DEBUG_WARN = 3, // 警告 DEBUG_ERROR = 4, // エラー DEBUG_FATAL = 5, // 致命的エラー DEBUG_OFF = 6 // 出力なし } t_debug_level;

extern t_debug_level g_debug_level;

#define DEBUG_LOG(level, fmt, ...) do { \ if ((level) >= g_debug_level) { \ fprintf(stderr, "[%s] %s:%d: " fmt "\n", \ debug_level_str(level), __FILE__, __LINE__, \ ##__VA_ARGS__); \ } \ } while(0)

#define TRACE(fmt, ...) DEBUG_LOG(DEBUG_TRACE, fmt, ##__VA_ARGS__) #define DEBUG(fmt, ...) DEBUG_LOG(DEBUG_DEBUG, fmt, ##__VA_ARGS__) #define INFO(fmt, ...) DEBUG_LOG(DEBUG_INFO, fmt, ##__VA_ARGS__) #define WARN(fmt, ...) DEBUG_LOG(DEBUG_WARN, fmt, ##__VA_ARGS__) #define ERROR(fmt, ...) DEBUG_LOG(DEBUG_ERROR, fmt, ##__VA_ARGS__) #define FATAL(fmt, ...) do { \ DEBUG_LOG(DEBUG_FATAL, fmt, ##__VA_ARGS__); \ print_backtrace(); \ abort(); \ } while(0)

/ アサーション 標準のassertより情報が豊富 /

#ifdef NDEBUG

define ASSERT(cond, msg) ((void)0)

#else

define ASSERT(cond, msg) do { \

if (!(cond)) { \ fprintf(stderr, "Assertion failed: %s\n", msg); \ fprintf(stderr, " Condition: %s\n", #cond); \ fprintf(stderr, " Location: %s:%d in %s()\n", \ __FILE__, __LINE__, __func__); \ print_backtrace(); \ abort(); \ } \ } while(0) #endif

/ バックトレース /

static inline void print_backtrace(void) { void array[50]; char *strings; int size;

size = backtrace(array, 50); strings = backtrace_symbols(array, size);

fprintf(stderr, "\n=== Backtrace ===\n"); for (int i = 0; i < size; i++) { fprintf(stderr, "#%d %s\n", i, strings[i]); } fprintf(stderr, "=================\n\n");

free(strings); }

/ シグナルハンドラ /

static inline void debug_signal_handler(int sig) { fprintf(stderr, "\n=== Signal %d received ===\n", sig); print_backtrace(); signal(sig, SIG_DFL); raise(sig); }

static inline void install_debug_handlers(void) { signal(SIGSEGV, debug_signal_handler); signal(SIGABRT, debug_signal_handler); signal(SIGBUS, debug_signal_handler); signal(SIGILL, debug_signal_handler); signal(SIGFPE, debug_signal_handler); }

/ メモリデバッグ支援 /

// メモリダンプ static inline void hexdump(const void ptr, size_t size) { const unsigned char p = ptr; char ascii[17];

fprintf(stderr, "\n=== Memory Dump at %p ===\n", ptr); for (size_t i = 0; i < size; i++) { if (i % 16 == 0) fprintf(stderr, "%08zx: ", i);

fprintf(stderr, "%02x ", p[i]); ascii[i % 16] = (p[i] >= 32 && p[i] < 127) ? p[i] : '.';

if (i % 16 == 15) { ascii[16] = '\0'; fprintf(stderr, " |%s|\n", ascii); } }

// 最後の行の処理 if (size % 16 != 0) { for (size_t i = size % 16; i < 16; i++) fprintf(stderr, " "); ascii[size % 16] = '\0'; fprintf(stderr, " |%s|\n", ascii); }

fprintf(stderr, "=========================\n\n"); }

#endif


---

## 10.5 ソフトウェア工学の原則

### 10.5.1 設計原則

**SOLID原則**

Robert C. Martin(Uncle Bob)によってまとめられたオブジェクト指向設計の5原則。C言語にも適用可能。

【SOLID原則のCへの適用】

S - Single Responsibility Principle(単一責任の原則) 「モジュールは変更の理由を1つだけ持つべき」

// 悪い例: 複数の責任 void process_and_log_and_save(Data d);

// 良い例: 責任の分離 void process(Data d); void log(const char message); void save(Data d, const char path);

O - Open/Closed Principle(開放閉鎖の原則) 「拡張に対して開き、修正に対して閉じている」

// 関数ポインタによる拡張 typedef void (operation_t)(void data);

void apply_operation(void data, operation_t op) { op(data); // 新しい操作を追加しても、この関数は変更不要 }

L - Liskov Substitution Principle(リスコフの置換原則) 「派生型は基底型と置換可能であるべき」

// 共通インターフェースを持つ構造体 typedef struct s_shape { double (area)(void self); void (draw)(void self); } t_shape;

// どの形状でも同じように扱える

I - Interface Segregation Principle(インターフェース分離の原則) 「クライアントは使わないインターフェースに依存すべきでない」

// 悪い例: 巨大なヘッダ #include "everything.h"

// 良い例: 必要な部分だけ #include "string_ops.h" // 文字列操作のみ

D - Dependency Inversion Principle(依存性逆転の原則) 「抽象に依存すべき」

// 関数ポインタで依存を注入 typedef int (reader_t)(void ctx, char buf, size_t size);

void process_input(reader_t reader, void ctx) { char buf[256]; reader(ctx, buf, sizeof(buf)); // 具体的な入力源に依存しない }


### 10.5.2 コードメトリクス

**循環的複雑度(Cyclomatic Complexity)**

Thomas McCabeが1976年に提案した、コードの複雑さを測る指標。

【循環的複雑度】

M = E - N + 2P

E: エッジ数(制御フローの遷移) N: ノード数(基本ブロック) P: 連結成分数(通常は1)

簡易計算: M = 分岐の数 + 1 = if + while + for + case + && + || + ?: + 1

推奨値: 1-10: シンプル、低リスク 11-20: やや複雑 21-50: 複雑、テスト困難 51+: テスト不可能、リファクタリング必須

例: int example(int x, int y) { // +1 if (x > 0) { // +1 if (y > 0) { // +1 return x + y; } else { return x - y; } } else if (x < 0) { // +1 return -x + y; } else { for (int i = 0; i < y; i++) { // +1 x += i; } return x; } } // 循環的複雑度: 5


### 10.5.3 コーディングスタイル

c // style.h - 42スタイルの拡張ガイドライン

/ ============================================ 42 Libft 拡張コーディングスタイル ============================================ 1. 命名規則 ────────────────────────────────────── 関数名: ft_[category]_[action] 例: ft_str_trim, ft_lst_find 構造体: s_[name] 例: s_list_node 列挙型: e_[name] 例: e_error_code 型定義: t_[name] 例: t_list_node 定数: UPPER_CASE 例: MAX_BUFFER_SIZE グローバル: g_[name] 例: g_error_handler 静的変数: s_[name](関数内) ポインタ: ptr, p, または意味のある名前 2. 関数設計 ────────────────────────────────────── - 1関数1責任 - 最大25行(42規定) - 最大4引数 - 副作用を最小化 - エラーは戻り値で通知 3. エラー処理パターン ────────────────────────────────────── // 早期リターン if (!ptr) return (NULL); // goto cleanup if (condition_fails) goto cleanup; 4. メモリ管理 ────────────────────────────────────── - 確保と解放の対称性 - create/destroy ペア - NULLチェックを徹底 - 解放後のNULLセット 5. ドキュメンテーション ────────────────────────────────────── ヘッダファイル: - 関数の目的 - 引数の説明 - 戻り値の説明 - エラー条件 ソースファイル: - 複雑なアルゴリズムの説明 - 重要な決定の理由 /

// 良い例: 明確な構造と責任分離

typedef struct s_string_builder { char buffer; size_t size; size_t capacity; } t_string_builder;

/ ft_sb_create - 新しい文字列ビルダーを作成 @param initial_capacity: 初期容量(0の場合はデフォルト値) @return: 新しいビルダー、失敗時NULL / t_string_builder ft_sb_create(size_t initial_capacity);

/ ft_sb_append - 文字列を追加 @param sb: ビルダー @param str: 追加する文字列 * @return: 成功時0、失敗時-1 / int ft_sb_append(t_string_builder sb, const char str);

/ ft_sb_to_string - バッファの内容を文字列として取得 @param sb: ビルダー @return: 新しく確保された文字列、失敗時NULL @note: 呼び出し側が解放責任を持つ / char ft_sb_to_string(t_string_builder sb);

/ ft_sb_destroy - ビルダーを破棄 @param sb: 破棄するビルダー(NULLでも安全) / void ft_sb_destroy(t_string_builder *sb);


---

## 10.6 継続学習のロードマップ

### 10.6.1 コンピュータサイエンスの体系

【CSカリキュラムの構造】

mindmap
  root((CS Curriculum))
    Level 1: Basics
      Programming Intro
        Libft
      Discrete Math
      Data Structures & Algo
      Computer Architecture
    Level 2: Intermediate
      OS
        Process Management
        Memory Management
        File Systems
      Networks
        Socket Programming
      Databases
      Software Engineering
    Level 3: Advanced
      Compilers
      Concurrent Programming
      Distributed Systems
      Computer Graphics
      Machine Learning
    Level 4: Specialized
      Security
      Formal Methods
      HPC
      Research

### 10.6.2 推奨学習リソース

**書籍**

【基礎】
  • "The C Programming Language" (K&R)
C言語のバイブル

  • "Computer Systems: A Programmer's Perspective" (Bryant & O'Hallaron)
コンピュータシステムの包括的理解

  • "Introduction to Algorithms" (CLRS)
アルゴリズムの標準教科書

【中級】

  • "Advanced Programming in the UNIX Environment" (Stevens)
UNIX システムプログラミング

  • "Operating Systems: Three Easy Pieces" (Arpaci-Dusseau)
OS の概念を理解しやすく

  • "Computer Networking: A Top-Down Approach" (Kurose & Ross)
ネットワークの理論と実践

【上級】

  • "The Art of Computer Programming" (Knuth)
アルゴリズムの芸術

  • "Compilers: Principles, Techniques, and Tools" (Aho et al.)
コンパイラの古典

  • "Modern Operating Systems" (Tanenbaum)
現代的なOS設計

**オンラインリソース**

【ドキュメント】
  • man pages (man 2, man 3)
  • POSIX規格書
  • cppreference.com

【コースウェア】

  • MIT OpenCourseWare
  • Stanford Online
  • Coursera / edX

【実践】

  • Linux Kernel Source
  • GNU C Library (glibc)
  • musl libc

### 10.6.3 次のステップ

【42プロジェクトでの発展】

ft_printf:

  • 可変長引数の理論
  • 書式指定子のパース
  • 数値変換アルゴリズム

get_next_line:

  • ファイル I/O の理論
  • バッファ管理
  • 静的変数の適切な使用

minishell:

  • プロセスモデル
  • シグナル処理
  • パイプとリダイレクション

philosophers:

  • 並行性の古典問題
  • デッドロック回避
  • 同期プリミティブ

miniRT / cub3D:

  • レイトレーシング理論
  • 線形代数の応用
  • 3Dグラフィックス数学
```

---

まとめ

この章では、ソフトウェア品質を支える理論的基盤を学びました。

学んだ理論的基盤:

  • エラー処理の理論
- 例外処理の歴史(CLU → C++ → Java → 関数型) - 戻り値、例外、Result型の比較 - Hoareの「10億ドルの過ち」

  • 並行計算理論
- Dijkstraのセマフォ、Hoareのモニタ - メモリ一貫性モデル - ロックフリーアルゴリズムとCAS - Amdahlの法則

  • コンピュータセキュリティ
- 攻撃の分類(STRIDE, OWASP) - 防御的プログラミング - バッファオーバーフロー対策 - タイミング攻撃対策

  • 形式手法
- Hoare論理 - 抽象解釈 - 静的解析

  • ソフトウェア工学原則
- SOLID原則 - 循環的複雑度 - コーディング規約

Libftを超えて

Libftは基礎の入口に過ぎません。ここで学んだ原理は、より高度なプロジェクト、より大規模なシステム、より複雑な問題に取り組む際の土台となります。

継続的な学習、コードの読み書き、そして常に「なぜ」を問い続ける姿勢が、優れたソフトウェアエンジニアへの道です。