FPGA開発日記

カテゴリ別記事インデックス https://msyksphinz.github.io/github_pages , English Version https://fpgadevdiary.hatenadiary.com/

RISC-Vテストパタン riscv-tests を使った命令セットシミュレータの検証 (Vモードのテストパタンの解析)

RISC-V の実装や命令セットシミュレータ (Instruction Set Simulator) を作ったとき、その実装の正当性をチェックするためにriscv-toolsのテストパタンを使用するという方法がある。

riscv-testsにはいくつかの種類があって、

  • rv32ui-p (32-bitモード、整数命令、Physical Addressing モード)
  • rv64uf-v (64-bitモード、浮動小数点命令、Virtual Addressingモード)

で、Virtual Addressingモードのテストパタンの検証なのだけれども結構ややこしいことになっている。 RISC-V の ISSであるSpikeで観測してみた。

この結果sretの動作についてはかなりわかってきたのだが、MMUの渡り方がかなり難しくて実装に手こずってしまった。

しかもRev1.7とRev1.10ではMMUのページテーブルの構成もかなり変わっている。 いろいろ試行を試したあたりでは、

  • AccessBit / DirtyBitのあたりの仕様が変わっている。
7. If pte:a = 0, or if the memory access is a store and pte:d = 0, either raise a page-fault exception

あたりもしっかり実装しないとPassできない。詳細は追ってブログに書こうと思う。

f:id:msyksphinz:20180406024424p:plain
図. RISC-V ページテーブルのアクセス制御表。

とりあえず、自作RISC-VシミュレータにV-ModeのMMUを実装したことでパタンの大半をPassできるようになってきた。

とはいえ400本中まだ140本か、、、先は長いな。それにRV32系のパタンが全部Failだ。

rv64ui-v-ld                    Fail
rv64ui-v-addw                  Pass
rv64ui-v-xor                   Pass
rv64ui-v-sltu                  Pass
rv64ui-v-subw                  Pass
rv64ui-v-bgeu                  Pass
rv64ui-v-andi                  Pass
rv64ui-v-srl                   Pass
rv64ui-v-sb                    Fail
rv64ui-v-slt                   Pass
rv64ui-v-sw                    Fail
rv64ui-v-and                   Pass
rv64ui-v-addi                  Pass
rv64ui-v-sraw                  Pass
rv64ui-v-ori                   Pass
rv64ui-v-sd                    Fail
rv64ui-v-srai                  Pass
rv64ui-v-sh                    Fail
rv64ui-v-jalr                  Pass
rv64ui-v-slti                  Pass
rv64ui-v-srlw                  Pass
rv64ui-v-sra                   Pass
rv64ui-v-lh                    Fail
rv64ui-v-auipc                 Pass
rv64ui-v-srliw                 Pass
rv64ui-v-lui                   Pass
rv64ui-v-blt                   Pass
rv64ui-v-bne                   Pass
rv64ui-v-srli                  Fail
rv64ui-v-sraiw                 Pass
rv64ui-v-lw                    Fail
rv64ui-v-bltu                  Pass
rv64ui-v-lbu                   Fail
rv64ui-v-slliw                 Pass
rv64ui-v-fence_i               Fail
rv64ui-v-beq                   Pass
rv64ui-v-or                    Pass
rv64ui-v-lb                    Fail
rv64ui-v-lhu                   Fail
rv64ui-v-add                   Pass
rv64ui-v-sub                   Pass
rv64ui-v-xori                  Pass
rv64ui-v-bge                   Pass
rv64ui-v-sll                   Pass
rv64ui-v-slli                  Pass
rv64ui-v-sltiu                 Fail
rv64ui-v-simple                Pass
rv64ui-v-addiw                 Pass
rv64ui-v-sllw                  Pass
rv64ui-v-lwu                   Fail
rv64ui-v-jal                   Pass

2018/04/07追記。32-bit V-Mode のパタンを追加してV-Modeのテストがかなり通るようになってきた。

grep Pass result.txt  | wc
    225     450    8100
grep Fail result.txt  | wc
    176     352    6336

RISC-Vテストパタン riscv-tests を使った命令セットシミュレータの検証 (Vモードのテストパタンの解析)

