C#でOpenBLASを使ってみた(標準的な実装との速度比較)

はじめに

ここ最近はNN(ニューラルネットワーク)についての勉強をしているのですが、ある程度内容もつかめてきたのでC#でNNライブラリを開発してみようと思いました。

しかし、筆者が使っているPCはGPU非搭載(厳密にはCPU内臓のGPUがある)であり、CUDAなどを使って手軽に高速化できる環境ではありません。

そこで様々なWebサイトや書籍などを参照して行列積などの重たい処理を高速化しようと試みましたがずいぶんと手間がかかってしまい、本来の目的である「NNのライブラリの開発」からかなり横道に反れてしまいました。

そこでnumpyなどはどのように実装しているのかGitHubでコードを読んでみたところBLASを利用しているようです。

そこで筆者もBLASの一種であるOpenBLASで高速化を試みてみました。

BLASとは

BLASとはBasic Linear Algebra Subprograms(直訳:基本線形代数演算サブプログラム)の略で、基本的な線形代数演算(行列積、内積...etc)を行うためのライブラリAPIデファクトスタンダード(事実上の標準規格)です。

あくまでもBLASは規格なので、その規格に準じたライブラリはたくさんあります。

有名どころは

OpenBLAS(旧:GotoBLAS)

ATLAS(自動チューニングによる高速実装)

Intel MKL(Intel製品専用)

cuBLAS(NVIDIA製品専用)

clBLAS(OpenCLによる記述)

なお、CPUで動作するものはOpenBLAS、ATLAS、Intel MKLであり、Intel CPUを扱うのであればIntel MKLが最も高速であり、CPUの種類を問わないのであればOpenBLASが最も高速です。ATLASは自動チューニングによる高速化という珍しい技術を使っていますがOpenBLASには負けてしまいます。

cuBLASはNVIDIA製のBLASNVIDIAGPUなどで行列演算を行うときに使います。

clBLASはOpenCLで記述されたBLASであり、AMDGPUなどで行列演算を行うときに使います。(OpenCLで記述されているのでintel HD Graphicsなどでも演算できるかもしれません。調べてはいない。)

上に挙げたライブラリはすべてBLASに準じた記述がなされているので、関数名などはちゃんと統一されています。(例えば行列積演算を行う関数名はgemmで統一されています。)

今回はOpenBLASを利用しました。intel MKLと比べ多くのCPUに対応している点と速度面でもintel MKLに負けるとはいえ、そこまで大きな差が開いているというわけではないからです。

標準的な行列積の実装

行列とは2次元配列に似たようなものです。以下のように数字が並んでおります。

\begin{pmatrix}x_{00} x_{01}\\x_{10} x_{11}\end{pmatrix}

プログラムにおいては行列を二次元配列ではなく一次元配列で表現します。上の行列の場合、一次元配列に以下のように配置します。

\begin{pmatrix}x_{00} x_{01}x_{10} x_{11}\end{pmatrix}

行をそのまま横に並べる形式です。(このような形式を行優先または横優先と呼びます。)

以下が実際に書いたC#での行列積の実装です。

static void SeqMult(Matrix left, Matrix right, Matrix product)
        {
            for (int i = 0; i < left.RowNum; i++)
                for (int j = 0; j < right.ColumnNum; j++)
                    for (int k = 0; k < left.ColumnNum; k++)
                        product[i, j] += left[i, k] * right[k, j];
        }

Matrixという謎の型があると思いますが、Matrixは内部で一次元配列で行列を管理する自作クラスです。アクセス速度はC#の一次元配列とほとんど変わりありません。
ついでにParallelクラスで軽く並列化したものが以下になります。

static void ParallelMult(Matrix left, Matrix right, Matrix product,
ParallelOptions options)
        {
            Parallel.For(0, left.RowNum, options, (i) =>
            {
                for (int j = 0; j < right.ColumnNum; j++)
                    for (int k = 0; k < left.ColumnNum; k++)
                        product[i, j] += left[i, k] * right[k, j];
            });
        }

またC++でも同様に行列積を実装しました。

