C++ - 对结构使用 GTest 值参数化测试会导致 valgrind 错误

C++ - using GTest value-parameterized tests with structs causes valgrind errors

你能帮我了解一下 GTest 和结构打包是怎么回事吗?

问题似乎与结构在 GTest 的值参数化测试中用作值时如何打包有关。采用为每个值实例化结构的直接方法会导致与未初始化值相关的 valgrind 错误。

相关代码如下:

#include <gtest/gtest.h>

struct TestItem
{
    const char * aString;
    int anInt0;
    int anInt1;
    int anInt2;
};

class TestBase : public ::testing::Test, public ::testing::WithParamInterface<TestItem> {};

TEST_P(TestBase, TestAtoi)
{
    TestItem item = GetParam();
    std::cout << sizeof(TestItem) << std::endl;
    ASSERT_FALSE(0);   // actual test doesn't matter
}

INSTANTIATE_TEST_CASE_P(
        TestBaseInstantiation,
        TestBase,
        ::testing::Values(
                TestItem { "0", 0, 0, 0 }
));

编译和链接时(我用 cmake 构建了 GTest):

g++ gtest_valgrind.c -o gtest_valgrind -I gtest-1.7.0/include -L gtest-1.7.0/build -lgtest -lpthread -lgtest_main -std=c++11

然后执行:

valgrind --leak-check=full --show-leak-kinds=all --track-origins=yes ./gtest_valgrind

生成以下输出:

