プログラミング
UTFS: 組み込みシステム向けTarライクなファイルシステム (2025)
UTFS: A Tar-Like File System for Embedded Systems (2025) (clisystems.com)
要約
UTFSは、CLI Systemsが開発した、組み込みシステム向けの小型ファイルシステム構造「micro TAR File System」です。これは、フラットなアドレス空間を持つストレージ媒体上でデータを整理し、データサイズや位置の変更をデータ損失なしに可能にするように設計されています。既存の固定データ構造が抱える密結合、バッファオーバーフローの脆弱性、構造の硬直性といった問題点を解決し、サブシステム間のデータ分離と独立した更新を可能にする新しいアプローチを提供します。
全文翻訳
UTFS:組み込みシステム向けTarライクなファイルシステム 2025年6月 この記事では、CLI Systemsが開発した小型組み込みファイルシステム構造である「micro TAR File System」ことUTFSについて見ていきます。GitHubでチェックしてみてください: https://github.com/clisystems/utfs/
UTFSは、文字列ベースのファイル名を使用してフラットなアドレス空間を持つストレージ媒体上でデータを整理し、データストレージの詳細をアプリケーションレベルのデータ構造から分離する方法です。また、データサイズと位置をデータ損失なしに変更できる機能を備えています。UTFSは、読み取り、更新、書き込み操作向けに設計されており、ストリーミングや追記操作の機能は限定的です。
問題 ほとんどすべての組み込みシステムは、不揮発性メモリにデータを保存する必要があります。一般的な例としては、シリアル番号、設定パラメータ、機能設定などがあります。マイクロコントローラは生のフラッシュ/EEPROMに簡単にアクセスでき、一般的な解決策は、これらのパラメータを起動時に読み取り、パラメータが変更されたときに書き込む固定データ構造を使用することです。この解決策は機能しますが、ファームウェアの構造に影響を与えるいくつかの深刻な制限があります。
一般的なデータストレージ構造と、複数のデータを持ついくつかのソースファイルの以下の例を見てみましょう。
```c
datastore.h:
typedef struct{
char myserial[8];
int subsystem1_settings;
int subsystem2_data;
int subsystem3_myval;
int subsystem2_data2;
}data_t;
extern data_t systemdata;
void loaddata();
void savedata();
datastore.c:
#include "datastore.h"
data_t systemdata;
void loaddata(){ ... }
void savedata(){ ... }
main.c:
#include "datastore.h"
loaddata();
printf("Serial: %s\n",systemdata.myserial);
subsystem1.c:
#include "datastore.h"
printf("Val: %d\n",systemdata.subsystem1_settings);
subsystem2.c:
#include "datastore.h"
printf("Val1: %d\n",systemdata.subsystem2_data);
printf("Val2: %d\n",systemdata.subsystem2_data2);
subsystem3.c:
#include "datastore.h"
printf("Val: %d\n",systemdata.subsystem3_myval);
```
この方法にはいくつかの問題があります:
アプリケーションロジックがデータ構造と密結合している データストレージ構造はdatastore.cソース内で定義されており、データ構造をグローバルにアクセス可能にするためにヘッダーにexternが提供されています。この方法では、すべてのコードがデータストレージ構造と密結合しているため、構造への変更はサブシステムの動作を変更する可能性があります。また、構造への変更は、それを含むソースの再コンパイルを引き起こします。
無関係なコードがデータを変更できる バッファオーバーフローは、無関係なコードのセクションに影響を与える可能性があります。例えば、新しいシリアル番号を次のように設定した場合を想像してください: `sprintf(systemdata.myserial,"000000123400")` これは、幅が8文字しかないバッファに12文字の新しいシリアル番号を設定し、`subsystem1_settings`の値を`0x30303433`のようなものに、`subsystem2_data`を`0x00######`に設定します。このシリアル番号のオーバーフローは、無関係なサブシステムを誤動作させ、追跡が非常に困難なバグを引き起こす可能性があります。バッファオーバーフローはC言語に固有の問題ですが、データを可能な限り分離することで、この種のバグの発生確率を減らすことができます。
構造の硬直性 例では、シリアル番号が8文字として定義されていますが、ビジネス要件が変更され、シリアル番号が16文字になる必要がある場合はどうなるでしょうか?通常、データはデータ構造の新しいセクションに再配置され、元の8バイトは未使用になります。datastore.h:
```c
typedef struct{
char unused[8];
int subsystem1_settings;
int subsystem2_data;
int subsystem3_myval;
int subsystem2_data2;
char myserial[16];
}data_t;
extern data_t systemdata;
void loaddata();
void savedata();
```
上記の例は、データが異なる順序で追加された場合に何が起こるかを示しています。Subsystem2が「data」を追加し、次にSubsystem3が「myval」を追加しましたが、後でSubsystem2が「data2」を追加する必要がありました。Subsystem3の変数は以前のリリースからのストレージ媒体上にある可能性があるため、新しい変数は既存の変数の後に追加されます。これにより、すべてのデータがインターリーブされた断片化した構造になります。時間の経過とともに構造が大きくなり、機能が追加および変更されるにつれて、その構成がいかに混乱するかが明らかになります。
変数名が非常に硬直している グローバルな`systemdata`変数、またはそのメンバー変数のいずれかが名前変更された場合、ファームウェア全体でそれを参照するすべてのコードが名前変更されなければなりません。これは不適切な設計慣行であり、無関係なソースの内部構造がシステム内の他のソースのコードに影響を与えてはなりません。
UTFSソリューション: UTFSは、異なるサブシステムのデータストレージを分離する取り組みとして始まりました。これにより、1つのサブシステムが更新または変更されても、システム内の他のソースは更新を必要としません。サブシステムを分離するには、そのサブシステムに対応するデータブロックを識別する方法が必要です。これには、文字列のような識別子、つまりファイル名が必要です。オプションを検討する中で、この問題は1970年代にテープドライブで既に解決されていたことを思い出しました。テープドライブは、ランダムアクセス書き込み機能がないフラットなメモリアドレス空間ストレージ媒体です。TARファイル形式は、これらのフラットなメモリ空間にデータを異なるファイルとして保存する方法でした。TARアーカイブ形式には、ファイルに関するすべての情報を格納する512バイトのヘッダーがあり、その後、ファイルは512バイトブロックで書き込まれます。後続のファイルはヘッダーとデータが次々に追記されます。TARアーカイブの基本コンセプトとメモリブロックへのポインタを組み合わせることで、ソースコードの実装とは完全に独立して任意のデータを保存および取得できます。
UTFSのユニークな特性 UTFSのソリューションに取り組む中で、現代のファイルシステムで使用されている一般的なオープン、読み書き、クローズのパラダイムを再評価しました。ファイルのオープン/クローズの性質は、主にファイルへのデータのストリーミングまたは追記に関連しており、ロード-変更-保存のパラダイムとは理想的に合致しません。このため、UTFSにはオープンおよびクローズ関数がなく、すべてのデータはRAMにロードされるか、ストレージ媒体からRAMに保存されます。TARはシンプルなフォーマットかもしれませんが、ブロックとヘッダーは依然として512バイトであり、マイクロコントローラのRAMやEEPROMのサイズの大部分を占める可能性があります。このため、UTFSヘッダーには最小限の情報のみを格納することに決定されました。ヘッダーをオクテット境界に保つため、ヘッダーを24バイトにし、ファイル名に12バイトを割り当てることに決定しました。これは、C文字列のNULL終端子(\0)を含めて最大11バイトのファイル名長を意味します。UTFSヘッダーには16ビットのシグネチャ変数も含まれています。この変数は、異なるバージョン管理を可能にするためにアプリケーションが設定するためのものです。シグネチャ値はデータとともに自動的にロードおよび保存されるため、ファイル内のデータ構造に追加情報を追加する必要はありません。
インターフェース UTFSインターフェースは、RAMデータブロックへのポインタに基づいています。UTFSファイルシステムがロードされると、ファイル名が一致するファイルがある場合、そのデータはRAMデータにロードされます。以下の例では、3つのサブシステムすべてが固有のデータを持っており、それぞれの構造が他方に影響を与えることはありません。
```c
subsystem1.c:
#include "utfs.h"
typedef struct{
int settings;
}s1data_t;
s1data_t s1data;
utfs_file_t s1file;
utfs_set(&s1file,"file1",&s1data,sizeof(s1data));
utfs_register(&s1file, UTFS_NOFLAGS, UTFS_NOOPT);
subsystem2.c:
#include "utfs.h"
typedef struct{
int data;
int data2;
}s2data_t;
s2data_t s2data;
utfs_file_t s2file;
utfs_set(&s2file,"file2",&s2data,sizeof(s2data));
utfs_register(&s2file, UTFS_NOFLAGS, UTFS_NOOPT);
subsystem3.c:
#include "utfs.h"
typedef struct{
int myval;
}s3data_t;
s3data_t s3data;
utfs_file_t s3file;
utfs_set(&s3file,"file3",&s3data,sizeof(s3data));
utfs_register(&s3file, UTFS_NOFLAGS, UTFS_NOOPT);
utfs.h:
utfs_result_e utfs_init(bool verbose);
utfs_result_e utfs_register(utfs_file_t * f, utfs_flags_e flags, utfs_options_e options);
utfs_result_e utfs_set(utfs_file_t * fp, char * name, void * data, uint32_t size);
```