0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 3 years have passed since last update.

GPTの上のFAT32を読む

Posted at

OS作りにあたってファイルシステムについて。
ほぼ自分の理解のために書いたので読みにくくても許して


HDDの1セクタは512byteとする
LBAとは:セクタの通し番号 0~2^28(Largeは^48)
書かないけどtypedef structのときに#pragma pack(1)なり__attribute__((packed))なりを忘れずに。

とりあえず今回はファイル名一覧を出すまで。

gpt

参考: https://wiki.osdev.org/GPT

GPTのHDDのレイアウトは

LBA0 PMBR(保護マスターブートレコード)。古い規格(MBR)のみ対応のPCとかに壊されるのを防ぐために開けとく
LBA1 ヘッダ。シグニチャでGPTか識別できる。
LBA2~33 パーティションエントリテーブル
... データ本体
LBA 後ろから2~33 パーティションエントリテーブルのミラー
LBA 後ろから1 ヘッダのミラー

よくあるやつでサイズの違うHDDをバックアップでddしたときになんかエラー吐くのは末尾のミラーがないから

で、ヘッダとテーブルエントリの構造はこんなかんじ

 typedef struct {
     uint32_t data1;
     uint16_t data2;
     uint16_t data3;
     uint64_t data4;
 } GUID;
 
 typedef struct {
     uint8_t signature[8];
     uint32_t revision;
     uint32_t header_size;
     uint32_t CRC32_checksum;
     uint32_t _reserved;
     uint64_t header_lba;
     uint64_t mirror_header_lba;
     uint64_t GPT_entry_first;
     uint64_t GPT_entry_last;
     GUID guid;
     uint64_t entry_strat_lba;
     uint32_t entry_count;
     uint32_t entry_size;
     uint32_t CRC32;
     uint8_t _must_be_0[];
 } GPTHeader;
 
 typedef struct {
     GUID type_GUID;
     GUID partition_GUID;
     uint64_t start_lba;
     uint64_t end_lba;
     uint64_t attributes;
     uint16_t name[36];
 } GPTPartitionEntry;
 

やることは、ヘッダ読む -> entry_start_lba(多分普通2だけどね)からentry_count*entry_sizeだけ読む -> エントリ見る

entry_countの意味はアプリによって変わってくるらしく、最大エントリ数の場合と使用されているエントリの数の場合があるらしい。

signatureは"EFI PART"。

nameはutf16。

読むとこんな感じ

ファイル名はいい感じにしてね。デバイスファイルにすると大抵rootで実行しないといけないと思う。

void read_fat32(GPTPartitionEntry partition);
_Bool is_fat32(GPTPartitionEntry partition);
FILE *device;

void print_partition_name(uint16_t *utf16) {
    for (uint64_t i = 0; utf16[i] && i < 36; i ++) {
        putchar((uint8_t)utf16[i]);
    }
}

int main() {
    device = fopen("/dev/sdd", "rb");
    if (device == NULL) {
        perror("fopen");
        return 1;
    }

    puts("----- GPT -----");

    uint8_t lba0[512];
    GPTHeader gpt_header;
    GPTPartitionEntry partition_entries[128];


    fread(lba0, 512, 1, device);
    fread(&gpt_header, 512, 1, device);

    char gpt_signature[9];
    strncpy(gpt_signature, gpt_header.signature, 8);
    gpt_signature[8] = 0;
    puts(gpt_signature);

    if (strncmp(gpt_header.signature, "EFI PART", 8) != 0) {
        puts("partition table is not GPT...");
        exit(1);
    }

    fread(partition_entries, 128, 128, device);

    for (uint64_t i = 0; i < 128; i ++) {
        if (partition_entries[i].start_lba) {
            printf("name: ");
            print_partition_name(partition_entries[i].name);
            printf(", strat: %ld, end: %ld\n", partition_entries[i].start_lba, partition_entries[i].end_lba);
            if (is_fat32(partition_entries[i])) {
                read_fat32(partition_entries[i]);
            } else puts("  not fat32 entry");
        }
    }
    return 0;
}

read_fat32とis_fat32は後から。

出力は多分こんなかんじ

これが

スクリーンショット 2020-07-09 00-26-47.png

こう

----- GPT -----
EFI PART
name: partition_1, strat: 2048, end: 7385087
  not fat32 entry
name: partition_2_fat, strat: 7385088, end: 15130623

fat32

参考: http://elm-chan.org/docs/fat.html#first

1つのFATパーティションを論理ボリュームという。MBRとかだとFATがそのままドーンと乗ってるので物理ボリューム==論理ボリュームとなる(多分)。現代は違う

論理ボリュームはこんなかんじ

ヘッダ(予約領域)
FAT(ファイルアロケーションテーブル)
データ

物理ボリュームのLBAに対して論理ボリュームではセクタ番号ということにする。

ヘッダの構造はこんなかんじ

