LoginSignup
115
94

More than 5 years have passed since last update.

Linuxのファイルディスクリプタをハックする

Last updated at Posted at 2016-10-04

Linuxでネットワーク経由のデータを処理するプログラムでも、ローカルファイルのデータを処理するプログラムでもファイルディスクリプタ(ソケットディスクリプタ)という言葉を聞いたことがありますよね。

C言語で学ぶソケットAPI入門 第1回 サーバ編にもソケットディスクリプタとして登場しました。

プロセスとやりとりできる状態になったファイルを識別するための整数値ということは、ほとんどの人が理解していると思いますがそれ以上になるとそこまで深くは知らないという方もいるのではないでしょうか?

もちろんLinux上でアプリを作る限りにおいては、それ以上知る必要はなくファイルディスクリプタを使ってCRUDにかかわるAPIを適切に選択してプログラミングすることができれば、プログラマーとしては何も問題ありませんよね。
(ファイルディスクリプタにまつわる問題として、プロセスが開くことができるファイルディスクリプタの最大数を超えてしまった、というようなことは起こりえますが、その時はその上限を引き上げてあげるか、本当にそんなにオープンしなければならないのかアプリケーション設計を見直して対応します。)

C言語では下記のような感じです。

#include <stdio.h>
#include <stdlib.h>
#include <fcntl.h>
#include <unistd.h>

int main (int argc, char* argv[]) {

    int fd; // File Descriptor

    fd = open("tmp.txt", O_RDONLY);
    if (fd < 0) {
        perror("open() failed.");
        exit(EXIT_FAILURE);
    }   

    printf("%d\n", fd); // 3

    close(fd);

    return EXIT_SUCCESS;
}

整数型のfdがファイルディスクリプタです。正常な場合は0以上の数値が取得されます。

また、通常はファイルディスクリプタはプログラミング言語やライブラリによってラップされ、もっと効率的な制御データ構造のもとでCRUDのやりとりをしていると思います。

下記の場合はfopenがシステムコールの呼び出し回数を調整してくれるなど、効率的なIO処理ができるような工夫がされているので通常はこういったAPIを使うことが多いと思います。
他のプログラミング言語にもそういったAPIは用意されていて、私はPHPを書くことが多いのですが、PHPにもfopenというユーティリティ関数があります。

#include <stdio.h>
#include <stdlib.h>

int main (int argc, char* argv[]) {

    FILE *fp; // File pointer

    fp = fopen("tmp.txt", "r");
    if (fp == NULL) {
        fprintf(stderr, "fopen() failed.\n");
        exit(EXIT_FAILURE);
    }   

    printf("%d\n", fp->_fileno); // 3

    fclose(fp);

    return EXIT_SUCCESS;
}

Linux(Unix)はシステム内のあらゆるものをファイルという抽象概念に昇華させたシステムです。
Linuxのファイルシステムの一般的な概念は何日あっても語り尽くせぬほど複雑なものではなく、すぐにでも語り尽くせてしまいそうなほどシンプルなものです。

一般ユーザーはもちろん、プログラマーもファイルという抽象概念のニュアンスを掴むことができれば、あらゆるものを直感的に等しく操作できるインターフェイスが提供されています。

そして、ファイルディスクリプタは抽象的なファイルの実体にアクセスする識別子として利用されます。

システムの根幹をなすファイルディスクリプタをもう少し知ることができれば、Linuxというシステムをもっとうまく活用できるのではないかと思い、調べてみました。

とはいえ、漠然とファイルディスクリプタと言われてもどう調べていいかわからないので、それに関連したシステムコールを調べることにしました。

openシステムコールのハック

さきほど出てきた、openの一連の処理を追ってみることで、探ってみます。
openの結果ファイルディスクリプタが取得できるのですから、この処理を追えば何かわかるはずです。

 415879:   b8 02 00 00 00          mov    $0x2,%eax
 41587e:   0f 05                   syscall 

手っ取り早く、open関数に対応するglibcの__libc_open関数の処理をディスアセンブルして確認してみます。
openシステムコールの番号は2です。