RISC-V の実装や命令セットシミュレータ (Instruction Set Simulator) を作ったとき、その実装の正当性をチェックするためにriscv-toolsのテストパタンを使用するという方法がある。

riscv-testsにはいくつかの種類があって、

  • rv32ui-p (32-bitモード、整数命令、Physical Addressing モード)
  • rv64uf-v (64-bitモード、浮動小数点命令、Virtual Addressingモード)

で、Virtual Addressingモードのテストパタンの検証なのだけれども結構ややこしいことになっている。 RISC-V の ISSであるSpikeで観測してみた。

spike -l rv64ui-v-add &> rv64ui-v-add.spike.log

sret命令は、割り込み処理から戻るための命令だ。この実装の実体は、ISSの実装から見ることができる。

  • riscv-isa-sim/riscv/insns/sret.h
require_privilege(get_field(STATE.mstatus, MSTATUS_TSR) ? PRV_M : PRV_S);
set_pc_and_serialize(p->get_state()->sepc);
reg_t s = STATE.mstatus;
reg_t prev_prv = get_field(s, MSTATUS_SPP);
s = set_field(s, MSTATUS_SIE, get_field(s, MSTATUS_SPIE));
s = set_field(s, MSTATUS_SPIE, 1);
s = set_field(s, MSTATUS_SPP, PRV_U);
p->set_privilege(prev_prv);
p->set_csr(CSR_MSTATUS, s);

中身を理解するのは少し難しいのだが、mstatusシステムレジスタの中身を理解しなければならないだろう。

f:id:msyksphinz:20180404224215p:plain
図. mstatusレジスタの割り当て。RV32モードとRV64モードで動作が異なる。

MPP / SPP / UPPレジスタは、モード遷移で現在のモードに移動してきた前のモードを保持している。

MPP : M-Mode に遷移してきたとき、一つ前のモード (S-Mode or U-Mode) SPP : S-Mode に遷移してきたとき、一つ前のモード (U-Mode) UPP : U-Modeに遷移してきたとき、一つ前のモード (なし)

実行結果をみてみると、

core   0: 0x00000000800000b4 (0x0f053f03) ld      t5, 240(a0)
core   0: 0x00000000800000b8 (0x0f853f83) ld      t6, 248(a0)
core   0: 0x00000000800000bc (0x05053503) ld      a0, 80(a0)
core   0: 0x00000000800000c0 (0x10200073) sret
core   0: exception trap_instruction_page_fault, epc 0x0000000000002ac8
core   0:           tval 0x0000000000002ac8
core   0: 0xffffffffffe000c4 (0x14011173) csrrw   sp, sscratch, sp
core   0: 0xffffffffffe000c8 (0x00113423) sd      ra, 8(sp)
core   0: 0xffffffffffe000cc (0x00313c23) sd      gp, 24(sp)
core   0: 0xffffffffffe000d0 (0x02413023) sd      tp, 32(sp)

まず、sret命令を実行してsepcからアドレスをPCに設定したのだが、このときにアドレス変換のページテーブル参照に失敗して、例外を発生した。 ところが、medelegレジスタのPage Table Fault には1が設定されており、M-Modeで処理されるはずの例外はS-Modeに移譲され、S-Modeで実行が継続される。

core   0: 0xffffffffffe000b8 (0x0f853f83) ld      t6, 248(a0)
core   0: 0xffffffffffe000bc (0x05053503) ld      a0, 80(a0)
core   0: 0xffffffffffe000c0 (0x10200073) sret
core   0: 0x0000000000002ac8 (0x00000093) li      ra, 0
core   0: 0x0000000000002acc (0x00000113) li      sp, 0
core   0: 0x0000000000002ad0 (0x00208f33) add     t5, ra, sp

つぎにsretで戻ってきたときに例外から戻ってきて、正しくページテーブル変換ができるようになって戻ってくる。

「30日でできる!OS自作入門」を読み始めた (14. 15日目)

遅まきながら、「30日でできるOS自作入門」を読み始めた。

30日でできる! OS自作入門

30日でできる! OS自作入門

今回はマルチタスクである。マルチタスク化するためにレジスタの退避などを行う機構を導入する。 っていうか、インテルx86はGDTに書き込むと自動的にタスクを認識して、jmpで自動的に切り替えてくれるのか。。。すごく便利だなあ。 trレジスタとGDTを使えば、簡単にマルチタスクが導入できる、というわけか。