==17290== Memcheck, a memory error detector
==17290== Copyright (C) 2002-2013, and GNU GPL'd, by Julian Seward et al.
==17290== Using Valgrind-3.10.0.SVN and LibVEX; rerun with -h for copyright info
==17290== Command: ./gtest_valgrind
==17290== 
Running main() from gtest_main.cc
==17290== Use of uninitialised value of size 8
==17290==    at 0x55B89F1: _itoa_word (_itoa.c:180)
==17290==    by 0x55BC6F6: vfprintf (vfprintf.c:1660)
==17290==    by 0x55E1578: vsnprintf (vsnprintf.c:119)
==17290==    by 0x55C3531: snprintf (snprintf.c:33)
==17290==    by 0x41F107: testing::(anonymous namespace)::PrintByteSegmentInObjectTo(unsigned char const*, unsigned long, unsigned long, std::ostream*) (in /home/davida/git/gitlab/fcdm/lwm2m.git/gtest_valgrind)
==17290==    by 0x41F1AA: testing::(anonymous namespace)::PrintBytesInObjectToImpl(unsigned char const*, unsigned long, std::ostream*) (in /home/davida/git/gitlab/fcdm/lwm2m.git/gtest_valgrind)
==17290==    by 0x41F252: testing::internal2::PrintBytesInObjectTo(unsigned char const*, unsigned long, std::ostream*) (in /home/davida/git/gitlab/fcdm/lwm2m.git/gtest_valgrind)
==17290==    by 0x409B8E: testing::internal2::TypeWithoutFormatter<TestItem, (testing::internal2::TypeKind)2>::PrintValue(TestItem const&, std::ostream*) (in /home/davida/git/gitlab/fcdm/lwm2m.git/gtest_valgrind)
==17290==    by 0x409B63: std::basic_ostream<char, std::char_traits<char> >& testing::internal2::operator<< <char, std::char_traits<char>, TestItem>(std::basic_ostream<char, std::char_traits<char> >&, TestItem const&) (in /home/davida/git/gitlab/fcdm/lwm2m.git/gtest_valgrind)
==17290==    by 0x409B3E: void testing_internal::DefaultPrintNonContainerTo<TestItem>(TestItem const&, std::ostream*) (in /home/davida/git/gitlab/fcdm/lwm2m.git/gtest_valgrind)
==17290==    by 0x409B19: void testing::internal::DefaultPrintTo<TestItem>(char, testing::internal::bool_constant<false>, TestItem const&, std::ostream*) (in /home/davida/git/gitlab/fcdm/lwm2m.git/gtest_valgrind)
==17290==    by 0x409ADB: void testing::internal::PrintTo<TestItem>(TestItem const&, std::ostream*) (in /home/davida/git/gitlab/fcdm/lwm2m.git/gtest_valgrind)
==17290==  Uninitialised value was created by a stack allocation
==17290==    at 0x404AAE: gtest_TestBaseInstantiationTestBase_EvalGenerator_() (in /home/davida/git/gitlab/fcdm/lwm2m.git/gtest_valgrind)
==17290== 
==17290== Conditional jump or move depends on uninitialised value(s)
==17290==    at 0x55B89F8: _itoa_word (_itoa.c:180)
==17290==    by 0x55BC6F6: vfprintf (vfprintf.c:1660)
==17290==    by 0x55E1578: vsnprintf (vsnprintf.c:119)
==17290==    by 0x55C3531: snprintf (snprintf.c:33)
==17290==    by 0x41F107: testing::(anonymous namespace)::PrintByteSegmentInObjectTo(unsigned char const*, unsigned long, unsigned long, std::ostream*) (in /home/davida/git/gitlab/fcdm/lwm2m.git/gtest_valgrind)
==17290==    by 0x41F1AA: testing::(anonymous namespace)::PrintBytesInObjectToImpl(unsigned char const*, unsigned long, std::ostream*) (in /home/davida/git/gitlab/fcdm/lwm2m.git/gtest_valgrind)
==17290==    by 0x41F252: testing::internal2::PrintBytesInObjectTo(unsigned char const*, unsigned long, std::ostream*) (in /home/davida/git/gitlab/fcdm/lwm2m.git/gtest_valgrind)
==17290==    by 0x409B8E: testing::internal2::TypeWithoutFormatter<TestItem, (testing::internal2::TypeKind)2>::PrintValue(TestItem const&, std::ostream*) (in /home/davida/git/gitlab/fcdm/lwm2m.git/gtest_valgrind)
==17290==    by 0x409B63: std::basic_ostream<char, std::char_traits<char> >& testing::internal2::operator<< <char, std::char_traits<char>, TestItem>(std::basic_ostream<char, std::char_traits<char> >&, TestItem const&) (in /home/davida/git/gitlab/fcdm/lwm2m.git/gtest_valgrind)
==17290==    by 0x409B3E: void testing_internal::DefaultPrintNonContainerTo<TestItem>(TestItem const&, std::ostream*) (in /home/davida/git/gitlab/fcdm/lwm2m.git/gtest_valgrind)
==17290==    by 0x409B19: void testing::internal::DefaultPrintTo<TestItem>(char, testing::internal::bool_constant<false>, TestItem const&, std::ostream*) (in /home/davida/git/gitlab/fcdm/lwm2m.git/gtest_valgrind)
==17290==    by 0x409ADB: void testing::internal::PrintTo<TestItem>(TestItem const&, std::ostream*) (in /home/davida/git/gitlab/fcdm/lwm2m.git/gtest_valgrind)
==17290==  Uninitialised value was created by a stack allocation
==17290==    at 0x404AAE: gtest_TestBaseInstantiationTestBase_EvalGenerator_() (in /home/davida/git/gitlab/fcdm/lwm2m.git/gtest_valgrind)
==17290== 
==17290== Conditional jump or move depends on uninitialised value(s)
==17290==    at 0x55BC742: vfprintf (vfprintf.c:1660)
==17290==    by 0x55E1578: vsnprintf (vsnprintf.c:119)
==17290==    by 0x55C3531: snprintf (snprintf.c:33)
==17290==    by 0x41F107: testing::(anonymous namespace)::PrintByteSegmentInObjectTo(unsigned char const*, unsigned long, unsigned long, std::ostream*) (in /home/davida/git/gitlab/fcdm/lwm2m.git/gtest_valgrind)
==17290==    by 0x41F1AA: testing::(anonymous namespace)::PrintBytesInObjectToImpl(unsigned char const*, unsigned long, std::ostream*) (in /home/davida/git/gitlab/fcdm/lwm2m.git/gtest_valgrind)
==17290==    by 0x41F252: testing::internal2::PrintBytesInObjectTo(unsigned char const*, unsigned long, std::ostream*) (in /home/davida/git/gitlab/fcdm/lwm2m.git/gtest_valgrind)
==17290==    by 0x409B8E: testing::internal2::TypeWithoutFormatter<TestItem, (testing::internal2::TypeKind)2>::PrintValue(TestItem const&, std::ostream*) (in /home/davida/git/gitlab/fcdm/lwm2m.git/gtest_valgrind)
==17290==    by 0x409B63: std::basic_ostream<char, std::char_traits<char> >& testing::internal2::operator<< <char, std::char_traits<char>, TestItem>(std::basic_ostream<char, std::char_traits<char> >&, TestItem const&) (in /home/davida/git/gitlab/fcdm/lwm2m.git/gtest_valgrind)
==17290==    by 0x409B3E: void testing_internal::DefaultPrintNonContainerTo<TestItem>(TestItem const&, std::ostream*) (in /home/davida/git/gitlab/fcdm/lwm2m.git/gtest_valgrind)
==17290==    by 0x409B19: void testing::internal::DefaultPrintTo<TestItem>(char, testing::internal::bool_constant<false>, TestItem const&, std::ostream*) (in /home/davida/git/gitlab/fcdm/lwm2m.git/gtest_valgrind)
==17290==    by 0x409ADB: void testing::internal::PrintTo<TestItem>(TestItem const&, std::ostream*) (in /home/davida/git/gitlab/fcdm/lwm2m.git/gtest_valgrind)
==17290==    by 0x409AA6: testing::internal::UniversalPrinter<TestItem>::Print(TestItem const&, std::ostream*) (in /home/davida/git/gitlab/fcdm/lwm2m.git/gtest_valgrind)
==17290==  Uninitialised value was created by a stack allocation
==17290==    at 0x404AAE: gtest_TestBaseInstantiationTestBase_EvalGenerator_() (in /home/davida/git/gitlab/fcdm/lwm2m.git/gtest_valgrind)
==17290== 
==17290== Conditional jump or move depends on uninitialised value(s)
==17290==    at 0x55B9659: vfprintf (vfprintf.c:1660)
==17290==    by 0x55E1578: vsnprintf (vsnprintf.c:119)
==17290==    by 0x55C3531: snprintf (snprintf.c:33)
==17290==    by 0x41F107: testing::(anonymous namespace)::PrintByteSegmentInObjectTo(unsigned char const*, unsigned long, unsigned long, std::ostream*) (in /home/davida/git/gitlab/fcdm/lwm2m.git/gtest_valgrind)
==17290==    by 0x41F1AA: testing::(anonymous namespace)::PrintBytesInObjectToImpl(unsigned char const*, unsigned long, std::ostream*) (in /home/davida/git/gitlab/fcdm/lwm2m.git/gtest_valgrind)
==17290==    by 0x41F252: testing::internal2::PrintBytesInObjectTo(unsigned char const*, unsigned long, std::ostream*) (in /home/davida/git/gitlab/fcdm/lwm2m.git/gtest_valgrind)
==17290==    by 0x409B8E: testing::internal2::TypeWithoutFormatter<TestItem, (testing::internal2::TypeKind)2>::PrintValue(TestItem const&, std::ostream*) (in /home/davida/git/gitlab/fcdm/lwm2m.git/gtest_valgrind)
==17290==    by 0x409B63: std::basic_ostream<char, std::char_traits<char> >& testing::internal2::operator<< <char, std::char_traits<char>, TestItem>(std::basic_ostream<char, std::char_traits<char> >&, TestItem const&) (in /home/davida/git/gitlab/fcdm/lwm2m.git/gtest_valgrind)
==17290==    by 0x409B3E: void testing_internal::DefaultPrintNonContainerTo<TestItem>(TestItem const&, std::ostream*) (in /home/davida/git/gitlab/fcdm/lwm2m.git/gtest_valgrind)
==17290==    by 0x409B19: void testing::internal::DefaultPrintTo<TestItem>(char, testing::internal::bool_constant<false>, TestItem const&, std::ostream*) (in /home/davida/git/gitlab/fcdm/lwm2m.git/gtest_valgrind)
==17290==    by 0x409ADB: void testing::internal::PrintTo<TestItem>(TestItem const&, std::ostream*) (in /home/davida/git/gitlab/fcdm/lwm2m.git/gtest_valgrind)
==17290==    by 0x409AA6: testing::internal::UniversalPrinter<TestItem>::Print(TestItem const&, std::ostream*) (in /home/davida/git/gitlab/fcdm/lwm2m.git/gtest_valgrind)
==17290==  Uninitialised value was created by a stack allocation
==17290==    at 0x404AAE: gtest_TestBaseInstantiationTestBase_EvalGenerator_() (in /home/davida/git/gitlab/fcdm/lwm2m.git/gtest_valgrind)
==17290== 
==17290== Conditional jump or move depends on uninitialised value(s)
==17290==    at 0x55B96DC: vfprintf (vfprintf.c:1660)
==17290==    by 0x55E1578: vsnprintf (vsnprintf.c:119)
==17290==    by 0x55C3531: snprintf (snprintf.c:33)
==17290==    by 0x41F107: testing::(anonymous namespace)::PrintByteSegmentInObjectTo(unsigned char const*, unsigned long, unsigned long, std::ostream*) (in /home/davida/git/gitlab/fcdm/lwm2m.git/gtest_valgrind)
==17290==    by 0x41F1AA: testing::(anonymous namespace)::PrintBytesInObjectToImpl(unsigned char const*, unsigned long, std::ostream*) (in /home/davida/git/gitlab/fcdm/lwm2m.git/gtest_valgrind)
==17290==    by 0x41F252: testing::internal2::PrintBytesInObjectTo(unsigned char const*, unsigned long, std::ostream*) (in /home/davida/git/gitlab/fcdm/lwm2m.git/gtest_valgrind)
==17290==    by 0x409B8E: testing::internal2::TypeWithoutFormatter<TestItem, (testing::internal2::TypeKind)2>::PrintValue(TestItem const&, std::ostream*) (in /home/davida/git/gitlab/fcdm/lwm2m.git/gtest_valgrind)
==17290==    by 0x409B63: std::basic_ostream<char, std::char_traits<char> >& testing::internal2::operator<< <char, std::char_traits<char>, TestItem>(std::basic_ostream<char, std::char_traits<char> >&, TestItem const&) (in /home/davida/git/gitlab/fcdm/lwm2m.git/gtest_valgrind)
==17290==    by 0x409B3E: void testing_internal::DefaultPrintNonContainerTo<TestItem>(TestItem const&, std::ostream*) (in /home/davida/git/gitlab/fcdm/lwm2m.git/gtest_valgrind)
==17290==    by 0x409B19: void testing::internal::DefaultPrintTo<TestItem>(char, testing::internal::bool_constant<false>, TestItem const&, std::ostream*) (in /home/davida/git/gitlab/fcdm/lwm2m.git/gtest_valgrind)
==17290==    by 0x409ADB: void testing::internal::PrintTo<TestItem>(TestItem const&, std::ostream*) (in /home/davida/git/gitlab/fcdm/lwm2m.git/gtest_valgrind)
==17290==    by 0x409AA6: testing::internal::UniversalPrinter<TestItem>::Print(TestItem const&, std::ostream*) (in /home/davida/git/gitlab/fcdm/lwm2m.git/gtest_valgrind)
==17290==  Uninitialised value was created by a stack allocation
==17290==    at 0x404AAE: gtest_TestBaseInstantiationTestBase_EvalGenerator_() (in /home/davida/git/gitlab/fcdm/lwm2m.git/gtest_valgrind)
==17290== 
[==========] Running 1 test from 1 test case.
[----------] Global test environment set-up.
[----------] 1 test from TestBaseInstantiation/TestBase
[ RUN      ] TestBaseInstantiation/TestBase.TestAtoi/0
24
[       OK ] TestBaseInstantiation/TestBase.TestAtoi/0 (76 ms)
[----------] 1 test from TestBaseInstantiation/TestBase (126 ms total)

