Как я могу создать призрачный гаджет на практике? - PullRequest
0 голосов
/ 12 июня 2018

Я разрабатываю (NASM + GCC для ELF64) PoC , который использует гаджет-призрак, который измеряет время для доступа к набору строк кэша ( FLUSH + RELOAD ).

Как создать надежный гаджет для призраков?

Мне кажется, я понимаю теорию, лежащую в основе техники FLUSH + RELOAD, однако на практике презираю некоторый шум, яЯ не могу произвести рабочий PoC.


Поскольку я использую счетчик меток времени и нагрузки очень регулярные, я использую этот сценарий, чтобы отключить предварительные выборки, турбо-буст и исправить / стабилизировать частоту процессора:

#!/bin/bash

sudo modprobe msr

#Disable turbo
sudo wrmsr -a 0x1a0 0x4000850089

#Disable prefetchers
sudo wrmsr -a 0x1a4 0xf

#Set performance governor
sudo cpupower frequency-set -g performance

#Minimum freq
sudo cpupower frequency-set -d 2.2GHz

#Maximum freq
sudo cpupower frequency-set -u 2.2GHz

У меня есть непрерывный буфер, выровненный по 4 КБ, достаточно большой, чтобы охватить 256 строк кэша, разделенных целым числом GAP строк.

SECTION .bss ALIGN=4096

 buffer:    resb 256 * (1 + GAP) * 64

Я использую эту функцию для очистки256 строк.

flush_all:
 lea rdi, [buffer]              ;Start pointer
 mov esi, 256                   ;How many lines to flush

.flush_loop:
  lfence                        ;Prevent the previous clflush to be reordered after the load
  mov eax, [rdi]                ;Touch the page
  lfence                        ;Prevent the current clflush to be reordered before the load

  clflush  [rdi]                ;Flush a line
  add rdi, (1 + GAP)*64         ;Move to the next line

  dec esi
 jnz .flush_loop                ;Repeat

 lfence                         ;clflush are ordered with respect of fences ..
                                ;.. and lfence is ordered (locally) with respect of all instructions
 ret

Функция проходит по всем строкам, касаясь каждой промежуточной страницы (каждой страницы более одного раза) и сбрасывая каждую строку.

Затем я использую эту функцию дляпрофилируйте доступы.

profile:
 lea rdi, [buffer]           ;Pointer to the buffer
 mov esi, 256                ;How many lines to test
 lea r8, [timings_data]      ;Pointer to timings results

 mfence                      ;I'm pretty sure this is useless, but I included it to rule out ..
                             ;.. silly, hard to debug, scenarios

.profile: 
  mfence
  rdtscp
  lfence                     ;Read the TSC in-order (ignoring stores global visibility)

  mov ebp, eax               ;Read the low DWORD only (this is a short delay)

  ;PERFORM THE LOADING
  mov eax, DWORD [rdi]

  rdtscp
  lfence                     ;Again, read the TSC in-order

  sub eax, ebp               ;Compute the delta

  mov DWORD [r8], eax        ;Save it

  ;Advance the loop

  add r8, 4                  ;Move the results pointer
  add rdi, (1 + GAP)*64      ;Move to the next line

  dec esi                    ;Advance the loop
 jnz .profile

 ret

MCVE приведен в приложении, а хранилище доступно для клонирования .

При сборке с GAP, установленным в 0, связанными выполняются с taskset -c 0, циклы, необходимые для извлечения каждой строки, показаны ниже.

Results showing that only the first 64 lines are loaded from memory

Только 64 строки загружены из памяти.

Вывод стабильный при разных прогонах.Если я установлю GAP на 1, из памяти будет извлечено только 32 строки, конечно 64 * (1 + 0) * 64 = 32 * (1 + 1) * 64 = 4096, так что это может быть связано с подкачкой?

Если хранилище выполняется до профилирования (но после сброса) до одной из первых 64 строк, выходное значение меняется на

The second block of 64 lines is loaded from memory

Любой магазин, остальные строки дают первый тип вывода.

Я подозреваю, что математика в ней сломана, но мне нужно еще пару глаз, чтобы выяснить, где.


РЕДАКТИРОВАТЬ

Хади Брайс указал неправильное использование энергозависимого регистра после исправления того, что выходные данные теперь несовместимы.
Я вижу, что в основном работаетгде тайминги низкие (~ 50 циклов) и иногда работают там, где хронометраж выше (~ 130 циклов).
Я не знаю, откуда взялась цифра в 130 циклов (слишком мало для памяти, слишком много для кеша)?).

Two runs of the corrected program

Код исправлен в MCVE (и в хранилище).

ЕслиСохранение в любой из первых строк выполняется перед профилированием, никакие изменения не отражаются в выходных данных.


ПРИЛОЖЕНИЕ - MCVE

BITS 64
DEFAULT REL

GLOBAL main