void seq_mult(float* left, float* right, float* product,
 int leftRowNum, int leftColumnNum, int rightColumnNum)
{
	for (int i = 0; i < leftRowNum; i++)
		for (int j = 0; j < rightColumnNum; j++)
			for (int k = 0; k < leftColumnNum; k++)
				product[i * rightColumnNum + j] 
                          += left[i * leftColumnNum + k] * right[k * rightColumnNum + j];
}

こちらがOpenMPによる並列化バージョンです。

void parallel_mult(float* left, float* right, float* product,
 int leftRowNum, int leftColumnNum, int rightColumnNum)
{
#pragma omp parallel for
	for (int i = 0; i < leftRowNum; i++)
		for (int j = 0; j < rightColumnNum; j++)
			for (int k = 0; k < leftColumnNum; k++)
				product[i * rightColumnNum + j] 
                          += left[i * leftColumnNum + k] * right[k * rightColumnNum + j];
}

OpenBLASをC#で利用する方法

OpenBLASの導入方法は割愛します。ここではOpenBLASをどうやってC#で利用するか説明します。
まずlibopenblas.dllを用意します。そしてSystem.Runtime.InteropServices名前空間のDllImportで以下のように呼び出します。

[DllImport("libopenblas.dll")]
        static extern void cblas_sgemm(
            BLASOrder order,
            BLASTranspose transa, BLASTranspose transb, 
            int m, int n, int k, 
            float alpha, float[] a, int ldA, 
            float[] b, int ldB, 
            float beta, float[] c, int ldC);

上記のコードはOpenBLASのcblas_sgemmを呼び出す例です。C++のfloat*はC#ではfloat[]として扱います。
またBLASOrderやBLASTransposeはOpenBLASに合わせて定義した以下のような列挙体です。

    enum BLASOrder
    {
        CblasRowMajor = 101,
        CblasColMajor = 102
    }

    enum BLASTranspose
    {
        CblasNoTrans = 111,
        CblasTrans = 112,
        CblasConjTrans = 113,
        CblasConjNoTrans = 114
    }

これで準備が整いました。次は実際に速度を比較していきます。

速度の計測

実際にC#での実装、C++での実装、OpenBLASで速度を比較していきます。C++での実装はDllImportでC#側のコードに呼び出してから実行します。

実行環境

OS : Windows 10 Pro (Version : 1809) 64bit
CPU : intel core i5 5250U 1.6GHz(TB : 2.7GHz)
メモリ : 4GB

計測方法

System.Diagnotics名前空間のStopWatchクラスで測定。
乱数で初期化された1000 x 1000のfloat型の行列の行列積計算を10回行い、平均実行速度算出する。

結果

C#(逐次処理): 35524.7[ms]
C#(並列処理): 15334.8[ms]
C++(逐次処理) : 7802.9[ms]
C++(並列処理) : 3950.5[ms]
OpenBLAS   : 20[ms]

なんとOpenBLASは桁違いに速いです。
また、配列へのアクセスが絡む処理はC++で記述したほうが2倍ほど速いということも見て取れます。実行速度が求められる場合は演算処理をまとめたライブラリをC++で作った後にC#側のコードに呼び出して使ったほうが良いと思います。
もちろんC#でもunsafeコードを許可すればポインタが利用でき実行速度が向上しますが、変なリスクを冒すぐらいならC++を使うべきです。(C#でポインタを利用する意味が分からん・・・)

まとめ

今回の結果を踏まえると行列計算をC#で高速で行いたいときはOpenBLASを呼び出すのが一つの解決策として有効です。また、BLASにはない行列の転置処理などを実装するときなどでも、速度を求めるのであればなるべくC#は使わずにC++でdllを作って呼び出したほうが1.5倍から2倍ほど高速化できます。C#の配列はオブジェクトなので添え字がオーバーしていないかチェックする機能などを内包しており安全に利用できる反面、アクセスは遅いです。C++の配列はポインタなので利用には気を付けなければならないですがアクセスは速いです。(C#でもポインタをそのまま使う方法がありますがやめたほうが良いです。)
あぁ・・・NVIDIAGPUが欲しい・・・

