Slide 1

Slide 1 text

PHP で読む楽しいコアダンプ 五十嵐 進士 / sji / sj-i / @sji_ch   株式会社インフィニットループ

Slide 2

Slide 2 text

自己紹介 @sji_ch SNS 上のアイコンは GitHub が自動生成した奴

Slide 3

Slide 3 text

生まれも育ちも仙台

Slide 4

Slide 4 text

PHP カンファレンス仙台とかやった

Slide 5

Slide 5 text

ふつうのサラリーマン 株式会社インフィニットループ仙台支社所属 スマホゲーのサーバサイドプログラマ 地元が仙台や札幌の人とかはぜひ一緒に働きま しょう

Slide 6

Slide 6 text

Agenda コアダンプの概要 コアダンプのとり方の例 gdb で読むふつうの使い方 コアダンプの内部構造 自作の PHP ツール Reli のメモリ解析機能 Reli によるコアファイルの読み込み まとめ

Slide 7

Slide 7 text

コアダンプの概要

Slide 8

Slide 8 text

コアダンプ、何のことか知ってますか?

Slide 9

Slide 9 text

コアダンプ = メモリダンプ

Slide 10

Slide 10 text

コア = メモリ 「ダンプ」の方は「ドサっと落とす」 「コア」は磁気コアメモリから 「コアダンプの出力」をよく「コアを吐く」とも言う

Slide 11

Slide 11 text

磁気コアメモリ 出典: https://en.wikipedia.org/wiki/Magnetic- core_memory#/media/File:Ferrite_core_memory.jpg Author: Orion 8 / License: CC BY 2.5 昔に主流だったメモリ実装方式 フェライトコアへ電線を通す 磁化によりデータを保持 1950 年代に普及し始める 1960 年代 DEC の PDP シリーズにも採用 1969 年 PDP-7 で UNIX が作られた頃もまだ現役 1970 年代に DRAM に急速にシェアを奪われる UNIX や Linux でメモリダンプが「コア」ダンプ と呼ばれ続ける

Slide 12

Slide 12 text

コアダンプのとり方の例

Slide 13

Slide 13 text

クラッシュさせる OS がプログラムのクラッシュを検知するとコアが吐かれる というのが王道パターン ただ状況によっては吐かれなかったりも

Slide 14

Slide 14 text

コアが吐かれるクラッシュの例 シグナル配送時の標準動作がコアを吐いて死ぬものの場合など 1. CPU 例外が発生 メモリアクセス違反とか 無効なCPU 命令とか 2. OS カーネル の例外ハンドラが補足 3. カーネルがプロセスに対応するシグナルを配送 4. プロセスで当該シグナルのハンドラが登録されてない 5. 標準動作によりプロセスがコアを吐いて終了

Slide 15

Slide 15 text

標準動作でコアの吐かれるシグナルの例 SIGQUIT キーボード(Ctrl+) による中止 SIGILL 不正な CPU 命令 SIGFPE 浮動小数点例外 SIGSEGV メモリアクセス違反 SIGSYS 不正なシステムコール などなど

Slide 16

Slide 16 text

PHP では処理系や拡張のバグくらいでしか起きない PHP スクリプトは ZendEngine 上で実行 適切なメモリアクセスや CPU 命令の実行、OS 機能へのアクセスは ZendEngine の責務 これらを間違うのは ZendEngine や処理系機能をカスタマイズする拡張側のバグ 言語改定の直後などでもなければそんなにバグらない 本番環境で不安定なバージョンの処理系を使うことはほぼない それでもたまに起きるとコアダンプが助けになる

Slide 17

Slide 17 text

OS 側の設定の影響 コアを吐くのに OS 側で設定が必要な部分も Ubuntu などのデフォルトはコアを吐かない設定 コアはメモリ内容のダンプ ディスクを圧迫するかも 流出すればセキュリティ的な問題もあるかも 有効にする例: ulimit -c unlimited SELinux のようなセキュリティ設定で止められてる場合も コアは吐くが apport のようなサービスに渡される場合も

Slide 18

Slide 18 text

PHP にシグナルを投げてコアダンプをとる例 $ ulimit -c unlimited $ php -r "while(1)sleep(1);" & [1] 266418 $ kill -SIGSEGV 266418 $ [1]+ Segmentation fault ( コアダンプ ) php -r "while(1)sleep(1);" $ tail -n1 /var/log/apport.log INFO: apport (pid 266441) 2024-02-24 04:59:06,602: writing core dump to /var/lib/apport/coredump/core._ f1337f44-235f-4327-b0ae-d0a998ffefa3.266418.10469917 (limit: -1)