ということで、とりあえず、マルチタスク化完了。タイマをたくさん使った。 カウンタを使いながら、ウィンドウでキーボードが叩けるようにしている。

カウンタを回すのがtask_b_main、そしてウィンドウを動かしているHariMain()そのものがtask_a_mainとなっている。

f:id:msyksphinz:20180331172750g:plain
図. マルチタスク化を実現
f:id:msyksphinz:20180331172824g:plain
図. マルチタスクを管理する機構を実装して、マルチタスク化を実現!

関連記事

「30日でできる!OS自作入門」を読み始めた (13. 14日目)

遅まきながら、「30日でできるOS自作入門」を読み始めた。

30日でできる! OS自作入門

30日でできる! OS自作入門

14日目はウィンドウやキーボードの操作など。画面を多少大きくした。

QEMUのバグなのか?画面モードを設定する場合のコードで、下記のMOV命令のアドレスを 0xfd000000 に変える必要があった。

f:id:msyksphinz:20180327224627p:plain
図. 大画面モード (640x480) に設定する
; 画面モードを設定

                MOV             BX,0x4101               ; VBEの640x480x8bitカラー
                MOV             AX,0x4f02
                INT             0x10
                MOV             BYTE [VMODE],8  ; 画面モードをメモする(C言語が参照する)
                MOV             WORD [SCRNX],640
                MOV             WORD [SCRNY],480
                MOV             DWORD [VRAM],0xfd000000
f:id:msyksphinz:20180327225529p:plain
図. 1024x768のモードに設定した。でかい!
f:id:msyksphinz:20180327230155p:plain
図. キーコードを受け取って表示した。
f:id:msyksphinz:20180327233624g:plain
図. テキストボックスを作り、キーボードからの入力を表示した。
f:id:msyksphinz:20180327234056g:plain
図. テキストボックスを作り、ウィンドウをマウスでクリックして移動できるようにした。

関連記事

CPUの脆弱性を突く新たなテクニック "BranchScope" で何が攻撃できるのか?(2)

前の記事はこちら。

msyksphinz.hatenablog.com

さて、BranchScopeを使うと分岐命令の挙動を抜き取ることで別プロセスのプログラムの動作、しいては別プロセスの秘密情報を読み取ることができるということが分かった。

本論文をもう少し進めていき、どのようなアプリケーションで影響があるのか。また対策は無いのか見ていく。

分岐予測ミスの判定はどのようにして行う?

さて、どのようにして分岐予測がヒットorミスであることを判定すれば良いのだろうか? とりあえず一番単純な方法として、ミスした場合のペナルティをタイマで測定してそれで判断すると言う方法が有り得る。 ユーザ空間でもrdtsc命令やrdtscp命令が使用可能なので、それで時間を計ればよいだろう。

そのために、1つの分岐命令で * 分岐するかしないか * 分岐予測がヒットしたかミスしたか に分けてペナルティを計測してみたのが図7となる。 ここで重要なのは、分岐するかしないかに関係なく、ミスが発生する場合に大きなペナルティが生じる。 図7aが、分岐しない場合における分岐予測のヒットとミスにおけるペナルティの分散具合、図7bが、分岐しない場合における分岐予測のヒットとミスにおけるペナルティの分散具合となる。

f:id:msyksphinz:20180330235713p:plain
図. 分岐予測ヒットとミスの場合におけるレイテンシの違い。分岐しようがしまいが、ミスが起きると大きなペナルティが発生する。(図は原論文から抜粋)

さらに、図8は上記の測定を何度も行い、システム上のノイズの影響をなるべく少なくした結果だ。 もちろん、最初の試行(キャッシュフェッチの影響が出る)よりも2番目の試行の方が誤差が小さい。

f:id:msyksphinz:20180331000032p:plain
図. 1回目に分岐予測を測定した場合と、2回目に分岐予測のペナルティを測定した場合のエラー率。1回目の場合はキャッシュミスの影響が出てしまう。(図は原論文から抜粋)

これにより、Victimプロセスが分岐を実行した後、Atackerプロセスが分岐命令を実行することによりVictimプロセスの動きを予測することが可能になる。

