前回は、可変長構造体の概要について説明しました。
実際、ファイルシステムは、このような構造体の集合として考えることができます。
セクタ上に記録されたバイナリデータを読み出し、その一つ一つを構造体として解釈していく。
そして、その先に続く情報をたどることで、ファイル名、フォルダ構造、配置情報、実データの位置などが見えてきます。
つまり、ファイルシステム解析とは、セクタ上にある情報を、構造体として順番に読み解いていく作業でもあります。
そこで、まず重要になるのがヘッダの概念です。
可変長構造体では、多くの場合、先頭にヘッダと呼ばれる領域があります。
このヘッダには、その構造体が何を表しているのか、どれくらいのサイズなのか、次のデータがどこにあるのか、といった情報が格納されます。
つまり、ヘッダを読むことで、その先にどのようなデータが、どのような形式で配置されているのかを判断できるようになります。
ファイルシステムでも、この考え方は非常に重要です。
多くの管理構造は、まずヘッダから始まり、その中に含まれるサイズ情報、オフセット情報、種別情報などをもとに、次の構造へ進んでいきます。
ここで関係してくるのが、データの連続性です。
これはC言語に限らず、データ構造全般に共通する考え方です。
情報は、メモリ上に連続したバイト列として配置されます。
配列も、その基本は同じです。
配列は、同じ型のデータをメモリ上に連続して並べた構造です。
そのため、要素の型のサイズが分かれば、先頭アドレスから順番に各要素へアクセスできます。
たとえば、4バイトの要素で構成された配列であれば、次の要素は4バイト先にあります。
このように、型のサイズがスケーリングファクタとして働くことで、配列を順番にたどることができます。
一方、構造体は、異なる型のメンバを一つにまとめたものです。
整数、フラグ、サイズ情報、オフセット、バイト列など、異なる意味を持つデータを、ひとまとまりの構造として扱うことができます。
本来、構造体のサイズはコンパイル時に決まる固定長のものです。
しかし、前回説明したように、構造体の最後に可変長データの開始位置を持たせることで、固定長のヘッダと可変長のデータ部分を組み合わせて扱うことができます。
この仕組みによって、ファイルシステムのような柔軟なデータ構造を解析できるようになります。
さて、実際の解析では、まずヘッダを読み込みます。
ここで気になるのが、エンディアンです。
つまり、複数バイトで構成される数値が、どの順番で記録されているのか、という問題です。
構造体には、uint16_t、uint32_t、uint64_tのように、エンディアンの影響を受ける型が多く含まれます。
たとえば、NTFSのようなWindows系のファイルシステムでは、多くの数値がリトルエンディアンで記録されています。
これは、IntelやAMDのCPUで一般的なメモリ表現とも一致します。
そのため、Windows環境でNTFSのセクタを読み出し、メモリ上に展開した場合、リトルエンディアンの値をそのまま構造体として解釈しやすい場面があります。
ただし、ここで注意が必要です。
ディスク上のデータは、単にCPU上の構造体がそのまま書き込まれているとは限りません。
ファイルシステムには、それぞれ仕様として定められたオンディスク形式があります。
つまり、NTFSであればNTFSの形式として、どの位置にどの情報があり、どのエンディアンで記録されているのかが決まっています。
解析では、その仕様に合わせて、セクタ上のバイト列を読み解いていく必要があります。
とはいえ、リトルエンディアンで記録されたファイルシステムを、同じくリトルエンディアン環境のC言語で扱う場合は、構造体として解釈しやすい利点があります。
セクタを読み出し、メモリに展開し、ヘッダを構造体として確認する。
そこからサイズ、オフセット、種別、次のデータ位置を読み取り、さらに先の構造へ進んでいく。
この流れが、ファイルシステム解析の基本になります。
ヘッダを読むことは、単に先頭の数バイトを見ることではありません。
その先に続くデータ構造全体を理解するための入口を確認する作業です。
ファイルシステムに触れるということは、セクタ上のバイト列を、ヘッダから順に意味のある構造として読み解いていくことなのです。
