可以在 C 中优化通过将数据与自身进行异或运算来对内存进行归零吗?
Can zeroisation of memory by xor-ing data with itself be optimized away in C?
有时,出于安全目的,我们需要将内存归零以防止意外访问敏感数据,比如在加密某些数据后安全地删除密钥。大多数人建议这样做的方法是将随机数据写入包含敏感信息的数组,因为编译器无法优化它。众所周知,如果它是数据超出范围之前对数据完成的最后一次操作,那么由于 as-if 规则,优化编译器可以优化像 memset
这样天真的使用函数。不过,获取和写入随机数据很慢,我可能已经找到了解决方案。不过,在将其部署到生产代码之前,我需要专家意见。
根据运算符的本质,将任何东西与自身进行异或运算,结果总是为零,而且速度非常快。遍历一块内存并将其与自身进行异或运算似乎是解决零化问题的一种非常有效的解决方案,但我担心它可能会被足够好的优化编译器优化掉。它是跨平台和可移植的,不需要使用标准库,除了使用 size_t
数据类型。我在下面包含了我的意思的参考实现。在其中我有一个名为 nuke
的函数,它接受一个指针 data_to_zero
并迭代地与自身进行 xor 的 size
字节。
void nuke (void *data_to_zero, size_t size)
{
size_t i;
for (i = 0; i < size; i++) {
((unsigned char*)data_to_zero)[i] ^= ((unsigned char*)data_to_zero)[i];
}
}
此实现相当慢,但比获取足够随机的数据并将其写入 data
快得多。优化后,它比我可以访问的 memset
实现更快,这令人惊讶。
我还没有学过汇编,但是在 O2 和 O3 级别使用 GCC 和 Clang 优化后的汇编输出,一个 64 位 x86 处理器在代码的某处有 xorl
指令,有时两次。这向我表明内存的异或运算实际上正在发生,但我希望有人知道他们在说什么来确认。
这是可行的解决方案吗?
正确的做法是调用memset_s()
函数。它使用 volatile 类型限定符通知编译器不应优化对 memset_s() 函数的调用。
不幸的是,由于 volatile 类型的性质,防止各种优化,此解决方案可能不是尽可能高效,它可能会阻止编译器使用最佳汇编指令,并可能导致代码效率较低. memset_s()
的另一个问题是它是在 C11 中引入的。
如果你不能使用memset_s()
,那么你就要考虑以下方法之一:
- 另一个解决方案可能是 "touch" 内存,通过在 memset() 之后访问内存,就像这样
*(volatile char*)pwd= *(volatile char*)pwd
。
此解决方案的问题是它可能不适用于所有实现。
- 编写您自己的版本
memset_s()
(示例 1)。这样做的问题是它仍然不能保证工作——C 标准声明访问 volatile 对象是不可改变的可观察行为的一部分——但它没有说明通过具有 volatile 类型的左值表达式进行访问
- 据我所知,最好的方法是使用可变函数指针(示例 2)
作为结论——无论您选择什么,都强烈建议您始终检查生成的汇编代码,确保内存实际上已被清除并且 none 内存调用已被优化。
示例 1.
static void secure_memzero(void * p, size_t len)
{
volatile uint8_t * _p = p;
while (len--) *_p++ = 0;
}
示例 2.
static void * (* const volatile memset_ptr)(void *, int, size_t) = memset;
static void secure_memzero(void * p, size_t len)
{
(memset_ptr)(p, 0, len);
}
void
dosomethingsensitive(void)
{
uint8_t key[32];
...
/* Zero sensitive information. */
secure_memzero(key, sizeof(key));
}
可能一种好的、语言中立的方法是让某些东西依赖于内存中的值。例如,您可以先将内存设置为全零,然后 然后 遍历所有字节,将它们异或到一个临时变量中。如果答案不是零,则抛出异常。这种异常当然永远不会发生——但编译器不太可能解决这个问题(特别是如果 XOR 不是在不同的循环结构中执行的,因此需要按顺序执行操作)。
这有抛出错误的额外好处
(如果比较字节,则可能是 256 个字节中的 255 个)如果数组被更改或如果(非零)键 material 留在数组中。在可以更改 CPU 的执行的嵌入式环境中,这可能是个好主意。
该操作相对有效,因为它由具有预定义循环次数的单个循环和单个 if 结构组成,当然除了内存访问之外。
这可以与将数组内容设置为零的安全方法结合使用,如 。
有时,出于安全目的,我们需要将内存归零以防止意外访问敏感数据,比如在加密某些数据后安全地删除密钥。大多数人建议这样做的方法是将随机数据写入包含敏感信息的数组,因为编译器无法优化它。众所周知,如果它是数据超出范围之前对数据完成的最后一次操作,那么由于 as-if 规则,优化编译器可以优化像 memset
这样天真的使用函数。不过,获取和写入随机数据很慢,我可能已经找到了解决方案。不过,在将其部署到生产代码之前,我需要专家意见。
根据运算符的本质,将任何东西与自身进行异或运算,结果总是为零,而且速度非常快。遍历一块内存并将其与自身进行异或运算似乎是解决零化问题的一种非常有效的解决方案,但我担心它可能会被足够好的优化编译器优化掉。它是跨平台和可移植的,不需要使用标准库,除了使用 size_t
数据类型。我在下面包含了我的意思的参考实现。在其中我有一个名为 nuke
的函数,它接受一个指针 data_to_zero
并迭代地与自身进行 xor 的 size
字节。
void nuke (void *data_to_zero, size_t size)
{
size_t i;
for (i = 0; i < size; i++) {
((unsigned char*)data_to_zero)[i] ^= ((unsigned char*)data_to_zero)[i];
}
}
此实现相当慢,但比获取足够随机的数据并将其写入 data
快得多。优化后,它比我可以访问的 memset
实现更快,这令人惊讶。
我还没有学过汇编,但是在 O2 和 O3 级别使用 GCC 和 Clang 优化后的汇编输出,一个 64 位 x86 处理器在代码的某处有 xorl
指令,有时两次。这向我表明内存的异或运算实际上正在发生,但我希望有人知道他们在说什么来确认。
这是可行的解决方案吗?
正确的做法是调用memset_s()
函数。它使用 volatile 类型限定符通知编译器不应优化对 memset_s() 函数的调用。
不幸的是,由于 volatile 类型的性质,防止各种优化,此解决方案可能不是尽可能高效,它可能会阻止编译器使用最佳汇编指令,并可能导致代码效率较低. memset_s()
的另一个问题是它是在 C11 中引入的。
如果你不能使用memset_s()
,那么你就要考虑以下方法之一:
- 另一个解决方案可能是 "touch" 内存,通过在 memset() 之后访问内存,就像这样
*(volatile char*)pwd= *(volatile char*)pwd
。 此解决方案的问题是它可能不适用于所有实现。 - 编写您自己的版本
memset_s()
(示例 1)。这样做的问题是它仍然不能保证工作——C 标准声明访问 volatile 对象是不可改变的可观察行为的一部分——但它没有说明通过具有 volatile 类型的左值表达式进行访问 - 据我所知,最好的方法是使用可变函数指针(示例 2)
作为结论——无论您选择什么,都强烈建议您始终检查生成的汇编代码,确保内存实际上已被清除并且 none 内存调用已被优化。
示例 1.
static void secure_memzero(void * p, size_t len)
{
volatile uint8_t * _p = p;
while (len--) *_p++ = 0;
}
示例 2.
static void * (* const volatile memset_ptr)(void *, int, size_t) = memset;
static void secure_memzero(void * p, size_t len)
{
(memset_ptr)(p, 0, len);
}
void
dosomethingsensitive(void)
{
uint8_t key[32];
...
/* Zero sensitive information. */
secure_memzero(key, sizeof(key));
}
可能一种好的、语言中立的方法是让某些东西依赖于内存中的值。例如,您可以先将内存设置为全零,然后 然后 遍历所有字节,将它们异或到一个临时变量中。如果答案不是零,则抛出异常。这种异常当然永远不会发生——但编译器不太可能解决这个问题(特别是如果 XOR 不是在不同的循环结构中执行的,因此需要按顺序执行操作)。
这有抛出错误的额外好处 (如果比较字节,则可能是 256 个字节中的 255 个)如果数组被更改或如果(非零)键 material 留在数组中。在可以更改 CPU 的执行的嵌入式环境中,这可能是个好主意。
该操作相对有效,因为它由具有预定义循环次数的单个循环和单个 if 结构组成,当然除了内存访问之外。
这可以与将数组内容设置为零的安全方法结合使用,如