例えば、Victimプロセスにより分岐予測の飽和カウンタがStrongly Taken(ST)になっているものとしよう。 Victimプロセスが分岐命令を実行し、これが実際に分岐したものとする。 Atackerが分岐命令を実行し、これが分岐しない場合、(NNパタン)、表1におけるMM(Miss & Miss)パタンが観測されることにある。

Victimプロセスが分岐命令を実行し、分岐しなかったものとすると、 Atackerが分岐命令を実行し、これが分岐しなかった場合(NNパタン)、表1におけるMH(Miss & Hit)パタンが観測されることになる。

BranchScopeにより攻撃するアプリケーション

このBranchScopeだが、原論文によるとIntel SGX(Software Guard eXtension)をも突破することが出来るということらしい。

まずは、SGXというものを調査してみると、アプリケーションがハードウェア的に保護されたメモリ領域を作り、 他のプロセスからアクセスすることができない領域を作ることができるらしい。 これはシステムソフトウェアであっても、特定のアプリケーションによって作られたメモリ領域にアクセスすることはできない。

SGX を使っていても...?

表はSGXでプロセスを保護した状態において、内部プロセスのビット情報を分岐命令から推測した例である。 たとえSGXを使っていても、高い予測率でビット情報を読み取れていることが分かる。

f:id:msyksphinz:20180331000333p:plain
表. SGXでプロセスを保護しても、保護したプロセスが転送したビットデータはSpyプロセスにより高い確率で盗み見ることができるようになる。(図は原論文から抜粋)

BranchScopeで攻撃できるアプリケーション達

Montgomery Ladder

この演算は、現代の暗号化において非常に重要な要素であるRSAに使われている演算である。 基本的に秘密鍵kに依存しない演算ではあるため、タイミングや電力を読み取る形式には強いとされているが、秘密鍵kiに基づいて分岐命令を実行するためにBranchScopeによる予測が可能であるとされる。 最新の暗号ライブラリは分岐命令を含まないように慎重に設計されているのでこの場合は抜き取れる情報は限られるが、古いライブラリであれば情報を抽出できる。

libjpeg

画像を復元する際にのIDCT(inverse cosine transform)の操作に分岐命令が含まれているため、jpegライブラリの動作を読み込んで画像を復元することができる。 もともとページフォルトの回数をカウントするサイドチャネル攻撃により可能であるとされていた。 この攻撃はページフォルトの攻撃では行列の行と列の要素0であるかどうかを判定する分岐命令を実行しているが、さらにBranchScopeはこの行列の要素が0でなかったとしても攻撃することができる。

ASLR value recovery

メモリ領域のアドレスマップをランダム化する暗号化方式(ASLR)に対して、分岐命令が衝突するかどうかを観測することによりアドレス空間のランダム化機能をバイパスすることができる。

じゃあどうやって防げるの?

このBranchScopeのもともとの発想は、ハードウェア的に分岐予測器が共有されているところ、からスタートしている。 じゃあこれはどのようにして防ぐことができるのだろう?

ソフトウェアによるBranchScope防止方法

まず最も単純な方法として、秘密情報に対して分岐命令を発生させない。 慎重にプログラムを構築する必要がある。 ただし、大規模なシステムに対してこれをすべて適用することはできない。なのでこの方法は限定的な解決策だ。

もう一つの方法は、そもそも分岐命令を発生させないコンパイラの最適化だ。これは"if-conversion"と呼ばれている。 例えば、条件分岐命令を発生させない代わりに、"conditional move(cmov)" でデータフローを選択する方法がある。これだと条件分岐が発生しない。

ハードウェアによるBranchScope防止方法

PHTエントリ選択のランダム化

BranchScopeはどのエントリを選択するかを仮想アドレスから推定することができることを前提にしているが、このインデックス選択をランダム化することによりBranchScopeを無効化することができる。 各プロセスでランダム値を発生させ、この値により使用するPHTを選択するようにすれば、BranchScopeの攻撃を防ぐことができる。

そもそも分岐命令に対して分岐予測を実行しない

性能が低下することを受け入れることができるならば、秘密情報を扱うブロックについては分岐予測を行わないという手法もあり得る。

BPUの分離