おまけ(OpenBLASのscopy関数と.NETのBuffer.BlockCopy関数との比較)

長さ1000000のfloat型配列のコピーをOpenBLASに付属するscopy関数と.NETのBuffer.BlockCopy関数を速度比較してみましたが、どちらも1[ms]前後であり大差ありませんでした。Buffer.BlockCopyは普通に速いですね。
Array.Copy関数も同様です。(←先入観でめちゃくちゃ遅いと思ってた・・・)






GPTの仕組みと操作(導入編)

はじめに

本記事はディスクのパーティション管理方式の一種であるGPTについての解説兼備忘録です。

OSの起動の仕組みを始め、段階的に解説していく予定です。また実際にパーティションをプログラムで操作する方法についても軽く解説する予定です。

よく調べたうえで記事を書いているつもりですが、内容に誤りを含む場合があります。

実際に本記事を参考にプログラムを書く際は自己責任でお願いします。

(誤りを見つけたらコメント欄で報告をお願いします。確認ができ次第、修正します。)

 

セクタとは

セクタ(Sector)とはディスクの細かい区切りのことです。大抵の記憶デバイス(以下、”ディスク”と表記)は512Bを一区切りとしてディスクを管理します。現在ではディスクの最初のセクタから最後のセクタまでに0から通し番号を振り分け、その番号で特定のセクタにアクセスするのが一般的です。*1この通し番号のことをLBA(Logical Block Addressing)と呼びます。

 

パーティションとは

パーティションとは、ディスクのデーター領域の区切りです。1つのディスクには複数のパーティションを作ることができます。似た意味の言葉にボリュームとドライブという言葉があります。ボリュームとはディスク上のデーターを格納する領域という意味です。つまりパーティションというのはディスクの区切りで、その中に存在するデーター領域をボリュームと呼びます。
本棚で例えると、本棚全体がディスクで、本棚を仕切りによって分けたときにできる区画がパーティションです。そしてその区画の中のことをボリュームと呼びます。下図でいえば青枠がパーティションで、その中の水色の領域がボリュームです。

f:id:KALMIA:20190329130335p:plainそしてドライブというのはボリュームを利用するための装置もしくはその概念です。例えばDVDにはデーターが格納されていますが、そのままでは読み込めません。なのでDVDドライブを用意してそれを経由してデーターにアクセスすると思います。またディスク上に作ったボリュームにしてもCとかDとかのドライブラベルを割り当てないとアクセスできないと思います。あの概念のこともドライブと呼びます。

 

 

 

ディスクの容量を表す単位

ディスクの容量を表す単位には10進数で計算する方式と2進数で計算する方式があります。

10進数で計算する方式は以下のように定義されます。

8bit = 1B(バイト)  1000B = 1KB(キロバイト)  1000KB = 1MB(メガバイト)  1000MB = 1GB(ギガバイト)  1000GB = 1TB(テラバイト)  1000TB = 1PB(ペタバイト)  1000PB = 1EB (エクサバイト) 1000EB = 1ZB(ゼタバイト)

割と一般的な方式ですね。1000倍ずつで単位が変化します。

 

2進数で計算する方式は以下のように定義されます。

