Skip to content

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

13 Commits
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

phpeek — inspect the PHP engine

A live look at how PHP runs a script: the compiled opcodes, the source, variables, the function/class structs, and the call stack — stepped one line at a time.

phpeek stepping through a script

Requirements

Linux or macOS.

Need For Get it
php CLI all views any PHP 8.x
phpdbg the live watch macOS brew install php; Debian/Ubuntu apt install php-cli php-phpdbg; or a php-src build
bash 4+ the script macOS ships 3.2 → brew install bash

opcache (for phpeek opcodes) and ext/reflection are normally already present.

phpeek finds php/phpdbg on PATH, or in a php-src build tree under the current directory. Override with --php=/path / --phpdbg=/path or $PHPEEK_PHP / $PHPEEK_PHPDBG.

phpeek check      # report each dependency and how to fix what's missing

Usage

phpeek                  # dashboard — lists examples/ to pick from
phpeek script.php       # dashboard on a script

The menu:

  o  opcodes      the compiled program (every opcode)
  f  functions    the function table after loading
  c  classes      user classes and their methods
  w  watch        step execution live (opcodes, vars, structs, call stack)
  x  run          execute it, show the output
  e  engine       loaded extensions + counts
  s  script       switch script — lists examples/, pick by number or type a path
  q  quit

Each panel also has a direct form: phpeek opcodes|funcs|classes|watch|run <script>, phpeek engine.

watch — step it live

