A memória do computador fica a cada 7,8 μs


SDRAM DDR3 moderno. Fonte: BY-SA / 4.0 by Kjerish

Durante uma recente visita ao Museu da História do Computador em Mountain View, uma amostra antiga de memória de ferrite chamou minha atenção.


Fonte: BY-SA / 3.0 de Konstantin Lanzet

Cheguei rapidamente à conclusão de que não tenho ideia de como essas coisas funcionam. Os anéis giram (não) e por que três fios passam por cada anel (ainda não entendo como eles funcionam). Mais importante, percebi que tenho muito pouca ideia de como funciona a moderna RAM dinâmica!


Fonte: Ciclo de Memória de Ulrich Drapper

Eu estava particularmente interessado em uma das consequências de como a RAM dinâmica funciona. Acontece que cada bit de dados é armazenado por uma carga (ou sua ausência) em um pequeno capacitor no chip de RAM. Mas esses capacitores perdem gradualmente sua carga ao longo do tempo. Para evitar a perda de dados armazenados, eles devem ser atualizados regularmente para restaurar a cobrança (se houver) ao seu nível original. Esse processo de atualização envolve a leitura de cada bit e a gravação novamente. Durante esta "atualização", a memória está ocupada e não pode executar operações normais, como gravar ou armazenar bits.

Isso me incomodou por um longo tempo, e eu me perguntava ... é possível notar um atraso na atualização no nível do programa?

Base de treinamento de atualização dinâmica de RAM


Cada DIMM consiste em "células" e "linhas", "colunas", "lados" e / ou "fileiras". Esta apresentação da Universidade de Utah explica a nomenclatura. A configuração da memória do computador pode ser verificada com o decode-dimms . Aqui está um exemplo:

  $ decode-dimms
 Tamanho 4096 MB
 Bancos x Linhas x Colunas x Bits 8 x 15 x 10 x 64
 Fileiras 2 

Não precisamos entender todo o esquema DDR DIMM, queremos entender a operação de apenas uma célula que armazena um pouco de informação. Mais precisamente, estamos interessados ​​apenas no processo de atualização.

Considere duas fontes:


Cada bit na memória dinâmica deve ser atualizado: isso geralmente acontece a cada 64 ms (a chamada atualização estática). Esta é uma operação bastante cara. Para evitar uma grande parada a cada 64 ms, o processo é dividido em 8192 operações de atualização menores. Em cada um deles, o controlador de memória do computador envia comandos de atualização para os chips DRAM. Após receber as instruções, o chip atualizará 1/8192 células. Se você contar, então 64 ms / 8192 = 7812,5 ns ou 7,81 μs. Isso significa o seguinte:

  • Um comando de atualização é executado a cada 7812,5 ns. É chamado tREFI.
  • O processo de atualização e recuperação leva algum tempo, para que o chip possa executar novamente operações normais de leitura e gravação. O chamado tRFC é igual a 75 ns ou 120 ns (como na documentação da Micron mencionada).

Se a memória estiver quente (acima de 85 ° C), o tempo de armazenamento de dados na memória diminuirá e o tempo de atualização estática será reduzido pela metade para 32 ms. Consequentemente, o tREFI cai para 3906,25 ns.

Um chip de memória típico está ocupado atualizando por uma parte significativa de sua vida: de 0,4% a 5%. Além disso, os chips de memória são responsáveis ​​pelo compartilhamento não trivial do consumo de energia de um computador típico, e a maior parte desse poder é gasta em atualizações.

O chip de memória inteiro está bloqueado durante a atualização. Ou seja, cada bit na memória é bloqueado por mais de 75 ns a cada 7812 ns. Vamos medir.

Preparação da experiência