8bit = 1B  1024B = 1KiB(キビバイト)1024KiB = 1MiB(メビバイト

1024MiB = 1GiB(ギビバイト) 1024GiB = 1TiB(テビバイト)1024TiB = 1PiB(ペビバイト)1024PiB = 1EiB(エクスビバイト)1024EiB = 1ZiB(ジビバイト)

これも見たことあると思います。1024倍、すなわち2の10乗で単位が変化します。

本記事では2進数で計算する方式を採用します。

 

ちなみに余談ですが、Windowsは単位はGB、TBであるのにもかかわらず、1024倍で単位が変化する方式を採用しています。確かに昔は1024MB = 1GBという定義と1000MB = 1GBという定義が混在していましたが、今はGB、GiBという単位に分けることによって区別しています。

「まあ名残だからいいんじゃね?」

個人的にダメです。なぜなら多くのLinux系のOSやMacOSはちゃんと定義に則って表記しているため、Windowsも守ってもらわないとユーザーのミスを誘発する原因になります。

まあ、Windowsは一般ユーザーが多いですから、定義を突然変えると混乱が起きるのも事実ですが・・・・

 

BIOSについて + OS起動の仕組み

BIOS(バイオス)とはBasic Input/Output Systemの略であり、PCを起動したら真っ先に起動するプログラムです。BIOSマザーボードのROM(Read Only Memory)に組み込まれており、PC起動直後にCPUやメモリ、HDDなどのハードウェアのエラーチェック及び初期化を行います。そしてマザーボードに接続された記憶デバイス(HDDなど)からブートローダーと呼ばれるプログラムを読み込み、それを実行します。ブートローダーはディスクのパーティション情報(パーティションテーブル)を読み込み、OSがインストールされているパーティションを把握し、そのパーティションのブートセクタと呼ばれる領域をロードします。ブートセクタにはOSを起動するためのプログラム*2が記述されています。それを実行することによってやっとOSが起動します。

 

1つのPCにOSが複数インストールされている場合はOSの選択画面が表示されると思います。あの選択画面を表示させているのもブートローダーです。

 

MBRについて + MBRパーティション管理法

さてBIOSの説明においてブートローダーを接続された記憶デバイスから読み込むという説明をしましたが、そもそもブートローダーは記憶デバイスのどこに書き込まれているのでしょうか?

GPT登場以前は、ディスクの先頭セクタ(LBA = 0)ありました。このディスクの先頭セクタのことをMBR(Master Boot Record)と呼びます。しかし最近のPCではMBRは使われなくなってきました。そして代わりに現れたのがGPTです。ですがGPTの説明の前にMBRについて説明させてください。

MBRにはブートローダーとパーティションテーブルというものが記述されています。

パーティションテーブルにはパーティションエントリと呼ばれるものが複数記述されており、パーティションエントリには、パーティションの開始位置、終了位置、パーティションの種類などが書かれています。ブートローダーはこのパーティションエントリを見てどのパーティションがOSの起動に使われるのかを判断します。

MBRの場合、パーティションテーブルに書き込めるパーティションエントリの最大数は4です。つまりMBRでフォーマットされたディスクにはパーティションを4つまでしか作れません。

ですが、あくまでも4つまでしか作れないのは普通のパーティション(プライマリーパーティション)のみです。

拡張パーティションと呼ばれるものを作り、さらにその中に論理パーティションというものを作ればさらにパーティションを作れます。

拡張パーティションとは1つのディスクにつき1つしか作れない特殊なパーティションです。拡張パーティションの先頭にはEBR(Extended Boot Record)というMBRと似た領域が存在し、MBRと同様にそこにパーティションテーブルが存在します。故に拡張パーティションというのはパーティショニングが可能なパーティションです。そして拡張パーティション内に作られるパーティションのことを論理パーティションと呼びます。

さらにややこしいことに論理パーティションもEBRを持ちます。すなわち論理パーティション内にもパーティションを作れます。このような仕組みからわかる通り、拡張パーティション内にはディスクの容量が許す限り、無限に論理パーティションを作れます。

ちなみに論理パーティションにもOSがインストールできます。(ブートローダーの種類によっては論理パーティション内のOSを認識できない場合があります。)

さてMBRパーティションを4つまでしか作れないという欠点がありましたが、拡張パーティション、論理パーティションという概念の登場により、無理矢理に克服することに成功しました。が、MBRには最大の欠点が存在します。 MBRで管理可能なディスクの最大容量は2TiBまでなのです。

 

UEFIの登場

先ほど説明したBIOSMBRは古くから存在するシステムです。現在でもディスクを購入したときにMBRでフォーマットする人が存在します。(マザボが古い場合、GPTは使えません)

ですが、BIOSMBRは古いが故に時代遅れな部分が多いです。BIOSMBRが生まれた当時は8GiBのHDDが大容量とか言われていた時代です。しかし現代ではノートPCでさえ256GBのSSDもしくはHDDが一般的です。256GBのディスクの場合、パーティションを均等に4つ作ったとしても、1つのパーティションにつき64GBも使えます・・・

それなのにMBRパーティションは最大4つまで・・・ それを解決するために拡張パーティションが現れたわけですが、2TiBの壁はMBRではどうしても超えられません・・・

となるとMBRに代わる新たなシステムが必要です。そのシステムこそがGPTと言いたいところなんですが、まだ壁があります。BIOSさんです・・・・ BIOSは基本的にMBRを前提に作られています。なのでまずはBIOSに代わるシステムが必要なのです・・・

そこで現れたのがUEFI(Unified Extensible Firmware Interface)です。*3UEFIとはOSとファームウェアの間のインターフェースの仕様です。分かりやすく言えばOSとハードの仲介役です。ちなみにUEFIが採用されているBIOSのことをUEFI BIOSと呼びますが、BIOSは略してUEFIと呼ぶことが多いです。またUEFIのことをBIOSと呼び、従来のBIOSのことを旧BIOSと呼ぶ人もいます。

UEFIについてはこの程度の説明で済ませます。詳しくはWikipediaでどうぞ。

ja.wikipedia.org

GPTの登場

GPTとはUEFIで定義されているディスクのパーティション管理に関する規格であり、MBRに代わるものとして登場しました。現在販売されているPCのマザーボードには基本的にUEFIが採用されているため、ディスクもGPTでフォーマットされています。

GPTはGUID Partition Tableの略称です。さてPartition Tableは文字通りパーティションテーブルなので意味は分かると思いますが、GUIDとは何者でしょうか?

 

GUIDとはGlobally Unique Identifieの略で日本語訳はグローバル一意識別子です。

128bitの二進数の数値であり(表記されるときは16進数です。)ファイルやディスク、データーなどを識別する時に利用されます。

乱数によりGUIDは作成可能で、それによって作られたGUIDは世界で唯一存在する識別子になります。

「え?乱数で生成するんだろ? 自分が作ったGUIDとまったく同じGUIDが世界のどこかで生まれるかもしれないじゃん・・・」

実際に計算すると確率はほぼ0%です。

実際の計算方法は鳩の巣原理*4の応用です。詳しい話は別のサイトに譲ります。

ただし乱数のシード値を固定してGUIDを生成し続けたら、高い確率で衝突します。気を付けてください。

 

さて話を戻します。このGUIDですがGPTにおいてはパーティションの識別に使用されます。1つのパーティションにはパーティションタイプGUIDとパーティションユニークGUIDが振り分けられます。パーティションタイプGUIDというのはパーティションの種類を表すGUIDです。

どのGUIDが何を表すのかはWikipediaにまとめられていますので参照してください。(下記のWikipediaの「パーティションの型を表すGUID」という項目を参照)

ja.wikipedia.orgユニークGUIDというのは文字通りユニークなGUID、すなわち乱数で生成されるそのパーティション固有のGUIDです。

 

GPTは2番目のセクタ(LBA = 1)からスタートします。*5LBA = 1にはGPTヘッダーと呼ばれるものが記述されており、LBA = 2 ~ LBA = 33にはパーティションテーブルが記述されます。MBRは1つのセクタにブートローダーとパーティションテーブルをまとめていたのに対し、GPTは32個のセクタを利用しています。これによりパーティションの最大数は128個までとなりました。また大容量のディスクにも対応し、最大約8ZiBまで利用できます。TiBに換算すると909494702‬TiBです。当分はこんな大容量のディスクは現れないと思うので十分だと思います。(技術的に可能だとしても普通は必要ない)

 

 

GPTにおけるOS起動プロセス

 GPTにはMBRでいうブートローダーが記述されていません。代わりにEFIシステムパーティションというパーティション内にあるファイルが(拡張子は".efi")担当します。つまりこのファイルに記述されたプログラムがブートローダというわけです。MBRはディスクの先頭セクタに記述されたブートローダーがOSがインストールされたパーティションを探し出し起動していたのに対し、GPTは最初にEFIシステムパーティションを参照し、あとはそこにあるプログラムに全てを委ねます。*6

このようにブートローダパーティション内のファイルとして存在するので、MBRと違ってサイズの制限がない上に、普通のファイルと同様にOSからアクセスできます。

しかもこのブートローダーはUEFIが提供するAPIC言語で簡単に記述することが可能です。*7

 

 次回に続く

とりあえずGPTについて説明する前の導入は以上です。次回はGPTディスクを実際にダンプして細かく見ていきます。

*1:一昔前はCHSという方式でディスクの特定のセクタにアクセスしていましたが、ディスクの大容量化に伴い使われなくなりました。

*2:このプログラムのこともブートローダーと呼んだりすることもあります。

*3:実際はそのちょっと前にEFIという規格が登場し、最終的にそれがUEFIに発展しました。

*4:「鳩が11羽、箱が10箱あり、その箱に鳩を詰め込むとき、詰め込み方によらず必ずどれか1箱には鳩を2羽詰め込まなければならない。」という話です。

*5:LBA = 0はMBRとの互換性維持のために残してあります。

*6:ちなみにEFIシステムパーティションを参照するプログラムそのものはマザーボードのROMに組み込まれているUEFIが担当しています。

*7:MBRの場合はアセンブリなどを用いる必要があります。

ファイル隠蔽ソフト開発日記(その1)

エピローグ

皆さんは他人に見られたくないファイルなどはありませんか?

小学生であれば親と共有のPCというのも珍しくありませんし、社会人であれば会社に自分のラップトップを持っていくことでしょう。

前者の場合、自分がこっそりDLした見られたくないファイルを親に見られる可能性がありますし、後者の場合もひょんなことで自分の秘蔵ファイルが他人に見られてしまうこともあるでしょう。

また、学生が学校にラップトップを持っていく場合、先生などから「ゲームなどは持ってくるな」と指示されるかもしれません。これは無理な話です。学校用のPCと自宅用のPCをどちらも持っているならともかく、PCを一台しかもっていない人はなんて可哀そうなことでしょう。でも先生方は「ゲームのファイルを消せ」と言ってきます。学校でこっそりプレイする輩がいるからでしょう。(あっ・・ブーメラン・・・)

 

先生も先生でファイルチェックなどをしてくる方々もいます。(プライバシーの権利は生徒にもあるので、先生とはいえ許可なくファイルを見るのは普通はダメです)

生徒はあの手この手でファイルを隠します。まず思いつくのは隠しファイルやシステムファイル属性をファイルに付加することでしょう。しかし先生も手強い。そんなことは知っています。だって「ファイルの隠し方」でGoogleに検索をかけたら出てきますもん・・・

先生方は「ファイルを消す」まで納得してくれないでしょう。

ん?「ファイルを消す」だと!? そもそもファイルを消すとはどういうことでしょう? ファイルをゴミ箱に入れて空にすることですか?確かにシステム上はファイルは消えています。しかし消した直後であれば? ええ、ファイルの実体はまだディスク上に残っているのです・・・・

 

ファイル隠蔽ソフトの大まかな仕組み

まず、ディスク上に新たなパーティションを作成し、それをFAT32でフォーマットします。

そしてそこに隠したいファイルを放り込んだり、隠したいゲームをインストールします。

これで下準備は完了です。

次にそのパーティションを隠します。隠し方は簡単です。ディスクの先頭にあるパーティションエントリ*1をゼロで埋めることでOS上からはそのパーティションは空き領域として認識されます。

しかしこの状態ではフリーソフトなどで簡単にファイルを探すことができます。

最終処理としてそのパーティションのFAT領域*2を暗号化します。

これで隠蔽完了です。パーティションは空き領域として認識され、仮にそのパーティションを開けたとしてもファイルは存在しないorフォーマットが壊れている状態として認識されます。

パーティションエントリやFAT領域などよく分からない言葉が登場したと思いますが、別記事にて備忘録として後日にまとめようと思います。

 

※以降、隠蔽対象のパーティションのことを「SEC_PART」と呼称します。

実際に作ってみた(第一の壁)

このプロジェクトは開発言語C#でスタートしました。

リサーチの結果、"\\.\PhysicalDrive0"というパスにアクセスすれば、普通のファイルアクセスと同じようにディスクのRAWデータを読み書きできるということが判明したので、FileStreamを用いてそこへのアクセスを試みました。が、失敗。

f:id:KALMIA:20190326152146p:plain

上記の例外が発生しました。どうやらFileStreamではディスクの読み書きがサポートされていないようですね・・・

仕方がないのでWindowsAPIのCreateFile関数を用いて読み書きします。とはいってもC#からWindowsAPIの関数を呼び出すのは面倒なので、先にC++でディスクを読み書きする関数を作ってそれをC#から呼び出すことにしました。

やっぱり辞めました。kernel.dllから直接呼び出しました。これで余計なdllファイルが増えなくてシンプル。

 

実際に作ってみた(第二の壁)

最近のPCであればGPTでディスクがフォーマットされていると思うので、まずはGPTでフォーマットされている前提でプログラムを書き始めました。

GPTの仕組みについて軽く調べ、考えなしにプログラムを書きました。

パーティションを消去する関数を作って実行してみた次の瞬間・・・

 

ブルースクリーン

当たり前です。考えなしにパーティションエントリをゼロで塗りつぶしたのですから。

GPTはGPTヘッダーとパーティションエントリのそれぞれのCRC32チェックサムをとることによってGPTの破損をチェックしているようです・・・(←ちゃんと調べろし・・・)

CRC32チェックサムを求める関数を作って再チャレンジ。

f:id:KALMIA:20190326154057p:plain

次はうまくいきました。第2の壁を突破です。

そもそも事前にリサーチをしておけば、壁でも何でもないんですけどね・・・

 

実際に作ってみた(第三の壁)

「実際に隠蔽するファイルは、SEC_PARTに放り込んでいるけど、そのファイルのショートカットがCドライブ上にあるんだけど・・・」

そんなケースもあると思うので、SEC_PARTの隠蔽時にCドライブ上のショートカットを移動させ、一緒に隠蔽することにしました。ショートカットファイル程度であれば一瞬で転送できるので大したロスになりません。

が、復元時になんと問題が発生・・・

 

SEC_PARTを隠蔽している間は、OS上からSEC_PARTは存在していない扱いになります。

なのでその間にUSBメモリなど外部記憶装置をPCに接続した場合、もともとSEC_PARTが使っていたドライブラベル(DとかEなど)が振り分けられるケースがあります。

そうなるとSEC_PARTを復元したとき、隠蔽前と復元後で異なるラベルがそれに振り分けられるのです・・・・

これがめんどくさい・・・ 一度、SEC_PARTに移動させたショートカットファイルを戻すとき、ドライブラベルが変わってしまうと適切に戻せないのです。隠蔽時は"E:\ShortCut.lnk"だったものが復元時に"G:\ShortCut.lnk"などに代わっているからです。

ドライブラベルが変わらない前提でファイルパスを保存していたのが災いしました・・・

なんとかパーティション番号からドライブラベルを取得できないか調べていたところ、面白い情報を得ることができました。

ドライブラベルを使わなくともディスク番号とパーティション番号のみでファイルにアクセスできるというのです。

 

例えばCドライブがディスク0上の4番目パーティションに存在するとします。そこでWindowsフォルダー内のexplorer.exeにアクセスしたいときは以下のようなパスを指定すると思います。

"C:\Windows\explorer.exe"

このパスをドライブラベルではなくディスク0上の4番目パーティションという情報を用いて書き換えると以下のようになります。

"\\?\Harddisk0Partition4\Windows\explorer.exe"

ちなみにこのパスをエクスプローラーやコマンドプロンプトで試してみましたがうまくいきませんでした。当然、FileStreamでもダメでした。しかし、WindowsAPIの関数(CreateFile関数など)に放り込んだらなんとうまくいきました!

SUGEEE!!初めて知った!

どうやらWindowsNTオブジェクトマネージャーのGLOBAL名前空間なる場所にドライブラベルに関する情報がまとめられているらしい。なので"\\?\Harddisk0Partition4\"は内部で"C:\"に置き換えられているとのこと。実際にWinObj*3で調べてみたところ・・・

f:id:KALMIA:20190326230114p:plain

見つけました。どうやらWindowsはここに参照することによってドライブラベルを確認していたのですね。(にわかの知識なので間違っていたらご指摘ください。)

さて第三の壁突破。

 

試作品が完成

これまで第一、第二、第三の壁を突破しました。最終処理であるFATの暗号化は非公式のFATの仕様書*4を参照しながらコツコツと書きついに完成です。(ちなみに暗号化方式はAES128です。)

ドライブを不可視化させるのでソフト名は「InvisibleDrive」と命名し、略称を「INdrV」としました。"INV"が"Invisible"の略で"drV"が"Drive"の略です。

実際に動作させてみました。

www.youtube.comkonnkai

この実験で隠蔽するファイルはTARGETという名前のテキストファイルといくつかのゲームソフトです。

デスクトップにはTARGET.txtのショートカットとMoveFileListCreatorというソフトのショートカットがあります。

このMoveFileListCreatorというのはパーティションの隠蔽時に移動を行うファイル(主にファイルのショートカットなど)を設定するプログラムです。

 

まずMoveFileListCreatorにデスクトップ上のショートカットを登録します。

 

次に管理者権限のコマンドプロンプトでInvisibleDriveを実行します。(事前にInvisibleDriveを環境変数Pathに登録しておきました。)

 

"indrv /hide"というコマンドでドライブを隠し、"indrv /show"というコマンドでドライブを復元します。

 

ブルースクリーンにならなくてよかった・・・・ まあ、仮想マシンで実験しろよ という話なんですけど・・・

 

まだまだ課題が・・・

このソフト、わざわざパーティションを作ってそこに隠したいファイルを入れなければならないんですよね。めんどくさい。

しかもFATを暗号化しているとは言っても、ディスク上にファイルはあるわけですからある程度復元しようと思えば復元できてしまします。

まず前者の課題を解決するために、ファイル隠蔽ソフト実行時に動的にCドライブをぶった切ってファイルを隠すという方法がとれると思います。なんか危なそうですね・・・   まあ後日、いろいろと検討してみます。

 

後者の課題は、セクタをシャッフルするなどして意図的にディスクを断片化させることで解決できそうです、RWの速度は大幅に落ちますが秘匿性は増します。が、この方法はSSD限定ですね・・・ HDDの場合はRWの速度が落ちるだけではすみません・・・

HDDはヘッドが物理的に動くことによってデーターを参照するので、断片化を起こすと寿命が減ります。

なのでHDD向けの解決案として、ドライブの隠蔽時にランダムにいくつかのセクタを選択してそこを暗号化するという策です。もちろん断片化を起こす方法より秘匿性は落ちます。

 

次回に続くよ

今更だけどFAT32だと4GBを超えるファイルは扱えませんね・・・  NTFSのMFTを暗号化する方式にしようかな・・・  でもめんどそう・・・ FAT32のほうが資料が多いんですよね・・・

まあ、先程の課題が解決したら記事を書きます。コードはなんやかんやで汚くなってしまったのでリファクタリングしたらgithubに(気が向いたら)UPします。

本音を言えば、自分の幼稚なコードを見られるのが恥ずかしい・・・・

 

 

 

*1:ディスク上のパーティションの情報を管理している領域です。

*2:あるデーターがディスクのどこに配置されているかを記録している領域です。

*3:NTオブジェクトマネージャーの名前空間を閲覧できるソフトです。次のリンクからDLできます。

https://technet.microsoft.com/ja-jp/sysinternals/winobj.aspx

*4:FAT32に関してはそもそも公式が仕様書を出していません。

Twitter始めました

Twitterを始めました。
ブログの更新情報やどうでもいいつぶやきをしていく予定です。
どうかフォローをお願いします。
まあ、まだ大した記事を書いていませんが......

ブログをはじめました。

こんにちは。 本日よりブログをはじめましたKalmiaと申します。

情報系の大学や専門学校を卒業していない、完全な趣味の素人プログラマーです。

 

本ブログでは、自身の開発日記などを書いていきます。 更新の周期などは決めておりません。完全に気まぐれです。

よろしくお願いします。