0

EDIT: Added full MCV example project.

I have a strange problem where the same code and same input produce different output values.

The purpose of the code is to test a function that takes a value packed into 4 bytes, and unpack it into a single 32bit value. The expected value of value1, value2 and value3 in test_unpack() is 2018915346 (i.e. 0x78563412 because of little-endian unpacking). I got this method of unpacking from another answer. Below is an MCV example that you can easily build and see the problem for yourself. Note that if you comment out the body of test1() test_unpack() magically passes with the correct value.

test_canserialcomm.cpp

#include "test_canserialcomm.h"

#include <QtTest/QtTest>
#include <QByteArray>

long unpack() noexcept
{
    quint8 a_bytes[] = {0x12, 0x34, 0x56, 0x78};
    QByteArray a = QByteArray(reinterpret_cast<char*>(a_bytes), 4);
    long value1 = *((long*)a.data());
    qDebug() <<  value1; // outputs "32651099317351442" (incorrect value)

    quint8 b_bytes[] = {0x12, 0x34, 0x56, 0x78};
    QByteArray b = QByteArray(reinterpret_cast<char*>(b_bytes), 4);
    long value2 = *((long*)b.data());
    qDebug() << value2; // outputs "2018915346" (correct value)

    quint8 c_bytes[] = {0x12, 0x34, 0x56, 0x78};
    QByteArray c = QByteArray(reinterpret_cast<char*>(c_bytes), 4);
    long value3 = *((long*)c.data());
    qDebug() << value3; // outputs "2018915346" (correct value)

    return value1;
}

void TestCanSerialComm::test1()
{
    QCOMPARE("aoeu", "aoeu"); // If you comment this line, the next test will pass, as expected.
}

void TestCanSerialComm::test_unpack()
{
    long expected {0x78563412};
    QCOMPARE(unpack(), expected);
}

test_canserialcomm.h

#ifndef TEST_CANSERIALCOMM_H
#define TEST_CANSERIALCOMM_H
#include <QtTest>

class TestCanSerialComm: public QObject
{
    Q_OBJECT
private slots:
    void test1();
    void test_unpack();
};
#endif // TEST_CANSERIALCOMM_H

test_main.cpp

#include <QtTest>
#include "test_canserialcomm.h"
#include <QCoreApplication>

int main(int argc, char** argv) {
    QCoreApplication app(argc, argv);
    TestCanSerialComm testCanSerialComm;
    // Execute test-runner.
    return QTest::qExec(&testCanSerialComm, argc, argv); }

tmp.pro

QT += core \
    testlib
QT -= gui
CONFIG += c++11

TARGET = tmp
CONFIG += console
CONFIG -= app_bundle
TEMPLATE = app
TARGET = UnitTests

HEADERS += test_canserialcomm.h
SOURCES += test_canserialcomm.cpp \
    test_main.cpp

The output of value1 in test_unpack() is wrong, despite the same code and same inputs. Strangely, if I remove the qDebug() calls and set a breakpoint, the debugger expression evaluator now shows that value2 has the wrong value.

Any idea why this is happening? Or even how to troubleshoot this further?

Additional Notes: If I add a line qDebug() << "garbage"; at the top of my function, all 3 values produced are correct.

Community
  • 1
  • 1
DBedrenko
  • 4,871
  • 4
  • 38
  • 73
  • 1
    look at the in-memory representations of the values, might give a clue (I don't have qt) - another thought: why not use a simple union to do this? – slashmais Apr 21 '16 at 11:03
  • @slashmais Thanks for the tip. By "in-memory representations" do you mean the values for the vars I see in the debugger when I set a breakpoint? Those values are the same as `qDebug()` outputs. I'm not sure how I can use a union in this case. – DBedrenko Apr 21 '16 at 11:31
  • I run your code and I get the same result for all three values. – Paraboloid87 Apr 21 '16 at 11:47
  • Is there something else going on in your code? Can you post a MCV? – Paraboloid87 Apr 21 '16 at 11:50
  • @Paraboloid87 Thanks to your comments I managed to isolate the problem even further, and have add an MCV example project that reproduces the problem (I have tested it). – DBedrenko Apr 21 '16 at 13:19
  • 1
    What size is `long` on your system? – ecatmur Apr 21 '16 at 13:42
  • So, what happens (ignoring alignment) when you attempt to alias a *4*-byte array as an 8-byte integral? – ecatmur Apr 21 '16 at 13:47
  • @ecatmur By God! What happens is that when I cast to an 8-byte, it reads 4 bytes past what the `QByteArray` actually contains. The lesson is to use fixed-width types, and `qint32` instead of `long` worked for me. If you post this answer I will accept it :) Thanks for your help – DBedrenko Apr 21 '16 at 14:00
  • @ecatmur Is there anything I should prepare or be aware of in terms of data alignment when doing packing/unpacking? – DBedrenko Apr 21 '16 at 14:10
  • 1
    by in-memory I mean you look at the memory-addresses containing the values / vars - may have helped you spot what ecatmur suggested. – slashmais Apr 21 '16 at 14:16

1 Answers1

3

You're compiling and running this program on a system where long is 8 bytes, but your QByteArray has only 4 bytes. That means that when you alias the array as a long (using *((long*)a.data())) you're reading 4 bytes past the end of the array, into uninitialized heap storage.

The fix is to use a type that is guaranteed to be 4 bytes in size, e.g. std::int32_t.

As an aside, using *((long*)[...]) to alias memory is not guaranteed to work, primarily because of alignment issues but also (in the general case) because aliasing is only supported for types equivalent to char or a signed or unsigned variant. The safer technique is to use memcpy:

std::uint32_t value1;
assert(a.size() == sizeof(value1));
memcpy(&value1, a.data(), a.size());
ecatmur
  • 152,476
  • 27
  • 293
  • 366