#c #linux #caching #cpu #side-channel-attacks
#c #linux #кэширование #процессор #атаки по побочным каналам
Вопрос:
Привет, я пытался проверить пропуски и попадания в кэш в Linux. Для этого я выполнил программу на C, где я измеряю время в цикле процессора для выполнения инструкции printf() . Первая часть определяет время, необходимое для промаха, а вторая — для попадания. Вот данная программа :
#include <stdint.h>
#include <stdio.h>
#include <string.h>
#include <sched.h>
#include <sys/types.h>
#include <unistd.h>
#include <signal.h>
uint64_t rdtsc() {
uint64_t a, d;
asm volatile ("mfence");
asm volatile ("rdtsc" : "=a" (a), "=d" (d));
a = (d<<32) | a;
asm volatile ("mfence");
return a;
}
int main(int argc, char** argv)
{
size_t time = rdtsc();
printf("Hey ");
size_t delta1 = rdtsc() - time;
printf("delta: %zun", delta1);
size_t time2 = rdtsc();
printf("Hey ");
size_t delta2 = rdtsc() - time2;
printf("delta: %zun", delta2);
sleep(100);
}
Теперь я хотел бы показать, что два процесса (два терминала) имеют кэш в commun. Итак, я подумал, что запуск этой программы в двух терминалах приведет к :
Terminal 1:
miss
hit
Terminal 2:
hit
hit
Но теперь у меня есть что-то вроде:
Terminal 1:
miss
hit
Terminal 2:
miss
hit
Я неправильно понимаю? Или моя программа неверна?
Комментарии:
1. Как вы думаете, что здесь следует кэшировать?
2. Я думал, что функция printf() будет кэшироваться, поэтому терминалу 2 не придется ждать первого printf() так долго, как терминалу 1.
3. можете ли вы распечатать дельту? возможно, 2 терминала работают с 2 разных ядер.
4.
mfence
не гарантируется взаимодействиеrdtsc
вообще. (Это происходит в Skylake из-за обновлений микрокода, которые делаютmfence
его еще медленнее и безопаснее даже при загрузке NT из памяти WC, добавляя к нему семантику, подобную lfence.)lfence
— это инструкция, которая гарантированно упорядочивает выполнение инструкций на процессорах Intel. Кроме того, кстати, есть встроенные функции для_mm_lfence()
. Если вы используете процессор, в которомmfence
нетlfence
встроенной семантики, вы можете использовать оба, если хотите дождаться истощения буфера хранилища и завершения всех предыдущих инструкций (истощения ROB), например, сериализации insn.
Ответ №1:
Ваше предположение в некоторой степени верно.
printf
является частью libc
библиотеки. Если вы используете динамическое связывание, операционная система может оптимизировать использование памяти, загрузив библиотеку только один раз для всех процессов, использующих ее.
Однако есть несколько причин, по которым я не ожидаю, что вы измерите какую-либо значительную разницу:
- по сравнению с разницей между попаданием в кэш и промахом в кэше,
printf
для завершения требуется огромное количество времени, и происходит много событий, которые создают шум. С помощью всего лишь одного измерения очень маловероятно, что вы сможете измерить эту крошечную разницу. - фактической причиной того, что первое измерение занимает больше времени, вероятно, является отложенное связывание библиотечной функции
printf
, разрешаемое загрузчиком (https://maskray.me/blog/2021-09-19-all-about-procedure-linkage-table ) или происходит какое-то другое волшебство (настраиваются буферы и т.д.) для первого вывода. - множество
libc
функций используется многими различными процессами. Если библиотека является общей, вполне вероятно, что printf может быть кэширован, даже если вы его не использовали.
Я бы предложил смонтировать атаку Flush Reload (https://eprint.iacr.org/2013/448.pdf ) включите printf
в одном из терминалов и используйте его в другом терминале. Затем вы можете увидеть разницу во времени.
Примечание: чтобы найти фактический адрес printf
для атаки, вам нужно быть знакомым с динамическим связыванием и plt. Просто использовать что-то вроде void* addr = printf
, вероятно, не сработает!