という、気になる話が目に留まった。
プロセッサ脆弱性「Meltdown」と「Spectre」のまとめサイト開設
ふーん、で終わろうかと思ったんだけども、品川先生のtweet
Meltdown の本質ってC言語で書くとこれだけだよね。
— 品川 高廣 (@utshina2) 2018年1月4日
a = *kptr;
b = array[a<<12];
aにカーネルメモリの値が入って、arrayの対応する部分がキャッシュに乗る。もちろん例外を起こして実行は取り消されるけど、キャッシュはそのままなので、arrayのアクセス速度を測ればaの値がわかる。
を見て、tweetだけでは理解できなかったので、ついつい論文2つを斜め読みしてみた。
理解内容は最後にして、まずは結論から。
結論
内容は、特にMeltdownの方はプロセッサの根本部分だし簡単に読み出せそうだし結構まずそう。対策は取れるだろうけど、いずれの方法にしてもプロセッサパフォーマンスをある程度犠牲にする必要があるのではないかしら。
感想
対策関係者は大変だろうけどちょっとそれは置いておいて、内容を理解できたし、μOPを身近に感じることができた初めての経験だった。うれしい。
μOPやOut Of Order(OoO)を説明するときの、ちょっとしたネタになりそうな予感。実験コードも簡単に書けそうだし。
新人研修向けのワンポイント小ネタとしてもおもしろいんじゃないかしら。
#自分の理解
さて。
a = *kptr;
b = array[a<<12];
kptrにはカーネルのアドレスが入っていて、ユーザからは見ることができない場所。Windowsなら1行目で「このアプリは不正アクセスで落ちました」的なダイアログとともにアプリがお亡くなりになる。
C言語的に見れば、1行目を実行してから2行目を実行する。1行目で例外が発生すれば2行目は実行されない。
非常に当たり前な話だ。
しかし、C言語は機械語となり、さらにIntel CPUの中ではμOPコードというさらに細かい単位に分けられ、かつ、関連性のないものは複数同時に実行したり、実行順序を入れ替えたり(Out Of Order)、分岐に関しては予想で実行してしまったり(分岐予測)、ととんでもないことをしながら辻褄が合うようにしている。考えた人ほんと凄い。
(ちなみに、Spectreは分岐予測による投機実行を利用している。)
a = *kptr;
の行で何が起こっているかといえば、この行に来る前のすでにkptrの値が決まった時点で、メモリへのアクセスを開始してしまっている。なぜならメモリとのやりとりはプロセッサ動作に比べると亀のように遅いため、実行前にできるだけ早くキャッシュまでは持ってきておきたいから。
そして、それを変数aに入れるわけだが、CPUにはレジスタという入れ物があってそこに入れることになる。が、実はCPUの中には、見えているレジスタの裏に見えないレジスタがたくさんあって、実はそこに入る。変数aに割り当てられたレジスタにはまだ入らない。でも、実質aの内容は決まったので2行目のメモリも先読みできてしまうため、メモリへのアクセスを開始する。その頃、実は1行目のアドレスは不正アクセスですよーの例外処理が動きだす。
結果として、変数a,bに割り当てられたレジスタには値は入らないが、実はキャッシュにまではデータが来てしまっている。bの方はタイミング次第ということもあるようだけど。
あとは、そのキャッシュの内容を読み出せばよい。
どうやって読みだすのか。
array[]の要素を0から順番に読み出していきそれぞれの読み出しにかかる時間を計る。実行前にarray[]の内容を含んだキャッシュをすべてFlushしておけば、キャッシュに乗っている要素の呼び出しとそうでない呼び出しでは、読み出し時間に大きな差がでるため特定できる。いや、特定できるようにa<<12をしてpageを分けてキャッシュが被らないようにしている1。つまり、飛び抜けて読み出しが早かった要素のインデックス値が、通常は読めるはずのないメモリの内容ということになる。つまり、どのアドレスでも読めてしまう、と。
細かい実装は、Spectreの方にコードがあってZero page確保の省略(Copy on Write)を避けたり、最適化を逃れたり、読み出し時間の判定とか泥臭くて面白い。
このおもしろさは、Magic Ring Buffer以来かも。
おわり。
参考
-
キャッシュライン単位でもよいような気もするけど、TLBやPTEあたりも関係するからということかしら。 ↩