プロセス毎に使用するBPUをハードウェア的に分離してしまう。 これはハードウェアの増加などの問題を招く。

CPUの脆弱性を突く新たなテクニック "BranchScope" の仕組みを読み解く (1)

ASPLOS-18 にて、 BranchScope と呼ばれる新しいCPUの脆弱性を付く攻撃手法が発表された。

今年の頭に ”Meltdown", "Spectre" で世間を騒がせたCPUの脆弱性の件だが、今回別の形で攻撃する手法を発見したということで、面白そうなので調査してみることにした。

こちらも現代のコンピュータアーキテクチャが誇る高性能CPUの機構をうまく活用するものであり、Spectreと考え方は近いようだ。 ちなみに、原論文は以下で入手することが可能だ。

  • BranchScope: A New Side-Channel Attack on Directional Branch Predictor

http://www.cs.ucr.edu/~nael/pubs/asplos18.pdf

ざっくりと前半のしくみの部分を読んで、なるほど、こういう手もあるのか。という感じ。 Spectreのキーとなる技術は、L1Dキャッシュに乗ることでデータアクセスのタイミングが変わり、それで他プロセスの挙動を知ることができるのだが、このBranchScopeはL1Dキャッシュを使わない。 その代わりに分岐予測器を使う。L1Dキャッシュがなくてもこの攻撃手法は成立するし、Spectreとは考え方の異なる手法と言えるだろう。 はてさて、新しいIntelのプロセッサはこの攻撃を回避できるように作られているのか?

ちなみに、筆者は例によってセキュリティの専門家ではないし、CPUアーキテクチャにしてもデスクトップクラスの本格的なものは設計経験がないので、いまいち本文から読み取れない部分があったりとか、間違っている部分があるかもしれない。 概念的な部分でしか読み取れないのはいつも悲しいところだ。。。


Directional Branch Predictor とは

本文中でDirectional Branch Predictor という用語が出ており、これは何かというと「分岐をするかしないか」というのを決める機構のことを言っている。

BPU(Branch Prediction Unit) は大きく分けて2つのユニットで構成されており、それが"Branch Target Buffer (BTB)" および "Directional Branch Predictor" である。 ちなみに、Spectre と Meltdown などのCPU脆弱性は前者のBTBを使う攻撃手法であり、 Directional Branch Predictor を使った攻撃については本論文が初めてである、とのこと。

一般的にアウトオブオーダプロセッサなどの高性能プロセッサは、分岐予測において "Hybrid Branch Prediction" というものが搭載されており、 ”Local分岐予測" と "Global 分岐予測" を両方活用したものだ。

"Local Predictor" または本文で "1-level Predictor" と呼ばれている分岐予測は、プログラムカウンタの値に依存した分岐予測であり、「そのPCアドレスの命令が過去にどのような挙動(分岐したかしないか?)」に基づいて分岐予測を行うものである。 アーキテクチャ界隈で有名な図で以下のものが相当する。これは2ビット飽和カウンタで、あるPCアドレスに存在する分岐命令がジャンプするかしないかを2ビットで予測する。

2ビットで予測するため、例えばループを実行中に、分岐予測器はそのループを抜けるときだけ予測を外すのだが、再度そのループに入ったときにもう一度分岐予測が外れてしまうことを防いでいる。

1ビット分岐予測で都合が悪い例として、例えば以下のようなプログラムが存在するとき、

  • j = 0 / i = 0 ... 10 : i が0から順番に進んでいく。内側のループは i = 2(i=1のときに分岐予測を外すと仮定) から i = 9までは分岐予測が当たるが、i = 10で外す。このとき、1ビット分岐予測だと「次の:A は分岐成立しない」と予測する (1ビットしか記憶がないため)
  • j = 1 で再び内側のループを実行すするとき、 j = 1 / i = 1の場合(内側の最初のループ実行時)に、↑で「:Aは成立しない」と予測したため再び外す。i=2から当たり始める。

上記のように、1ビット分岐予測だと、10回のループが発生した場合、最初と最後は必ず外すことになってしまい、少し予測精度が悪い。

for (j < 10) {
    for (i < 10) {
        // 処理
        i++
    }  : A  // i == 10 のときだけループを抜ける (→ 分岐予測が外れる)
}

