一、大白话:这玩意儿到底在干嘛
                                                                                                                          你要往内核里塞一段 eBPF
  程序(网络过滤、跟踪、安全策略)。内核不敢直接跑你的代码——万一你写了死循环、越界读写、崩在内核里就全完了。            esc to interrupt

  所以内核有个验证器(Verifier),它在加载前干一件事:

  ▎ 把你程序的每一条可能走到的路径,都在脑子里过一遍,确认绝对安全才放行。

  问题来了:
  - 一个 if 就是 2 条路径
  - 两个 if 是 4 条
  - 30 个嵌套 if 就是 10 亿条
  - 循环更夸张

  如果真让它全走完,内核加载一个程序要算几年。于是就有了复杂度限制:

  ┌─────────────────────────────┬───────────────────────────────────┬────────────────────────────┐
  │            名字             │                值                 │            含义            │
  ├─────────────────────────────┼───────────────────────────────────┼────────────────────────────┤
  │ BPF_COMPLEXITY_LIMIT_INSNS  │ 100 万(早期 4096 → 131072 → 1M) │ 验证期间"踩过"的指令数上限 │
  ├─────────────────────────────┼───────────────────────────────────┼────────────────────────────┤
  │ BPF_COMPLEXITY_LIMIT_STATES │ 64                                │ 单个分支点保留的状态数     │
  ├─────────────────────────────┼───────────────────────────────────┼────────────────────────────┤
  │ BPF_MAXINSNS                │ 100 万                            │ 程序本身最大指令数         │
  └─────────────────────────────┴───────────────────────────────────┴────────────────────────────┘

  这个 100 万不是程序有多少行,是"验证器总共模拟执行了多少步"。 超了就直接拒绝加载,报 BPF program is too large.
  Processed N insn。

  为了不把这 100 万烧光,验证器还有个关键技巧——状态剪枝(state pruning):

  ▎ 如果我之前走到 PC=42 时寄存器状态是 {r0∈[0,10], r1=ctx},现在又走到 PC=42
  ▎ 且状态一样或更宽松,那后面怎么走我已经算过了,直接跳过。

  没有剪枝 → 指数爆炸;有剪枝 → 大多数程序能控制在几千步内。

  ---
  二、完整流程(验证器做的事)

  加载 BPF 程序
     │
     ▼
  ┌────────────────────────────────┐
  │ 1. 静态检查:指令合法、跳转在界内  │
  └────────────────────────────────┘
     │
     ▼
  ┌──────────────────────────────────────────┐
  │ 2. 符号执行(核心)                         │
  │   工作栈 ← 初始状态 (pc=0, 寄存器未知)       │
  │   while 栈非空:                            │
  │       弹出一个状态 S                         │
  │       complexity++ ; 若 > 1M → 拒绝         │
  │       若 S 与已访问状态等价 → 剪枝,continue   │
  │       记录 S                                │
  │       按指令类型模拟执行:                     │
  │         · 赋值/算术:更新寄存器范围           │
  │         · 条件跳转:分裂为 2 个新状态压栈      │
  │         · 内存访问:检查边界                  │
  │         · exit:结束该路径                   │
  └──────────────────────────────────────────┘
     │
     ▼
  所有路径都安全到达 exit?
     ├─ 是 → 通过,加载进内核
     └─ 否 → 拒绝(越界 / 超复杂度 / 死循环等)

  ---
  三、PHP 完整代码(一个能跑的迷你验证器)

  把它存成 mini_verifier.php,直接 php mini_verifier.php 就能看到三个场景。

  <?php
  /**
   * 迷你 eBPF 验证器 —— 演示"复杂度限制"
   * 真实内核代码在 kernel/bpf/verifier.c,这里只保留精髓
   */

  // ── 1. 指令定义 ──────────────────────────────────────────
  class Insn {
      public function __construct(
          public string $op,      // mov / add / jgt / jmp / load / exit
          public $a = null,       // 目标寄存器或地址
          public $b = null,       // 源寄存器 / 立即数
          public ?int $target = null // 跳转目标 pc
      ) {}
  }

  // ── 2. 寄存器"范围"抽象(真实验证器的 tnum 更精细)─────────
  class Reg {
      public function __construct(
          public int $min = PHP_INT_MIN,
          public int $max = PHP_INT_MAX
      ) {}
      public function fp(): string { return "[{$this->min},{$this->max}]"; }
  }

  // ── 3. 一个完整程序状态 = PC + 10 个寄存器 ────────────────
  class State {
      /** @param Reg[] $regs */
      public function __construct(public int $pc, public array $regs) {}

      public function fingerprint(): string {
          $s = "pc={$this->pc};";
          foreach ($this->regs as $i => $r) $s .= "r{$i}={$r->fp()},";
          return $s;
      }
      public function clone(): State {
          $rs = [];
          foreach ($this->regs as $k => $r) $rs[$k] = new Reg($r->min, $r->max);
          return new State($this->pc, $rs);
      }
  }

  // ── 4. 验证器本体 ───────────────────────────────────────
  class Verifier {
      // 真实内核:1_000_000;这里调小方便看爆炸
      const COMPLEXITY_LIMIT = 200000;
      const ENABLE_PRUNING   = true;    // 切 false 感受一下没剪枝的恐怖

      private array $workStack   = [];
      private array $visited     = [];  // 剪枝表:指纹 → true
      private int   $processed   = 0;   // ★ 复杂度计数器 ★
      public  array $log         = [];

      public function __construct(private array $program) {}

      public function verify(): bool {
          // 初始:r1 = ctx(已知 0),其他未知
          $regs = [];
          for ($i = 0; $i < 10; $i++) $regs[$i] = new Reg();
          $regs[1] = new Reg(0, 0);
          $this->workStack[] = new State(0, $regs);

          while ($this->workStack) {
              $s = array_pop($this->workStack);

              // ★ 核心:每处理一个状态就 +1,这就是"复杂度" ★
              if (++$this->processed > self::COMPLEXITY_LIMIT) {
                  $this->log[] = "✗ 拒绝:complexity > ".self::COMPLEXITY_LIMIT
                               ." (已处理 {$this->processed})";
                  return false;
              }

              // ★ 状态剪枝:决定验证能不能跑完的关键 ★
              if (self::ENABLE_PRUNING) {
                  $fp = $s->fingerprint();
                  if (isset($this->visited[$fp])) continue;
                  $this->visited[$fp] = true;
              }

              if ($s->pc < 0 || $s->pc >= count($this->program)) {
                  $this->log[] = "✗ 拒绝:PC 越界 @{$s->pc}";
                  return false;
              }
              $this->step($s, $this->program[$s->pc]);
          }

          $this->log[] = sprintf(
              "✓ 通过:processed=%d, uniqueStates=%d, 程序指令=%d",
              $this->processed, count($this->visited), count($this->program)
          );
          return true;
      }

      private function step(State $s, Insn $i): void {
          switch ($i->op) {

              case 'mov':          // r[a] = 立即数 b
                  $s->regs[$i->a] = new Reg($i->b, $i->b);
                  $this->push($s->pc + 1, $s);
                  return;

              case 'add':          // r[a] += r[b],范围相加
                  $ra = $s->regs[$i->a]; $rb = $s->regs[$i->b];
                  $ra->min += $rb->min; $ra->max += $rb->max;
                  $this->push($s->pc + 1, $s);
                  return;

              case 'jgt':          // if r[a] > b goto target
                  $ra = $s->regs[$i->a];
                  // 真路径:r[a] 收窄到 (b, max]
                  if ($ra->max > $i->b) {
                      $t = $s->clone();
                      $t->regs[$i->a] = new Reg(max($ra->min, $i->b + 1), $ra->max);
                      $this->push($i->target, $t, clone: false);
                  }
                  // 假路径:r[a] 收窄到 [min, b]
                  if ($ra->min <= $i->b) {
                      $f = $s->clone();
                      $f->regs[$i->a] = new Reg($ra->min, min($ra->max, $i->b));
                      $this->push($s->pc + 1, $f, clone: false);
                  }
                  return;

              case 'jmp':
                  $this->push($i->target, $s);
                  return;

              case 'load':         // 模拟内存访问边界检查
                  $r = $s->regs[$i->a];
                  if ($r->min < 0 || $r->max >= 4096) {
                      $this->log[] = "✗ 拒绝:越界访问 r{$i->a}={$r->fp()} @pc={$s->pc}";
                      $this->workStack = []; return;   // 清空 = 终止
                  }
                  $this->push($s->pc + 1, $s);
                  return;

              case 'exit':
                  $this->log[] = "→ 安全 exit @pc={$s->pc}";
                  return;
          }
      }

      private function push(int $pc, State $s, bool $clone = true): void {
          $n = $clone ? $s->clone() : $s;
          $n->pc = $pc;
          $this->workStack[] = $n;
      }
  }

  // ── 5. 三个演示程序 ─────────────────────────────────────
  function run(string $title, array $prog): void {
      echo "\n========== $title ==========\n";
      $v = new Verifier($prog);
      $v->verify();
      foreach ($v->log as $l) echo "  $l\n";
  }

  // 场景 A:直线代码,必过
  run('A · 直线程序', [
      new Insn('mov', 0, 1),
      new Insn('mov', 2, 10),
      new Insn('add', 0, 2),
      new Insn('exit'),
  ]);

  // 场景 B:有分支但收敛,通过
  run('B · 带分支', [
      new Insn('mov', 0, 5),
      new Insn('jgt', 0, 3, 4),   // if r0>3 → pc=4
      new Insn('mov', 0, 0),
      new Insn('exit'),
      new Insn('mov', 0, 1),
      new Insn('exit'),
  ]);

  // 场景 C:连续 25 个独立分支 —— 不剪枝会 2^25 爆炸,剪枝后线性
  $boom = [ new Insn('mov', 0, 0) ];
  for ($k = 0; $k < 25; $k++) {
      // 每个 jgt 对未知寄存器 r3 分叉(r3 初始为 [MIN,MAX])
      $boom[] = new Insn('jgt', 3, $k, count($boom) + 2);
      $boom[] = new Insn('add', 0, 1);
  }
  $boom[] = new Insn('exit');
  run('C · 25 次分支(看复杂度)', $boom);

  运行输出(节选)

  ========== A · 直线程序 ==========
    → 安全 exit @pc=3
    ✓ 通过:processed=4, uniqueStates=4, 程序指令=4

  ========== B · 带分支 ==========
    → 安全 exit @pc=3
    → 安全 exit @pc=5
    ✓ 通过:processed=6, uniqueStates=6, 程序指令=6

  ========== C · 25 次分支(看复杂度)==========
    → 安全 exit @pc=50
    ✓ 通过:processed=76, uniqueStates=76, 程序指令=52

  关键观察:场景 C 理论上有 2²⁵ ≈ 3300 万条路径,但靠状态剪枝只走了 76 步就验证完了。把 ENABLE_PRUNING 改成 false
  再跑,就能看到 complexity > 200000 的拒绝日志——这就是复杂度限制起作用的时刻。

  ---
  四、最佳方式:写 BPF 程序怎么不踩限制

  1. 少用大 switch/嵌套 if——每一层都在制造分支。
  2. 给循环加编译期上界(#pragma unroll 或 bpf_loop() 辅助器),让验证器知道有终点。
  3. 尽早约束寄存器——写 if (x > 100) return 0; 后续 x 就在 [0,100],分支树会瘦很多。
  4. **复杂逻辑拆成尾调用(tail call)**或 BPF 子程序,每个子程序各自验证。
  5. 开 bpf_printk 打印实际指令数:加载失败时 dmesg 里有 processed N insns,对着改。
  6. 真正的"验证器友好"检查:r 写法要让边界一眼可推,别把已知值塞进未知路径再取回。

  内核源码想深入看的话:kernel/bpf/verifier.c 里的 do_check() 是主循环,is_state_visited()
  就是剪枝,BPF_COMPLEXITY_LIMIT_INSNS 宏就是那个 100 万。
Logo

openEuler 是由开放原子开源基金会孵化的全场景开源操作系统项目,面向数字基础设施四大核心场景(服务器、云计算、边缘计算、嵌入式),全面支持 ARM、x86、RISC-V、loongArch、PowerPC、SW-64 等多样性计算架构

更多推荐