include/asm-x86_64/unistd.h
#define __NR_open                                2
__SYSCALL(__NR_open, sys_open)

sys_call_tableテーブルの中で対応する関数はsys_openだとわかります。

それでは、sys_openの処理を追ってみます。

fs/open.c(933-941)
asmlinkage long sys_open(const char __user * filename, int flags, int mode)
{
    char * tmp; 
    int fd, error;

#if BITS_PER_LONG != 32
    flags |= O_LARGEFILE;
#endif

まず、64ビットマシンの場合、O_LARGEFILEフラグを自動的に有効にします。
これによって2GB以上のファイルを扱えるようになります。
64ビットマシンの昨今、このへんは気にしなくていいと思いますが、32ビットマシンのファイルを扱う処理をアプリで実装する時は注意が必要ですね。
そういえば、32ビットマシン時代で開発がとまってしまったモジュールなんかで2GBバイト以上のファイルが開けないなんて、話を聞いたことがありますが、このフラグを指定してないからですね。

fs/open.c(941)
    tmp = getname(filename);

getname関数を用いて、ユーザー空間からカーネル空間へファイル名を格納したオブジェクトを転送します。なお、メモリ領域の割り当てはスラブアロケータを用いて行います。スラブアロケータは効率的なメモリ割り当てのためにLinuxが採用している方法の1つです。スラブアロケータについてもそのうち掘り下げたいと思います。

fs/open.c(942-944)
    fd = PTR_ERR(tmp);
    if (!IS_ERR(tmp)) {
        fd = get_unused_fd();

エラーがなければ、get_unused_fd関数を実行して、空いているファイルディスクリプタを探します。関数の名前から察するに、この処理でファイルディスクリプタが何なのかわかりそうな気がします。

fs/open.c(838-845)
int get_unused_fd(void)
{
    struct files_struct * files = current->files;
    int fd, error;

    error = -EMFILE;
    spin_lock(&files->file_lock);

forkシステムコールの時も説明しましたが、currentは現在のCPU上で実行されているプロセスのプロセスディスクリプタ(task_struct構造体)のポインタを取得するマクロです。

current->filesはfiles_struct構造体へのポインタが格納されており、files_struct構造体は現在のプロセスによって開かれているファイルの情報が格納されています。

files_struct構造体は下記のような構造になっています。最後らへんにここをもう一度確認することになりますので、心に留めておいて下さい。

include/linux/file.h
/*
 * Open file table structure
 */
struct files_struct {
        atomic_t count;
        spinlock_t file_lock;     /* Protects all the below members.  Nests inside tsk->alloc_lock */
        int max_fds;
        int max_fdset;
        int next_fd;
        struct file ** fd;      /* current fd array */
        fd_set *close_on_exec;
        fd_set *open_fds;
        fd_set close_on_exec_init;
        fd_set open_fds_init;
        struct file * fd_array[NR_OPEN_DEFAULT];
};

files->open_fdsはfd_set構造体であるfiles->open_fds_initへのポインタとなっていて、files->open_fds_initには現在オープンされているファイルディスクリプタが64×16=1024のビットマップとして表現されています。
下記は私がそう判断した計算の根拠の抜粋です。

include/linux/posix_types.h
#define __NFDBITS   (8 * sizeof(unsigned long))
#define __FD_SETSIZE    1024
#define __FDSET_LONGS   (__FD_SETSIZE/__NFDBITS)

typedef struct {
    unsigned long fds_bits [__FDSET_LONGS];
} __kernel_fd_set;

通常は1024ビットで事足りると思いますが、足りなくなった場合はexpand_files関数によって拡張されます。
それが冒頭に少し話した、開けるプロセスが少なくなった場合、上限を引き上げてやれば良いという話に関係します。

fs/open.c(847)
    fd = find_next_zero_bit(files->open_fds->fds_bits,
                files->max_fdset,
                files->next_fd);

ここから、find_next_zero_bit関数により空いているビットを探して取得します。

fs/open.c(872-874)
    FD_SET(fd, files->open_fds);
    FD_CLR(fd, files->close_on_exec);
    files->next_fd = fd + 1; 

新しいファイルディスクリプタをオープンしているファイルディスクリプタの集合に追加し、exec()時にクローズされるディスクリプタの集合から削除します。

この処理が子プロセスとの通信に有利に働くことは、なんとなくわかると思います。プロセス間通信を実現するパイプの処理にも関係してきますね。

そしてnext_fdメンバを割り当てられているファイルディスクリプタの最大数に1を足したものに更新しておきます。次回のファイルディスクリプタ走査に利用されます。

さて、ファイルディスクリプタを取得してみたものの、まだファイルディスクリプタの実体は見えてきません。まだ、とりあえずキーとして空いているファイルディスクリプタを取得しただけという感じがしますね。

再びsys_openの処理に戻ります。fdがファイルディスクリプタとして返されてます。

fs/open.c(945-951)
        if (fd >= 0) { 
            struct file *f = filp_open(tmp, flags, mode);
            error = PTR_ERR(f);
            if (IS_ERR(f))
                goto out_error;
            fd_install(fd, f);
        }    

filp_open関数を実行して、引数として渡したファイルのパス、アクセスモード、パーミッションビットに基づいて取得したfile構造体のアドレスを取得します。

file構造体は下記のようになっています。
重要なメンバーが揃っているので、file構造体だけでも別途違う記事にしたいですね。

include/linux/fs.h
struct file {
    struct list_head    f_list;
    struct dentry       *f_dentry;
    struct vfsmount         *f_vfsmnt;
    struct file_operations  *f_op;
    atomic_t        f_count;
    unsigned int        f_flags;
    mode_t          f_mode;
    int         f_error;
    loff_t          f_pos;
    struct fown_struct  f_owner;
    unsigned int        f_uid, f_gid;
    struct file_ra_state    f_ra;

    size_t          f_maxcount;
    unsigned long       f_version;
    void            *f_security;

    /* needed for tty driver, and maybe others */
    void            *private_data;

#ifdef CONFIG_EPOLL
    /* Used by fs/eventpoll.c to link all the hooks to this file */
    struct list_head    f_ep_links;
    spinlock_t      f_ep_lock;
#endif /* #ifdef CONFIG_EPOLL */
    struct address_space    *f_mapping;
};

さて、filp_open関数の処理も追っていきましょう。

fs/open.c(753-762)
struct file *filp_open(const char * filename, int flags, int mode)
{
    int namei_flags, error;
    struct nameidata nd;

    namei_flags = flags;
    if ((namei_flags+1) & O_ACCMODE)
        namei_flags++;
    if (namei_flags & O_TRUNC)
        namei_flags |= 2;

namei_flagsにアクセスモードを適切に設定します。
namei_flagsのフラグを特殊な形式に変換していることに注意して下さい。

2進数で00(読み取り用)は01へ、01(書き込み専用)は10へ10(読み書き専用)は11となります。
つまり0ビットが立っていれば読み込み、1ビットが立っていれば書き込みという意味になります。

この変換されたフラグは後ほどの処理に使われます。

fs/open.c(764)
    error = open_namei(filename, namei_flags, mode, &nd);

open_namei関数が実際のファイルオープンの重要な部分の処理を行います。
引数として、ファイル名、変換されたアクセスモードのフラグ、パーミッションビット、nameidata構造体へのポインタを渡します。

nameidata構造体は下記にのように定義されています。

include/linux/namei.h
struct nameidata {
    struct dentry   *dentry;
    struct vfsmount *mnt;
    struct qstr last;
    unsigned int    flags;
    int     last_type;
    unsigned    depth;
    char *saved_names[MAX_NESTED_LINKS + 1]; 

    /* Intent data */
    union {
        struct open_intent open;
    } intent;
};

これも重要な構造体ですので、別途時間を設けて掘り下げられたらと思いますが、今注目すべきはdentry構造体を指すポインタである、dentryメンバと、システムにマウントされているファイルシステムの状態を記録する、vfsmount構造体へのポインタであるmntメンバです。

open_namei関数内の処理は、パス名とフラグに基づいてnameidata構造体のオブジェクトを取得することにあります。

その処理の主な部分はpath_lookupという別の関数内で行われているのですがアクセスモードによってその探索方法を細かく調整し、現在のプロセスディスクリプタからプロセスに紐付いたファイルシステムの情報を格納するfsの情報を基点にして、検索処理を開始します。

その結果、nameidata構造体にはパス名で検索した結果のデータが格納されています。

fs/open.c(764-768)
    if (!error)
        return dentry_open(nd.dentry, nd.mnt, flags);

    return ERR_PTR(error);
}

この時点でパス名に対するdentry構造体を指すポインタとvfsmount構造体へのポインタ取得することができています。
dentry_open関数では、その2つの情報と変換されたフラグをもとにfileオブジェクトを作成します。

fs/open.c(945-951)
        if (fd >= 0) { 
            struct file *f = filp_open(tmp, flags, mode);
            error = PTR_ERR(f);
            if (IS_ERR(f))
                goto out_error;
            fd_install(fd, f);
        }    

変数fにdentry_open関数で取得したfileオブジェクトへのポインタを格納します。
そしてエラーがなければ、fd_install関数にファイルディスクリプタとfileオブジェクトへのポインタを渡すのですが、この処理がファイルディスクリプタの核心となります。

fs/open.c(945-951)
void fastcall fd_install(unsigned int fd, struct file * file)
{
    struct files_struct *files = current->files;
    spin_lock(&files->file_lock);
    if (unlikely(files->fd[fd] != NULL))
        BUG();
    files->fd[fd] = file;
    spin_unlock(&files->file_lock);
}

現在のプロセスディスクリプタのメンバ(files_struct *)filesのメンバ(fd_set *)fdに先程filp_open関数で取得したfileオブジェクトへのポインタを格納しています。

このfdメンバはfileオブジェクトへのポインタの配列へのポインタ(struct file **)となっているのですが、ファイルディスクリプタの整数はこの配列の添字と一致します。

ちなみにfdは同じ構造体内のfd_arrayメンバを指していますが64ビットマシンの場合は、ファイルディスクリプタの数が64を超える場合、新たな領域を確保してそのアドレスを指すことになります。

つまりfiles->fd[0]はファイルディスクリプタ0の、files->fd[1]はファイルディスクリプタ1のfileオブジェクトを指すことになります。

よく知られているように通常、0は標準入力、1は標準出力、2は標準エラー出力に対応していますので、ユーザー自身が最初にオープンしたファイルのディスクリプタは3になっているはずで、最初にデモで作成したプログラムでも3と表示されます。

したがいまして、Linuxにおいてファイルディスクリプタとは何か?について答えるとすると、

カレントプロセスにオープンされているfileオブジェクトが格納されている配列の添字

と言えます。

言葉にすると少しわかりにくかったので、直感的には下記です。

//[fd]がファイルディスクリプタ
current->files->fd[fd]

そして、オープンしたファイルに対する操作はfdメンバの添字を使って、fileオブジェクトの情報にアクセスすることによって行います。

fileオブジェクトには転送されたバイトまでのオフセットを記録するf_posメンバがあるので、さらに転送されたバイト数でその値を更新することによって、継続的なファイル処理をすることができます。
なお、操作自体はfileオブジェクト内のf_opメンバに定義されているファイル操作用関数(readやwriteなど)を用いて行います。

ちなみに2.6.14からはfdtable構造体というものが新たに出来まして、それにともない、files_struct構造体も変化しています。
下記の構造は少しずつ変化していますが、根本の構造はLinux4.9現在まで同じです。

Linux2.6.14
struct fdtable {
    unsigned int max_fds;
    int max_fdset;
    int next_fd;
    struct file ** fd;      /* current fd array */
    fd_set *close_on_exec;
    fd_set *open_fds;
    struct rcu_head rcu;
    struct files_struct *free_files;
    struct fdtable *next;
};

struct files_struct {
    atomic_t count;
    struct fdtable *fdt;
    struct fdtable fdtab;
    fd_set close_on_exec_init;
    fd_set open_fds_init;
    struct file * fd_array[NR_OPEN_DEFAULT];
    spinlock_t file_lock;     /* Protects concurrent writers.  Nests inside tsk->alloc_lock */
};

void fastcall fd_install(unsigned int fd, struct file * file)
{
    struct files_struct *files = current->files;
    struct fdtable *fdt;
    spin_lock(&files->file_lock);
    fdt = files_fdtable(files);
    BUG_ON(fdt->fd[fd] != NULL);
    rcu_assign_pointer(fdt->fd[fd], file);
    spin_unlock(&files->file_lock);
}

したがいまして、files_struct構造体から直接fdメンバーを参照するのではなく、一度fdtメンバーを経由することによって、fd(fileオブジェクトへのポインタの配列へのポインタ)を参照するという流れになっています。

汎用的なファイルディスクリプタ

さて、ここからファイルディスクリプタについてもう一歩踏み込んでみます。

冒頭で、Linuxでネットワーク経由のデータを処理するプログラムでも、ローカルファイルのデータを処理するプログラムでもファイルディスクリプタ(ソケットディスクリプタ)を使って処理していると言いました。

C言語で学ぶソケットAPI入門 第3回 サーバ/クライアント編 #1ではrecvやsendといったソケットAPIを使っていますが、この処理はreadやwriteといったベーシックな入出力APIでそっくり置き換えることができます。

これはsocketが取得したものがファイルディスクリプタであり、それによってアクセスできる実体がfileオブジェクトに他ならないからです。下記はソケットをオープンする際の処理を一部抜粋したものです。

net/socket.c(377)
struct file *file = get_empty_filp();

get_empty_filp関数によって新しいfileオブジェクトへのポインタを取得します。この関数もスラブアロケータによってfileオブジェクトのメモリ領域を確保します。

net/socket.c(402-407)
        sock->file = file;
        file->f_op = SOCK_INODE(sock)->i_fop = &socket_file_ops;
        file->f_mode = FMODE_READ | FMODE_WRITE;
        file->f_flags = O_RDWR;
        file->f_pos = 0; 
        fd_install(fd, file);

取得したfileオブジェクトに対して各種設定をして、最後はfd_installです。openシステムコールの時と同じですね。

ソケットだけではありません。パイプの処理だってそうです。以下はpipeシステムコールの処理の一部抜粋です。

fs/pipe.c(402-407)
    f1 = get_empty_filp();
    if (!f1)
        goto no_files;

    f2 = get_empty_filp();
    if (!f2)
        goto close_f1;

f1が読み込み用のfileオブジェクトで、f2が書き込み用のfileオブジェクトです。

fs/pipe.c(760-774)
    /* read file */
    f1->f_pos = f2->f_pos = 0;
    f1->f_flags = O_RDONLY;
    f1->f_op = &read_pipe_fops;
    f1->f_mode = FMODE_READ;
    f1->f_version = 0;

    /* write file */
    f2->f_flags = O_WRONLY;
    f2->f_op = &write_pipe_fops;
    f2->f_mode = FMODE_WRITE;
    f2->f_version = 0;

    fd_install(i, f1);
    fd_install(j, f2);

それぞれのfileオブジェクトに各種設定をして、最後はやはりfd_installなのでopenと同じ処理であることがわかります。

このようにファイルディスクリプタはあらゆるものをファイルで操作しようとするLinuxというシステムにおいて、とても重要な役割を果たしていることがわかりました。
念のため現在ステイブルなLinux4.7のコードも読んでみたんですがこの原理的な処理は変わっておらず、未だこの思想は健在です。

今回あまり説明しなかった各種データ構造や処理などは、別途ファイルシステムについて探求する時にでも詳しく見ていきたいと思います。

参考にした各ソースコード

Linuxカーネル:2.6.11
glibc:glibc-2.12.1
CPU:x86_64
※バージョンについては特に理由がありませんが、古すぎず新しすぎずみたいなところです。

115
94
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
115
94