寻找一个恒定时间字符串相等测试我发现他们中的大多数在返回值上使用了一些技巧。例如这段代码:
int ctiszero(const void* x, size_t n)
{
volatile unsigned char r = 0;
for (size_t i = 0; i < n; i += 1) {
r |= ((unsigned char*)x)[i];
}
return 1 & ((r - 1) >> 8);
}
return 1 & ((r - 1) >> 8);
的目的是什么?为什么不是一个简单的return !r;
?
答案 0 :(得分:4)
正如我的一条评论中所提到的,这个函数检查一个任意字节的数组是否为零。如果所有字节都为零,则返回1
,否则将返回0
。
如果至少有一个非零字节,则r
也将为非零。减去1
,您将获得零或正值(因为r
为unsigned
)。将所有位移出r
,结果为零,然后用1
屏蔽,结果为零,返回。
如果所有字节都为零,那么r
的值也将为零。但是来了#34;魔法&#34;:在r - 1
表达式中,r
的值经历了所谓的usual arithmetic conversion,这导致了r
的值成为promoted到int
。该值仍然为零,但现在它是签名的整数。减去1
,您将获得-1
,其中通常的two's complement符号等于0xffffffff
。移动它使其变为0x00ffffff
并使用1
进行掩码会导致1
。哪个被退回。
答案 1 :(得分:1)
使用常量时间代码,通常可以分支代码(并产生运行时间差异),例如return !r;
。
请注意,经过优化的编译器可能会为return 1 & ((r - 1) >> 8);
发出与return !r;
完全相同的代码。因此,本练习充其量只是用于哄骗发送常量时间代码的编译器输入的代码。
不常见的平台怎么样?
当return 1 & ((r - 1) >> 8);
是8位2的补码时,@Some programmer dude很好地解释了 int
- 这是非常常见的事情。
对于8位unsigned char
和r > 0
,r-1
为非负数,1 & ((r - 1) >> 8)
返回0,即使int
是2的补码,1的补码或符号幅度,16位,32位等。
r == 0
时,r-1
为-1。它是实现定义行为 1 & ((r - 1) >> 8)
返回的内容。它返回1,int
为2的补码或1的补码,但0为符号幅度。
// fails with sign-magnitude (rare)
// fails when byte width > 8 (uncommon)
return 1 & ((r - 1) >> 8);
在更多情况下 1 ,可以根据需要修改小的更改。另请参阅@Eric Postpischil
通过使用r - 1
数学保证unsigned
,int
编码无关紧要。
// v--- add u v--- shift by byte width
return 1 & ((r - 1u) >> CHAR_BIT);
1 有点罕见:当unsigned char
大小与unsigned
相同时,OP的代码和此修复失败。如果有更宽的数学整数,代码可以使用它:例如:return 1 & ((r - 1LLU) >> CHAR_BIT);
答案 2 :(得分:0)
这是r&gt;的缩写。 128或零。也就是说,它是一个非ASCII字符。如果将r的高位设置为减去1将保留高位设置,除非高位是仅位置位。因此大于128(0x80)并且如果r为零,则下溢将设置高位。
for循环的结果是,如果任何字节设置了高位,或者如果所有字节都为零,则返回1。但是如果所有非零字节都没有设置为高位,则返回0。
奇怪的是,对于所有0x80和0x00字节的字符串,仍将返回0。不确定这是否是“功能”!