一方で2ビット分岐予測により「1回外したけどこれまでずっと分岐成立してたし次も分岐するだろ」という予想を立てておけば、内側のループの最初 (i=0)で分岐予測を外すことがなくなり、予測精度が増す。

と、長々と説明したがこれが "1-level Predictor" の仕組みである。(2ビット持っていても1-level Predictorなのだ)

次に、"2-level Predictor" というのは、分岐命令を実行したPCアドレスに依存せず、過去に実行した分岐命令の相対位置関係を利用する。プログラムを実行してきた中で、過去にどのようなパタンで分岐が実行されたかの情報をBranch History Table (BHT)に記録する。このパタン記録と分岐アドレスから、Pattern History Tableのどのエントリにアクセスするかインデックスを計算し、どの分岐予測器を使用するか決定する。

BranchScope の攻撃の概要

  • ステージ1. PHT エントリの準備 攻撃プロセスにより、ターゲットとするPHTエントリを特定の状態にする。PHTエントリは、各エントリ内に2ビット飽和カウンタが入っているので、特定パタンの分岐を何度も実行すると、特定の状態にPHTエントリを設定できる。
  • ステージ2: victimプロセスを実行する。victimプロセスによりPHTの状態が変更されるまで、攻撃プロセスは待ち合わせを行う。
  • ステージ3: PHTエントリの状態を検出する。攻撃プロセスターゲットのPHTエントリを叩くようなは分岐命令をさらに実行する。 これにより攻撃者は分岐予測の予測結果のタイミングを測定することで、victimプロセスの分岐方向を特定することでvictimプロセスを予測できるようになる。

ここで必要なのは、victimプロセスと攻撃プロセスで使用するPHTエントリをあわせることが出来ること。 PHTエントリは何で決まるかというと分岐アドレスで決まるため、それぞれのプロセスの仮想アドレスを取得して一致させせることでPHTエントリを衝突させることが出来る。

一方でアドレスのランダム化技法(ASLR: Address Space Layout Randomization)を使っていてもそれを元に戻す手法を使うことが出来るとしている([48])。

さて、ここでで問題になるのがどのようにしてPHTの衝突を起こさせるかと言うkとである。 このためには、非常に複雑なG-share分岐予測を使うのではなく、1-level分岐予測器が使われるように仕向けるのが良いだろう。 現代のプロセッサはハイブリッド分岐予測といって、1-level Predictorと2-level Predictorの両方を使い、その中でヒット率の良いものを採用すると言う機構を取っているため、わざと1-level Predictorの分岐予測精度を上げてやり、1-level Predictorを使うように仕向ける必要がある。

次に、PHTの衝突を起こさせたとして、次にどのようにPHTの状態を特定するか、ということになる。 PHTの状態を読み取るには、分岐予測の方向を読み取ることが出来るようになる。このためには、victimプロセスによりPHTの状態を特定の状態にしておくことが必要になる。

選択論理を制御することで、VictimプロセスとAtackerプロセスで衝突を発生させる方法

まず、victimプロセスと衝突プロセスをが使用するPHTを一致させるためには、選択論理を制御する必要がある。 上記のように構成のプロセッサはハイブリッド分岐予測により1-level Predictorと2-level Predictorを使い分けているため、どうにかしてプログラムを工夫して1-level Predictorを使うように仕向けたい。

1-level Predictorと2-level Predictorの特徴を上手く使い分ければこれが実現可能だ。

まず、1-level Predictorは同一の分岐命令であっても、その結果がランダムである場合に予測することが難しい。それは、その命令の過去の分岐結果が、次の分岐の結果に依存しないからだ。 一方で、2-level Predictorはうまくパタンにはまれば予測できるようになる。

そこで、以下のような実験を行った。

  • 10ビットの変数を用意し、変数の値をランダムに初期化する。この10ビットの値は、それぞれのビットにより分岐が成立するかどうかを決定する要素となる。
  • 単一の(つまり同じアドレス上に配置された)分岐命令で、この10ビットの変数の内容に基づいて分岐を行う。 この分岐を20回実行するとどうなるだろうか? 1-level Predictorは分岐予測精度が50%程度になるだろう。しかし、10ビットの変数を2回使うので、g-shareの分岐予測器はパタンを記憶し分岐予測が出来るようになる。 この結果を示したのがグラフ2.となっている。縦軸は10回の分岐命令においてヒットした回数で、横軸は試行回数だ。
