章节大纲

  • Meltdown 和 Spectre 攻击都将 CPU 缓存作为侧信道来窃取受保护的机密。这种侧信道技术称为 FLUSH + RELOAD。我们将首先研究这种技术。这两个任务开发的代码将作为后续任务的基础。

    CPU 缓存是一种硬件缓存,用于计算机中的 CPU 以减少访问主内存数据的平均成本(时间或耗能)。从主内存访问数据要比从缓存中快得多。当数据从主内存读取时,它们通常会被 CPU 缓存,因此如果再次使用相同的数据,访问速度将变得更快。因此,当 CPU 需要访问某些数据时,它会先查看其缓存。如果数据在缓存中(这被称为缓存命中),则它会被直接获取;如果没有找到数据(这被称为未命中),CPU 将去主内存获取数据。后者的花费时间明显更长。大多数现代 CPU 都有 CPU 缓存。
     
    CPU 缓存
     
    缓存被用于以更快的速度为高速处理器提供数据。缓存比主内存快得多。我们来看一下时间差异。在代码(CacheTime.c)中,我们有一个大小为 10*4096 的数组。我们首先访问其中的两个元素 array[3*4096] 和 array[7*4096]。因此,包含这两个元素的页面将被缓存。然后我们从 array[0*4096] 到 array[9*4096] 读取元素并测量内存读取的时间。上图展现了时间差异。在代码中,行 🅰 在内存读取之前读取 CPU 的时间戳(TSC)计数器的值,而行 🅱 在内存读取之后读取该计数器的值。它们的差值就是内存读取所花费的时间(以 CPU 周期数为单位)。需要指出的是,缓存操作是一个一个缓存块来进行的,而不是一个一个字节。典型的缓存块大小为 64 字节。我们使用 array[k*4096],所以程序中使用的两个元素不会落在同一个缓存块里。

    /* CacheTime.c */
    #include <emmintrin.h>
    #include <x86intrin.h>
    
    uint8_t array[10*4096];
    
    int main(int argc, const char **argv) {
      int junk=0;
      register uint64_t time1, time2;
      volatile uint8_t *addr;
      int i;
    
      // 初始化数组
      for(i=0; i<10; i++) array[i*4096]=1;
    
      // 将数组从 CPU 缓存中清除
      for(i=0; i<10; i++) _mm_clflush(&array[i*4096]);
    
      // 访问数组中的某些元素
      array[3*4096] = 100;
      array[7*4096] = 200;
    
      for(i=0; i<10; i++) {
        addr = &array[i*4096];
        time1 = __rdtscp(&junk);                  🅰
        junk = *addr;
        time2 = __rdtscp(&junk) - time1;          🅱
        printf("访问 array[%d*4096] 的时间:%d 个CPU周期\n",i, (int)time2);
      }
      return 0;
    }

    请使用 gcc -march=native CacheTime.c 编译上述代码,并运行它。访问数组的 array[3*4096] 和 array[7*4096]}是否比其他元素访问得更快?你需要至少运行该程序 10 次并描述你的观察结果。通过实验,你需要找到一个阈值来区分从缓存读取数据与从主内存读取数据两种类型的内存访问。这个阈值对于后续的任务是非常重要的。