typedef struct {
     uint8_t jmp_boot[3];  // BS 使わない
     uint8_t OEM_name[8];  // BS 使わない
     uint16_t bytes_per_sector;
     uint8_t sector_per_cluster;
     uint16_t header_sector_count;
     uint8_t num_fats;
     uint16_t root_entry_count;
     uint16_t _must_be_0;
     uint8_t media;
     uint16_t _must_be_0_2;
     uint16_t sector_per_track;
     uint16_t num_heads;
     uint32_t hide_sector;
     uint32_t total_sector_32;
     uint32_t sector_per_fat;
     struct {
         unsigned fat_0_active: 1;
         unsigned fat_1_active: 1;
         unsigned fat_2_active: 1;
         unsigned fat_3_active: 1;
         unsigned _reserved: 3;
         unsigned single_active: 1;
         uint8_t _reserved_2;
     } ext_flags;
     uint16_t version;
     uint32_t root_cluster;
     uint16_t fs_info;
     uint16_t backup_boot_sector;
     uint8_t must_be_0[12];
     uint8_t drive_num;  // BS 使わない
     uint8_t must_be_0_2;  // BS 使わない
     uint8_t ex_boot_signature;  // BS 使わない
     uint32_t volume_id;  // BS 使わない
     uint8_t volume_label[11];  // BS 使わない
     uint8_t signature[8];  // BS "FAT32   "
     uint8_t boot_code[420];  // BS 使わない
     uint16_t boot_signature;  // BS 使わない
     uint16_t _[];  // 1セクタが512より大きいとき0で埋める
 } FAT32Header;

各領域の先頭のセクタ番号・セクタ数はこんな計算

     uint64_t fat_start_sector = fatHeader.header_sector_count;
     uint64_t fat_sector_count = fatHeader.sector_per_fat * fatHeader.num_fats; 
     uint64_t data_start_sector = fat_start_sector + fat_sector_count;
     uint64_t data_sector_count = fatHeader.total_sector_32 - data_start_sector;

データ領域はクラスタという単位に分割されている。クラスタはsector_per_clusterセクタごとの塊。
クラスタの通し番号をクラスタ番号とする。

エントリとクラスタは1:1で対応している。
エントリ(0)とエントリ(1)は予約済みらしい。
ちなみにエントリの中身は特に構造体とかではなくてただのuint32_t。今回のファイル名一覧を出すまででは特に使わない。

ところで、不良セクタの対策としてテーブルは複数ミラーしてある。この数がnum_fats

エントリ(0), (1)は予約済みで対応したクラスタはないので、クラスタ番号は2から。
ただしクラスタ(0), (1)は大きさ0なので、クラスタ(2)の先頭のセクタはデータ領域の先頭と一致する。
よって、クラスタ(n)の先頭セクタはdata_start_sector + (n - 2) * fatHeader.sector_per_cluster

クラスタより大きいファイルがあった場合、ファイルデータは複数クラスタにまたがる。
またがるときは前のクラスタ番号に対応するエントリに次のクラスタ番号が入っている。
このクラスタチェーンの最後のクラスタに対応するエントリは終了を示す特別な値が入る。(0x0FFFFFF8~0x0FFFFFFF)

また、不良セクタが混ざったクラスタに対応するエントリには不良クラスタを示す値が。(0x0FFFFFF7)

ここで、FAT32にはクラスタ数の上限がないことからクラスタ番号と不良クラスタマークが一致する可能性がある。それではやばいので、クラスタの数は実質0x0FFFFFF5個までに制限される。

FATエントリの値が0のクラスタは未使用を示す。
エントリ(0) = 0xFFFFFF00 | fatHeader.media
エントリ(1) = 0xFFFFFFFF

FATはこんな感じになる

エントリ番号
0x0 0x ffffff8
0x1 0x fffffff
0x2 0x ffffff8
0x3 0x fffffff
0x4 0x fffffff
0x5 0x 6
0x6 0x 7
0x7 0x 8
0x8 0x 9
0x9 0x a
0xa 0x b
0xb 0x c
0xc 0x d
0xd 0x e
0xe 0x f
0xf 0x 10
0x10 0x 11
0x11 0x 12
0x12 0x 13
0x13 0x 14
0x14 0x fffffff
0x15 0x fffffff
0x16 0x 0
0x17 0x 0
0x18 0x 0
0x19 0x 0
...

0と1は予約
2,3,4,15は1つのクラスタで完結してるファイルorディレクトリ
5~14で1つのファイルorディレクトリ
16~は空き

まあさっき言ったように今回はFATは特につかわない。
あ、あとFSInfoについても触れない。


ファイルorディレクトリという表記をしたが、ディレクトリの実態は中身のファイル一覧ファイル。
ディレクトリエントリテーブルの中にファイル・他のディレクトリの情報がまとまっている。

typedef struct {
    struct {
        uint8_t name[8];
        uint8_t ext[3];
    } name;
    uint8_t attr;
    uint8_t NT_res;
    uint8_t crt_time_tenth;
    uint16_t crt_time;
    uint16_t crt_date;
    uint16_t lst_acc_data;
    uint16_t first_cluster_high;
    uint16_t write_time;
    uint16_t write_date;
    uint16_t first_cluster_low;
    uint32_t file_size;
} DirEntry;

