定制 reliforp/php-memory-dump 二次开发

按需修改功能、优化性能、对接业务系统,提供一站式技术支持

邮箱:yvsm@zunyunkeji.com | QQ:316430983 | 微信:yvsm316

reliforp/php-memory-dump

Composer 安装命令:

composer require reliforp/php-memory-dump

包简介

Capture a PHP process's own memory in reli's RDUMP format from inside the process, in pure PHP, with no extension and no FFI. The dump is analysed offline with reli (finding leaks, reference cycles, memory bottlenecks, etc.).

README 文档

README

Take a memory dump of the current PHP process, from inside the process, in pure PHP — no extension, no FFI — for later analysis with reli.

use Reliforp\PhpMemoryDump\MemoryDumper;

(new MemoryDumper())->dump('/tmp/app.rdump');

This is the pure-PHP sibling of ext-rdump. ext-rdump is a C extension that takes the dump with memcpy; this package produces the same RDUMP file (magic RDUMP, format version 3) using only /proc/self/maps, /proc/self/mem, and the PHP binary's own ELF symbol table. The output feeds the very same reli workflow:

reli inspector:memory:analyze app.rdump -f rmem -o app.rmem
reli inspector:memory:report  app.rmem

Why this works (and how)

reli normally snapshots a PHP process from the outside via process_vm_readv / ptrace. To do that it locates the engine globals (executor_globals, compiler_globals, basic_globals) by reading the PHP binary's symbol table and adding the load bias from /proc/<pid>/maps, then reads the relevant regions out of the target's address space.

Everything reli does from the outside, a process can do to itself from the inside, in plain PHP:

  1. Resolve the engine globals. Read /proc/self/maps to find the PHP binary's load base, parse its ELF .symtab / .dynsym to get each symbol's link-time address, and add the bias. (Debian/Ubuntu strip the PHP binary, but the three engine globals are exported in .dynsym, so a stock distro build is still resolvable.)
  2. Read the regions. /proc/self/mem exposes the whole address space as a seekable file: fseek() to a virtual address, fread() the bytes. On a 64-bit build PHP's fseek takes a 64-bit offset, so the entire user address range is reachable.
  3. Write the RDUMP file. The header records the resolved addresses, the memory map mirrors /proc/self/maps, and each captured region is streamed straight from /proc/self/mem to disk.

reli then does all the hard work — walking the Zend heap, the object store, class/function tables, the VM stack — offline, from the dump file.

Status: it works