EXTERN printf
EXTERN exit

;Space between lines in the buffer
%define GAP 0

SECTION .bss ALIGN=4096



 buffer:    resb 256 * (1 + GAP) * 64   


SECTION .data

 timings_data:  TIMES 256 dd 0


 strNewLine db `\n0x%02x: `, 0
 strHalfLine    db "  ", 0
 strTiming  db `\e[48;5;16`,
  .importance   db "0",
        db `m\e[38;5;15m%03u\e[0m `, 0  

 strEnd     db `\n\n`, 0

SECTION .text

;'._ .''._ .''._ .''._ .''._ .''._ .''._ .''._ .''._ .''._ .''._ .' 
;   '     '     '     '     '     '     '     '     '     '     '   
; _' \  _' \  _' \  _' \  _' \  _' \  _' \  _' \  _' \  _' \  _' \ 
;/    \/    \/    \/    \/    \/    \/    \/    \/    \/    \/    \
;
;
;FLUSH ALL THE LINES OF A BUFFER FROM THE CACHES
;
;

flush_all:
 lea rdi, [buffer]  ;Start pointer
 mov esi, 256       ;How many lines to flush

.flush_loop:
  lfence        ;Prevent the previous clflush to be reordered after the load
  mov eax, [rdi]    ;Touch the page
  lfence        ;Prevent the current clflush to be reordered before the load

  clflush  [rdi]    ;Flush a line
  add rdi, (1 + GAP)*64 ;Move to the next line

  dec esi
 jnz .flush_loop    ;Repeat

 lfence         ;clflush are ordered with respect of fences ..
            ;.. and lfence is ordered (locally) with respect of all instructions
 ret


;'._ .''._ .''._ .''._ .''._ .''._ .''._ .''._ .''._ .''._ .''._ .' 
;   '     '     '     '     '     '     '     '     '     '     '   
; _' \  _' \  _' \  _' \  _' \  _' \  _' \  _' \  _' \  _' \  _' \ 
;/    \/    \/    \/    \/    \/    \/    \/    \/    \/    \/    \
;
;
;PROFILE THE ACCESS TO EVERY LINE OF THE BUFFER
;
;


profile:
 lea rdi, [buffer]      ;Pointer to the buffer
 mov esi, 256           ;How many lines to test
 lea r8, [timings_data]     ;Pointer to timings results


 mfence             ;I'm pretty sure this is useless, but I included it to rule out ..
                ;.. silly, hard to debug, scenarios

.profile: 
  mfence
  rdtscp
  lfence            ;Read the TSC in-order (ignoring stores global visibility)

  mov ebp, eax          ;Read the low DWORD only (this is a short delay)

  ;PERFORM THE LOADING
  mov eax, DWORD [rdi]

  rdtscp
  lfence            ;Again, read the TSC in-order

  sub eax, ebp          ;Compute the delta

  mov DWORD [r8], eax       ;Save it

  ;Advance the loop

  add r8, 4         ;Move the results pointer
  add rdi, (1 + GAP)*64     ;Move to the next line

  dec esi           ;Advance the loop
 jnz .profile

 ret

;'._ .''._ .''._ .''._ .''._ .''._ .''._ .''._ .''._ .''._ .''._ .' 
;   '     '     '     '     '     '     '     '     '     '     '   
; _' \  _' \  _' \  _' \  _' \  _' \  _' \  _' \  _' \  _' \  _' \ 
;/    \/    \/    \/    \/    \/    \/    \/    \/    \/    \/    \
;
;
;SHOW THE RESULTS
;
;

show_results:
 lea rbx, [timings_data]    ;Pointer to the timings
 xor r12, r12           ;Counter (up to 256)

.print_line:

 ;Format the output

 xor eax, eax
 mov esi, r12d
 lea rdi, [strNewLine]      ;Setup for a call to printf

 test r12d, 0fh
 jz .print          ;Test if counter is a multiple of 16

 lea rdi, [strHalfLine]     ;Setup for a call to printf

 test r12d, 07h         ;Test if counter is a multiple of 8
 jz .print

.print_timing:

  ;Print
  mov esi, DWORD [rbx]      ;Timing value

  ;Compute the color
  mov r10d, 60          ;Used to compute the color 
  mov eax, esi
  xor edx, edx
  div r10d          ;eax = Timing value / 78

  ;Update the color 


  add al, '0'
  mov edx, '5'
  cmp eax, edx
  cmova eax, edx
  mov BYTE [strTiming.importance], al

  xor eax, eax
  lea rdi, [strTiming]
  call printf WRT ..plt     ;Print a 3-digits number

  ;Advance the loop 

  inc r12d          ;Increment the counter
  add rbx, 4            ;Move to the next timing
  cmp r12d, 256
 jb .print_line         ;Advance the loop

  xor eax, eax
  lea rdi, [strEnd]
  call printf WRT ..plt     ;Print a new line

  ret

