Valgrind条件跳转...从文件阅读时PCRE2 JIT出错

w41d8nur  于 2022-12-17  发布在  其他
关注(0)|答案(1)|浏览(153)

我有个很有趣的问题。
我想使用PCRE2和它的JIT函数。任务很简单:从文件中读取行并找到模式。
下面是示例代码:

#include <stdio.h>
#include <string.h>

#define PCRE2_CODE_UNIT_WIDTH 8
#include <pcre2.h>

int search(pcre2_code *re, unsigned char * subject) {

    pcre2_match_data *match_data_real = pcre2_match_data_create_from_pattern(re, NULL);

    size_t len_subject = strlen((const char *)subject);

    int rc = pcre2_match(
        re,
        (PCRE2_SPTR)subject,
        len_subject,
        0,
        0,
        match_data_real,
        NULL
    );

    pcre2_match_data_free(match_data_real);
    return rc;
}

int main(int argc, char ** argv) {

    unsigned char subject[][100]     = {
        "this is a foobar",
        "this is a barfoo",
        "this is a barbar",
        "this is a foofoo"
    };

    pcre2_code *re;
    PCRE2_SPTR  pattern = (unsigned char *)"foo";
    int         errornumber;
    PCRE2_SIZE  erroroffset;

    re = pcre2_compile(
        pattern,
        PCRE2_ZERO_TERMINATED,
        0,
        &errornumber,
        &erroroffset,
        NULL
    );
    pcre2_jit_compile(re, PCRE2_JIT_COMPLETE);

    FILE *fp;

    int s = 0;
    while(s < 2) {
        search(re, subject[s++]);
    }

    if (argc >= 2) {
        fp = fopen(argv[1], "r");
        if (fp != NULL) {
            char tline[2048];
            while(fgets(tline, 2048, fp) != NULL) {
                search(re, (unsigned char *)tline);
            }
            fclose(fp);
        }
    }

    pcre2_code_free(re);

    return 0;
}

编译代码:

gcc -Wall -O2 -g pcretest.c -o pcretest -lpcre2-8

如您所见,在第58行中,我检查了是否有给定的参数,代码尝试将其作为文件打开。
另外,如第49行所示,我希望使用PCRE2的JIT。
这段代码也能正常工作,但我用Valgrind检查了一下,发现了一个有趣的行为:

  • 如果我添加一个文件作为参数,那么Valgrind报告Conditional jump or move depends on uninitialised value(s)Uninitialised value was created by a stack allocation,但是它指向main()valgrind --tool=memcheck --leak-check=full --show-leak-kinds=all --track-origins=yes -s ./pcretest myfile.txt
  • 没有参数,就没有任何Valgrind报告。命令:valgrind --tool=memcheck --leak-check=full --show-leak-kinds=all --track-origins=yes -s ./pcretest
  • 如果我在第55行注解掉pcre2_jit_compile((*re), PCRE2_JIT_COMPLETE);,那么一切都正常,没有任何Valgrind报告。命令:valgrind --tool=memcheck --leak-check=full --show-leak-kinds=all --track-origins=yes -s ./pcretest myfile.txt

Valgrind的相关输出:

==31385== Conditional jump or move depends on uninitialised value(s)
==31385==    at 0x4EECD1A: ???
==31385==    by 0x1FFEFFFC1F: ???
==31385==  Uninitialised value was created by a stack allocation
==31385==    at 0x1090FA: main (pcretest.c:27)
...
==31385== HEAP SUMMARY:
==31385==     in use at exit: 0 bytes in 0 blocks
==31385==   total heap usage: 12 allocs, 12 frees, 13,486 bytes allocated
==31385== 
==31385== All heap blocks were freed -- no leaks are possible
==31385== 
==31385== ERROR SUMMARY: 1 errors from 1 contexts (suppressed: 0 from 0)
==31385== 
==31385== 1 errors in context 1 of 1:
==31385== Conditional jump or move depends on uninitialised value(s)
==31385==    at 0x4EECD1A: ???
==31385==    by 0x1FFEFFFC1F: ???
==31385==  Uninitialised value was created by a stack allocation
==31385==    at 0x1090FA: main (pcretest.c:27)
==31385== 
==31385== ERROR SUMMARY: 1 errors from 1 contexts (suppressed: 0 from 0)

第27行是int main(...)
我错过了什么?

xwbd5t1u

xwbd5t1u1#

意见

  • Valgrind报告告诉您正在访问的未初始化数据位于对main()的初始调用的堆栈帧中。
  • 即使你使用调试信息进行编译,Valgrind报告也不会暗示一个特定的变量。
  • 报告对错误的堆栈跟踪不会显示函数名,也不会追溯到main()。当然,
  • 当您禁用模式的JIT编译时,不会报告错误。

显然,错误与PCRE2的JIT编译器生成的机器码有关。如果您不执行JIT编译,则通过普通匹配路径获得正确的操作。如果您 * 执行 * JIT编译,则使用JIT生成的代码,并且该代码触发Valgrind错误。尽管如此,您仍可能获得正确的匹配。但对于触发Valgrind错误的代码,我不会依赖于此。
我仔细研究了您代码的变体,发现错误与函数search()中对pcre2_match_data_create_from_pattern()pcre2_match()的调用有关。任何一个都会导致Valgrind报告错误。但是为什么错误只发生在对search()的某些调用中
这似乎是因为JIT编译在main()的堆栈帧中设置了数据结构,而这些结构在执行if (argc > 2)语句的主体时被破坏了,这与我通过在该块中添加变量tline的初始化器来避免错误的事实是一致的:

char tline[2048] = {0};

我可以想象出各种各样的场景来说明为什么这会产生不同的效果,所有这些场景都与JIT生成的代码和编译器生成的代码如何操作堆栈指针有关。
就我个人而言,发现这样的问题可能会说服我远离PCRE的JIT编译器。至少在我有证据表明模式匹配是我的程序的性能热点之前,我肯定会这样做。但是,如果你必须使用JIT,那么这里有一些建议,可能会(也可能不会)帮助你避免麻烦:
1.把“及时”放在心上:尽可能接近实际使用模式的时间执行JIT。
1.不要假设JIT代码是长期可行的。特别是,在调用JIT编译器的函数返回之后使用它可能是不安全的,但即使是那么长时间,它也可能不好。
1.(仅)在运行JIT编译器的同一函数中使用JIT编译的正则表达式。
1.使该功能尽可能简单。
1.在函数开始时声明该函数的所有局部变量,并使用初始化式。
1.彻底测试。
对于您的特定示例代码来说,这似乎不是解决问题所必需的,但它的目的更普遍地是减少编译后的程序违反JIT所做假设的横截面。

相关问题