热门标签 | HotTags
当前位置:  开发笔记 > 编程语言 > 正文

php如何进行内存调试

本章是有关PHP源代码的内存调试的简要介绍,这不是一门完整的课程,内存调试并不难,但是你需要一些它的使用经验,大量的练习可能是你在设计任何C编写的代码时都必须要做的事情,我们将在这里介绍一个非常好的方法。

-与Zend Memory Manager相比,Valgrind显然是更好的工具,可以查找泄漏和其他与内存相关的问题。您应该始终在代码上运行valgrind,这实际上是每个C程序员都必须执行的步骤。无论是因为崩溃而想要找到并调试它,还是作为看起来好像没有任何坏处的高质量工具来运行它,valgrind都是这种工具,它可以指出隐藏的瑕疵,准备好将其吹拂一次或以后。即使您认为代码似乎一切都很好,也可以使用它:您可能会感到惊讶。

Warning

您**必须在程序上使用valgrind(或任何内存调试器)。对于每个强大的C程序,要不调试内存就不可能100%充满信心。内存错误会导致有害的安全问题,并且程序崩溃通常取决于许多参数,通常是随机的。

内存泄漏检测示例

入门

Valgrind是一个完整的堆内存调试器。它还可以调试过程内存映射和功能堆栈。请在其文档中获取更多信息。

让我们去检测动态内存泄漏,并尝试一个简单的,最常见的泄漏:

PHP_RINIT_FUNCTION(pib)
{
    void *foo = emalloc(128);
}

上面的代码每次请求都会泄漏128字节,因为它没有与此类缓冲区有关的efree()相关调用。由于它是对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输出的不同字段的详细信息,请查看。

Note

我们使用USE_ZEND_ALLOC = 0禁用并完全绕过Zend Memory Manager。对其API的每次调用(例如emalloc())将直接导致libc调用,就像我们在calgrind输出堆栈帧上可以看到的那样。

Valgrind抓住了我们的漏洞。

很容易,现在我们可以使用持久分配(也就是绕过ZendMM并使用传统libc的动态内存分配)来产生泄漏。走:

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)

也抓到了。

Note

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);
}

这里有两个泄漏。首先,我们分配一个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并在必要时释放它。做完了

Note

如您所见,PHP-像任何C语言强程序一样-充满了嵌套的指针。zend_string封装在zval中,其本身是zend_array的一部分。泄漏数组显然会泄漏zvalzend_string,但是zvals没有分配堆(我们在堆栈上分配),因此没有泄漏报告。您应该习惯这样一个事实,即忘记释放/释放诸如zend_array之类的复合结构会导致大量泄漏,因为结构经常会嵌入结构,嵌入结构等。

缓冲区上溢/下溢检测

内存泄漏很糟糕。这将导致您的程序一次或以后触发OOM,并且将大大降低主机的速度,因为随着时间的流逝,后者将获得越来越少的可用内存。这是内存泄漏的征兆。

但是更糟的是:缓冲区越界访问。访问超出分配限制的指针是许多邪恶操作(例如在计算机上获得root shell)的根源,因此您绝对应该防止它们。较轻的越界访问也经常会由于内存损坏而导致程序崩溃。但是,这全部取决于硬件目标计算机,使用的编译器和选项,操作系统内存布局,使用的libc等……许多因素。

因此,越界访问非常令人讨厌,它们是炸弹,可能会爆炸,也可能不会爆炸,或者在一分钟内,或者如果您非常幸运,它们将永远不会爆炸。

  • Valgrind *是一个内存调试器,因此能够检测到来自任何内存区域(堆和堆栈)的任何越界访问。这与查找泄漏使用的是相同的memcheck工具。

让我们看一个简单的例子:

PHP_MINIT_FUNCTION(pib)
{
    char *foo = malloc(16);
    foo[16] = 'a';
    foo[-1] = 'a';
}

这段代码分配了一个缓冲区,并故意在边界后一个字节和边界后一个字节写入数据。现在,如果您运行这样的代码,您就有大约两次机会中有一次立即崩溃,然后随机崩溃。您可能还已经在PHP中创建了一个安全漏洞,但是它可能无法被远程利用(这种行为很少见)。

Warning

越界访问导致不确定的行为。无法预料会发生什么,但是请确保它不好(立即崩溃)或可怕(安全问题)。记得。

让我们问一下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)

这两个无效的写入都已被检测到,现在您的目标是跟踪并修复它们。

