尝试访问静态字符串时出现段错误,但仅有时取决于构建环境

问题描述 投票:0回答:1

等等,回来,我保证这不是关于未初始化的指针!

问题

我使用 Criterion 编写了一些单元测试。被测试的代码并不重要;问题出现在测试本身。这是测试的简化版本:

#include <stdio.h>
#include <criterion/criterion.h>
#include <criterion/parameterized.h>

typedef struct {
    char *input;
} paramspec;

TestSuite(Example);

ParameterizedTestParameters(Example, test_example) {
    static paramspec params[] = {
        {"this is a test"},
    };

    size_t nb_params = sizeof (params) / sizeof (paramspec);
    return cr_make_param_array(paramspec, params, nb_params);
}

ParameterizedTest(paramspec *param, Example, test_example) {
    printf("input value is: %s\n", param->input);
}

当使用 gcc-11 (11.4.0) 或 gcc-10 (10.5.0) 在 Ubuntu 22.04 容器中编译时,运行此测试会产生:

[====] Running 1 test from Example:
[RUN ] Example::test_example
[----] test_example.c:20: Unexpected signal caught below this line!
[FAIL] Example::test_example: CRASH!
[====] Synthesis: Tested: 1 | Passing: 0 | Failing: 1 | Crashing: 1

输出中没有说明,但这是一个 SIGSEGV。如果我使用 gdb 附加到测试并打印

*param
的值,我会看到
error: Cannot access memory at address ...
:

Thread 1 "test_example" hit Breakpoint 1, Example_test_example_impl (param=0x7ffff7fa1330)
    at test_example.c:21
21          printf("input value is: %s\n", param->input);
(gdb) p *param
$1 = {input = 0x55abdb8e81ec <error: Cannot access memory at address 0x55abdb8e81ec>}

但是!

谜团愈加浓厚

如果我在 Fedora 34 下构建代码(我选择它是因为其中包含 gcc 11.3.1,它与 11.4.0 非常匹配),则代码可以正常工作:

[====] Running 1 test from Example:
[RUN ] Example::test_example
input value is: this is a test
[PASS] Example::test_example: (0.00s)
[====] Synthesis: Tested: 1 | Passing: 1 | Failing: 0 | Crashing: 0

该代码不仅在构建它的 Fedora 环境中运行良好,而且在 Ubuntu 环境中也运行没有错误!

在这两种环境中,gdb 都能够看到字符串值:

(gdb) p *param
$1 = {input = 0x41aac9 "this is a test"}

问题

build环境的哪些方面导致了段错误?这只是同一文件中的代码访问的静态字符串;没有指针分配会出错或类似的事情。

在 Ubuntu 方面,我使用 gcc-{9,10,11} 构建了这个,并且在所有情况下行为都是相同的。

使用消毒剂建造

使用

-fsanitize=undefined,address
构建代码会产生以下结果:

==16==ERROR: AddressSanitizer: SEGV on unknown address 0x5594e31d7400 (pc 0x7f1491d4e086 bp 0x7ffdfe1765b0 sp 0x7ffdfe175cf8 T0)
==16==The signal is caused by a READ memory access.
    #0 0x7f1491d4e086 in __sanitizer::internal_strlen(char const*) ../../../../src/libsanitizer/sanitizer_common/sanitizer_libc.cpp:167
    #1 0x7f1491cdf2ed in printf_common ../../../../src/libsanitizer/sanitizer_common/sanitizer_common_interceptors_format.inc:551
    #2 0x7f1491cdf6cc in __interceptor_vprintf ../../../../src/libsanitizer/sanitizer_common/sanitizer_common_interceptors.inc:1660
    #3 0x7f1491cdf7c6 in __interceptor_printf ../../../../src/libsanitizer/sanitizer_common/sanitizer_common_interceptors.inc:1718
    #4 0x555fd432c365 in Example_test_example_impl /src/test_example.c:21
    #5 0x7f1491c18298 in criterion_internal_test_main ../src/core/test.c:97
    #6 0x555fd432c2e7 in Example_test_example_jmp /src/test_example.c:20
    #7 0x7f1491c16849 in run_test_child ../src/core/runner_coroutine.c:230
    #8 0x7f1491c28a92 in bxfi_main ../subprojects/boxfort/src/sandbox.c:57
    #9 0x7f14913ced8f in __libc_start_call_main ../sysdeps/nptl/libc_start_call_main.h:58
    #10 0x7f14913cee3f in __libc_start_main_impl ../csu/libc-start.c:392
    #11 0x555fd432c1a4 in _start (/src/test_example+0x21a4)

AddressSanitizer can not provide additional info.

...但这几乎就是

gdb
之前告诉我们的。这个错误仅在 Ubuntu 上构建时出现;我在 Fedora 构建上没有看到类似的错误。

完整的再现器

如果有人有兴趣仔细查看,我已经在here整理了一个完整的重现器,其中包括测试代码、Makefile、Dockerfile 和 README。

c unit-testing segmentation-fault c-criterion
1个回答
0
投票

好的,有了测试用例后,这看起来很容易。

首先,我正在 Debian 上进行测试,发现它确实在那里崩溃了。

其次,令人惊讶的是,当我在 GDB 下运行测试二进制文件时,它没有崩溃。但我注意到这一行:

[Detaching after fork from child process 3071548]

所以那里有多个进程。通过

strace
,我发现它确实是
fork()
+
exec()
是一个孩子,而且是那个崩溃的孩子。

所以我添加了一个调试 printf (不知道为什么你自己没有这样做,从你的评论中我得到的印象是你检查了这个):

const char* const test_msg = "this is a test";
ParameterizedTestParameters(Example, test_example) {
    static paramspec params[] = {
        {test_msg},
    };

    printf("param is %p, %llx, %p\n", params, *(intptr_t*)params, test_msg);
    size_t nb_params = sizeof (params) / sizeof (paramspec);
    return cr_make_param_array(paramspec, params, nb_params);
}

ParameterizedTest(paramspec *param, Example, test_example) {
    printf("param is %p, %llx, %p\n", param, *(intptr_t*)param, test_msg);

一分钱开始掉下来:

param is 0x557fdad5e040, 557fdad5c004, 0x557fdad5c004
param is 0x7f764ba45330, 557fdad5c004, 0x5632b4277004

看看

param
中的指针是相同的,但字符串的实际地址不同。这是由 ASLR 引起的。因此,子级在与父级不同的地址上拥有静态数据,但父级传递(似乎是通过共享内存)指针,逐字逐句地,这对子级没有多大用处。

解决此问题的最佳方法是使用动态分配的参数和函数 cr_malloc

static paramspec params[] = {
    {0},
};
params[0].input = cr_malloc(strlen(test_msg)+1);
strcpy(params[0].input, test_msg);

© www.soinside.com 2019 - 2024. All rights reserved.