この章では、PHP ソース コードのメモリ デバッグについて簡単に説明します。これは完全なコースではありません。メモリのデバッグは難しくありませんが、使用にはある程度の経験が必要です。また、C で記述されたコードを設計する場合は、おそらく多くの練習が必要になります。ここでは、非常に有名なメモリ デバッガ valgrind; と、それを PHP で使用してメモリの問題をデバッグする方法を紹介します。
関連する学習の推奨事項: PHP プログラミングの入門から熟練度まで
Valgrind は多くの Unix 環境で使用されていますC/C で書かれたソフトウェアにおける多くの一般的なメモリ問題をデバッグするためのよく知られたツール。 Valgrind は、メモリ デバッグ用の多用途のフロントエンド ツールです。最も一般的に使用される低レベル ツールは、「memcheck」と呼ばれます。これが機能する方法は、各 libc のヒープ割り当てを独自のヒープ割り当てに置き換え、それらを使用して何を行うかを追跡することです。 「massif」にも興味があるかもしれません。これは、プログラムの一般的なヒープ メモリ使用量を理解するのに役立つメモリ トラッカーです。
NOTE
さらに理解するには、Valgrind のドキュメントを読む必要があります。いくつかの素晴らしい例とともに、よく書かれています。
メモリ割り当ての置換を行うには、プロファイリングするプログラム (この場合は PHP) を valgrind 経由で実行する、つまり valgrind バイナリを開始する必要があります。
valgrind がすべての libc のヒープ割り当てを置き換えて追跡すると、デバッガの速度が大幅に低下する傾向があります。 PHP の場合は、それに気づくでしょう。 PHP の速度低下はそこまで急激ではありませんが、それでも明らかに目立ちます。それに気づいた場合でも、これは正常な現象なので心配しないでください。
Valgrind は使用できる唯一のツールではありませんが、最も一般的に使用されています。他にもDr.Memory、LeakSanitizer、Electric Fence、AddressSanitizerなどのツールがあります。
以下は、メモリ デバッグの経験を積み、欠陥が見つかる可能性を軽減し、デバッグ時間を短縮するために必要な手順です。
- 常に行う必要があります。 PHPのデバッグバージョンを使用します。実稼働ビルドでメモリをデバッグしようとすることは無関係です。
- デバッガーは常に USE_ZEND_ALLOC = 0 環境で起動する必要があります。 Zend Memory Manager の章で学んだかもしれませんが、この環境変数は、現在のプロセスの開始時に ZendMM を無効にします。メモリ デバッガを起動するときにこれを行うことを強くお勧めします。 ZendMM を完全にバイパスして、valgrind によって生成されたトレースを理解するのに役立ちます。
- 環境 ZEND_DONT_UNLOAD_MODULES = 1 でメモリ デバッガーを起動することを強くお勧めします。これにより、プロセスの最後に PHP が拡張機能の .so ファイルをアンロードすることがなくなります。これは、valgrind のレポートをより適切に追跡するためです。valgrind がエラーを表示しようとしているときに PHP が拡張機能をアンロードすると、情報を取得したファイルがプロセス メモリ イメージの一部ではなくなるため、後で拡張機能が不完全になります。
- 何らかの抑制が必要な場合があります。プロセスの最後に拡張機能をアンロードしないように PHP に指示すると、valgrind 出力で誤検知が発生する可能性があります。 PHP 拡張機能はリークがチェックされ、プラットフォームで誤検出が発生した場合は、次のように抑制を使用してそれらをオフにすることができます。このような例に基づいて、自由に独自の内容を作成してください。
- Zend Memory Manager と比較すると、リークやその他のメモリ関連の問題を見つけるには、明らかに Valgrind の方が優れたツールです。コードに対して valgrind を常に実行する必要があります。これは、事実上すべての C プログラマが実行する必要があるステップです。クラッシュしているため、それを見つけてデバッグしたい場合でも、害を及ぼさないように見える高品質のツールとして実行している場合でも、valgrind は隠れた欠陥を指摘し、それらを吹き飛ばす準備ができているツールです。一度または後で離れてください。コードに問題がないと思われる場合でも、これを使用してください。驚かれるかもしれません。
警告
プログラムでは valgrind (または任意のメモリ デバッガー) を使用する必要があります。すべての強力な C プログラムと同様に、メモリをデバッグせずに 100% の信頼を得るのは不可能です。メモリ エラーは有害なセキュリティ問題を引き起こす可能性があり、プログラムのクラッシュは多くのパラメータに依存し、ランダムに発生することがよくあります。
Valgrind は完全なヒープ メモリ デバッガです。プロシージャル メモリ マップと関数スタックをデバッグすることもできます。詳細については、ドキュメントを参照してください。
動的メモリ リークを検出し、最も一般的な単純なリークを試してみましょう:
PHP_RINIT_FUNCTION(pib) { void *foo = emalloc(128); }
上記のコードには、そのようなバッファに関連する efree()
関連呼び出しがないため、リクエストごとに 128 バイトがリークします。これは emalloc()
への呼び出しであるため、Zend Memory Manager を通過するため、ZendMM の章で見たように、後で警告が表示されます。また、valgrind がリークを認識できるかどうかも確認します。
> ZEND_DONT_UNLOAD_MODULES=1 USE_ZEND_ALLOC=0 valgrind --leak-check=full --suppressions=/path/to/suppression --show-reachable=yes --track-origins=yes ~/myphp/bin/php -dextension=pib.so /tmp/foo.php
valgrind を使用して PHP-CLI プロセスを開始します。ここでは「pib」という拡張子を想定します。出力は次のとおりです。
==28104== 128 bytes in 1 blocks are definitely lost in loss record 1 of 1 ==28104== at 0x4C2DB8F: malloc (in /usr/lib/valgrind/vgpreload_memcheck-amd64-linux.so) ==28104== by 0xA3701E: __zend_malloc (zend_alloc.c:2820) ==28104== by 0xA362E7: _emalloc (zend_alloc.c:2413) ==28104== by 0xE896F99: zm_activate_pib (pib.c:1880) ==28104== by 0xA79F1B: zend_activate_modules (zend_API.c:2537) ==28104== by 0x9D31D3: php_request_startup (main.c:1673) ==28104== by 0xB5909A: do_cli (php_cli.c:964) ==28104== by 0xB5A423: main (php_cli.c:1381) ==28104== LEAK SUMMARY: ==28104== definitely lost: 128 bytes in 1 blocks ==28104== indirectly lost: 0 bytes in 0 blocks ==28104== possibly lost: 0 bytes in 0 blocks ==28104== still reachable: 0 bytes in 0 blocks ==28104== suppressed: 7,883 bytes in 40 blocks
私たちの観点からは、「絶対損失」に注目する必要があります。
Note
memcheck によって出力されるさまざまなフィールドの詳細については、こちらをご覧ください。
注
USE_ZEND_ALLOC = 0
を使用して、Zend Memory Managerを無効にして完全にバイパスします。 calgrind 出力スタック フレームでわかるように、その API (例:emalloc()
) を呼び出すと、直接 libc 呼び出しが行われます。
Valgrind は私たちの脆弱性を発見しました。
これは簡単です。永続的な割り当て (つまり、ZendMM をバイパスし、従来の libc の動的メモリ割り当てを使用) を使用してリークを生成できるようになりました。 Go:
PHP_RINIT_FUNCTION(pib) { void *foo = malloc(128); }
これはレポートです:
==28758== 128 bytes in 1 blocks are definitely lost in loss record 1 of 1 ==28758== at 0x4C2DB8F: malloc (in /usr/lib/valgrind/vgpreload_memcheck-amd64-linux.so) ==28758== by 0xE896F82: zm_activate_pib (pib.c:1880) ==28758== by 0xA79F1B: zend_activate_modules (zend_API.c:2537) ==28758== by 0x9D31D3: php_request_startup (main.c:1673) ==28758== by 0xB5909A: do_cli (php_cli.c:964) ==28758== by 0xB5A423: main (php_cli.c:1381)
も捕まりました。
注意
Valgrind はすべてをキャッチします。巨大なプロセス メモリ マップのどこかにある忘れられた小さなバイトはすべて、valgrind の目によって報告されます。通行できません。
これはより複雑な設定です。以下のコードのリークを見つけることができますか?
static zend_array ar; PHP_MINIT_FUNCTION(pib) { zend_string *str; zval string; str = zend_string_init("yo", strlen("yo"), 1); ZVAL_STR(&string, str); zend_hash_init(&ar, 8, NULL, ZVAL_PTR_DTOR, 1); zend_hash_next_index_insert(&ar, &string); }
ここには 2 つのリークがあります。まず、zend_string を割り当てますが、解放しません。次に、新しい zend_hash を割り当てますが、それも解放しません。 valgrind で開始して結果を見てみましょう:
==31316== 296 (264 direct, 32 indirect) bytes in 1 blocks are definitely lost in loss record 1 of 2 ==32006== by 0xA3701E: __zend_malloc (zend_alloc.c:2820) ==32006== by 0xA814B2: zend_hash_real_init_ex (zend_hash.c:133) ==32006== by 0xA816D2: zend_hash_check_init (zend_hash.c:161) ==32006== by 0xA83552: _zend_hash_index_add_or_update_i (zend_hash.c:714) ==32006== by 0xA83D58: _zend_hash_next_index_insert (zend_hash.c:841) ==32006== by 0xE896AF4: zm_startup_pib (pib.c:1781) ==32006== by 0xA774F7: zend_startup_module_ex (zend_API.c:1843) ==32006== by 0xA77559: zend_startup_module_zval (zend_API.c:1858) ==32006== by 0xA85AF5: zend_hash_apply (zend_hash.c:1508) ==32006== by 0xA77B25: zend_startup_modules (zend_API.c:1969) ==31316== 32 bytes in 1 blocks are indirectly lost in loss record 2 of 2 ==31316== by 0xA3701E: __zend_malloc (zend_alloc.c:2820) ==31316== by 0xE880B0D: zend_string_alloc (zend_string.h:122) ==31316== by 0xE880B76: zend_string_init (zend_string.h:158) ==31316== by 0xE896F9D: zm_activate_pib (pib.c:1781) ==31316== by 0xA79F1B: zend_activate_modules (zend_API.c:2537) ==31316== by 0x9D31D3: php_request_startup (main.c:1673) ==31316== by 0xB5909A: do_cli (php_cli.c:964) ==31316== by 0xB5A423: main (php_cli.c:1381) ==31316== LEAK SUMMARY: ==31316== definitely lost: 328 bytes in 2 blocks
予想通り、両方のリークが報告されています。ご覧のとおり、valgrind は正確で、必要な場所に目を向けます。
今すぐ修正してください:
PHP_MSHUTDOWN_FUNCTION(pib) { zend_hash_destroy(&ar); }
PHP プログラムの最後の MSHUTDOWN で永続配列を破棄しました。これを作成するときに、デストラクターとして ZVAL_PTR_DTOR
に渡します。これにより、挿入されたすべての項目に対してそのコールバックが実行されます。これは、内容を解析する zval を破壊する zval のデストラクターです。型 IS_STRING
の場合、デストラクターは zend_string
を解放し、必要に応じて解放します。完了
#注
ご覧のとおり、PHP は、他の強力な C プログラムと同様に、ネストされたポインタでいっぱいです。
zend_string
はzval
にカプセル化されており、これ自体はzend_array
の一部です。配列をリークすると、明らかにzval
とzend_string
がリークしますが、zvals
はヒープ上に割り当てられていない (スタック上に割り当てている) ため、リークは報告されません。zend_array
のような複合構造を解放/解放するのを忘れると、構造には多くの場合、埋め込み構造や埋め込み構造などが含まれるため、大量のリークが発生する可能性があるという事実に慣れる必要があります。
メモリ リークは問題があります。これにより、プログラムが OOM を 1 回以上トリガーし、時間の経過とともにホストの空きメモリが少なくなるため、ホストの速度が大幅に低下します。これはメモリ リークの兆候です。
しかしさらに悪いことに、境界外のアクセスをバッファリングします。割り当て制限を超えてポインタにアクセスすることは、多くの不正な操作 (コンピュータ上で root シェルを取得するなど) の根源となるため、これらの操作は確実に防止する必要があります。軽度の範囲外アクセスによっても、メモリ破損によりプログラムがクラッシュすることがよくあります。ただし、それはすべて、ハードウェア ターゲット マシン、使用するコンパイラーとオプション、OS メモリ レイアウト、使用する libc など、多くの要因に依存します。
したがって、境界外へのアクセスは非常に迷惑です。これは 爆弾 であり、1 分以内に爆発するか爆発しない可能性があり、非常に運が良ければ永遠にかかる可能性があります。爆発することはありません。
簡単な例を見てみましょう:
PHP_MINIT_FUNCTION(pib) { char *foo = malloc(16); foo[16] = 'a'; foo[-1] = 'a'; }
このコードはバッファを割り当て、境界の後に 1 バイト、境界の後に 1 バイトのデータを意図的に書き込みます。このようなコードを実行すると、約 2 分の 1 の確率ですぐにクラッシュし、その後ランダムにクラッシュします。 PHP にセキュリティ ホールが作成されている可能性もありますが、リモートから悪用できない可能性があります (このような動作はまれです)。
警告
境界外にアクセスすると、未定義の動作が発生します。何が起こるかを予測する方法はありませんが、それがひどいもの(即時クラッシュ)またはひどいもの(セキュリティの問題)であることを確認してください。覚えて。
valgrind に問い合わせてみましょう。前とまったく同じコマンド ラインを使用して起動します。出力以外は何も変更されていません:
==12802== Invalid write of size 1 ==12802== at 0xE896A98: zm_startup_pib (pib.c:1772) ==12802== by 0xA774F7: zend_startup_module_ex (zend_API.c:1843) ==12802== by 0xA77559: zend_startup_module_zval (zend_API.c:1858) ==12802== by 0xA85AF5: zend_hash_apply (zend_hash.c:1508) ==12802== by 0xA77B25: zend_startup_modules (zend_API.c:1969) ==12802== by 0x9D4541: php_module_startup (main.c:2260) ==12802== by 0xB5802F: php_cli_startup (php_cli.c:427) ==12802== by 0xB5A367: main (php_cli.c:1348) ==12802== Address 0xeb488f0 is 0 bytes after a block of size 16 alloc'd ==12802== at 0x4C2DB8F: malloc (in /usr/lib/valgrind/vgpreload_memcheck-amd64-linux.so) ==12802== by 0xE896A85: zm_startup_pib (pib.c:1771) ==12802== by 0xA774F7: zend_startup_module_ex (zend_API.c:1843) ==12802== by 0xA77559: zend_startup_module_zval (zend_API.c:1858) ==12802== by 0xA85AF5: zend_hash_apply (zend_hash.c:1508) ==12802== by 0xA77B25: zend_startup_modules (zend_API.c:1969) ==12802== by 0x9D4541: php_module_startup (main.c:2260) ==12802== by 0xB5802F: php_cli_startup (php_cli.c:427) ==12802== by 0xB5A367: main (php_cli.c:1348) ==12802== ==12802== Invalid write of size 1 ==12802== at 0xE896AA6: zm_startup_pib (pib.c:1773) ==12802== by 0xA774F7: zend_startup_module_ex (zend_API.c:1843) ==12802== by 0xA77559: zend_startup_module_zval (zend_API.c:1858) ==12802== by 0xA85AF5: zend_hash_apply (zend_hash.c:1508) ==12802== by 0xA77B25: zend_startup_modules (zend_API.c:1969) ==12802== by 0x9D4541: php_module_startup (main.c:2260) ==12802== by 0xB5802F: php_cli_startup (php_cli.c:427) ==12802== by 0xB5A367: main (php_cli.c:1348) ==12802== Address 0xeb488df is 1 bytes before a block of size 16 alloc'd ==12802== at 0x4C2DB8F: malloc (in /usr/lib/valgrind/vgpreload_memcheck-amd64-linux.so) ==12802== by 0xE896A85: zm_startup_pib (pib.c:1771) ==12802== by 0xA774F7: zend_startup_module_ex (zend_API.c:1843) ==12802== by 0xA77559: zend_startup_module_zval (zend_API.c:1858) ==12802== by 0xA85AF5: zend_hash_apply (zend_hash.c:1508) ==12802== by 0xA77B25: zend_startup_modules (zend_API.c:1969) ==12802== by 0x9D4541: php_module_startup (main.c:2260) ==12802== by 0xB5802F: php_cli_startup (php_cli.c:427) ==12802== by 0xB5A367: main (php_cli.c:1348)
これら 2 つの無効な書き込みが検出されました。ここでの目標は次のとおりです。それらを追跡して修正します。
在这里,我们使用了一个示例,其中我们超出范围地写入内存,这是最糟糕的情况,因为您的写入操作成功后(可能会立即导致SIGSEGV)将覆盖该指针旁边的一些关键区域。当我们使用libc的malloc()
进行分配时,我们将覆盖libc用于管理和跟踪其分配的关键头尾块。取决于许多因素(平台,使用的libc,如何编译等等),这将导致崩溃。
Valgrind也可能报告无效读取。这意味着您将在分配的指针的范围之外执行内存读取操作。更好的情况是块被覆盖,但您仍然不应该访问内存区域,在这种情况下又可能会导致立即崩溃,或者稍后崩溃,或者永远不会访问?不要那样做
Note
一旦您在valgrind的输出中读取“ Invalid”,那对您来说真的很不好。无论是无效的读取还是写入,您的代码中都存在问题,因此您应该将这个问题视为高风险:现在就真正修复它。
这是有关字符串连接的第二个示例:
char *foo = strdup("foo"); char *bar = strdup("bar"); char *foobar = malloc(strlen("foo") + strlen("bar")); memcpy(foobar, foo, strlen(foo)); memcpy(foobar + strlen("foo"), bar, strlen(bar)); fprintf(stderr, "%s", foobar); free(foo); free(bar); free(foobar);
你能发现问题吗?
让我们问一下valgrind:
==13935== Invalid read of size 1 ==13935== at 0x4C30F74: strlen (in /usr/lib/valgrind/vgpreload_memcheck-amd64-linux.so) ==13935== by 0x768203E: fputs (iofputs.c:33) ==13935== by 0xE896B91: zm_startup_pib (pib.c:1779) ==13935== by 0xA774F7: zend_startup_module_ex (zend_API.c:1843) ==13935== by 0xA77559: zend_startup_module_zval (zend_API.c:1858) ==13935== by 0xA85AF5: zend_hash_apply (zend_hash.c:1508) ==13935== by 0xA77B25: zend_startup_modules (zend_API.c:1969) ==13935== by 0x9D4541: php_module_startup (main.c:2260) ==13935== by 0xB5802F: php_cli_startup (php_cli.c:427) ==13935== by 0xB5A367: main (php_cli.c:1348) ==13935== Address 0xeb48986 is 0 bytes after a block of size 6 alloc'd ==13935== at 0x4C2DB8F: malloc (in /usr/lib/valgrind/vgpreload_memcheck-amd64-linux.so) ==13935== by 0xE896B14: zm_startup_pib (pib.c:1774) ==13935== by 0xA774F7: zend_startup_module_ex (zend_API.c:1843) ==13935== by 0xA77559: zend_startup_module_zval (zend_API.c:1858) ==13935== by 0xA85AF5: zend_hash_apply (zend_hash.c:1508) ==13935== by 0xA77B25: zend_startup_modules (zend_API.c:1969) ==13935== by 0x9D4541: php_module_startup (main.c:2260) ==13935== by 0xB5802F: php_cli_startup (php_cli.c:427) ==13935== by 0xB5A367: main (php_cli.c:1348)
第1779行指向fprintf()
调用。该调用确实要求fputs()
,其本身称为strlen()
(均来自libc),在这里strlen()
读取1个字节无效。
我们只是忘记了\ 0
来终止我们的字符串。我们传递fprintf()
无效的字符串。它首先尝试计算调用strlen()
的字符串的长度。然后strlen()
将扫描缓冲区,直到找到\ 0
,并且它将扫描缓冲区的边界,因为我们忘记了对其进行零终止。我们在这里很幸运,strlen()
仅从末尾传递一个字节。那可能更多,并且可能崩溃了,因为我们真的不知道下一个\ 0
在内存中的位置,这是随机的。
解:
size_t len = strlen("foo") + strlen("bar") + 1; /* note the +1 for \0 */ char *foobar = malloc(len); /* ... ... same code ... ... */ foobar[len - 1] = '\0'; /* terminate the string properly */
Note
上述错误是C语言中最常见的错误之一。它们被称为一次性错误:您忘记仅分配一个字节,但是由于以下原因,您将在代码中产生大量问题那。
最后,这里是最后一个示例,展示了一个有余使用的场景。这也是C编程中的一个非常常见的错误,与错误的内存访问一样严重:它创建了安全缺陷,可能导致非常讨厌的行为。显然,valgrind可以检测到无用后使用。这是一个:
char *foo = strdup("foo"); free(foo); memcpy(foo, "foo", sizeof("foo"));
同样,这里是一个与PHP无关的PHP场景。我们释放一个指针,然后再使用它。这是一个大错误。让我们问一下valgrind:
==14594== Invalid write of size 1 ==14594== at 0x4C3245C: memcpy@GLIBC_2.2.5 (in /usr/lib/valgrind/vgpreload_memcheck-amd64-linux.so) ==14594== by 0xE896AA1: zm_startup_pib (pib.c:1774) ==14594== by 0xA774F7: zend_startup_module_ex (zend_API.c:1843) ==14594== by 0xA77559: zend_startup_module_zval (zend_API.c:1858) ==14594== by 0xA85AF5: zend_hash_apply (zend_hash.c:1508) ==14594== by 0xA77B25: zend_startup_modules (zend_API.c:1969) ==14594== by 0x9D4541: php_module_startup (main.c:2260) ==14594== by 0xB5802F: php_cli_startup (php_cli.c:427) ==14594== by 0xB5A367: main (php_cli.c:1348) ==14594== Address 0xeb488e0 is 0 bytes inside a block of size 4 free'd ==14594== at 0x4C2EDEB: free (in /usr/lib/valgrind/vgpreload_memcheck-amd64-linux.so) ==14594== by 0xE896A86: zm_startup_pib (pib.c:1772) ==14594== by 0xA774F7: zend_startup_module_ex (zend_API.c:1843) ==14594== by 0xA77559: zend_startup_module_zval (zend_API.c:1858) ==14594== by 0xA85AF5: zend_hash_apply (zend_hash.c:1508) ==14594== by 0xA77B25: zend_startup_modules (zend_API.c:1969) ==14594== by 0x9D4541: php_module_startup (main.c:2260) ==14594== by 0xB5802F: php_cli_startup (php_cli.c:427) ==14594== by 0xB5A367: main (php_cli.c:1348) ==14594== Block was alloc'd at ==14594== at 0x4C2DB8F: malloc (in /usr/lib/valgrind/vgpreload_memcheck-amd64-linux.so) ==14594== by 0x769E8D9: strdup (strdup.c:42) ==14594== by 0xE896A70: zm_startup_pib (pib.c:1771) ==14594== by 0xA774F7: zend_startup_module_ex (zend_API.c:1843) ==14594== by 0xA77559: zend_startup_module_zval (zend_API.c:1858) ==14594== by 0xA85AF5: zend_hash_apply (zend_hash.c:1508) ==14594== by 0xA77B25: zend_startup_modules (zend_API.c:1969) ==14594== by 0x9D4541: php_module_startup (main.c:2260) ==14594== by 0xB5802F: php_cli_startup (php_cli.c:427) ==14594== by 0xB5A367: main (php_cli.c:1348)
这里的一切再次变得清晰。
在投入生产之前,请使用内存调试器。正如您在本章中学到的那样,您在计算中忘记的小字节可能导致可利用的安全漏洞。它还经常(非常频繁地)导致简单的崩溃。这意味着您的扩展很酷,可以减少整个服务器(服务器)及其每个客户端的数量。
C是一种非常严格的编程语言。您将获得数十亿字节的内存来进行编程,并且必须安排这些内存来执行一些计算。但是请不要搞砸这种强大的功能:在最好的情况下(罕见),什么都不会发生,在更坏的情况下(非常常见),您会在这里和那里随机崩溃,在最坏的情况下,您会创建一个漏洞在恰好可以被远程利用的程序中...
您的工具娴熟,聪明,请确实照顾机器内存。
以上がPHPでメモリデバッグを実行する方法の詳細内容です。詳細については、PHP 中国語 Web サイトの他の関連記事を参照してください。