Para medir operações com precisão de nanossegundos, você precisa de um ciclo muito apertado, talvez em C. Parece o seguinte:

  for (i = 0; i < ...; i++) { //   . *(volatile int *) one_global_var; //   CPU.    _mm_clflush(one_global_var); //   ,     //    (25    160). // , - . asm volatile("mfence"); //     clock_gettime(CLOCK_MONOTONIC, &ts); } 

O código completo está disponível no GitHub.

O código é muito simples. Execute a leitura da memória. Despejamos dados do cache da CPU. Nós medimos o tempo.

(Nota: no segundo experimento, tentei usar o MOVNTDQA para carregar dados, mas isso requer uma página de memória especial não armazenável em cache e direitos de root).

No meu computador, o programa exibe os seguintes dados:

  # timestamp, tempo de ciclo
 3101895733, 134
 3101895865, 132
 3101896002, 137
 3101896134, 132
 3101896268, 134
 3101896403, 135
 3101896762, 359
 3101896901, 139
 3101897038, 137 

Geralmente, é obtido um ciclo com uma duração de cerca de 140 ns, periodicamente o tempo salta para cerca de 360 ​​ns. Às vezes, resultados estranhos aparecem acima de 3200 ns.

Infelizmente, os dados são muito barulhentos. É muito difícil verificar se há um atraso perceptível associado aos ciclos de atualização.

Transformação rápida de Fourier


Em algum momento, ocorreu-me. Como queremos encontrar um evento com um intervalo fixo, podemos enviar dados para o algoritmo FFT (transformação rápida de Fourier), que descriptografa as principais frequências.

Não sou o primeiro a pensar sobre isso: Mark Seaborn com a famosa vulnerabilidade que Rowhammer implementou essa mesma técnica em 2015. Mesmo depois de analisar o código de Mark, fazer a FFT funcionar foi mais difícil do que eu esperava. Mas no final, juntei todas as peças.

Primeiro você precisa preparar os dados. A FFT requer entrada com um intervalo de amostragem constante. Também queremos cortar os dados para reduzir o ruído. Por tentativa e erro, descobri que o melhor resultado é alcançado após o processamento preliminar dos dados:

  • Pequenos valores (abaixo da média de 1,8) das iterações do loop são cortados, ignorados e substituídos por zeros. Realmente não queremos fazer barulho.
  • Todas as outras leituras são substituídas por unidades, uma vez que a amplitude do atraso causado por algum ruído não é realmente importante para nós.
  • Eu estabeleci um intervalo de amostragem de 100 ns, mas qualquer número até a frequência Nyquist (dupla frequência esperada) servirá .
  • Os dados devem ser coletados em um horário fixo antes de serem enviados à FFT. Todos os métodos razoáveis ​​de amostragem funcionam bem, decidi pela interpolação linear básica.

O algoritmo é algo como isto:

 UNIT=100ns A = [(timestamp, loop_duration),...] p = 1 for curr_ts in frange(fist_ts, last_ts, UNIT): while not(A[p-1].timestamp <= curr_ts < A[p].timestamp): p += 1 v1 = 1 if avg*1.8 <= A[p-1].duration <= avg*4 else 0 v2 = 1 if avg*1.8 <= A[p].duration <= avg*4 else 0 v = estimate_linear(v1, v2, A[p-1].timestamp, curr_ts, A[p].timestamp) B.append( v ) 

Que em meus dados produz um vetor bastante chato como este:

  [0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 
  0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 0, 0, 0
  0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0 
  0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, ...] 

No entanto, o vetor é bastante grande, geralmente cerca de 200 mil pontos de dados. Com esses dados, você pode usar o FFT!

 C = numpy.fft.fft(B) C = numpy.abs(C) F = numpy.fft.fftfreq(len(B)) * (1000000000/UNIT) 

Muito simples, certo? Isso produz dois vetores:

  • C contém números complexos de componentes de frequência. Não estamos interessados ​​em números complexos, e você pode suavizá-los com o comando abs() .
  • F contém rótulos, cujo intervalo de frequência está em qual local do vetor C. Nós normalizamos o expoente em hertz multiplicando pela frequência de amostragem do vetor de entrada.

O resultado pode ser plotado em um gráfico:



Eixo Y sem unidades, pois normalizamos o tempo de atraso. Apesar disso, as rajadas são claramente visíveis em algumas faixas de frequência fixas. Vamos considerá-los mais perto:



Vemos claramente os três primeiros picos. Após um pouco de aritmética inexpressiva, incluindo a leitura de filtragem pelo menos dez vezes a média, você pode extrair as frequências básicas:

  127850.0
 127900.0
 127950.0
 255700.0
 255750.0
 255800.0
 255850.0
 255900.0
 255950.0
 383600.0
 383650.0 

Consideramos: 1000000000 (ns / s) / 127900 (Hz) = 7818,6 ns

Viva! O primeiro salto na frequência é realmente o que estávamos procurando e realmente se correlaciona com o tempo de atualização.

Os picos restantes em 256 kHz, 384 kHz, 512 kHz são os chamados harmônicos que são múltiplos da nossa frequência base de 128 kHz. Esse é o efeito colateral totalmente esperado da aplicação da FFT em algo como uma onda quadrada .

Para facilitar os experimentos, fizemos uma versão para a linha de comando . Você pode executar o código você mesmo. Aqui está um exemplo de inicialização no meu servidor:

  ~ / 2018-11-refresh-memory $ make
 gcc -msse4.1 -ggdb -O3 -Wall -Wextra measure-dram.c -o measure-dram
 ./measure-dram |  python3 ./analyze-dram.py
 [*] Verificando ASLR: main = 0x555555554890 pilha = 0x7fffffefe2ec
 [] Curiosidade.  Eu fiz 40663553 clock_gettime () por segundo
 [*] Medindo o tempo MOVQ + CLFLUSH.  Executando 131072 iterações.
 [*] Gravando dados
 [*] Dados de entrada: min = 117 avg = 176 med = 167 max = 8172 itens = 131072
 [*] Intervalo de corte 212-inf
 [] 127849 itens abaixo do limite, 0 itens acima do limite, 3223 itens diferentes de zero
 [*] Executando FFT
 [*] A frequência máxima acima de 2kHz abaixo de 250kHz tem magnitude de 7716
 [+] Os picos de frequência superior a 2kHZ estão em:
 127906Hz 7716
 255813Hz 7947
 383720Hz 7460
 511626Hz 7141 

Devo admitir que o código não é completamente estável. Em caso de problemas, é recomendável desativar o Turbo Boost, a escala de frequência da CPU e a otimização para desempenho.

Conclusão


Existem duas conclusões principais deste trabalho.

Vimos que os dados de baixo nível são bastante difíceis de analisar e parecem bastante barulhentos. Em vez de avaliar a olho nu, você sempre pode usar a boa e velha FFT. Na preparação dos dados, é necessário, em certo sentido, uma ilusão.

Mais importante, mostramos que muitas vezes é possível medir o comportamento sutil do hardware a partir de um processo simples no espaço do usuário. Esse tipo de pensamento levou à descoberta da vulnerabilidade original do Rowhammer , que foi implementada nos ataques Meltdown / Spectre e novamente mostrada na recente reencarnação do Rowhammer para a memória ECC .

Muito resta além do escopo deste artigo. Mal tocamos na operação interna do subsistema de memória. Para uma leitura mais aprofundada, recomendo:


Finalmente, aqui está uma boa descrição da antiga memória de ferrite:

Source: https://habr.com/ru/post/pt431010/


All Articles