データベースファイルの内部構造を探ってみた
Firebird日本ユーザー会
アナハイムテクノロジー
はやしつとむ
2008/12/12
GlinkのソースコードからFirebirdの実装を見る
今回はこれをDelphiでリライトして、多少機能を追加してみました。
変更したのは、新しく指定する2次データベースファイルの存在チェックと絶対パスであるかどうかのチェック、-Hオプションで表示されるヘッダ情報にPAGESとnext_pageを追加、2次データベースファイル以降チェックサムエラーが表示される不具合の修正、ヘッダの表示をBasicPageHeaderとそのほかを分けて見やすくした、などです。
ソースコードはそれぞれ以下のリンク先から取得できます。
Glinkの実装を追いかける
まず、ParamCountから引数をパースするかどうかを判断しています。
グローバルな変数の初期化もこの中で行っているので、引数がなくても取りあえず呼ぶようになっています。起動すると引数に従って、グローバル変数sw_show_version、sw_show_help、sw_show_header、sw_show_header_full が設定され、その後の動作が決定されます。
ページヘッダの内容は、get_db_header() 関数で取得され、hdr_page で宣言されたレコード型の変数header_page に読み込まれます。このときに、まずMIZ_PAGE_SIZE=1024bytes を読み込んでから、再度header_page.fix_data.hdr_page_size の大きさで読み込んでいます。可変長のページサイズを処理するためにこのような手続きを取っています。
謎の数値40587を探る
show_db_header() 関数の中では、このheader_page の各フィールドを書式化して表示しています。Firebirdの内部時刻型は日付部分を32ビット整数で、時刻部分を同じく32ビット整数で格納しています。これをCのtime_t型に変換するためにIvan氏が書いたコードが以下の部分です。
Firebirdの内部時刻型をtime_t型に変換する(Glink.exeソースコード424〜427行目)ti = 86400 * (header_page.fix_data.hdr_creation_date[0] - 40587) + _timezone + (header_page.fix_data.hdr_creation_date[1] / 10000); if (_daylight) ti = ti - 3600;
hdr_creation_date[0]から40587を引いた値に86400(1日の秒数)を乗じているのは、これが日付部分であるためですが、40587はどこから来ているのでしょうか。
バイナリエディタでささっと調査
いろいろと調べてみればどこかに書いてあるのでしょうが、面倒なのでバイナリエディタでhrd_creation_date[0][1]をそれぞれ0で書き換えてみました。結果、1858/11/17 00:00:00 が起点となっていることが分かりました。40587を365で割ると約111年になりますから、time_t型の1970/1/1 00:00:00からの経過日数との差がこの数値になっていることが分かります。ちなみに、FirebirdのTimeStamp型自体は西暦100年から32768年まで対応しています。
Delphiでは日付時刻型は整数部が1899年12月31日からの経過日数で、時刻部分は小数部で表現されています。そのため、以下のような変換を行いました。また、time_t型との日付差を修正するため、Delphiに組み込みのUnixDateDelta 定数を利用しました。
t_time型との日付差に対応するための修正ti := (header_page.fix_data.hdr_creation_date[0] - 40587); ti := incsecond(ti, trunc(header_page.fix_data.hdr_creation_date[1] / 10000)); if (_daylight > 0) then ti := incsecond(ti, -3600); ti := ti + UnixDateDelta;
本来は、ibase.h で定義されている、isc_decode_date() 関数を利用するのが本筋なのでしょうが、内部格納形式を理解する一助にはなるかと思います。もっとも、今後内部格納形式が変更された場合には、APIルーチンはそれを考慮して正しい値を返すはずですが、上述の方法では対応できないことはいうまでもありません。
clumpletsの実装チェックと置き換え
さて、struct hdrの最終バイトであるUCHAR hdr_data[1]以降にはclumpletsと呼ばれる可変長のデータが格納されています。以下にods.hの定義を示します。
ods.hでのclumpletsの定義/* Header page clumplets */ /* Data items have the format <type_byte> <length_byte> <data...> */ #define HDR_end 0 #define HDR_root_file_name 1 /* Original name of root file */ #define HDR_journal_server 2 /* Name of journal server */ #define HDR_file 3 /* Secondary file */ #define HDR_last_page 4 /* Last logical page number of file */ #define HDR_unlicensed 5 /* Count of unlicensed activity */ #define HDR_sweep_interval 6 /* Transactions between sweeps */ #define HDR_log_name 7 /* replay log name */ #define HDR_journal_file 8 /* Intermediate journal file */ #define HDR_password_file_key 9 /* Key to compare to password db */ #define HDR_backup_info 10 /* WAL backup information */ #define HDR_cache_file 11 /* Shared cache file */ #define HDR_max 11 /* Maximum HDR_clump value */
clumpletsのタイプが1、2、3、7、11のときは文字列型なので、length_byteの長さに従って文字列を取り出しています。また、8、9、10は現在使用されていないため除外しています。4、5、6の場合は整数型になるのですが、Ivanさんのコードでは後ろから前へ1byteずつ取り出してInteger型に加算しては左へ8 ビットシフトしていました。数値型の値が4bytesまでで入っているという仮定では、これは合理的なやり方です。
Glink.exeソースコードでのclumplets操作の実装int i, j, k, len; for (j = 0, k = len; k--; j = (j << 8) + header_page.var_data[i+k+2]); printf (str, j);
ひとまず、そのままDelphiのコードに置き換えてると以下のようになります。
clumplets操作部分の移植i,j,k,len:integer; j := 0; k := len; repeat dec(k); j := (j shl 8) + header_page.var_data[i+k+2]; until (k = 0); writeln(format(str, [j]));
Cに比較すると長くなってしまうのは致し方ないですが、もっとうまい書き方があるかもしれません。しかし、今回は数値型のデータにはすべて4bytesで入っているようなので、以下のように書き換えることもできます。本来的には可変長なので、lenに応じてbyte、word、integerで受けるようにしないといけませんが、もしlenが3だったらbyteのarrayを作って対応するしかないので、やはり上記の方法が正解なのでしょう。
整数型がすべて4bytesの場合の別の移植方法i,k,len:integer; j:^integer; j := @header_page.var_data[i+2]; writeln(format(str, [j^]);
update_db_header()
次に、データベースファイルのリンク先を書き換えているupdate_db_header() 関数ですが、ここではnew_header_page のfix_data にheader_page.fix_data をコピーしてから、clumpletsを1つずつコピーしていって、type_byteが3のときだけリンク先を変更して書き換えを行っています。その後、書き換えがうまくいっていれば、データベースファイルの先頭へpage_size分を書き込んで完了となります。
おわりに
今回は、DelphiからFirebirdのデータベースファイルを直接操作してみました。本当に簡単な紹介だけとなりましたが、データベースファイルの内部構造に触れることで、Firebirdへの理解が少しでも深まっていただけたならなによりです。今後は、さらにデータが実際にどのように格納されているのか、インデックスはどのように格納されているのかなど、踏み込んだ内容をお伝えできればと考えています。
3/3 |
データベースファイルの内部構造を探ってみた
Page types
Firebirdのページサイズ
ページサイズの選択肢が多い理由
メモリサイズの制限
ページタイプの種類と目的
・Glink
Glinkの実装を追いかける
謎の数値40587を探る
バイナリエディタでささっと調査
clumpletsの実装チェックと置き換え
update_db_header()
Yet another OSS DB:Firebird |
- Oracleライセンス「SE2」検証 CPUスレッド数制限はどんな仕組みで制御されるのか (2017/7/26)
データベース管理システムの運用でトラブルが発生したらどうするか。DBサポートスペシャリストが現場目線の解決Tipsをお届けします。今回は、Oracle SE2の「CPUスレッド数制限」がどんな仕組みで行われるのかを検証します - ドメイン参加後、SQL Serverが起動しなくなった (2017/7/24)
本連載では、「SQL Server」で発生するトラブルを「どんな方法で」「どのように」解決していくか、正しい対処のためのノウハウを紹介します。今回は、「ドメイン参加後にSQL Serverが起動しなくなった場合の対処方法」を解説します - さらに高度なSQL実行計画の取得」のために理解しておくべきこと (2017/7/21)
日本オラクルのデータベーススペシャリストが「DBAがすぐ実践できる即効テクニック」を紹介する本連載。今回は「より高度なSQL実行計画を取得するために、理解しておいてほしいこと」を解説します - データベースセキュリティが「各種ガイドライン」に記載され始めている事実 (2017/7/20)
本連載では、「データベースセキュリティに必要な対策」を学び、DBMSでの「具体的な実装方法」や「Tips」などを紹介していきます。今回は、「各種ガイドラインが示すコンプライアンス要件に、データベースのセキュリティはどのように記載されているのか」を解説します
|
|