Upgrade to Pro — share decks privately, control downloads, hide ads and more …

PHP で読む楽しいコアダンプ

sji
March 07, 2024

PHP で読む楽しいコアダンプ

sji

March 07, 2024
Tweet

More Decks by sji

Other Decks in Programming

Transcript

  1. 磁気コアメモリ 出典: 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 でメモリダンプが「コア」ダンプ と呼ばれ続ける
  2. コアが吐かれるクラッシュの例 シグナル配送時の標準動作がコアを吐いて死ぬものの場合など 1. CPU 例外が発生 メモリアクセス違反とか 無効なCPU 命令とか 2. OS

    カーネル の例外ハンドラが補足 3. カーネルがプロセスに対応するシグナルを配送 4. プロセスで当該シグナルのハンドラが登録されてない 5. 標準動作によりプロセスがコアを吐いて終了
  3. PHP では処理系や拡張のバグくらいでしか起きない PHP スクリプトは ZendEngine 上で実行 適切なメモリアクセスや CPU 命令の実行、OS 機能へのアクセスは

    ZendEngine の責務 これらを間違うのは ZendEngine や処理系機能をカスタマイズする拡張側のバグ 言語改定の直後などでもなければそんなにバグらない 本番環境で不安定なバージョンの処理系を使うことはほぼない それでもたまに起きるとコアダンプが助けになる
  4. OS 側の設定の影響 コアを吐くのに OS 側で設定が必要な部分も Ubuntu などのデフォルトはコアを吐かない設定 コアはメモリ内容のダンプ ディスクを圧迫するかも 流出すればセキュリティ的な問題もあるかも

    有効にする例: ulimit -c unlimited SELinux のようなセキュリティ設定で止められてる場合も コアは吐くが apport のようなサービスに渡される場合も
  5. 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)
  6. 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]
  7. gdb でのコアダンプの読み込み gdb に実行ファイルとコアファイ ルを指定して実行 ソースコードとの対応を知るには 実行ファイルのデバッグ情報が必 要 ふつうのディストリビューション 配布のパッケージではデバッグ情

    報は分離配布 最近の gdb では debuginfod に よる自動ダウンロードも可能 $ gdb php --core ./core.270182 < 中略 > Reading symbols from php... This GDB supports auto-downloading debuginfo from the following <https://debuginfod.ubuntu.com> Enable debuginfod for this session? (y or [n])
  8. バックトレース 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=<optimized out>, 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=<optimized out>, str_len=<optimized out>, 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=<optimized out>, str_len=<optimized out>, retval_pt
  9. 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
  10. 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;
  11. コアダンプの内容は「公式の」仕様がない ET_CORE の具体的な内容は ELF の仕様には何も規定がない 実装こそ仕様 Linux カーネルが何を吐くか gcore が何を吐くか

    gdb や lldb が何を読めるか 解析結果を公開している人も Anatomy of an ELF core file https://www.gabriel.urdhr.fr/2015/05/29/core-file/
  12. プログラムヘッダーテーブルから必要な情報をたどれる ELF には ELF ヘッダと別に 2 系統のヘッダ領域 セクションヘッダテーブル 主に静的リンク用 プログラムヘッダテーブル

    主に実行・動的リンク用 種別・ファイルにより片方しかない可能性も コアダンプで確実にあるのはプログラムヘッダ Linux カーネルはセクションヘッダを生成せず gcore は同じ情報を両方からたどれるよう出力
  13. PT_LOAD: ファイル内オフセットと メモリアドレスを紐付け プログラムヘッダテーブル内に複数のエントリ 各エントリが異なる領域に対応 先頭に領域の種別を表すフィールド p_type 実行可能ファイルや .so なら:

    PT_LOAD: ファイルのどこをどのアドレスへ読むか PT_DYNAMIC: 動的リンク情報 PT_NOTE: その他の情報 コアダンプで使うのは PT_LOAD と PT_NOTE PT_LOAD: プロセスの各メモリ領域がファイルのどこに記録されたか 実行可能ファイルや .so と逆向きのマップ
  14. PT_NOTE: プロセスの状態や依存ファイルの情報 コアダンプでのデバッグに必要な雑多な情報 プロセス ID やプロセス内の各スレッド ID CPU レジスタの値やシグナルの状態 依存ファイルの情報

    パス 読み込み先メモリ領域 依存ファイルの読み取り専用領域はコアダンプ内にコピーされない PT_LOAD でファイル内領域サイズが 0 PT_NOTE に依存ファイルのパスと各部分の読み込み先のアドレス範囲を記録 ダンプの解釈時は実際の依存ファイルを参照 依存ファイル内のプロセスが未アクセスで mmap されてなかった領域もたどれる この部分はコアダンプの PT_LOAD には含まれない
  15. 自作の PHP ツール Reli の紹介 Reli PHP 製の PHP プロファイラ

    処理系の情報をプロセス外から解析 コールトレースをサンプリングして取得して集計することで遅い部分が分かる 最近にメモリ解析機能も追加 スクリプト内の変数内容や参照関係を解析 参照グラフや何にどれだけメモリが消費されているかの統計情報が得られる 今回これを改造してコアダンプに対応 https://github.com/reliforp/reli-prof
  16. 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
  17. 処理系内部のC 言語構造体内のポインタを順次たどる PHPerKaigi 2024 のパンフレットの記事に載せ てるような情報を地道にたどる 実行中のスクリプトの情報がかなり集められる /** * @template-covariant

    T of Dereferencable */ class Pointer { /** @param class-string<T> $type */ public function __construct( public string $type, public int $address, public int $size, ) { } interface Dereferencer { /** * @template T of Dereferencable * @param Pointer<T> $pointer * @return T */ public function deref(Pointer $pointer): mixe