编译器省略外部循环

Compiler omitting outer loop

本文关键字:循环 外部 编译器省      更新时间:2023-10-16

我已经在一个C++项目中实现了(阅读:从wiki复制和粘贴(XXTEA密码。为了清楚起见,我将加密和解密分开在单独的函数中:(注意:这不是密码学问题!请不要评论所选密码(

#define DELTA 0x9e3779b9
#define MX (((z>>5^y<<2) + (y>>3^z<<4)) ^ ((sum^y) + (key[(p&3)^e] ^ z)))
static void btea_enc( unsigned int *v, unsigned n, const unsigned int* key ) {
    unsigned int y, z, sum;
    unsigned p, rounds, e;
    rounds = 16 + 52/n;
    sum = 0;
    z = v[n-1];
    do {
        sum += DELTA;
        e = (sum >> 2) & 3;
        for (p=0; p<n-1; p++) {
            y = v[p+1]; 
            z = v[p] += MX;
        }
        y = v[0];
        z = v[n-1] += MX;
    } while (--rounds);
}
static void btea_dec( unsigned int *v, unsigned n, const unsigned int* key ) {
    unsigned int y, z, sum;
    unsigned p, rounds, e;
    rounds = 16 + 52/n;
    sum = rounds*DELTA;
    y = v[0];
    do {
        e = (sum >> 2) & 3;
        for (p=n-1; p>0; p--) {
            z = v[p-1];
            y = v[p] -= MX;
        }
        z = v[n-1];
        y = v[0] -= MX;
    } while ((sum -= DELTA) != 0);
}
#undef MX
#undef DELTA

当此代码在调试中编译时,它可以完美运行。但是,当我使用 Visual Studio 2013 (v120( 进行(默认(优化编译此代码时,btea_dec会丢失其外部循环(导致解密产生垃圾(。

用于加密和解密的反汇编列表。请注意解密过程中缺少的外部循环!(如果你想要代码作为文本,我很乐意上传,这只是一堵文字墙(

查看实际代码,结束条件是一个溢出的无符号 int 'sum':

while ((sum -= DELTA) != 0)我不明白编译器做了什么让它认为它可以摆脱这个循环(afaik 溢出只对整数未定义,无符号溢出完全没问题(。

问题:为什么编译器要"优化"外部循环?我该如何解决它?

MCVE:(在 include 和 main 之间粘贴包含 btea_enc 和 btea_dec 的上一个代码块(

#define _CRT_RAND_S
#include <cstdlib>
int main(int argc, char* argv[])
{
    // Random key
    unsigned int key[4];
    rand_s(&key[0]);
    rand_s(&key[1]);
    rand_s(&key[2]);
    rand_s(&key[3]);
    // Buffer we'll be encrypting
    unsigned int utext[4];
    memcpy(utext, "SecretPlaintext", 16);
    // Encrypt
    btea_enc(utext, 4, key);
    // Decrypt
    btea_dec(utext, 4, key);
    // Should still be equal!
    bool s = !strcmp((char*)utext, "SecretPlaintext");
    // Print message
    printf("Compared: %sn", s ? "equal" : "falsly");
    return s?0:1;
}

/GL,编译器知道n == 4,因此rounds == 29。 它肯定是预先计算sum的初始值,这也是rounds*DELTA

接下来,它可能会尝试计算循环迭代次数并展开外循环。 如果它做错了(就像我在另一个答案中所做的那样(,它可能正在做uint32_t(rounds * DELTA) / DELTA,这是一个。 添加第一次迭代作为do-while,这就是外循环的去向。

Gnasher 的循环控制代码对于编译器来说要容易得多,正好有 rounds (29( 次迭代,它可能会也可能不会决定展开,但几乎没有空间来弄乱迭代次数。

为什么会发生这种情况超出了我的范围。您可以尝试更换

} while ((sum -= DELTA) != 0);

    sum -= DELTA;
} while ((--rounds) != 0);

第 1 步:宏是非常非常糟糕的编程风格。 用参数重写它,比如

#define MX(key,sum,p,e,y,z) (((z>>5^y<<2) + (y>>3^z<<4)) ^ ((sum^y) + (key[(p&3)^e] ^ z)))

这样阅读您的代码的人实际上可以看到表达式中出现了哪些变量。

更好的是,使用内联函数。


编译器优化嵌套循环是正确的,因为可见的副作用是可预测的。

唯一可见的副作用是v[p] -= MX;,这种情况发生次数可预测。 因此,编译器可以通过v[p] -= loopcount * MX;替换嵌套循环

ezy 被重复写入但从不读取,因此编译器可以完全消除它们及其计算。

请注意,对无能操作的这种优化可能会让您看到您认为已仔细消除的定时攻击。

整个函数体变成

int p=n;
int subtrahend = rounds * DELTA / DELTA * MX;
do {
   v[--p] -= subtrahend;
} while (p);