是什么导致我的测试 PHP 扩展中出现这种奇怪的内存损坏?

What is causing this strange memory corruption in my test PHP extension?

我最近需要一个编译信号名称的列表,这样我就可以打印像 "Interrupted by SIGINT (2)" 这样的好消息。

get_defined_constants() 对此不可用,因为它混淆了 SIGINTSIGTRAP 等完全不相关的定义(具有相同的整数值)。

信号名称根据 OS 映射到不同的值,有时它们并没有全部编译到 PHP,因此最直接的干净解决方案是一个新函数,它只是 returns 编译信号名称数组。

嗯...一个 returns 返回 PHP 用户空间的静态数组的函数...这听起来像是一个非常好的第一个源代码黑客项目,对吧?

没有:)


下面的代码(再往下一点)是一个超级最小化的测试用例,它说明了我撞到的非常奇怪的砖墙。

我有一个 GINIT 函数将扩展全局 test_array 初始化为一个数组,然后我用 pcntl 填充一些条目(就像我对 pcntl 所做的更改一样)add_assoc_long()(在本例中使用 sprintf() 为数组键生成虚拟字符串,如 !!!"""### 等)。

然后我有一个演示函数 test_test1(),它 ZVAL_COPY 是预构建的 test_arrayreturn_value

请击鼓;看看当我尝试 print_r() 结果时会发生什么:

Array
(
    [PWD] => 0
    [i336] => 1
    [LOGNAME] => 2
    [tty] => 3
    [HOME] => 4
    [LANG] => 5
    [user] => 6
    [xterm] => 7
    [TERM] => 8
    [i336] => 9
    [USER] => 10
    [:0] => 11
    [DISPLAY] => 12
    [SHLVL] => 13
    [9:22836] => 14
    [PATH] => 15
    [111] => 16
    [222] => 17
    [333] => 18
    [444] => 19
    [555] => 20
    [666] => 21
    [777] => 22
    [888] => 23
    [999] => 24
    [HG] => 25
    [MAIL] => 26
    [OLDPWD] => 27
    [] => 28
    [] => 29
    [] => 30
    [STDIN] => 31
    [STDOUT] => 32
    [STDERR] => 33
    [print_r] => 34
    [DDD] => 35
    [EEE] => 36
    [FFF] => 37
    [GGG] => 38
    [HHH] => 39
    [III] => 40
    [JJJ] => 41
    [KKK] => 42
    [LLL] => 43
    [MMM] => 44
    [NNN] => 45
    [OOO] => 46
    [PPP] => 47
    [QQQ] => 48
    [RRR] => 49
<<snipped>>

真正奇怪的是条目 0 到 15 已损坏;条目 16 到 24 没问题;条目 25 到 34 已损坏;第 35 个条目很好。

0-15 / 16-24 有点奇怪; 25-34 / 35-∞ 不是.

在任何情况下,如果我 test_test1 替换为以下内容(对 GINIT 函数中的代码进行轻微修改):

    zval test;
    array_init(&test);

    for (int i = 0; i < 80; i++) {
        char buf[4];
        sprintf(buf, "%1$c%1$c%1$c", i+33);
        add_assoc_long(&test, buf, i);
    }

    ZVAL_COPY_OR_DUP(return_value, &test);

    zval_ptr_dtor(&test);

我得到了更多的期望

(
    [!!!] => 0
    ["""] => 1
    [###] => 2
    [$$$] => 3
    [%%%] => 4
    [&&&] => 5
    ['''] => 6
    [(((] => 7
    [)))] => 8
    [***] => 9
    [+++] => 10
    [,,,] => 11
    [---] => 12
    [...] => 13
    [///] => 14
    [000] => 15
    [111] => 16
    [222] => 17
    [333] => 18
    [444] => 19
    [555] => 20
    [666] => 21
    [777] => 22
    [888] => 23
    [999] => 24
    [:::] => 25
    [;;;] => 26
    [<<<] => 27
    [===] => 28
<<snipped>>

除了一些关于我做错了什么的提示(我 知道 我有一些倒退的东西... :) ),我会 非常 很想了解 为什么 PHP 将似乎是随机环境变量的部分转储到我的数组中!


我停止自己的 exploration/solving 过程并发布这个问题的主要原因是我意识到我不知道我不知道什么,再加上我不知道去哪里尝试解决这个问题。

提供 PHP 文档的资源越来越多,但不幸的是,弄清楚如何完成简单的任务似乎需要将来自不同来源的大量细节拼凑在一起(我被困在某事上老实说,表面上看起来很简单)。

我也有疑问,我正在阅读的内容

一个例子:ZEND_MODULE_GLOBALS_ACCESSOR() 宏,用于线程安全地访问每个模块的全局值,被使用了 37 次(看起来不到 ext/ 内容的一半)。然而,我阅读的 所有 信息,包括 phpinternals.net 和 phpinternalsbook.net 等网站上的信息,都指定了包含某个 5 行的硬性要求#define 以设置对模块全局变量的访问。我偶然发现了前面提到的宏,它在 PHP 本身中实现了 #define,所以没有人需要通过阅读源代码来自己做。

我完全可以接受事情并不完全同步 - 也许那个宏是新的。

但是,我应该从哪里获得更新的参考信息来回答我的问题?

真问题。


我在下面包含了 config.m4,因此可以编译它进行测试:

php_test.h:

#ifndef PHP_TEST_H
# define PHP_TEST_H

extern zend_module_entry test_module_entry;
# define phpext_test_ptr &test_module_entry

# define PHP_TEST_VERSION "0.1.0"

ZEND_BEGIN_MODULE_GLOBALS(test)
    zval test_array;
ZEND_END_MODULE_GLOBALS(test)

# if defined(ZTS) && defined(COMPILE_DL_TEST)
ZEND_TSRMLS_CACHE_EXTERN()
# endif


ZEND_DECLARE_MODULE_GLOBALS(test)

#endif  /* PHP_TEST_H */

test.c:

#ifdef HAVE_CONFIG_H
# include "config.h"
#endif

#include "php.h"
#include "ext/standard/info.h"
#include "php_test.h"

PHP_FUNCTION(test_test1)
{
    ZVAL_COPY(return_value, &ZEND_MODULE_GLOBALS_ACCESSOR(test, test_array));   
}

PHP_RINIT_FUNCTION(test)
{
#if defined(ZTS) && defined(COMPILE_DL_TEST)
    ZEND_TSRMLS_CACHE_UPDATE();
#endif

    return SUCCESS;
}

PHP_MINIT_FUNCTION(test)
{
    return SUCCESS;
}


PHP_GSHUTDOWN_FUNCTION(test)
{ }

PHP_GINIT_FUNCTION(test)
{

    // Thanks to #php.pecl on efnet for pointing me in the direction of `GINIT`.
    // I'd seriously hit my SIGSEGV limit, and really appreciated the valid pointers (punintended).

    array_init(&ZEND_MODULE_GLOBALS_ACCESSOR(test, test_array));

    for (int i = 0; i < 80; i++) {
        char buf[4];
        sprintf(buf, "%1$c%1$c%1$c", i+33);
        add_assoc_long(&ZEND_MODULE_GLOBALS_ACCESSOR(test, test_array), buf, i);
    }

    return SUCCESS;

}

PHP_MINFO_FUNCTION(test)
{
    php_info_print_table_start();
    php_info_print_table_header(2, "test support", "enabled");
    php_info_print_table_end();
}

ZEND_BEGIN_ARG_INFO(arginfo_test_test1, 0)
ZEND_END_ARG_INFO()

ZEND_BEGIN_ARG_INFO(arginfo_test_test2, 0)
    ZEND_ARG_INFO(0, str)
ZEND_END_ARG_INFO()

static const zend_function_entry test_functions[] = {
    PHP_FE(test_test1, arginfo_test_test1)
    PHP_FE_END
};

zend_module_entry test_module_entry = {
    STANDARD_MODULE_HEADER,
    "test",                     /* Extension name */
    test_functions,             /* zend_function_entry */
    PHP_MINIT(test),            /* PHP_MINIT - Module initialization */
    NULL,                       /* PHP_MSHUTDOWN - Module shutdown */
    PHP_RINIT(test),            /* PHP_RINIT - Request initialization */
    NULL,                       /* PHP_RSHUTDOWN - Request shutdown */
    PHP_MINFO(test),            /* PHP_MINFO - Module info */
    PHP_TEST_VERSION,           /* Version */
    PHP_MODULE_GLOBALS(test),
    PHP_GINIT(test),
    PHP_GSHUTDOWN(test),
    NULL,                       /* PRSHUTDOWN() */
    STANDARD_MODULE_PROPERTIES_EX
};

#ifdef COMPILE_DL_TEST
# ifdef ZTS
ZEND_TSRMLS_CACHE_DEFINE()
# endif
ZEND_GET_MODULE(test)
#endif

config.m4:

PHP_ARG_ENABLE([test2],
  [whether to enable test2 support],
  [AS_HELP_STRING([--enable-test2],
    [Enable test2 support])],
  [no])

if test "$PHP_TEST2" != "no"; then
  AC_DEFINE(HAVE_TEST2, 1, [ Have test2 support ])

  PHP_NEW_EXTENSION(test2, test2.c, $ext_shared)
fi

GINIT 在请求启动之前被调用。 array_init()add_assoc_long()(以及大多数其他 API)使用按请求分配器。

您可以改用持久分配(通过使用较低级别的 zend_hash 和 zend_string API 并传递 persistent=1 标志),但您仍然不能 return 这样一个来自 PHP 函数的数组,因为这违反了 PHP 内存模型(不允许在请求期间更改持久值的引用计数)。

如果你想在全局中使用每个请求分配器放置一个值,你需要在 RINIT 中这样做(然后在 RSHUTDOWN 中销毁)。这些处理程序作为每个请求的一部分被调用。

尽管对于您的特定用例,我建议您完全不要使用全局变量,而只是在每次调用该函数时重新构造数组。它不是性能关键。