.print:

  call printf WRT ..plt     ;Print a string

jmp .print_timing

;'._ .''._ .''._ .''._ .''._ .''._ .''._ .''._ .''._ .''._ .''._ .' 
;   '     '     '     '     '     '     '     '     '     '     '   
; _' \  _' \  _' \  _' \  _' \  _' \  _' \  _' \  _' \  _' \  _' \ 
;/    \/    \/    \/    \/    \/    \/    \/    \/    \/    \/    \
;
;
;E N T R Y   P O I N T
;
;
;'._ .''._ .''._ .''._ .''._ .''._ .''._ .''._ .''._ .''._ .''._ .' 
;   '     '     '     '     '     '     '     '     '     '     '   
; _' \  _' \  _' \  _' \  _' \  _' \  _' \  _' \  _' \  _' \  _' \ 
;/    \/    \/    \/    \/    \/    \/    \/    \/    \/    \/    \

main:

 ;Flush all the lines of the buffer
 call flush_all

 ;Test the access times
 call profile

 ;Show the results
 call show_results

 ;Exit
 xor edi, edi
 call exit WRT ..plt

1 Ответ

0 голосов
/ 18 июля 2018

Буфер выделяется из раздела bss, поэтому при загрузке программы ОС отобразит все строки кэша buffer на одной физической странице CoW.После очистки всех строк только доступ к первым 64 строкам в виртуальном адресном пространстве пропускается на всех уровнях кэша 1 , поскольку все последующие обращения 2 относятся к одной странице 4K.Вот почему задержки первых 64 обращений попадают в диапазон задержки основной памяти, а задержки всех последующих обращений равны задержке попадания L1 3 , когда GAP равен нулю.

Когда GAP равен 1, к каждой другой строке той же физической страницы обращаются, и поэтому число обращений к основной памяти (пропущено L3) равно 32 (половина из 64).Таким образом, первые 32 задержки будут в диапазоне задержек основной памяти, а все последующие задержки будут попаданиями L1.Аналогично, когда GAP равно 63, все обращения к одной и той же строке.Поэтому только первый доступ пропустит все кэши.

Решение состоит в том, чтобы изменить mov eax, [rdi] в flush_all на mov dword [rdi], 0, чтобы гарантировать, что буфер размещен на уникальных физических страницах.(Инструкции lfence в flush_all можно удалить, поскольку в руководстве Intel указано, что clflush не может быть переупорядочен с записью 4 .) Это гарантирует, что после инициализации и очистки всех строк все обращения будутпропустить все уровни кэша (но не TLB, см .: Удаляет ли clflush также записи TLB? ).

Вы можете обратиться к Почему в пользовательском режиме L1 хранятся события пропущенных событийучитывается только при наличии цикла инициализации магазина? для другого примера, где страницы CoW могут быть обманчивыми.


В предыдущей версии этого ответа я предлагал удалить вызов к flush_all ииспользуйте значение GAP, равное 63. С этими изменениями все задержки доступа оказались очень высокими, и я неправильно сделал вывод, что во всех доступах отсутствуют все уровни кэша.Как я уже говорил выше, при значении GAP, равном 63, все обращения осуществляются к одной и той же строке кэша, которая фактически находится в кэше L1.Однако причина, по которой все задержки были высокими, заключается в том, что каждый доступ был к другой виртуальной странице, и у TLB не было каких-либо сопоставлений для каждой из этих виртуальных страниц (к одной и той же физической странице), поскольку путем удаления вызоваflush_all, ни одна из виртуальных страниц не была затронута ранее.Таким образом, измеренные задержки представляют собой задержку пропадания TLB, даже если доступная строка находится в кэше L1.

В предыдущей версии этого ответа я также неверно утверждал, что существует логика предварительной выборки L3, которую нельзя отключитьчерез MSR 0x1A4.Если определенный предварительный выборщик отключен путем установки его флага в MSR 0x1A4, то он полностью отключается.Также отсутствуют средства предварительной выборки данных, кроме тех, которые задокументированы Intel.


Сноски:

(1) Если вы не отключите предварительную выборку IP DCU, она фактически выполнит предварительную выборку обратновсе линии в L1 после их очистки, поэтому все обращения будут по-прежнему попадать в L1.

(2) В редких случаях выполнение обработчиков прерываний или планирование других потоков на одном и том же ядре может вызвать некоторые изстроки, которые должны быть удалены из L1 и, возможно, с других уровней иерархии кэша.

(3) Помните, что вам нужно вычесть накладные расходы на инструкции rdtscp.Обратите внимание, что метод измерения, который вы использовали, на самом деле не позволяет вам надежно различать попадание L1 и попадание L2.См .: Измерение задержки памяти с помощью счетчика меток времени .

(4) Руководство Intel, похоже, не указывает, упорядочен ли clflush с чтениями, но мне кажется, что оноесть.

...