On a stock Debian PHP 8.4 (NTS, x86-64) a self-dump analysed by reli reconstructs the live state, including the call stack at the moment of capture (which, fittingly, shows this library's own dump path):

Call Stack at capture:
  #0 fread:-1
  #1 Reliforp\PhpMemoryDump\RegionReader::readAt:90
  #2 Reliforp\PhpMemoryDump\RegionReader::copyInto:75
  #3 Reliforp\PhpMemoryDump\RdumpWriter::writeRegion:129
  #4 Reliforp\PhpMemoryDump\MemoryDumper::dump:98
  #5 <main>:23

…and the full type breakdown / object inventory / leak findings reli produces for any other dump.

Notes on the two worries you'd expect

  • Does PHP's fread cope with procfs? For /proc/self/mem, yes: fseek to an absolute address (low heap and high stack addresses like 0x7fff…) followed by fread returns the bytes. We read unbuffered and seek before every read. (Reading another process's /proc/<pid>/mem from PHP streams is a different story — that's why reli uses FFI/pread there — but self is well-behaved.)
  • Doesn't the dumper perturb the VM while it runs? It does — this is PHP code mutating the heap as it dumps it, so the snapshot is not stop-the-world. We keep the window small: the memory map is frozen before any output buffer is allocated, and regions are streamed rather than accumulated. The captured heap therefore contains the dumper's own transient objects (you'll see MapEntry instances and the like in the report), but the dump is still internally consistent enough for reli to walk. This mirrors the inconsistency ext-rdump documents for busy ZTS processes.

Memory footprint of the dump itself

The dumper is deliberately frugal so it barely disturbs the heap it is snapshotting. It does not slurp the multi-megabyte PHP binary to parse its symbols — only the ELF/program/section headers and the .dynsym / .dynstr slices are read — and regions are streamed through a small fixed buffer (256 KiB) rather than buffered whole. On a stock PHP 8.4 the measured peak added by dump() is on the order of ~0.6–1 MiB of emalloc, with no new 2 MiB ZendMM chunk forced (memory_get_peak_usage(true) delta 0) — and it is independent of how big the dumped heap or the output file is (a 26 MB dump costs the same as a tiny one). Permanent growth left behind is a couple of hundred KiB at most.

Install

composer require reliforp/php-memory-dump

Usage

use Reliforp\PhpMemoryDump\MemoryDumper;

$result = (new MemoryDumper())->dump('/tmp/app.rdump');
// $result->region_count, $result->total_bytes, $result->memory_area_count

// Self-contained dump (also embeds read-only file-backed segments, so it can
// be analysed on a host without the original binaries). Larger output.
(new MemoryDumper())->dump('/tmp/app-full.rdump', full: true);

From a signal handler

pcntl_async_signals(true);
pcntl_signal(SIGUSR2, function () {
    (new \Reliforp\PhpMemoryDump\MemoryDumper())->dump('/tmp/sig.rdump');
});
// kill -USR2 <pid>

Auto-dump on memory_limit death

OomDumpHandler::register() installs a shutdown handler that dumps the process the moment it dies of memory_limit exhaustion — the analogue of reli's sidecar client MemoryLimitHandler::register(), but doing the dump in-process instead of asking a daemon.

use Reliforp\PhpMemoryDump\OomDumpHandler;

// %p -> pid, %t -> unix time, %% -> literal % (so workers don't collide)
OomDumpHandler::register('/var/log/php-oom-%p-%t.rdump');

With options:

OomDumpHandler::register(
    path: '/var/log/php-oom-%p.rdump',
    full: false,
    on_dump:  fn ($result, $path) => error_log("OOM dump: $path"),
    on_error: fn (\Throwable $e)  => error_log('OOM dump failed: ' . $e->getMessage()),
    reserve_bytes: 4 * 1024 * 1024, // emergency reserve (must be >= 2 MiB)
    memory_limit: -1,               // raised inside the handler before dumping
);

Surviving the OOM that you are trying to capture takes some care, because the handler runs in-process with the heap already at the wall. Two levers, both on by default:

  • Raising memory_limit (the reliable one). memory_limit is enforced on memory actually taken from the OS (ZendMM's real usage). The handler lifts it (to unlimited by default — the process is exiting anyway) before dumping, which deterministically gives the dumper the ~one 2 MiB ZendMM chunk of working set it needs. Pass memory_limit: false to opt out.
  • The emergency reserve — and why it must be ≥ 2 MiB. A pre-allocated block is freed first thing in the handler. But freeing a sub-2 MiB block only returns it to a ZendMM chunk's free list; the chunk is not unmapped, so real usage — and thus memory_limit headroom — does not change. Measured: a 1 MiB reserve frees zero headroom, the dump then re-OOMs mid-write, and you get a truncated, unusable file (reli refuses it). 2 MiB is the practical minimum (it is a huge allocation, unmapped on free); the default 4 MiB leaves margin. The reserve is mainly the fallback for when ini_set('memory_limit', …) is restricted.

Best-effort, by nature. A shutdown handler cannot catch every OOM: when the exhausting allocation is itself a VM-stack page push, pushing the handler's own call frame needs another allocation that also fails, so the handler body never runs — no reserve or memory_limit bump can help, because nothing executes. Catching those needs ext-rdump's C zend_error_cb hook. In practice this handler covers the common "accumulated too much, then one more append tipped it over" OOM, and the resulting dump pinpoints the offender — e.g. reli's report flags the very array that overflowed the limit as the dominant retained branch.

Requirements & limitations

  • 64-bit little-endian Linux, PHP 7.0+, built NTS. (The released package is downgraded to 7.0 syntax — see PHP version support below — so it matches ext-rdump's 7.0–8.5 reach; develop against PHP 8.1+.)
  • NTS only. ZTS keeps the engine globals in thread-local storage reached through TSRM; resolving those without walking the TLS block is out of reach for a pure-PHP reader, so ZTS is rejected with a clear error. Use ext-rdump (or reli attaching from the outside) for ZTS.
  • The PHP binary must expose executor_globals / compiler_globals / basic_globals in .symtab or .dynsym. Stock distro builds do (they're exported); a binary stripped of both tables is not resolvable.
  • Reads /proc/self/mem, which a restrictive sandbox/seccomp profile or a Yama ptrace_scope policy targeting the self-read path may block.
  • The dump is taken synchronously in the calling code; it is not stop-the-world (see above).

PHP version support (and the downgrade build)

The library is developed in modern PHP (8.1+) — readonly properties, constructor promotion, match, named arguments — but released downgraded to PHP 7.0 syntax, so it installs and runs anywhere from PHP 7.0 to 8.5, the same floor as ext-rdump.

This mirrors reli's own sidecar client (reliforp/reli-prof-sidecar-client), which ships the same way. The downgrade is composer downgrade:

  1. Rector's downgrade level set rewrites 8.x → 7.1 (withDowngradeSets(php71: true)).
  2. Three small custom rules under tools/rector/ fill the 7.1 → 7.0 gap that Rector no longer ships: strip nullable type hints (?T), void return types, and class-constant visibility. (Lifted from reli's tools/rector/Downgrade*ToPhp70Rector.)
  3. A sed pass rewrites 0o… octal literals.

The result lands in build/php70/src/. CI builds it on PHP 8.4, asserts no 7.1+ syntax survives, and runs a smoke dump against the downgraded tree on PHP 7.0 / 7.1 / 7.2 / 7.4 / 8.0 — the runtimes where the published artifact lives but Rector and PHPUnit can't. The git source stays modern; only the release artifact is downgraded.

Cross-version round-trip against reli

A separate workflow (reli-cross-version.yml, the pure-PHP analogue of ext-rdump's same-named CI) proves the full producer→analyzer contract: it loads the downgraded build on every php:7.0-cli … php:8.5-cli image, writes a self-contained (full: true) dump, then sets reli up once on PHP 8.4 and analyses every dump — asserting the RDUMP magic, format version 3, the recorded origin PHP-version tag, and that inspector:memory:analyze produces a report. The analyzer step reuses ext-rdump's reli-analyze.sh almost verbatim, since the RDUMP format is identical regardless of which producer wrote it. A ZTS leg asserts the NTS-only contract fails fast (no bogus dump) on php:*-zts.

What gets captured

Mirroring ext-rdump: every VMA appears in the dump's memory map, and the bytes of every readable, volatile mapping are embedded — writable segments (ZendMM chunks, the brk heap, VM stacks, library .data/.bss where NTS globals live), anonymous mappings, and opcache /dev/zero shared memory. With full: true, read-only file-backed segments are embedded too. Read-only binary segments left out of a non-full dump are recovered from the on-disk binaries by reli at analysis time (use full: true, or reli's --dependency-root, to analyse on a different host).

Security

A dump is a verbatim copy of your process's memory, so treat the file as highly sensitive — it can contain environment variables, credentials, session tokens, decrypted request/response bodies, and private keys. The file is created 0600; still, write it somewhere only the intended user can read, move it over a secure channel, and delete it once analysed. See ext-rdump's README for the longer discussion; the same cautions apply.

Relationship to ext-rdump

ext-rdump php-memory-dump
Form C extension pure PHP library
Globals resolution linker (compile-time) ELF symbol table + maps load bias
Region copy memcpy / /proc/self/mem /proc/self/mem
OOM auto-dump (zend_error_cb) yes no (no hook available in pure PHP)
ZTS yes no
Output RDUMP v3 RDUMP v3 (same reader)

If you can install an extension, ext-rdump is faster, works under ZTS, and can auto-dump on memory_limit exhaustion. This package is for when you can't add an extension but can still run PHP.

License

MIT.

统计信息

  • 总下载量: 0
  • 月度下载量: 0
  • 日度下载量: 0
  • 收藏数: 0
  • 点击次数: 2
  • 依赖项目数: 0
  • 推荐数: 0

GitHub 信息

  • Stars: 0
  • Watchers: 0
  • Forks: 0
  • 开发语言: PHP

其他信息

  • 授权协议: MIT
  • 更新时间: 2026-07-10

承接程序开发

PHP开发

VUE

Vue开发

前端开发

小程序开发

公众号开发

系统定制

数据库设计

云部署

网站建设

安全加固