Slide 19

Slide 19 text

gcore で殺さず吐かせる gcore は gdb 付属の ツール プロセス ID を指定 して実行 対象プロセスを一瞬 停止させる 停止させている間に メモリを読む 取得が終わったらプ ロセスを再開 対象プロセスを終了 させずにコアダンプ がとれる $ sudo gcore 270182 [Thread debugging using libthread_db enabled] Using host libthread_db library "/lib/x86_64-linux-gnu/libthread_db.so.1". 0x00007f1cd06e5706 in clock_nanosleep () from /lib/x86_64-linux-gnu/libc.so warning: target file /proc/270182/cmdline contained unexpected null charact warning: Memory read failed for corefile section, 4096 bytes at 0xfffffffff Saved corefile core.270182 [Inferior 1 (process 270182) detached]

Slide 20

Slide 20 text

gdb で読むふつうの使い方

Slide 21

Slide 21 text

gdb でのコアダンプの読み込み gdb に実行ファイルとコアファイ ルを指定して実行 ソースコードとの対応を知るには 実行ファイルのデバッグ情報が必 要 ふつうのディストリビューション 配布のパッケージではデバッグ情 報は分離配布 最近の gdb では debuginfod に よる自動ダウンロードも可能 $ gdb php --core ./core.270182 < 中略 > Reading symbols from php... This GDB supports auto-downloading debuginfo from the following Enable debuginfod for this session? (y or [n])

Slide 22

Slide 22 text

バックトレース bt コマンドでバックトレースを得られる ただし処理系の C ソースコードレベルで (gdb) bt #0 0x00007f1cd06e5706 in __GI___clock_nanosleep (clock_id=clock_id@entry=0, flags=flags@entry=0, req=r at ../sysdeps/unix/sysv/linux/clock_nanosleep.c:78 #1 0x00007f1cd06f99b7 in __GI___nanosleep (req=req@entry=0x7fff345f9140, rem=rem@entry=0x7fff345f9140) #2 0x00007f1cd070e32e in __sleep (seconds=0) at ../sysdeps/posix/sleep.c:55 #3 0x000055c608a0a5a5 in zif_sleep (execute_data=, return_value=0x7fff345f91e0) at /usr #4 0x000055c608b4d4da in ZEND_DO_ICALL_SPEC_RETVAL_UNUSED_HANDLER () at /usr/src/php8.2-8.2.10-2ubuntu #5 execute_ex (ex=0x0) at /usr/src/php8.2-8.2.10-2ubuntu1/Zend/zend_vm_execute.h:56040 #6 0x000055c608b536f5 in zend_execute (op_array=0x7f1ccde84100, return_value=0x7fff345f92c0) at /usr/s #7 0x000055c608ad1309 in zend_eval_stringl (str=, str_len=, retval_ptr=0 at /usr/src/php8.2-8.2.10-2ubuntu1/Zend/zend_execute_API.c:1298 #8 0x000055c608ad14dd in zend_eval_stringl_ex (str=, str_len=, retval_pt

Slide 23

Slide 23 text

PHP スクリプトのレベルでのバックトレース PHP の処理系は GitHub にリポジ トリがある リポジトリに .gdbinit というファ イル PHP 処理系のデバッグ用の gdb 拡張コマンドの定義ファイル --command オプションで渡すと PHP スクリプトレベルの情報を得 るコマンドが増える zbacktrace で PHP レベルのバッ クトレース $ php --version PHP 8.2.10-2ubuntu1 (cli) (built: Sep 5 2023 14:37:47) (NTS) Copyright (c) The PHP Group Zend Engine v4.2.10, Copyright (c) Zend Technologies $ wget https://raw.githubusercontent.com/php/php-src/PHP-8.2/.g $ gdb php --core ./core.270182 --command ./.gdbinit (gdb) zbacktrace [0x7f1ccde12080] sleep(30) [internal function] [0x7f1ccde12020] (main) [internal function] https://github.com/php/php-src

Slide 24

Slide 24 text

コアダンプのファイル構造

Slide 25

Slide 25 text

