十年匠心定制 · 商业建站与技术教学双线并行 咨询热线:400-886-1026 service@lmnt.cn
ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

龙芯LoongArch指令集下PHP 8.x JIT编译器机器码生成崩溃的根因剖析与魔改修复》

龙芯LoongArch指令集下PHP 8.x JIT编译器机器码生成崩溃的根因剖析与魔改修复》 核实完了。这个题目我必须先纠正一个错误前提——在龙芯上JIT 机器码生成崩溃这件事根本不会发生因为龙芯压根没有 JIT 后端机器码一行都没法生成。真正会在龙芯上崩的是另一个东西opcache 的 huge_code_pages。下面把这两件事的根因都拆给你最后给你一条真正可行的给龙芯加 JIT移植路线以及诚实的工程量评估。---0.先拆错前提龙芯上没有JIT 机器码生成谈何崩溃 PHP 的 JIText/opcache/jit/在编译期就决定支持哪些架构代码在 zend_jit.h 里#ifdefined(__x86_64__)||defined(i386)||defined(ZEND_WIN32)#defineZEND_JIT_TARGET_X861#defineZEND_JIT_TARGET_ARM640#elifdefined(__aarch64__)#defineZEND_JIT_TARGET_X860#defineZEND_JIT_TARGET_ARM641#else#errorJIT not supported on this platform#endif大白话翻译这段代码-你是 x86_64 →走 x86 后端-你是 aarch64鲲鹏/飞腾→走 ARM64 后端-其他任何架构包括龙芯 loongarch64→直接 #error编译当场失败。 所以结论很硬 ▎ 龙芯上如果开了 JIT不传--disable-opcache-jit结果是编译失败configure/make 报错不是运行崩溃。JIT 机器码生成崩溃这个表述在龙芯上是不存在的现象——因为zend_jit.h 直接挡住了你连生成机器码这一步都进不去。这是编译期 #error不是运行期段错误。---1.那崩溃这个词从哪来的—— 三个容易混淆的崩逐个对号 信创圈里传的PHP 龙芯崩溃其实混着三件不同的事 ┌─────┬──────────────────────────────────────────┬───────────────────────────────────────┬────────────────────────┐ │ # │ 现象 │ 真实原因 │ 属于 │ ├─────┼──────────────────────────────────────────┼───────────────────────────────────────┼────────────────────────┤ │ ① │ make 报 JIT not supported on this │ zend_jit.h 的 #error │ 编译失败不是崩溃 │ │ │ platform │ │ │ ├─────┼──────────────────────────────────────────┼───────────────────────────────────────┼────────────────────────┤ │ ② │ PHP 起来就 Segmentation fault │ opcache.huge_code_pages1│ 真正的运行时崩溃 │ │ │ │ 重映射代码段 │ │ ├─────┼──────────────────────────────────────────┼───────────────────────────────────────┼────────────────────────┤ │ ③ │ 某个扩展加载即崩 │ 扩展里含 x86 汇编/未适配龙芯 │ 依赖问题不是 PHP │ │ │ │ │ 本体 │ └─────┴──────────────────────────────────────────┴───────────────────────────────────────┴────────────────────────┘ 你要的根因剖析与魔改修复真正值得做的对象是 ②——它是龙芯/ARM64 上真实存在、有官方修复补丁的运行时崩溃。下面重点讲它。---2.真正的崩溃opcache.huge_code_pages 在龙芯上段错误2.1现象 # 龙芯上php.ini 里如果写了 opcache.huge_code_pages1# 启动 PHP 直接崩 $ php-v Segmentationfault(core dumped)2.2根因大白话版 huge_code_pages 这个选项的本意是把 PHP 的代码段.text 段就是可执行指令放的地方用 Linux 的2MB 大页Huge Page来映射减少 TLB 未命中加快指令取指。x86 上这招能提速。 但旧版实现有个致命的做法它试图在运行时把已经加载好的.text 段重映射成大页。做法是用 mmapMAP_HUGETLB/madvise(MADV_HUGEPAGE)去 remap 当前进程的代码段。这个 remap 逻辑在龙芯/aarch64 上非常脆弱-它假设了代码段的页对齐方式和地址布局这些在龙芯上跟 x86 不一样-remap 一个正在执行的代码段一旦地址没对上下次执行到那段代码就跳到非法地址 →段错误-龙芯内核的 hugetlb 支持跟 x86 也不完全一致/proc/sys/vm/nr_hugepages、MAP_HUGETLB 行为有差异。 一句话根因不是 JIT 的问题JIT 早被 #error 挡在门外了是 huge_code_pages 的代码段重映射逻辑在龙芯上踩了空。2.3官方修复php-src commit0ee7732现已合入 官方后来重写了这个功能commit0ee7732对应 GH-19388Broken opcache.huge_code_pages把脆弱的 remap 换成了一套安全机制-用固定的 ZEND_HUGE_PAGE_SIZE2MiB-用一个专门对齐的__attribute__((section(.remap_stub)))桩函数确保要重映射的区域对齐正确-用 dl_iterate_phdr 定位可执行段而不是猜地址-用/proc/self/exe 打开自身二进制BSD 用sysctl(KERN_PROC_PATHNAME)。 这套修复的核心思路就是不再猜代码段在哪、怎么对齐而是让内核自己告诉你 ELF 段的确切位置再对齐映射。2.4你能立刻做的两件事不用等补丁 办法一最稳立刻见效——关掉它;php.ini opcache.huge_code_pages0龙芯上这个选项基本没有收益大页提速主要吃 x86 那套 TLB 特性关了既安全又不损失性能。 办法二——如果已经编译了新版本PHP含0ee7732 补丁打开前先确认系统配了大页 #1.系统是否预留了大页 cat/proc/sys/vm/nr_hugepages # 输出0说明没预留huge_code_pages1也没用 #2.预留一些如果内核支持 echo128|sudo tee/proc/sys/vm/nr_hugepages #3.再开 huge_code_pages 才安全2.5复现脚本证明问题/验证修复 # 复现崩溃旧版 PHP开 huge_code_pages php-d opcache.enable_cli1-d opcache.huge_code_pages1-v # 期望Segmentation fault # 验证修复关掉后正常 php-d opcache.enable_cli1-d opcache.huge_code_pages0-v # 期望正常打印版本---3.真正的魔改给龙芯加一个 JIT 后端完整路径诚实评估 如果你或你的团队真要做让 PHP JIT 支持龙芯这是一条能走通、但工作量巨大的路。下面给你完整的架构认知移植流程骨架代码同时把工程量如实讲清不画饼。3.1先搞懂 PHP JIT 是怎么生成的这是移植的地图 PHP JIT 的机器码生成分三层 PHP 字节码(op_array)│ ▼zend_jit_compile()中间表示 IRSSA 形式与架构无关 ←zend_jit.c 干这个 │ ▼ 寄存器分配指令选择架构相关 汇编模板.dasc 文件 ←每个架构一份 │ ▼ DynASMLuaJIT 的汇编生成器 机器码 →写进代码缓冲区 →执行 关键认知zend_jit.c 里80%的逻辑IR 生成、优化、类型推断是架构无关的真正架构相关的只有两块1.寄存器定义zend_jit_internal.h 里的 ZREG_*每个架构一套寄存器2.指令编码.dasc 汇编模板dasm_架构.h 编码器。 所以移植补上这两块不用重写整个 JIT。3.2现有两个后端长什么样你的模板 ext/opcache/jit/├── zend_jit.c # 架构无关的主编译器 ├── zend_jit_internal.h # 寄存器定义含 x86/arm64 两套 ZREG_* ├── zend_jit.h # 架构选择就是那个 #error ├── zend_jit_x86.dasc # x86 汇编模板DynASM 源 ├── zend_jit_arm64.dasc # arm64 汇编模板 ├── dasm_x86.h # x86 编码器DynASM 预生成 ├── dasm_arm64.h # arm64 编码器DynASM 预生成 └── ir/# DynASM 框架本体来自 LuaJIT 大白话加龙芯新建 zend_jit_loongarch.dascdasm_loongarch.h再在 zend_jit.h 和 zend_jit_internal.h 里各加一个分支。3.3完整移植流程6步 第1步改 zend_jit.h让龙芯进得了门#ifdefined(__x86_64__)||defined(i386)||defined(ZEND_WIN32)#defineZEND_JIT_TARGET_X861#defineZEND_JIT_TARGET_ARM640#defineZEND_JIT_TARGET_LOONGARCH0#elifdefined(__aarch64__)#defineZEND_JIT_TARGET_X860#defineZEND_JIT_TARGET_ARM641#defineZEND_JIT_TARGET_LOONGARCH0#elifdefined(__loongarch__)/* 新增分支 */#defineZEND_JIT_TARGET_X860#defineZEND_JIT_TARGET_ARM640#defineZEND_JIT_TARGET_LOONGARCH1#else#errorJIT not supported on this platform#endif第2步写 dasm_loongarch.hDynASM 编码器 这是最底层、最枯燥的一步把龙芯每条指令的机器码编码规则告诉 DynASM。LoongArch 指令编码是公开文档化的龙芯官方《LoongArch 参考手册》LSX/LASX 也都有完整编码表。 骨架对照 dasm_arm64.h 结构/* dasm_loongarch.h ——骨架仅示意结构完整实现需逐条指令编码 */#ifndefDASM_LOONGARCH_H#defineDASM_LOONGARCH_H/* LoongArch 指令统一 32 位定长这是它比 x86 可变长好写的地方 */typedefuint32_tloongarch_insn;/* 每个操作码一个编码宏例如 add.w 的编码 6 位 opcode | 5 位 rd | 5 位 rj | 5 位 rk | 5 位功能码 ... 真实位域以龙芯参考手册为准 */#defineLOONGARCH_ENC_ADD_W(rd,rj,rk)\(0x00100000|((rd)0x1f)|(((rj)0x1f)5)|(((rk)0x1f)10))/* 寄存器编码r00, ra1, ..., t012, a04(参数), ... 按 LP64D ABI */#defineLOONGARCH_R_ZERO0#defineLOONGARCH_R_RA1#defineLOONGARCH_R_A04/* 第一个参数寄存器 */#defineLOONGARCH_R_A15#defineLOONGARCH_R_S023/* callee-saved做 JIT 的帧指针用 *//* ... 其余按手册补齐 ... */#endif大白话这一步就是把龙芯手册的每条指令编码表抄成 C 宏。枯燥、量大但没有算法难度就是体力活细心错一个位域指令就跳飞。 第3步写 zend_jit_loongarch.dasc汇编模板 DynASM 的.dasc 文件用|前缀写汇编会被生成器转成 C 代码。骨架asm|.arch loongarch64||// 定义 JIT 会用的寄存器和 zend_jit_internal.h 的 ZREG_* 对应|.define ZREG_SP,3// $sp|.define ZREG_FP,22// $fp, JIT 用它做栈帧|.define ZREG_A0,4// 参数|.define ZREG_A1,5|.define ZREG_T0,12// 临时|.define ZREG_RET,4// 返回值放 a0||-emit_prologue:|addi.d $sp,$sp,-FRAME_SIZE|st.d $fp,$sp,0|...||-zend_jit_add:|add.w $a0,$a0,$a1// 整数加法|b $ra// 返回大白话这是把PHP 里一次加法该编译成哪几条龙芯指令写清楚。arm64 的.dasc 就是你的参考模板照着它的函数逐个翻译成龙芯指令。 第4步在 zend_jit_internal.h 加寄存器定义#elifZEND_JIT_TARGET_LOONGARCH/* LoongArch 64 位寄存器32 个通用寄存器 32 个浮点/向量寄存器 */#defineZREG_RAXZREG_NONE// 没有 x86 的 rax映射到龙芯寄存器/* ... 逐个映射把 zend_jit.c 里用的抽象寄存器对应到龙芯物理寄存器 ... */#defineZREG_R0zend_reg_0#defineZREG_A0zend_reg_a0#defineZREG_SPzend_reg_sp#defineZREG_FPzend_reg_fp#defineZREG_T0zend_reg_t0/* 浮点f0~f31 是标量浮点xr0~xr31 是 LSX/LASX 向量 */#defineZREG_XMM0zend_reg_f0// x86 的 xmm 概念龙芯对应标量 f 或向量 xr大白话让架构无关的 zend_jit.c 能叫出龙芯的寄存器名字。它写的 ZREG_T0 之类在这里落到具体的 $t0。 第5步处理调用约定LP64D ABI 龙芯 LP64D 的调用约定要点JIT 必须严格遵守否则一调函数就崩-前8个整数参数a0~a7$r4~$r11-前8个浮点参数fa0~fa7$f0~$f7-返回值整数 a0浮点 fa0-callee-saveds0~s8$r23~$r31JIT 用它们要负责保存/恢复-栈16字节对齐跟 arm64 一样。 第6步重编验证 cd php-src # 现在龙芯上可以开 JIT 了./configure--host$CROSS--buildx86_64-linux-gnu \--enable-opcache--enable-opcache-jit \ CFLAGS-O2make-j$(nproc)# 验证先确认 JIT 真的在生成机器码 php-d opcache.enable_cli1-d opcache.jittracing-recho 12;# 再跑 JIT 自测 make test TESTS-d opcache.jittracing3.4诚实的工程量评估很重要别冲动 ┌────────────────────────────────────────────┬────────────┬───────────────────────┐ │ 步骤 │ 难度 │ 工作量估计 │ ├────────────────────────────────────────────┼────────────┼───────────────────────┤ │ 改 zend_jit.h/加寄存器定义 │ ⭐ │1~2天 │ ├────────────────────────────────────────────┼────────────┼───────────────────────┤ │ 写 dasm_loongarch.h逐条编码 │ ⭐⭐⭐ │1~3周纯体力细心 │ ├────────────────────────────────────────────┼────────────┼───────────────────────┤ │ 写 zend_jit_loongarch.dasc翻译全部模板 │ ⭐⭐⭐⭐ │1~3个月 │ ├────────────────────────────────────────────┼────────────┼───────────────────────┤ │ 调通调用约定浮点向量 │ ⭐⭐⭐⭐⭐ │ 数周 │ ├────────────────────────────────────────────┼────────────┼───────────────────────┤ │ 通过全部 JIT 测试 │ ⭐⭐⭐⭐⭐ │ 数月迭代 │ └────────────────────────────────────────────┴────────────┴───────────────────────┘ 结论这是一个单人3~6个月、团队1~2个月的工程不是一个补丁能搞定的。而且—— ▎ 一个关键的技术判断龙芯3A6000 之前3A5000 等没有 LASX256位向量纯标量 JIT 的收益其实有限JIT 对 PHP ▎ 的提速主要来自类型特化不是纯靠 SIMD。所以给龙芯加 JIT的性价比需要你团队先评估清楚再做。---4.收尾给你的务实建议 按性价比排序别一上来啃最硬的1.先修崩溃opcache.huge_code_pages0第2节。这是龙芯上真实存在的段错误一行配置搞定立刻止血。2.确认 JIT 现状接受龙芯无 JIT 后端用 opcache 字节码缓存--enable-opcache 但--disable-opcache-jit即可这是官方推荐的龙芯形态。3.真要 JIT先做原型验证用第3.3的骨架先让 zend_jit_loongarch.dasc 能跑通整数加法这一个函数、生成正确机器码并执行——这一步通了再决定要不要投入全部工程量。别一上来就铺开全部指令 。---来源zend_jit.h 架构检测、huge_code_pages 修复、JIT 支持范围的出处-php-src:ext/opcache/jit/zend_jit.h#errorJIT not supported on this platform原文(https://github.com/php/php-src/blob/master/ext/opcache/jit/zend_jit.h)-php-src:Fix opcache.huge_code_pagescommit0ee7732remap 逻辑重写(https://github.com/php/php-src/commit/0ee7732c01)-php-src:NEWSGH-19388Broken huge_code_pages、GH-19601aarch64 JIT 栈修复(https://github.com/php/php-src/commit/3625cbb344845628080658f4a200bd731200feee.diff)-PHP bug #79255JIT 编译失败排查(https://news-web.php.net/php.bugs/225521)-Buildroot:PCRE JIT 在非支持平台需禁用旁证 JIT 平台限制(https://patchwork.ozlabs.org/project/buildroot/patch/20180406085403.19096-1-thomas.petazzonibootlin.com/)
返回列表