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:
- Resolve the engine globals. Read
/proc/self/mapsto find the PHP binary's load base, parse its ELF.symtab/.dynsymto 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.) - Read the regions.
/proc/self/memexposes the whole address space as a seekable file:fseek()to a virtual address,fread()the bytes. On a 64-bit build PHP'sfseektakes a 64-bit offset, so the entire user address range is reachable. - 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/memto 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
freadcope with procfs? For/proc/self/mem, yes:fseekto an absolute address (low heap and high stack addresses like0x7fff…) followed byfreadreturns the bytes. We read unbuffered and seek before every read. (Reading another process's/proc/<pid>/memfrom PHP streams is a different story — that's why reli uses FFI/preadthere — butselfis 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
MapEntryinstances 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_limitis 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. Passmemory_limit: falseto 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_limitheadroom — 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 whenini_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_limitbump can help, because nothing executes. Catching those needs ext-rdump's Czend_error_cbhook. 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_globalsin.symtabor.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 Yamaptrace_scopepolicy 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:
- Rector's downgrade level set rewrites
8.x → 7.1 (
withDowngradeSets(php71: true)). - Three small custom rules under
tools/rector/fill the 7.1 → 7.0 gap that Rector no longer ships: strip nullable type hints (?T),voidreturn types, and class-constant visibility. (Lifted from reli'stools/rector/Downgrade*ToPhp70Rector.) - A
sedpass rewrites0o…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
其他信息
- 授权协议: MIT
- 更新时间: 2026-07-10