在这里,我们使用了一个示例,其中我们超出范围地写入内存,这是最糟糕的情况,因为您的写入操作成功后(可能会立即导致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社区 其它相关文章!


推荐阅读
  • 本文介绍如何解决在 IIS 环境下 PHP 页面无法找到的问题。主要步骤包括配置 Internet 信息服务管理器中的 ISAPI 扩展和 Active Server Pages 设置,确保 PHP 脚本能够正常运行。 ... [详细]
  • 1:有如下一段程序:packagea.b.c;publicclassTest{privatestaticinti0;publicintgetNext(){return ... [详细]
  • 本文详细介绍了如何在Linux系统上安装和配置Smokeping,以实现对网络链路质量的实时监控。通过详细的步骤和必要的依赖包安装,确保用户能够顺利完成部署并优化其网络性能监控。 ... [详细]
  • 优化联通光猫DNS服务器设置
    本文详细介绍了如何为联通光猫配置DNS服务器地址,以提高网络解析效率和访问体验。通过智能线路解析功能,域名解析可以根据访问者的IP来源和类型进行差异化处理,从而实现更优的网络性能。 ... [详细]
  • 在计算机技术的学习道路上,51CTO学院以其专业性和专注度给我留下了深刻印象。从2012年接触计算机到2014年开始系统学习网络技术和安全领域,51CTO学院始终是我信赖的学习平台。 ... [详细]
  • 深入理解 SQL 视图、存储过程与事务
    本文详细介绍了SQL中的视图、存储过程和事务的概念及应用。视图为用户提供了一种灵活的数据查询方式,存储过程则封装了复杂的SQL逻辑,而事务确保了数据库操作的完整性和一致性。 ... [详细]
  • 在哈佛大学商学院举行的Cyberposium大会上,专家们深入探讨了开源软件的崛起及其对企业市场的影响。会议指出,开源软件不仅为企业提供了新的增长机会,还促进了软件质量的提升和创新。 ... [详细]
  • 如何配置Unturned服务器及其消息设置
    本文详细介绍了Unturned服务器的配置方法和消息设置技巧,帮助用户了解并优化服务器管理。同时,提供了关于云服务资源操作记录、远程登录设置以及文件传输的相关补充信息。 ... [详细]
  • 本文详细分析了Hive在启动过程中遇到的权限拒绝错误,并提供了多种解决方案,包括调整文件权限、用户组设置以及环境变量配置等。 ... [详细]
  • 使用Vultr云服务器和Namesilo域名搭建个人网站
    本文详细介绍了如何通过Vultr云服务器和Namesilo域名搭建一个功能齐全的个人网站,包括购买、配置服务器以及绑定域名的具体步骤。文章还提供了详细的命令行操作指南,帮助读者顺利完成建站过程。 ... [详细]
  • 在现代网络环境中,两台计算机之间的文件传输需求日益增长。传统的FTP和SSH方式虽然有效,但其配置复杂、步骤繁琐,难以满足快速且安全的传输需求。本文将介绍一种基于Go语言开发的新一代文件传输工具——Croc,它不仅简化了操作流程,还提供了强大的加密和跨平台支持。 ... [详细]
  • 本文详细介绍了Java编程语言中的核心概念和常见面试问题,包括集合类、数据结构、线程处理、Java虚拟机(JVM)、HTTP协议以及Git操作等方面的内容。通过深入分析每个主题,帮助读者更好地理解Java的关键特性和最佳实践。 ... [详细]
  • 网络攻防实战:从HTTP到HTTPS的演变
    本文通过一系列日记记录了从发现漏洞到逐步加强安全措施的过程,探讨了如何应对网络攻击并最终实现全面的安全防护。 ... [详细]
  • 本文详细介绍了macOS系统的核心组件,包括如何管理其安全特性——系统完整性保护(SIP),并探讨了不同版本的更新亮点。对于使用macOS系统的用户来说,了解这些信息有助于更好地管理和优化系统性能。 ... [详细]
  • 解决MongoDB Compass远程连接问题
    本文记录了在使用阿里云服务器部署MongoDB后,通过MongoDB Compass进行远程连接时遇到的问题及解决方案。详细介绍了从防火墙配置到安全组设置的各个步骤,帮助读者顺利解决问题。 ... [详细]
author-avatar
鄙人fisher_779
这个家伙很懒,什么也没留下!
PHP1.CN | 中国最专业的PHP中文社区 | DevBox开发工具箱 | json解析格式化 |PHP资讯 | PHP教程 | 数据库技术 | 服务器技术 | 前端开发技术 | PHP框架 | 开发工具 | 在线工具
Copyright © 1998 - 2020 PHP1.CN. All Rights Reserved | 京公网安备 11010802041100号 | 京ICP备19059560号-4 | PHP1.CN 第一PHP社区 版权所有