【问题标题】:What is causing this strange memory corruption in my test PHP extension?是什么导致我的测试 PHP 扩展中出现这种奇怪的内存损坏?
【发布时间】:2020-01-18 12:42:11
【问题描述】:

我最近需要一个已编译信号名称的列表,这样我就可以打印出像“被 SIGINT 中断 (2)”这样的好消息。

get_defined_constants() 对此无法使用,因为它在完全不相关的定义(具有相同的整数值)中混杂了 SIGINTSIGTRAP 等。

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

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

没有:)


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

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

然后我有一个演示函数test_test1()ZVAL_COPYs 预构建的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 将部分看似随机的环境变量转储到我的数组中!


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

提供 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

【问题讨论】:

  • 循环遍历get_defined_constants() 并获取以SIG 开头的那些怎么样?
  • 一个很好的建议,也是我尝试开始的。由于所有重叠值,返回的数组以 [-1 =&gt; SIG_DFL, 0 =&gt; SIG_IGN, 1 =&gt; SIG_UNBLOCK, 2 =&gt; SIG_SETMASK, 3 =&gt; SIGQUIT, ...] 开头。我真的,真的不想做preg_match('/^SIG[^_]/') - 更改pcntl 理论上可能会破坏未来的行为。
  • @Barmar:[感谢添加php-internals 标签!]
  • 虽然这样做需要解释条件化名称的#ifdef 行。
  • 您可能想通过chat.stackoverflow.com/rooms/11/php 寻求有关 PHP 扩展开发的帮助。

标签: php c php-internals


【解决方案1】:

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

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

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

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

【讨论】:

  • 啊,我明白了。 (数组不能被访问,这样引用计数就不会被触及?)FWIW,我正在使用 MINIT “借用”,因为 pcntl 那时正在注册它的所有信号处理程序常量......而且我不知道什么内部 API(s ) 我会用来存储信号名称列表,以便以后返回它们:) 也许我可以将该初始化函数移至 RINIT...
  • 你可以创建一个持久数组并使用它,只是不要直接返回它(你必须复制它)。你必须下拉到 zend_hash APIs,尤其是 zend_hash_init 中的“persistent”参数。或者,您可以将注册 SIG 常量的逻辑提取到调用回调的单独函数中。然后你可以使用它一次来注册信号和一次创建数组。
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 2017-12-13
  • 1970-01-01
  • 2011-08-20
  • 2017-07-29
  • 2011-09-30
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多