php方案 CFG(控制流图)构建

发布时间:2026/7/5 2:54:04

php方案 CFG(控制流图)构建 大白话解释 基本块一段顺序执行、中间没有跳转的指令序列。进来就从头跑到尾不会中途跳走。CFG把代码切成基本块然后用箭头连起来表示执行完这块可能去哪。 找领导指令基本块起点的规则只有三条1.第0条指令2.跳转的目标GOTO/JIF的目的地3.跳转指令的下一条跳了以后别人能顺序跑到这里 其他全是算法切块 → 加边 → 完事。---代码?php// 纯内置无需扩展。实际解析 PHP 源码见文末 nikic/php-parser 用法classBlock{publicint$id;publicarray$code[];publicarray$succ[];// 出边后继块 idpublicarray$pred[];// 入边前驱块 idpublicfunction__construct(int$id){$this-id$id;}}functionbuildCFG(array$ops):array// Block[]key 指令起始索引{$ncount($ops);$lpos[];// label名 → 指令下标foreach($opsas$i$op)if($op[0]LBL)$lpos[$op[1]]$i;// ① 找所有基本块起点领导指令$lead[01];foreach($opsas$i$op){if($op[0]JMP){$lead[$lpos[$op[1]]]1;if($i1$n)$lead[$i1]1;}if($op[0]JIF){$lead[$lpos[$op[1]]]1;if($i1$n)$lead[$i1]1;}if($op[0]LBL)$lead[$i]1;}ksort($lead);$startsarray_keys($lead);// ② 切块两个领导指令之间的指令归一块$blk[];foreach($startsas$k$s){$blk[$s]newBlock($s);for($i$s,$e$starts[$k1]??$n;$i$e;$i)$blk[$s]-code[]$ops[$i];}// ③ 加边看每块最后一条指令决定出边$edgefunction(int$a,int$b)use($blk){$blk[$a]-succ[]$b;$blk[$b]-pred[]$a;};foreach($startsas$k$s){$last$ops[($starts[$k1]??$n)-1];$next$starts[$k1]??null;if($last[0]JMP)$edge($s,$lpos[$last[1]]);elseif($last[0]JIF){$edge($s,$lpos[$last[1]]);if($next!null)$edge($s,$next);}elseif($last[0]!RET$next!null)$edge($s,$next);}return$blk;}// DOT 格式输出丢给 graphviz 渲染dot -Tpng cfg.dot -o cfg.pngfunctiontoDot(array$blk):string{$sdigraph CFG {\n node[shaperecord fontnamemonospace]\n;foreach($blkas$id$b){$codeimplode(\l,array_map(fn($o)implode( ,$o),$b-code)).\l;$s. B$id[label\{B$id|$code}\]\n;foreach($b-succas$to)$s. B$id- B$to\n;}return$s.}\n;}// ── 测试for ($i0; $i10; $i) sum $i ─────────────────────────────────$ops[[MOV,i,0],// 0 init[LBL,head],// 1 ← 循环头[JIF,done,i 10],// 2 条件i10 则跳出[ADD,sum,i],// 3 循环体[INC,i],// 4[JMP,head],// 5 回边[LBL,done],// 6 ← 出口[RET,sum],// 7];$cfgbuildCFG($ops);foreach($cfgas$id$b){$codeimplode( | ,array_map(fn($o)implode( ,$o),$b-code));printf(B%-2d %-42s succ:%-10s pred:%s\n,$id,$code,[.implode(,,$b-succ).],[.implode(,,$b-pred).]);}echo\n.toDot($cfg);---输出B0MOVi0succ:[1]pred:[]B1LBLhead|JIFdone i10succ:[6,3]pred:[0,3]B3ADDsumi|INCi|JMPhead succ:[1]pred:[1]B6LBLdone|RETsum succ:[]pred:[1]digraphCFG{node[shaperecord fontnamemonospace]B0[label{B0|MOV i 0\l}]B0-B1B1[label{B1|LBL head\lJIF done i 10\l}]B1-B6B1-B3B3[label{B3|ADD sum i\lINC i\lJMP head\l}]B3-B1B6[label{B6|LBL done\lRET sum\l}]}Graphviz 渲染结果 ┌──────────┐ │B0:init │ └────┬─────┘ ↓ ┌─────────────────┐ ←──────┐ │B1:i10?JIF│ │ └──┬──────────┬───┘ │ │ taken │ fall-thru │ ↓ ↓ │ ┌──────┐ ┌────────────┐ │ │B6│ │B3:body │───┘(back-edge)│RET│ │ i│ └──────┘ └────────────┘---用 nikic/php-parser 解析真实PHP源码 composerrequirenikic/php-parser?phprequirevendor/autoload.php;usePhpParser\{NodeTraverser,NodeVisitorAbstract,ParserFactory,Node};// php-parser 给你 AST你自己走 AST 节点建 CFG// 关键节点类型// Node\Stmt\If_ → JIF 语义// Node\Stmt\While_ → 回边// Node\Stmt\For_ → init cond incr 三块// Node\Stmt\Return_ → RET// Node\Stmt\Break_ → JMP 到循环出口// Node\Stmt\Continue_ → JMP 到循环头$parser(newParserFactory)-createForNewestSupportedVersion();$ast$parser-parse(?php function sum10(): int { $s 0; for ($i 0; $i 10; $i) $s $i; return $s; } );// 用 NodeTraverser 自定义 Visitor 把 AST 节点翻译成上面的 $ops 格式// 然后传给 buildCFG() 即可---三行总结 ┌────────────┬─────────────────────────────────────┐ │ 步骤 │ 干啥 │ ├────────────┼─────────────────────────────────────┤ │ 找领导指令 │ 第0条跳转目标跳转后一条 │ ├────────────┼─────────────────────────────────────┤ │ 切块 │ 两个领导指令之间的指令一个基本块 │ ├────────────┼─────────────────────────────────────┤ │ 加边 │ 看块尾指令类型JMP/JIF/顺序/RET│

相关新闻