等等,回来,我保证这不是关于未初始化的指针!
我使用 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。
好的,有了测试用例后,这看起来很容易。
首先,我正在 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);