クラスタにこのエントリのテーブルが入っている。

必要なものについて書くと、

  • ファイル名nameは11文字、前8文字が名前、後ろ3文字が拡張子。8文字・3文字より少ない場合空白が入る。
    • ファイル名の先頭name.name[0]について
      • 0のときエントリは空。以降のエントリもすべて空である。
      • 0xe5のとき空。
      • 0x05のとき、0xe5の代わりであるため0xe5に置換する必要がある。
  • attrは
    • 0x01: ATTR_READ_ONLY (書き込み禁止)
    • 0x02: ATTR_HIDDEN (隠し)
    • 0x04: ATTR_SYSTEM (システム)
    • 0x08: ATTR_VOLUME_ID (ボリューム ラベル)
    • 0x10: ATTR_DIRECTORY (ディレクトリ)
    • 0x20: ATTR_ARCHIVE (アーカイブ)
    • 0x0F: ATTR_LONG_NAME (LFNエントリ)
      • LFNエントリについては触れない。とりあえずあっても無視する。
  • エントリが指すファイルorディレクトリの本体のクラスタ番号 first_cluster。highとlowに分かれる。

ルートディレクトリのクラスタ番号はヘッダにあるので使う。
ディレクトリで名前が.と..については読むと無限ループするので飛ばす。

こんなかんじ



FAT32Header fatHeader;
uint64_t fat_start_sector;
uint64_t fat_sector_count;
uint64_t data_start_sector;
uint64_t data_sector_count;

uint64_t calc_cluster_first_sector(GPTPartitionEntry partition, uint64_t n) {
    return data_start_sector + partition.start_lba + (n - 2) * fatHeader.sector_per_cluster;
}

void print_dir(GPTPartitionEntry partition, uint64_t cluster_num, uint64_t tab) {
    void *cluster = malloc(512 * fatHeader.sector_per_cluster);
    if (cluster == NULL) {
        perror("malloc");
        exit(1);
    }

    uint8_t buf[20];

    read_sector(*cluster, calc_cluster_first_sector(partition, cluster_num));
    DirEntry *dir = (DirEntry*)cluster;
    for (uint64_t i = 0; ; i ++) {

        if (dir[i].name.name[0] == 0) break;

        else if (dir[i].name.name[0] != 0xe5 && dir[i].attr != 0x0f) {

            uint64_t file_cluster_num = (((uint64_t)dir[i].first_cluster_high) << 12u) | ((uint64_t)dir[i].first_cluster_low);

            if (strncmp(dir[i].name.name, ".       ", 8) == 0 || strncmp(dir[i].name.name, "..      ", 8) == 0)
                continue;

            strncpy(buf, dir[i].name.name, 8);
            buf[8] = '.';
            strncpy(buf + 9, dir[i].name.ext, 3);
            buf[12] = 0;

            for (uint64_t _ = 0; _ < tab; _ ++) {
                printf("    ");
            }
            printf("%s\n", buf);

            if (dir[i].attr & 0x10) {
                print_dir(partition, file_cluster_num, tab + 1);
            }
        }
    }

    free(cluster);
}

void read_fat32(GPTPartitionEntry partition) {
    puts("----- FAT32 -----");
    uint8_t buf[20];

    read_sector(fatHeader, partition.start_lba);

    strncpy(buf, fatHeader.signature, 8);
    buf[8] = 0;
    puts(buf);

    fat_start_sector = fatHeader.header_sector_count;
    fat_sector_count = fatHeader.sector_per_fat * fatHeader.num_fats;

    dump(fat_start_sector);
    dump(fat_sector_count);

    data_start_sector = fat_start_sector + fat_sector_count;
    data_sector_count = fatHeader.total_sector_32 - data_start_sector;

    dump(data_start_sector);
    dump(data_sector_count);

    puts("----- dir -----");

    print_dir(partition,fatHeader.root_cluster, 0);
}

_Bool is_fat32(GPTPartitionEntry partition) {
    FAT32Header head;
    read_sector(head, partition.start_lba);
    return strncmp(head.signature, "FAT32   ", 8) == 0;
}


これで

これが

スクリーンショット 2020-07-09 00-51-15.png

こう

----- GPT -----
EFI PART
name: partition_1, strat: 2048, end: 7385087
  not fat32 entry
name: partition_2_fat, strat: 7385088, end: 15130623
----- FAT32 -----
FAT32   
fat_start_sector: 32
fat_sector_count: 15104
data_start_sector: 15136
data_sector_count: 7730400
----- dir -----
DIR_A   .   
    FILE_A_1.   
    FILE_A_2.TXT
DIR_B   .   
    FILE_B_2.TXT
    FILE_B_1.   
FILE_R  .   
FILE_R_2.TXT
0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?