Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

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

Can you help me understand what is going on here with GTest and struct packing?

The problem seems to be related to how a struct is packed when used as a value in a value-parameterised test in GTest. Taking the straightforward approach of instantiating a struct for each value results in valgrind errors relating to uninitialised values.

Here's the code concerned:

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

When this is compiled and linked (I built GTest with cmake):

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

And then executed with:

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

The following output is produced:

==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)

That's a lot of output, but essentially I think it's indicating that there's something unusual about the struct that is instantiated due to the TestItem { "0", 0, 0, 0 } line in INSTANTIATE_TEST_CASE_P.

Note also that the size of TestItem is output as 24. On a 64-bit system, I'm not quite sure how to reconcile this. 8 bytes for the char *, and 4 * 3 = 12 bytes for the int members. If they are aligned to 8 byte boundaries, this ought to be size 24 + 4 = 28 bytes (or 32 if including packing). If they are aligned to 4 byte boundaries, then this ought to be 20. There's something I don't understand here about struct packing.

The valgrind warning can be eliminated by packing the struct differently. For example, all three of these modifications result in a clean valgrind run:

Using #pragma pack:

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

That outputs 20.

Using GNU g++ packing attribute:

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

That outputs 20 too.

Lastly, adding a dummy value to the struct:

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

That outputs 24.

I have a fair amount of project code that uses this exact technique for value-parameterized tests, and in all cases valgrind will complain if one of the avoidance strategies is not used.

I'd like to know if there's something fundamentally wrong with my approach of creating values for this test, or is this a consequence of something gtest does with instantiating test cases? Or, most unlikely, is this a gtest bug?

like image 750
davidA Avatar asked Nov 17 '15 00:11

davidA


1 Answers

The output of sizeof for the TestItem structure is a consequence of compiler structure alignment and trailing padding.

From the link above:

[...] 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];
};

That explains why you are getting 24 for sizeof(TestItem) since the trailing padding will align the structure to a multiple of sizeof (const char*), which is 8.

This trailing padding bytes are uninitialized and that is what valgrind reports. Gtest is running some code in order to print the actual value of the TestItem parameter when a test fails. This could be confirmed if you make your test pass and valgrind doesn't show the error.

When you force the compiler to use a specific alignment or add a new member to the structure so that the structure doesn't need any trailing padding, valgrind doesn't find any uninitialized bytes in the TestItem instances.

Gtest usually calls operator<< when printing values and falls back to print the raw array of bytes in the object if it's not available. That's probably why the uninitialized trailing padding bytes are getting accessed. So you may also get rid of the valgrind errors by defining operator<< for TestItem.

like image 122
Antonio Pérez Avatar answered Oct 08 '22 15:10

Antonio Pérez