[----------] Global test environment tear-down
[==========] 1 test from 1 test case ran. (246 ms total)
[  PASSED  ] 1 test.
==17290== 
==17290== HEAP SUMMARY:
==17290==     in use at exit: 0 bytes in 0 blocks
==17290==   total heap usage: 239 allocs, 239 frees, 46,622 bytes allocated
==17290== 
==17290== All heap blocks were freed -- no leaks are possible
==17290== 
==17290== For counts of detected and suppressed errors, rerun with: -v
==17290== ERROR SUMMARY: 20 errors from 5 contexts (suppressed: 0 from 0)

这是很多输出,但基本上我认为这表明由于 INSTANTIATE_TEST_CASE_P 中的 TestItem { "0", 0, 0, 0 } 行而实例化的结构存在一些异常。

另请注意,TestItem 的大小输出为 24。在 64 位系统上,我不太确定如何协调这一点。 char * 为 8 个字节,int 成员为 4 * 3 = 12 个字节。如果它们与 8 字节边界对齐,则大小应为 24 + 4 = 28 字节(如果包括打包,则为 32)。如果它们对齐到 4 字节边界,那么这应该是 20。这里有一些关于结构打包的东西我不明白。

可以通过不同方式打包结构来消除 valgrind 警告。例如,所有这三个修改都会产生干净的 valgrind 运行:

使用#pragma pack:

#pragma pack(1)
struct TestItem
{
    const char * aString;
    int anInt0;
    int anInt1;
    int anInt2;
};

输出 20.

使用 GNU g++ 打包属性:

struct TestItem
{
    const char * aString;
    int anInt0;
    int anInt1;
    int anInt2;
} __attribute__((packed));

也输出 20

最后,向结构中添加一个虚拟值:

struct TestItem
{
    const char * aString;
    int anInt0;
    int anInt1;
    int anInt2;
    int anInt3;   // add an extra member
};

输出 24.

我有相当多的项目代码使用这种精确技术进行值参数化测试,在所有情况下,如果未使用其中一种回避策略,valgrind 都会报错。​​

我想知道我为这个测试创建值的方法是否存在根本性错误,或者这是 gtest 对实例化测试用例所做的事情的结果?或者,这不太可能是 gtest 错误?

TestItem 结构的 sizeof 输出是编译器 structure alignment and trailing padding.

的结果

来自上面的link:

[...] It is the first address following the structure data that has the same alignment as the structure.

The general rule of trailing structure padding is this: the compiler will behave as though the structure has trailing padding out to its stride address. This rule controls what sizeof() will return.

Consider this example on a 64-bit x86 or ARM machine:

struct foo3 {
    char *p;     /* 8 bytes */
    char c;      /* 1 byte */
};
struct foo3 singleton;
struct foo3 quad[4];

You might think that sizeof(struct foo3) should be 9, but it’s actually 16. The stride address is that of (&p)[2]. Thus, in the quad array, each member has 7 bytes of trailing padding, because the first member of each following struct wants to be self-aligned on an 8-byte boundary. The memory layout is as though the structure had been declared like this:

struct foo3 {
     char *p;     /* 8 bytes */
     char c;      /* 1 byte */
     char pad[7];
};

这就解释了为什么 sizeof(TestItem) 会得到 24,因为尾部填充会将结构对齐到 sizeof (const char*) 的倍数,即 8。

此尾随填充字节未初始化,这就是 valgrind 报告的内容。 Gtest 是 运行 一些代码,以便在测试失败时打印 TestItem 参数的实际值。如果您通过了测试并且 valgrind 没有显示错误,则可以确认这一点。

当您强制编译器使用特定对齐方式或向结构添加新成员以使结构不需要任何尾随填充时,valgrind 不会在 TestItem 中找到任何未初始化的字节实例。

Gtest 通常在打印值时调用 operator<<,如果对象中的原始字节数组不可用,则回退到打印对象中的原始字节数组。这可能就是访问未初始化尾随填充字节的原因。因此,您也可以通过为 TestItem.

定义 operator<< 来消除 valgrind 错误