f:id:msyksphinz:20180330010344p:plain
図. 10ビット変数の値に基づく分岐命令の分岐予測結果。回数を繰り返すごとに予測精度が向上する。これがg-share分岐予測器の能力だ。(図は本論文から抜粋)

最初の1ループ目はほぼ50%の精度となっている。 これは1-level Predictorではこのスタイルのループは予測することは出来ず、また2-level Predictorではまだパタンを記憶できていないため予測が出来ない。 しかしパタンを覚え始める、つまり2ループ目から先は2-level Predictorがパタンを記憶しているため分岐精度は向上しており、ほぼ100%に近づく。 つまり、このようなプログラムのパタンでは2-level Predictorが予測精度を向上させていることが分かる。

では、ループの最初、つまり1-level Predictorと2-level Predictorのどちらも学習を行っていない場合はどうか。 ここで、そのような場合は1-level Predictorが使われると推測する。 これは、2-level Predictorのほうが学習に時間が掛かるため、学習時間が短い1-level Predictorが使われるであろうと言う直感である。

なんとかプログラムを工夫すれば、1-level Predictorのみ使うことが出来そうだ。

必要なのは、 - 2-level Predictorに検知されやすい分岐パタンが発生することを避けること。分岐パタンが予測しやすいと、2-level Predictorの予測結果が選択されてしまう。 - ターゲットとなる分岐命令が使われてないこと。BPUにとって始めて遭遇する分岐命令ならば、2-level Predictorが使われず1-level Predictorが選択されるはず。

まず、 * 完全に分岐予測が難しい状況を作り出すこと。2-level Predictorの分岐精度が低下している場合、1-level Predictorが使用されるため、アドレスの衝突が起こしやすい。 * Branch Predictorが学習する前の状態を作り出すこと。分岐予測率が低ければ、1-level Predictorが利用されやすい。

そこで、以下のようなプログラムを作ってみる。これがSpyプログラムである。 分岐命令の後続にランダムにnopが挿入されている。これにより分岐先アドレスが分散され、PHTをまんべんなく初期化する目的がある。 すべてのPHTテーブルを初期化するのに、100,000個分の分岐を実行すればよいということが実験の結果分かった。

randomize_pht:
cmp %rcx, %rcx;
je .L0; nop; .L0: jne .L1; nop; .L1: je .L2;
............
.L99998: je .L99999; nop; .L99999: nop;

次に、分岐予測の論理を見てみよう。分岐予測器の内部、つまりPHTのエントリの中身はFSM(Finite State Machine)だ。これは上記で説明したように、飽和カウンタとして動作し、2段階の分岐予測器として動作する。 と、教科書的には言われているが、実際にはどうだろう?分岐予測器の内部を事実から推論してみよう。

分岐予測器を思い通りに動かすテクニック

分岐予測器を使った攻撃について見てみる。ここでは、3ステージに分けて攻撃手法を説明している。

  1. Primeステージ : 同じ方向に分岐命令を3回実行する。これにより、間違いなく2ステージ飽和カウンタは、Strong Taken(ST) / Strong NotTaken (SN)に移動する。
  2. Targetステージ : 分岐予測器を今度は1回だけ、Taken or NotTaken で分岐命令を実行する。
    1. もし、もともとST状態にいて、Takenならば相変わらずSTステージのままであるはず。
    2. もし、もともとSN状態にいて、TakenならばWN(Weakly NotTaken)に移動するはず。
    3. もし、もともとST状態にいて、NotTakenならばWT(Weakly Taken)に移動するはず。
    4. もし、もともとSN状態にいて、NotTakenならば相変わらずSNステージのままであるはず。
  3. Probingステージ : 分岐予測器が予測ミスをするように、もう2回ほど分岐命令を実行する。このときの分岐予測結果を観測することにより、2. のTargetステージの分岐結果を予測できるようになる。

