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

问题

我用Criterion编写了一些单元测试.测试中的代码并不重要;问题突然出现在in the test itself处.以下是这项测试的简化版本:

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

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

[====] 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下构建代码(我之所以 Select 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环境中运行良好--它also在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和自述文件.

推荐答案

好的,在准备好测试用例之后,这似乎很容易.

首先,我在Debian上测试,注意到它does在那里崩溃.

其次,相当令人惊讶的是,当我在GDB下运行测试二进制文件时,它didn't崩溃了.但我注意到了这句话:

[Detaching after fork from child process 3071548]

所以那里有多个进程.在strace的时候,我发现它确实是一个fork()s+exec()s的子元素,而崩溃的是这个子元素.

所以我添加了一个调试打印文件(不知道你为什么没有自己做,从你的 comments 中我得到的印象是你判断了这个):

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,如Criterion documentation所示.

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

C++相关问答推荐

为什么listen()(在调用accept()之前)足以让应用程序完成3次握手?

如何通过Zephyr(Devicetree)在PR Pico上设置UTE 1?

如何将FileFilter添加到FileDialog GTK 4

通过MQTT/蚊子发送大文件—限制在4MB

VS代码C/C++扩展intellisense无法检测环境特定函数'

使用额外的公共参数自定义printf

如何识别Linux中USB集线器(根)和连接到集线器(根设备)的设备(子设备)?

如何确保在C程序中将包含uft8字符的字符串正确写入MySQL?

如何在GDB中查看MUSL的源代码

MacOS下C++的无阻塞键盘阅读

这段代码用于在C中以相反的顺序打印数组,但它不起作用

pthread_create的用法

Fprintf正在写入多个 struct 成员,并且数据过剩

条件跳转或移动取决于未初始化值(S)/未初始化值由堆分配创建(Realloc)

用于计算位数和的递归C函数

GETS()在C++中重复它前面的行

struct 中的qsort,但排序后的 struct 很乱

程序打印一些随机空行

C/C++编译器可以在编译过程中通过按引用传递来优化按值传递吗?

从管道读取数据时丢失