FATで遊んだ話

お久しぶりです。今は人権がない状態なのですが、ここ最近はFAT(File Allocation Tableですよ)で遊んでます。特にFAT12/16をターゲットにしています。仕様はとてもシンプルですし、ドキュメントも、品質が高いものから低いものまでいろいろあります。

なにはともあれ、まずは
dd if=/dev/zero of=testimg1 bs=1024 count=32
mkfs.vfat ./testimg1
mkdir imgmnt
mount -t vfat testimg1 imgmnt
として環境を作って、その中で(英数字ファイル名、内容で)ファイルを作ったり書き込んだりしてから
umount testimg1
として、イメージ(testimg1)をバイナリエディタで見てみましょう。0埋めされている間にところどころ見覚えのある単語が見えるはずです。

下のスクショはルートディレクトリ領域。ここを主に触るソフトウェアを書きました。後述するように、SFNにASCII範囲外の文字列が入っていたり、LFNがUTF-8なのがわかるかと思います。実に謎です。

スクリーンショット_2016-04-20_21-39-27

ボリュームの最初にあるのがセクタサイズやクラスタサイズなどの読み込むのに必要な情報(予約領域)、次はどのセクタが使用済みか(FAT領域)、そしてファイル名及びその内容が記録されているクラスタ番号(ルートディレクトリ領域)、そのあとはデータ領域です。

とりあえずlsするものを作ってみました。8.3のSFN(Short File Name)とLFN(Long File Name)が記録されているので、それを読み出して表示するものです。FAT16でフォーマットされていれば、もちろん本物のUSBからでも読み込めます。マルチバイトファイル名にも対応しています。
./fat_ls /dev/sdb1
のように。マウントしないで中身を見られるのは面白い。


#!/usr/bin/env python3
import struct
from sys import argv
UTF8_prior = False
#FILENAME = input('img name?: ')
FILENAME = argv[1]
def getLFN(ldir_wholename):
ldir_wholename = ldir_wholename.replace(b'\xff\xff',b'') #remove padding
ldir_utf8name = ldir_wholename.replace(b'\0',b'') #UTF8だと想定してnull文字削除
if not UTF8_prior:
try:
ldir_strname = ldir_wholename.decode('utf_16_le')
#UTF8だった場合
except UnicodeDecodeError:
ldir_strname = ldir_utf8name.decode('utf_8')
else:
try:
ldir_strname = ldir_utf8name.decode('utf_8')
#UTF16だった場合
except UnicodeDecodeError:
ldir_strname = ldir_wholename.decode('utf_16_le')
return ldir_strname
data = open(FILENAME, 'rb').read()
BytesPerSec, SecPerClus, RsvdSecCnt, NumFATs, FATSize = struct.unpack_from(b'< 3x 8x H B H B 2x 2x x H', data)
dir_start = BytesPerSec * (RsvdSecCnt + FATSize * NumFATs) #BPB+FAT*2 0x600
entry_size = 32
saved_data = b''
delLFNContinue = False
while True:
name, attr = struct.unpack_from(b'< 11s c', data, dir_start)
if name==b'\x00'*11:
break
if attr==b'\x0f': #LFN
LDIR_Ord, LDIR_Name1, LDIR_Chksum, LDIR_Name2, LDIR_Name3 = struct.unpack_from(b'< B 10s x x c 12s 2x 4s', data, dir_start)
ldir_rawname = LDIR_Name1 + LDIR_Name2 + LDIR_Name3
#16進の一の位が1以外なら連続なので連結
ord_hex = format(LDIR_Ord, 'x')
if len(ord_hex) == 1: #2桁化
ord_hex = '0' + ord_hex
if ord_hex[1]!='1' and ord_hex!='e5': #e5は削除済みファイル
saved_data = ldir_rawname + saved_data
#削除済みLFNはどこまでか判定できないのでとりえあずスタックする
elif ord_hex=='e5':
saved_data = ldir_rawname + saved_data
delLFNContinue = True
else:
ldir_wholename = ldir_rawname + saved_data
saved_data = b''
ldir_strname = getLFN(ldir_wholename)
print('LFN: '+ldir_strname)
else: #SFN
#削除済みLFNが終わったことを確認
if delLFNContinue:
delLFNContinue = False
ldir_wholename = saved_data
saved_data = b''
ldir_strname = getLFN(ldir_wholename)
print('LFN: '+ldir_strname,' (DELETED)')
try:
if name[0:1]==b'\xe5': #一文字目が削除フラグ0xE5か
print('SFN: ?'+name[1:].decode('ascii'),' (DELETED)')
else:
print('SFN: '+name.decode('ascii'))
except UnicodeDecodeError:
print('SFN: (decoding failed)')
print()
dir_start += entry_size

