代码之家  ›  专栏  ›  技术社区  ›  Grant Sanders

在C语言中,通过对数据本身进行异或运算来实现内存的归零是否可以得到优化?

  •  3
  • Grant Sanders  · 技术社区  · 8 年前

    有时,出于安全目的,我们需要清空内存以防止意外访问敏感数据,例如在加密某些数据后安全地删除密钥。大多数人建议这样做的方式是将随机数据写入包含敏感信息的数组,因为编译器无法对此进行优化。很好地理解,天真地使用函数 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 .优化后,速度比 清零 无论如何,我都可以访问实现,这令人惊讶。

    我还没有学会汇编,但在O2和O3级别使用GCC和Clang进行优化后,64位x86处理器的汇编输出具有 xorl 指令,有时两次。这向我表明,内存的异或运算实际上正在发生,但我希望知道他们在说什么的人能够证实这一点。

    这是可行的解决方案吗?

    2 回复  |  直到 8 年前
        1
  •  7
  •   малин чекуров    8 年前

    正确的方法是调用 memset_s() 作用它使用volatile类型限定符通知编译器对memset\u s()函数的调用不应优化。

    不幸的是,这种解决方案可能效率不高,因为易失性类型的性质可以防止各种优化,它可能会阻止编译器使用最佳的汇编指令,并可能导致效率较低的代码。另一个问题 memset\u s() 它是在C11中引入的。

    如果你不能使用 memset\u s() ,则必须考虑以下方法之一:

    1. 另一种解决方案可能是通过访问memset()之后的内存来“触摸”内存,如下所示 *(volatile char*)pwd= *(volatile char*)pwd 。 此解决方案的问题是,它可能不适用于所有实现。
    2. 编写您自己的版本 memset\u s() (示例1)。这方面的问题是它仍然不能保证工作-C标准指出,访问易失性对象是不可改变的可观察行为的一部分,但它没有提到通过易失性类型的左值表达式进行访问

    总之,无论您选择什么,强烈建议您始终检查生成的可组装代码,确保内存实际上已被清除,并且没有任何内存调用被优化掉。

    示例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));
    }
    
        2
  •  1
  •   Maarten Bodewes    8 年前

    也许一个好的、语言中立的方法是让某些东西依赖于内存中的值。例如,您可以先将内存全部设置为零, 然后 迭代所有字节,将它们异或到一个临时变量中。如果答案不是零,则引发异常。当然,这种异常永远不会发生,但编译器不太可能解决这个问题(特别是如果XOR不是在不同的循环构造中执行的,因此需要按顺序执行操作)。

    这还有抛出错误的额外好处 (如果比较字节,则256个字节中可能有255个字节)如果阵列被更改或(非零)关键材料留在阵列中。在可以更改CPU执行的嵌入式环境中,这可能是一个好主意。

    该操作相对高效,因为它由一个具有预定义循环数的单循环和一个 如果 -构造,当然除了内存访问。

    这可以与将数组内容设置为零的安全方法相结合,如 in the answer of Malin