使用和不使用 -std=c99 的代码会产生不同的结果(UMAC AE 实现)

Code with and without -std=c99 produces different results (UMAC AE implementation)

长话短说 - 我使用 Ted Krovetz 的实现来计算 UMACUMAC AE 加密 (http://www.fastcrypto.org/)。

当我用 -std=c99 编译我的代码(and/or umac.c 中的测试)时,计算的 UMAC 与预期完全不同(并且是错误的)。当我删除这个选项时,一切都很顺利。

知道是什么原因造成的吗?我可以做些什么来检查发生了什么以及是什么产生了不同的结果?


$ gcc --version
gcc (Ubuntu 4.8.2-19ubuntu1) 4.8.2

$ uname -a
xxx 3.13.0-43-generic #72-Ubuntu SMP .. x86_64 x86_64 x86_64 GNU/Linux

我不使用任何其他选项 - 只是使用和不使用 -std=c99


多说几句:

我会尝试联系 Ted Krovetz 并向他询问这个问题(这可能是一些错误或其他问题),但这不是重点。这个问题有点笼统,这个具体问题可以看作是一个例子。

我运行valgrind-没什么特别。再次添加 -Wall-Wextra - nothing。听起来像 UB,但 valgrind 没有抱怨任何事情。

情况很有趣,花了我 很多 天和头疼来理解,问题不在我的代码中(我使用这个实现来实现一个复杂的协议), 但在算法中,尤其是在这个选项中。所以决定征求意见

Can code that is valid in both C and C++ produce different behavior when compiled in each language? 根本没有关系,因为我们在这里谈论的是同一种语言。
Massive fprintf speed difference without "-std=c99" 接近,但还不够..


编辑

这是我的测试结果和我所做的(sources/headers 只是下载,我没有做任何更改):

$ ll
total 176K
-rw-r----- 1 kk kk  63K Jan 20 11:00 rijndael-alg-fst.c
-rw-r----- 1 kk kk 2.0K Jan 20 11:00 rijndael-alg-fst.h
-rw-r----- 1 kk kk 3.4K Jan 20 11:00 umac_ae.h
-rw-r----- 1 kk kk  76K Jan 20 11:00 umac.c
-rw-r----- 1 kk kk 4.2K Jan 20 11:00 umac.h

$ gcc -c *.c

$ gcc *.o

$ ./a.out 
AES Test :::
Digest is       : 3AD78E726C1EC02B7EBFE92B23D9EC34
Digest should be: 3AD78E726C1EC02B7EBFE92B23D9EC34

UMAC Test :::
Msg           Should be        Is
---           ---------        --
'a' *     0 : 4D61E4F5AAB959C8 4D61E4F5AAB959C8
'a' *     3 : 67C1700CA30B532D 67C1700CA30B532D
'a' *  1024 : 05CB9405EC38D9F0 05CB9405EC38D9F0
'a' * 32768 : 048C543CB72443A4 048C543CB72443A4

Verifying consistancy of single- and multiple-call interfaces.
Done.

Authenticating       44 byte messages:  6.45 cpb.
Authenticating       64 byte messages:  4.18 cpb.
Authenticating      256 byte messages:  1.63 cpb.
Authenticating      512 byte messages:  1.20 cpb.
Authenticating      552 byte messages:  1.22 cpb.
Authenticating     1024 byte messages:  1.00 cpb.
Authenticating     1500 byte messages:  1.04 cpb.
Authenticating     8192 byte messages:  0.90 cpb.
Authenticating   262144 byte messages:  0.89 cpb.

UMAC-AE Tests :::
0 bytes ('abc' * 0):
Encrypt/decrypt match, tags match
Should be: 0000000000000000
Is       : E8D1DAC3EA21E56D
3 bytes ('abc' * 1):
Encrypt/decrypt match, tags match
Should be: 0000000000000000
Is       : 6BEDBA31E074E2A4
48 bytes ('abc' * 16):
Encrypt/decrypt match, tags match
Should be: 0000000000000000
Is       : A3F6069B913969DA
300 bytes ('abc' * 100):
Encrypt/decrypt match, tags match
Should be: 0000000000000000
Is       : C5B7F3822179FC36
3000000 bytes ('abc' * 1000000):
Encrypt/decrypt match, tags match
Should be: 0000000000000000
Is       : EE7F50FDDA60AA04
  16 bytes, 38.12 cpb
  32 bytes, 25.04 cpb
  64 bytes, 19.39 cpb
 128 bytes, 16.41 cpb
 256 bytes, 14.79 cpb
 512 bytes, 13.96 cpb
1024 bytes, 13.79 cpb
2048 bytes, 13.46 cpb
4096 bytes, 13.47 cpb










$ ll
total 176K
-rw-r----- 1 kk kk  63K Jan 20 11:00 rijndael-alg-fst.c
-rw-r----- 1 kk kk 2.0K Jan 20 11:00 rijndael-alg-fst.h
-rw-r----- 1 kk kk 3.4K Jan 20 11:00 umac_ae.h
-rw-r----- 1 kk kk  76K Jan 20 11:00 umac.c
-rw-r----- 1 kk kk 4.2K Jan 20 11:00 umac.h

$ gcc -std=c99 -c *.c 

$ gcc -std=c99 *.o

$ ./a.out 
AES Test :::
Digest is       : 3AD78E726C1EC02B7EBFE92B23D9EC34
Digest should be: 3AD78E726C1EC02B7EBFE92B23D9EC34

UMAC Test :::
Msg           Should be        Is
---           ---------        --
'a' *     0 : 4D61E4F5AAB959C8 9492DE86794C9F2B
'a' *     3 : 67C1700CA30B532D CF9505F52928360E
'a' *  1024 : 05CB9405EC38D9F0 9C48C0D4EFAFAA37
'a' * 32768 : 048C543CB72443A4 7F63C29BB54BB141

Verifying consistancy of single- and multiple-call interfaces.
Done.

Authenticating       44 byte messages:  7.91 cpb.
Authenticating       64 byte messages:  5.20 cpb.
Authenticating      256 byte messages:  3.03 cpb.
Authenticating      512 byte messages:  2.60 cpb.
Authenticating      552 byte messages:  2.71 cpb.
Authenticating     1024 byte messages:  2.41 cpb.
Authenticating     1500 byte messages:  2.43 cpb.
Authenticating     8192 byte messages:  2.27 cpb.
Authenticating   262144 byte messages:  2.23 cpb.

UMAC-AE Tests :::
0 bytes ('abc' * 0):
Encrypt/decrypt match, tags match
Should be: 0000000000000000
Is       : 899C50FD244BBA83
3 bytes ('abc' * 1):
Encrypt/decrypt match, tags match
Should be: 0000000000000000
Is       : 892D14F581A3A4DD
48 bytes ('abc' * 16):
Encrypt/decrypt match, tags match
Should be: 0000000000000000
Is       : 621AB4A63383F3C5
300 bytes ('abc' * 100):
Encrypt/decrypt match, tags match
Should be: 0000000000000000
Is       : 324BEF6489F57787
3000000 bytes ('abc' * 1000000):
Encrypt/decrypt match, tags match
Should be: 0000000000000000
Is       : 1A25FE3714C9345A
  16 bytes, 40.80 cpb
  32 bytes, 25.87 cpb
  64 bytes, 20.50 cpb
 128 bytes, 17.72 cpb
 256 bytes, 15.93 cpb
 512 bytes, 15.33 cpb
1024 bytes, 14.88 cpb
2048 bytes, 14.71 cpb
4096 bytes, 14.48 cpb

我刚在另一台机器上测试过,和我的一样。

Sounds like UB,

不必如此。有几个已知差异会导致同一程序被解释为 C99 和 C90。

but valgrind doesn't complain for anything.

Valgrind 甚至没有对任何一个标准中的所有未定义行为发出警告。

首先想到的区别(但好的编译器会发出警告)是整数常量的类型 3000000000。对于 32 位 int 和 64 位 long,C90 编译器类型 3000000000 作为 unsigned long。在 C99 中,unsigned long 不在不带后缀的整数常量可以具有的类型列表中,因此 3000000000 被键入为 long long(有符号)。

不用看,加密代码很可能有很多大整数常量,所以这是一种可能性。

当然,在解释为 C90 或 C99 的代码中可能存在未定义的行为,然后编译器在 C90 和 C99 模式下产生不同的结果是情有可原的。我只想说没有必要。

好吧,我明白了。仍然不确定如何完美地修复它 fine/portable,但我会继续挖掘。

长话短说 - 它似乎是特定于平台的,这就是为什么你们中的大多数人没有这个问题。
问题在于确定字节顺序。


详情:

比较汇编输出后,存在一些显着差异,这(几乎自动地)排除了一些大常量解释问题和类似这样的小问题。

然后我尝试了更高级别 - 预处理器输出。

最后,一切都导致了umac.c中的这段代码:

/* Message "words" are read from memory in an endian-specific manner.     */
/* For this implementation to behave correctly, __LITTLE_ENDIAN__ must    */
/* be set true if the host computer is little-endian.                     */

#ifndef __LITTLE_ENDIAN__
#if __i386__ || __alpha__ || _M_IX86 || __LITTLE_ENDIAN
#define __LITTLE_ENDIAN__ 1
#else
#define __LITTLE_ENDIAN__ 0
#endif
#endif

在我的平台上,__i386____alpha___M_IX86 没有定义。关键在__LITTLE_ENDIAN.

编译时:

  • -std=c99__LITTLE_ENDIAN 定义 => #define __LITTLE_ENDIAN__ 0
  • 没有 -std=c99__LITTLE_ENDIAN 定义 => #define __LITTLE_ENDIAN__ 1

硬编码 #define __LITTLE_ENDIAN__ 1,无论是否使用 -std=c99,一切都开始完美运行。


结论:__LITTLE_ENDIAN是gcc特有的宏,这里用来判断字节顺序;看来,-std=c99 会影响此宏(如果使用该选项则未定义),从而导致不同(错误)的结果。


编辑
我当前的 ("temporary") 解决方案是更新有问题的预处理器 if 语句。我知道这远非解决此问题的最佳方法,但检测字节顺序似乎并不那么容易,而且远非微不足道。

运行时检查似乎更可靠,但这会导致代码发生更多变化,这是我想避免的。看起来,最"harmless" "solution" 是更新和"fix" 当前解决方案。

所以,因为我只需要它(现在)与 GCC 一起工作,所以我做了以下修改:

#ifndef __LITTLE_ENDIAN__
    #if __GNUC__
        #include <endian.h>
        #if __BYTE_ORDER == __LITTLE_ENDIAN
            #define __LITTLE_ENDIAN__ 1
        #elif __BYTE_ORDER == __BIG_ENDIAN
            #define __LITTLE_ENDIAN__ 0
        #else
            #error "Cannot determine endianness! Please update this macro!"
        #endif
    #elif __i386__ || __alpha__ || _M_IX86
        #define __LITTLE_ENDIAN__ 1
    #else
        #warning "Endianness cannot be determined for this platform; using big endian by default! Please be aware and update this macro!"
        #define __LITTLE_ENDIAN__ 0
    #endif
#endif

此答案基于 Kiril Kirov 的现有答案

Kiril 已确定预处理器检查系统无法识别现有平台的问题:

#if __i386__ || __alpha__ || _M_IX86 || __LITTLE_ENDIAN
#define __LITTLE_ENDIAN__ 1
#else
#define __LITTLE_ENDIAN__ 0
#endif

尽管平台是 little-endian,但其中 none 的标识符是在 gcc -std=c99 模式下定义的。所以这里引用的代码有问题;需要更新代码以更好地识别它是否是 little-endian 平台。

我要做的第一件事就是停止使用默认情况,这样如果平台无法识别,就会生成错误,而不是默默地 运行 错误:

#if __i386__ || __alpha__ || _M_IX86 || __LITTLE_ENDIAN
#define __LITTLE_ENDIAN__ 1
#elif __arm__  // etc.
#define __LITTLE_ENDIAN__ 0
#else
#error Unrecognized platform, please update this macro
#endif

下一步是实际正确检测您所使用的系统。 Here is another thread 关于这个主题。

你可以做的一件事是发出 gcc -std=c99 -dM -E - <<<'' 这将导致 gcc 输出该模式下所有预定义宏的列表;然后你可以寻找有用的东西。就我而言,它有:

#define __i386 1
#define __i686 1

因此可以使用其中任何一个。

另一种专门针对 __LITTLE_ENDIAN__ 的方法是通过预处理器算法检测它,如 here 所述 - 尽管该页面上的第一个代码示例实际上并没有产生预处理常量,因此它不会' 可用作未来预处理器检查的条件。


解决了这个具体问题后,您仍应尝试在同一代码库中查找架构问题的其他实例。看起来作者拼凑了一些 he/she 知道的架构宏。例如,一个明显的缺席是 _M_IX64。明智的做法可能是通过代码库搜索 _M___i386__ 的任何其他实例,看看它是否依赖于这些实例。如果是;然后再尝试将该测试抽象成一个您可以更好地控制的宏。

理想情况下,您可以为整个代码库在单个 header 中定义所有此类宏;然后代码库的其余部分只使用在 header 中定义的宏。