view raw

fat_ls.py

hosted with ❤ by GitHub

LFNはUTF-16-LEだと書いてあるところが多いのですが、どうもUTF-8の間1バイトずつに0x00を入れた実装をしているものもあるようです。空イメージをmountでマウントして書き込んだところ、そうなりました。
しかし、XfceのThunarの自動マウントではUTF-16。また、mountもUTF-16で書き込まれたLFNが一つでもあるとUTF-16で読み書きするので、それまでに作ったUTF-8のファイル名は文字化けします。これはmountがイカれてるのか、そういう仕様も一部では使われているのかわかりませんが、どうも混沌としているようです。iocharsetというオプションで変わるみたいですが…もしかして、文字コードって指定されていないのかな。VFATの仕様書、どっかにないのかな…
UTF-16とUTF-8を100%正しく区別する方法はなくてどうしようもないので、今回作ったfat_lsでは、指定しない限りポピュラーなUTF-16を優先しています。
さらに、mountはUTF-8での読み書き時に、マルチバイトLFNの場合SFNにASCIIコード範囲外を使うという、実に訳のわからないことをします。これはいいんでしょうか。VFATは無法地帯ですね。

ファイルを削除すると、ディレクトリテーブルのSFNとLFNの1バイト目に0xE5がセットされます。チェックサムは元のままです。新たなファイルを作成すると、一番上にある削除済みエントリを上書きします。
ファイルの名前を変更すると、エントリでは削除した扱いになって、また新しいエントリを作成します。mv、renameともに同じ挙動でした。
データがあるクラスタ番号を書き換えて、2つのファイルで共通にすると、当たり前ですが、片方での変更がもう片方でも反映されます。ファイル長を変えてしまうと変な挙動になりますが。
そのうち、削除済みの0xE5を適当なコードに変えて、さらにチェックサムを書き換えてファイルをサルベージするものを書きたいです。チェックサムから元のSFNの1バイト目を逆算できるかもしれないです。

チェックサムを計算するやつはこれ


filename = 'TEST TXT'
total = 0
for newchar in filename:
#rotate(一番右のビットは一番左に)
total = ((total & 1) << 7) + (total >> 1)
#add
total += ord(newchar)
#桁溢れ防止
total = total & 0xFF
print(format(total, 'b'))
print(format(total, 'x'))
####結果
#$ python3 sfn_checksum.py
#10001111
#8f

view raw

sfn_checksum.py

hosted with ❤ by GitHub

それと、タイムスタンプを読むときに苦労したのですが、ビット0って一番右のことなんですね。リトルエンディアンなので左右のバイトを組み替えてから、右から読んでいく。気づきませんでした

バイナリエディタはOktetaというのを使っています。なかなかいいです。一列32バイトに設定すると、ちょうどルートディレクトリ領域が32バイトなのでちょうど一列に収まって見やすいです。

今日はこのへんで。スマホで書いたのでだいぶ微妙な文ですが。

最後に参考文献。ここを参照していろいろしました。

FATファイル システムのしくみと操作法

カテゴリー: コンピュータ パーマリンク

コメントを残す

以下に詳細を記入するか、アイコンをクリックしてログインしてください。

WordPress.com ロゴ

WordPress.com アカウントを使ってコメントしています。 ログアウト /  変更 )

Google フォト

Google アカウントを使ってコメントしています。 ログアウト /  変更 )

Twitter 画像

Twitter アカウントを使ってコメントしています。 ログアウト /  変更 )

Facebook の写真

Facebook アカウントを使ってコメントしています。 ログアウト /  変更 )

%s と連携中