watch drives a live phpdbg session. Each space executes one more line; every panel is re-queried from the running process and redrawn.

  phpeek · LIVE   loop.php   line 3   in combine   step 3
  OP_ARRAY  combine   ops 7  args 2  CVs 2  tmps 1  lines 6-8
  OPCODES  ▶ = line running now                 SOURCE
    L0006 0000  CV0($x) = RECV 1                   1  <?php
    L0006 0001  CV1($y) = RECV 2                   2  function add($a, $b) {
  ▶ L0007 0002  T2 = ADD CV0($x) CV1($y)         ▶ 3      return $a + $b;
  ▶ L0007 0003  RETURN T2                          4  }
  VARIABLES                                        6  class Accumulator {
    $a  integer = 0                                9      public function push(int $n) {
    $b  integer = 1                               14  $acc = new Accumulator();
  MEMORY  emalloc'd, in use  502,200 B            16      $acc->push($i);
    entered add()   +480 B  (call frame on the VM stack)
  ENGINE TABLES  2 function(s), 1 class(es)  ([t] to expand)
  CALL STACK  {main} is #0; each call stacks upward
    frame #2: add(a=0, b=1)
    frame #1: Accumulator->push(n=1)
    frame #0: {main}
  [space] step   [r] restart   [s] script   [i] opline+cap   [<-/->] move   [t] tables   [q] quit

The dashboard fits your terminal. OPCODES/SOURCE stays at the top as a window centered on the current line (... marks more above/below); everything below it — the status panels and the key bar — is one block pinned to the bottom of the screen, so the layout stays put as you step. Engine tables collapse to a one-line summary ([t] expands).

The OP_ARRAY band above OPCODES names the struct those oplines belong to — one op_array per function/method/{main} — with its own counts (ops, args, CVs, tmps) and source span, read straight from phpdbg's header. Step into a call and the band swaps to the callee's op_array, making the compilation-unit boundary visible.

Press i to open the opline view — it shows both the ZEND_OP panel and the zend_execute_data panel together. A source line usually compiles to several oplines (and phpeek steps by line, so they all run in one space), so it opens on the first and the arrow keys / walk through them (op k/N); i again closes both:

  ZEND_OP  line 20 -> 4 oplines; showing op 2/4  ([<-/->] to walk — all 4 run in one [space])
    opcode          SEND_VAR_EX
    op1             [CV ] CV1($i)
    op2             [CONST] 1
    result          (unused)
    lineno          20   (op #0012 in the op_array)
    handler         ZEND_SEND_VAR_EX_SPEC_CV_CONST_HANDLER  (SPEC-naming convention)

As you walk with the arrows, the zend_execute_data panel's +0x00 opline field follows the cursor, so you can watch execute_data->opline advance through the op_array one instruction at a time — exactly as it does during real execution.

Press d to overlay the current frame's zend_execute_data:

  ZEND_EXECUTE_DATA  current frame — 80-byte header, then CV slots
    +0x00  opline             -> op #0000 CV0($n) = RECV 1
    +0x18  func               -> Accumulator->push
    +0x20  This (zval,16B)    -> Accumulator#1   [u2 = num_args = 1]
    +0x30  prev_execute_data  -> {main}
    +0x50  CV slots (each a 16B zval, contiguous after the header):
      +0x50  $n = integer = 1
  • Op_array | Opcodes | Source — the OP_ARRAY band names the compiled unit these oplines live in (ops/args/CVs/tmps + line span, from phpdbg's header); below it the current function's opcodes sit beside the script, on the line about to run.
  • opline + execute_data (i opens both; / walk; i closes) — the line's oplines one at a time, each broken into its zend_op fields: opcode, op1/op2/ result with their operand types (CONST/TMP/VAR/CV), lineno, and the SPEC-specialized handler name. A source line compiles to several oplines (all run in one line-step), so the panel shows op k/N and the arrow keys move the cursor; the paired execute_data panel's opline field tracks it, and the OPCODES panel highlights that exact opline (yellow bar, distinct from the green line-band) so you can see which op the cursor is on. Fields are decoded from the opcode text (real); the handler follows the SPEC naming convention (which folds TMP/VAR/CV and adds axes like RETVAL_USED, so the real symbol may differ). Lets you watch how each dump row is a struct — and how operand types pick the specialized C handler.
  • Variables — the compiled variables (CV slots) with their current values.
  • Memory — emalloc'd bytes in use (memory_get_usage()), and the change this step attributed to what caused it: a variable assigned (+N B on the heap, or ±0 for a scalar stored inline), or a call entered / returned (±480 B as its frame is pushed or popped on the VM stack). It's all heap — memory_get_usage() never counts the C stack.
  • Engine tables (t to toggle) — each user function/class's structure: signatures, ce->parent, properties_info, function_table, read live via reflection. Collapsed to a summary line by default since it rarely changes while stepping.
  • execute_data (opened together with the opline panel by i; d toggles it alone) — the current frame's zend_execute_data overlaid on its real layout: opline, func, This/num_args, prev_execute_data, and the CV slots contiguous after the 80-byte header (offsets verified on this build). It's a reconstruction from data phpeek already has — the field values are semantic, not raw addresses — so you can watch each field change as you step without a debugger attached. With the opline panel (i) also open, the +0x00 opline field follows the [i] cursor (marked (follows [i])) — so you can watch execute_data->opline walk the op_array one instruction at a time, exactly as it does during real execution.
  • Call stack — frames pushing/popping as you step into and out of calls, with live argument values. {main} is #0 at the bottom and the number grows upward into each nested call.

Examples are in examples/: simple.php, loop.php, classes.php.

Limits

  • Single file — only the main script is breakpointed; require/included files run but aren't stepped. watch shows a note when it detects them.
  • phpdbg adds opcodes — under phpdbg the compiler emits EXT_STMT / EXT_FCALL_BEGIN / EXT_FCALL_END markers a normal compile does not. phpeek opcodes uses a plain compile, so compare the two to tell real opcodes from debug hooks. watch hides NOPs (they do nothing, and phpdbg keeps a leading one from an early-bound class/function declaration that a normal compile discards).
  • opcodes shows the pre-optimizer form (ADD before it's folded).
  • watch steps by line, so marks the whole opcode band for the current line.
  • No refcounts — phpdbg's info vars can abort on an undefined variable, so values come from an isset-guarded ev and refcounts are skipped.
  • Steps forward only (phpdbg can't reverse). Press r to restart; at the end of the run the footer switches to a ✓ finished bar and space loops back to the top.
  • Tested on Linux; the macOS path is the same code but not yet smoke-tested on macOS.

About

A live view of how PHP 8 runs a script — opcodes, source, variables & call stack.

Topics

Resources

Stars

Watchers

Forks

Releases

Packages

Contributors

Languages