Linux のコアダンプは ELF ファイル ELF = Executable and Linkable Format 先頭に "0x7F" "E" "L" "F" から始まる ELF ヘッダ ELF ヘッダの 16 バイト目の位置に種別を表すフ ィールド e_type ET_REL (=1): 再配置可能ファイル (.o) ET_EXEC (=2): 実行可能ファイル ET_DYN (=3): 共有オブジェクト (.so)) ET_CORE (=4): コアダンプ コアダンプ用の種別として ET_CORE が 予約 typedef struct { // ELF ヘッダ // "0x7f" "E" "L" "F" のマジックナンバーで始 unsigned char e_ident[16]; Elf64_Quarter e_type; // ファイル種別 /* 中略 */ Elf64_Off e_phoff; // プログラムヘッダの位置 Elf64_Off e_shoff; // セクションヘッダの位置 /* 中略 */ } Elf64_Ehdr;

Slide 26

Slide 26 text

コアダンプの内容は「公式の」仕様がない ET_CORE の具体的な内容は ELF の仕様には何も規定がない 実装こそ仕様 Linux カーネルが何を吐くか gcore が何を吐くか gdb や lldb が何を読めるか 解析結果を公開している人も Anatomy of an ELF core file https://www.gabriel.urdhr.fr/2015/05/29/core-file/

Slide 27

Slide 27 text

プログラムヘッダーテーブルから必要な情報をたどれる ELF には ELF ヘッダと別に 2 系統のヘッダ領域 セクションヘッダテーブル 主に静的リンク用 プログラムヘッダテーブル 主に実行・動的リンク用 種別・ファイルにより片方しかない可能性も コアダンプで確実にあるのはプログラムヘッダ Linux カーネルはセクションヘッダを生成せず gcore は同じ情報を両方からたどれるよう出力

Slide 28

Slide 28 text

PT_LOAD: ファイル内オフセットと メモリアドレスを紐付け プログラムヘッダテーブル内に複数のエントリ 各エントリが異なる領域に対応 先頭に領域の種別を表すフィールド p_type 実行可能ファイルや .so なら: PT_LOAD: ファイルのどこをどのアドレスへ読むか PT_DYNAMIC: 動的リンク情報 PT_NOTE: その他の情報 コアダンプで使うのは PT_LOAD と PT_NOTE PT_LOAD: プロセスの各メモリ領域がファイルのどこに記録されたか 実行可能ファイルや .so と逆向きのマップ

Slide 29

Slide 29 text

PT_NOTE: プロセスの状態や依存ファイルの情報 コアダンプでのデバッグに必要な雑多な情報 プロセス ID やプロセス内の各スレッド ID CPU レジスタの値やシグナルの状態 依存ファイルの情報 パス 読み込み先メモリ領域 依存ファイルの読み取り専用領域はコアダンプ内にコピーされない PT_LOAD でファイル内領域サイズが 0 PT_NOTE に依存ファイルのパスと各部分の読み込み先のアドレス範囲を記録 ダンプの解釈時は実際の依存ファイルを参照 依存ファイル内のプロセスが未アクセスで mmap されてなかった領域もたどれる この部分はコアダンプの PT_LOAD には含まれない

Slide 30

Slide 30 text

Reli のメモリ解析機能とコアファイルの読み込み

Slide 31

Slide 31 text

自作の PHP ツール Reli の紹介 Reli PHP 製の PHP プロファイラ 処理系の情報をプロセス外から解析 コールトレースをサンプリングして取得して集計することで遅い部分が分かる 最近にメモリ解析機能も追加 スクリプト内の変数内容や参照関係を解析 参照グラフや何にどれだけメモリが消費されているかの統計情報が得られる 今回これを改造してコアダンプに対応 https://github.com/reliforp/reli-prof

Slide 32

Slide 32 text

Reli が中でやっていること

Slide 33

Slide 33 text

Linux の procfs から対象プロセスのメモリマップを読む /proc/ プロセスID/maps を解析 どの領域に処理系バイナリが読み込まれているか $ sudo cat /proc/10364/maps 56187c10d000-56187c20e000 r--p 00000000 08:07 1181323 /usr/sbin/php-fpm 56187c30d000-56187c6a9000 r-xp 00200000 08:07 1181323 /usr/sbin/php-fpm 56187c70d000-56187cef3000 r--p 00600000 08:07 1181323 /usr/sbin/php-fpm 56187d26a000-56187d30d000 r--p 00f5d000 08:07 1181323 /usr/sbin/php-fpm 56187d30d000-56187d314000 rw-p 01000000 08:07 1181323 /usr/sbin/php-fpm 56187d314000-56187d335000 rw-p 00000000 00:00 0 56187f100000-56187f2c6000 rw-p 00000000 00:00 0 [heap] 56187f100000-56187f2c6000 rw-p 00000000 00:00 0 [heap] 7f6b76c00000-7f6b76e00000 rw-p 00000000 00:00 0

Slide 34

Slide 34 text

ELF パーサを実装し処理系バイナリを解析

Slide 35

Slide 35 text

処理系内部状態へのアクセス用シンボルのメモリアドレスを解決 拡張向けに公開されているシンボル EG (Executor Globals) を解決 procfs から得たメモリマップで処理系の配置 先ベースアドレスを得る ELF 解析で得たシンボルの相対アドレスをベ ースアドレスに加算

Slide 36

Slide 36 text

FFI でシステムコールを呼び対象プロセスのメモリを読む

Slide 37

Slide 37 text

処理系内部のC 言語構造体内のポインタを順次たどる PHPerKaigi 2024 のパンフレットの記事に載せ てるような情報を地道にたどる 実行中のスクリプトの情報がかなり集められる /** * @template-covariant T of Dereferencable */ class Pointer { /** @param class-string $type */ public function __construct( public string $type, public int $address, public int $size, ) { } interface Dereferencer { /** * @template T of Dereferencable * @param Pointer $pointer * @return T */ public function deref(Pointer $pointer): mixe

Slide 38

Slide 38 text

すでに PHP による ELF のパーサを持っている PT_NOTE 部分の解析コードはなかったので追加 コアダンプファイルのパースがシュッとできる

Slide 39

Slide 39 text

procfs 解析は 「メモリマップを表すオブジェクト」を生成する機能 コアダンプ内の PT_LOAD や PT_NOTE から procfs のメモリマップと同等の情報を組み立て可能 メモリマップ読み取り用部品をコアダンプ内から情報を得るものへ差し替えられる

Slide 40

Slide 40 text

もともと Reli のメモリ読み込み機能は抽象化してある 低レベルなメモリ読み取り部品を実装 その上モノで、取得結果を処理系内の構造体レイアウトに即してFFI でパース 指定アドレスの読み取りをコアダンプ内のオフセットに変換して読む実装を用意 生きているプロセスからデータを読み取るのと同じようにコアダンプを使ってメモリ解析可能 interface MemoryReaderInterface { /** @return \FFI\CData 読んだバイト列 */ public function read( int $pid, int $remote_address, int $size ): CData; }

Slide 41

Slide 41 text

わりと大したことない変更でコアダンプが読めた 大道芸みたいなツールでも適当に各所を抽象化 しておくと後が楽 今の master に入ってる 次のリリース(0.12.0 )に入る予定 ./reli i:coredump ./core.270182 -p 270182 \ >270182.memory_analyzed.json

Slide 42

Slide 42 text

嬉しいこと PHP レベルでのプロセス死亡時の状況を詳しく調べられる gdb も処理系のデバッグシンボルもいらない 生きてるプロセスもごく短い間止めるだけで解析できる コアダンプ取得の時間しか要らない 生きてるプロセスのメモリ解析は時間がかかり、プロダクションで使い辛い コアだけ吐いておけば後でじっくり根本原因を(現実的な手間の範囲で)追える トラブル対応でバッチプロセスの再起動など急場しのぎの対応が要る場合も その場にいない詳しい人が後で調べる、も可能

Slide 43

Slide 43 text

現状の制限 自動バージョン検知が正常動作していない 収録後わりと間もなく動くようになりました ZTS 未対応 依存ファイルのパスが解析環境と異なるケースに未対応 コンテナやリモートサーバからのダンプの解析に要る

Slide 44

Slide 44 text

まとめ

Slide 45

Slide 45 text

コアダンプ = メモリダンプ

Slide 46

Slide 46 text

コアは一部のシグナルでのプロセス終了時に吐かれる

Slide 47

Slide 47 text

gcore を使えばプロセスを生かしたままコアを吐ける

Slide 48

Slide 48 text

コアダンプは gdb で読み込める

Slide 49

Slide 49 text

今の Linux でコアダンプは ELF ファイル

Slide 50

Slide 50 text

コアダンプは気合があれば PHP で読める

Slide 51

Slide 51 text

大道芸でも適当な抽象化は大事

Slide 52

Slide 52 text

しつこく変なものを作ろう!

Slide 53

Slide 53 text

おしまい