当在 windows 上使用 mingw 构建应用程序时,如何防止 Qt 提供的 Zlib 的 deflateend(&compr_str) 函数发生段错误?

How to prevent the Qt-provided Zlib's deflateend(&compr_str) function from segfaulting when the application is built with mingw on windows?

简介

我在我的代码中使用以下代码进行 Gzip 压缩,并在非常特定的条件下遇到代码段错误。我自己尝试解决后基本上完全迷路了。

代码是使用以下配置构建的:

  1. Linux x86-64 Ubuntu 18.04,Qt 5.12.7 从源代码构建,使用系统 zlib。
  2. Linux arm x86-64 Yocto,Qt 5.12.7 从源代码构建,使用系统 zlib。
  3. Windows MSVC2017 64 位,来自官方安装程序的 Qt 5.12.7,使用自建的 zlib,因为 Qt 没有为 MSVC
  4. Windows MinGW-x86-64,来自官方安装程序的 Qt 5.12.7,使用附带的 zlib

配置[1-3]中,压缩代码测试调用成功。 在配置 [4] 中,当从 Qt 创建者 IDE 中启动时,对压缩代码的测试调用成功。 在配置 [4] 中,当从 Power shell 命令行构建时,对压缩代码的测试调用会在调用 deflateEnd(&cmpr_stream); 时立即导致段错误,如下所示: 1. 在 power shell 中设置相同的环境变量,就像 Qt Creator ide 用作构建 environment.ACLineStatus 2. 为 MinGw-x86-64 配置调用与在 Qt Creator IDE 中配置的完全相同的命令。

即使调用 inside qt creator 测试时,我也可以在该特定行重现类似的崩溃,当我尝试使用缓冲区大小时MSVC 和 MinGw 均为 32768。 MSVC 以 16384 崩溃,而 MinGw 以 32768 崩溃,因此我的缓冲区大小代码中的 #ifdef 下面。

问题

我做了两个 mingw 构建(qt creator 构建调用测试表单 inside IDE,以及手动 cli 构建从命令行调用测试)来测试在只安装 QT 的干净虚拟机上的行为,在开始任何构建之前拥有绝对 ide 干净的环境。

出现段错误的代码

#define BASE2_ZLIB_WINDOWSIZE 15
#define GZIP_ZLIB_WINDOWSIZE (16 + BASE2_ZLIB_WINDOWSIZE)
#define MOD_GZIP_ZLIB_CFACTOR 9
#define MOD_GZIP_ZLIB_BSIZE 8096

#ifdef _MSC_VER
#define COMPR_BUFFER_SIZE 32768
#else
#define COMPR_BUFFER_SIZE 16384
#endif

QByteArray gzipCompress(QByteArray data, int compressionlevel)
{
    char buffer[COMPR_BUFFER_SIZE];

    z_stream cmpr_stream;
    cmpr_stream.next_in = reinterpret_cast<unsigned char *>(data.data());
    cmpr_stream.avail_in = static_cast<uInt>(data.size());
    cmpr_stream.total_in = 0;
    cmpr_stream.total_out = 0;

    cmpr_stream.zalloc = Z_NULL;
    cmpr_stream.zalloc = Z_NULL;

    QByteArray compressed;

    // the actual compression work.
    if (deflateInit2(&cmpr_stream, compressionlevel, Z_DEFLATED, GZIP_ZLIB_WINDOWSIZE, MOD_GZIP_ZLIB_CFACTOR,
                        Z_DEFAULT_STRATEGY) != Z_OK)
    {
        return compressed;
    }
    // retrieve the compressed bytes blockwise
    int ret;
    do
    {
        cmpr_stream.next_out = reinterpret_cast<uint8_t *>(buffer);
        cmpr_stream.avail_out = COMPR_BUFFER_SIZE;
        ret = deflate(&cmpr_stream, Z_FINISH);

        if (static_cast<unsigned long>(compressed.size()) < cmpr_stream.total_out)
        {
            // append the block to the output string
            compressed.append(buffer, static_cast<int>(cmpr_stream.total_out) - compressed.size());
        }
    } while (ret == Z_OK);

    deflateEnd(&cmpr_stream);

    if (ret != Z_STREAM_END)
    {
        return QByteArray();
    }
    return compressed;
}

测试,调用代码

TEST(Compression, gzipSuccessfull)
{
    QString string_data(
        "Complex Test 256/n Data complete Lorem ipsum dolor sit amet, consetetur sadipscing elitr "
        "Lorem ipsum dolor sit amet, consetetur sadipscing elitr");
    QByteArray raw = string_data.toLatin1();
    qDebug() << "Orig size: " << raw.size();
    // Crashes in the next line
    QByteArray compressed = gzipCompress(raw);
    qDebug() << "Compressed size: " << compressed.size();
    QByteArray uncompressed = gzipDecompress(compressed);
    qDebug() << "Uncompressed size: " << uncompressed.size();
    QString restored = QString::fromLatin1(uncompressed);
    ASSERT_TRUE(string_data == restored);
}

不确定这是否会导致您的段错误,但您需要初始化 zfree。看起来你的代码中有一个 copy/paste 错误,因为你初始化了 zalloc 两次。应该是:

cmpr_stream.zalloc = Z_NULL;
cmpr_stream.zfree = Z_NULL;

并完成:

cmpr_stream.opaque = Z_NULL;

您对追加多少的计算看起来是正确的,但这是一种迂回的方法,如果 long 是 32 位,则可能会出现可移植性问题。在那种情况下,total_out 将不代表超过 4 GB 时的总量。相反,您应该直接使用 deflate():

的结果
    if (cmpr_stream.avail_out < COMPR_BUFFER_SIZE)
    {
        // append the block to the output string
        compressed.append(buffer, COMPR_BUFFER_SIZE - cmpr_stream.avail_out);
    }

由于 append 的参数是 int 而不是 unsigned,您应该确保 COMPR_BUFFER_SIZE 适合 int