たとえば、

  1. Primeステージにて分岐結果がTakenとなる命令を3回実行したとする。飽和カウンタはSTとなる。
  2. Targetステージでの分岐命令がTakenならば、飽和カウンタはSTのままである。Targetステージでの分岐命令がNotTakenならば、飽和カウンタはWTとなる。
  3. ProbingステージでNotTakenとなるように分岐命令を2回実行する。
    1. Targetステージの分岐がTakenならば、飽和カウンタはST → WT → WSとなり、2回とも分岐予測を外す。
    2. Targetステージの分岐がNotTakenならば、飽和カウンタはWT →WN → SNとなり、1回目は分岐予測を外すが、2回目は予測が当たる。

つまり、Primeステージで攻撃者が分岐予測器をある方向に持っていき、次にvictimプロセスが1回分岐を実行する。最後にTargetステージで攻撃者が分岐命令を実行することで、Targetステージの動作を予測できるようになる、ということだ。 これがBranchScopeの基本的な考え方となる。

f:id:msyksphinz:20180330010148p:plain
図. 2ビット飽和カウンタの動きと、その飽和カウンタの動きから、前の分岐結果を読み取る仕組み (本論文から抜粋)

BranchScopeの実装

BranchScopeの実装は疑似コードで提示されており、2つに分かれている。

  • Victimプロセス (つまり攻撃対象のプロセスをモデル化したもの)
  • Atackerプロセス (Victimプロセスの中身を読み取る)

それぞれのコードは以下の図のようになっている(本論文より抜粋)。

f:id:msyksphinz:20180330005425p:plain
図. VictimプログラムとAtckerプログラムの疑似コード

Victimプログラムは、sec_dataと呼ばれる配列に格納されているデータに基づきプログラムが動作する。このsec_dataの配列はAtackerプロセスからは見えていないもので、Atackerは分岐結果を見ながらsec_dataの内容を読み解いていく。

ここで重要になるのは、上記にも示した通り、Victimプログラムが一度だけ実行される環境を作り出すことだ。これにはいくつか手法があるようだが、Victimプログラムをわざと非常に低速に動くように設定し、分岐が一度だけ実行され、Atackerにプロセスが移るような環境を作り出す。

そしてAtacker側のコードだが、まずはPHTの初期化を行う(randomize_pht())。これにより2-level Predictorの予測精度が低下し、1-level Predictorが使われるようになる。

次にVictimプロセスが分岐を一度だけ実行される。その間Atackerプロセスは待っている (usleep(SLEEP_TIME))。

最後に、spy_functionコードを実行し、if(array[i]) (つまり方向の決まっている分岐命令)を2回実行し、その時の分岐予測結果を読み取る (これにはいろんな手法があるだろうが、おそらくカウンタを使って次の命令のフェッチまでにかかる時間を計測するのがよかろう)。

この計測時間の違いにより、Victimプロセスで分岐が行われたかどうかを判定し、しいてはsec_dataの値が推測できるようになる、というカラクリだ。

f:id:msyksphinz:20180330012237p:plain
図. Atacker と Victim プログラムの実行手順

「ゼロから作るDeep Learning」第7章のCNNでCIFAR-10に挑戦してみる (3. CNNにおけるConvolutionの理解)

「ゼロから作るDeep Learning」の第7章、CNNを勉強したので、PythonではなくてC言語で1から実装してみたい。

Convolutionの理解

例えばCIFAR-10を実行する場合、入力の画像は32×32×3である。これを[3,32,32]と表記する。

これに対して畳み込みのフィルタを適用する。本書で使っているフィルタは、30フィルタ、3チャネル、縦5、横5のフィルタなので、[30,3,5,5] と表記することができる。 とりあえずBのことは置いておいて、入力画像に対してこのフィルタを適用すると以下のようになる。 ちなみにここではバッチサイズは1と設定している。

30枚のフィルタを使っているのでフィルタとしては30個の畳み込み後の画像が作られる。 それぞれに3チャネル分がのフィルタがあるが、3次元の畳み込みでは、3チャネル分の畳み込み適用後の数値を加算するので、結果としては1枚の画像となる。 つまり、32×32に対して5×5のフィルタを適用した結果作成される28×28の畳み込み後の画像が、30枚分作られるということになる。

f:id:msyksphinz:20180325002647p:plain
図. CIFAR-10の画像に対する畳み込みの適用イメージ。

と、ここまで頑張ってみたのは良いものの、C++に落とし込む時間がない... とりあえず今日はここまで...