LeavaTailの日記

LeavaTailの日記

Linuxエンジニアを目指した技術者の備忘録

QEMUでARM用Linuxカーネルを起動する

はじめに

ARMアーキテクチャは、CPUアーキテクチャの一つである。
私たちの身近なPCはx86_64アーキテクチャであることが多いが、組み込み機器はARMアーキテクチャであることが多い。

また最近でも、AppleARMアーキテクチャを採用したMacを発表したことによって、ARMアーキテクチャはより一層注目されている。

一方で、アーキテクチャ毎に命令セットが異なるため、ARM用にビルドされたバイナリを別のアーキテクチャで実行することはできない。

そこで、プロセッサエミュレータでもあるQEMUを用いてARM環境を構築し、ARM用にビルドされたLinuxカーネルを動かす方法を解説する。
また本記事では、以下の動作をする環境を目指す。

f:id:LeavaTail:20201212144827p:plain
カーネル起動ワークフローとメモリマップイメージ図

変更履歴

  • 2020/12/12: 記事公開
  • 2021/2/25: initramfsの作成手順を修正

initramfs とは

initramfsでは、kernel起動直後にRAM上に展開されるファイルシステムとなっている。
カーネルドキュメントに詳細な説明が記載されているので、興味のある方は一読することを推奨する。

https://www.kernel.org/doc/Documentation/filesystems/ramfs-rootfs-initramfs.txt

initramfsを利用する意義として、ルートファイルシステムをマウントする前に必要なドライバのインストールや処理 (ファイルシステムの復号や改ざんチェックなど)があげられる。

同じような仕掛けとしてinitrdがある。それらの大きな違いはフォーマット形式である。 initramfsはアーカイブ形式(cpio + gzip)、initrdはファイルシステム形式(gzip)となっている。

そして、initramfsはinitスクリプト内で下記のようなルートファイルシステムを変更する処理が必要となる。 一般的には、switch_rootコマンドで実現している。

f:id:LeavaTail:20201212030218p:plain
initramfsからルートファイルシステムへの切り替え

環境構成

本記事は、下記の環境とソフトウェアバージョンに基づいて説明する。

環境 パラメータ
ホスト環境 x86_64
ホストOS Ubuntu 20.04 (python3arm-linux-gnueabi-をインストール済み)
Buildroot buildroot-2020.02.8
QEMU QEMU emulator version 5.0.0
ターゲットボード vexpress-a9
linux 5.4.58
U-Boot 2020.07
Busybox 1_32_stable

ARM用のLinuxを構築する

Buildrootを用いてLinux(kernel, rootfs, qemu, device tree)を構築する。

  1. Buildrootをインターネットからダウンロード、ファイルを解凍する。

     leava@ubuntu-bionic:~$ wget https://buildroot.org/downloads/buildroot-2020.02.8.tar.gz
     leava@ubuntu-bionic:~$ tar zxvf  buildroot-2020.02.8.tar.gz && cd buildroot-2020.02.8
    
  2. arm専用のデフォルトコンフィグqemu_arm_vexpress_defconfig (ちなみに、vexpressVersatile Expressの略で汎用的なarmの評価ボードを意味する) を利用する。

     leava@ubuntu-bionic:~/buildroot-2020.02.8$ make qemu_arm_vexpress_defconfig
    
  3. Buildrootのビルド対象にU-Bootを追加する。

     leava@ubuntu-bionic:~/buildroot-2020.02.8$ make menuconfig
    
     Bootloaders  --->
         [*] U-Boot
         (vexpress_ca9x4) Board defconfig 
    
  4. Buildrootのビルド (rootユーザでビルドしてしまうと失敗してしまうので注意)

     leava@ubuntu-bionic:~/buildroot-2020.02.8$ make
    
  5. カーネルが実行できるかどうかQEMUで確認してみる。

     leava@ubuntu-bionic:~/buildroot-2020.02.8$ ./output/images/start-qemu.sh serial-only
     ...
     buildroot login:                                                  # rootでログイン可能
     # 
    

ARM用のinitramfsを構築する

BusyBoxを用いて簡易initramfsを構築する。

  1. Buildrootをインターネットからダウンロード、stable versionにチェックアウトする。(投稿時は1_32_stableが最新版)

     leava@ubuntu-bionic:~$ git clone git://busybox.net/busybox.git
     leava@ubuntu-bionic:~$ cd busybox
     leava@ubuntu-bionic:~/busybox$ git checkout remotes/origin/1_32_stable
    
  2. arm専用のデフォルトコンフィグを利用する。

     leava@ubuntu-bionic:~/busybox$ make ARCH=arm CROSS_COMPILE=arm-linux-gnueabi- defconfig
    
  3. initramfs用に最小限の設定を変更する。(initramfsの場合、スタティックリンクであるほうが好ましい)

     leava@ubuntu-bionic:~/busybox$ make ARCH=arm CROSS_COMPILE=arm-linux-gnueabi- menuconfig
    
     Settings  --->
         [*] Build static binary (no shared libs)
    
  4. BusyBoxのビルド (make installを実行してもホスト環境に何かしらパッケージを変更するわけではなく、_installディレクトリにディレクトリツリーが構築される)

     leava@ubuntu-bionic:~/busybox$ make ARCH=arm CROSS_COMPILE=arm-linux-gnueabi-
     leava@ubuntu-bionic:~/busybox$ make ARCH=arm CROSS_COMPILE=arm-linux-gnueabi- install
    
  5. initramfs用のディレクトリに移動する

     leava@ubuntu-bionic:~/busybox$ cd _install
    
  6. initramfsとして必要なディレクトリを作成する

     leava@ubuntu-bionic:~/busybox/_install$ mkdir dev
     leava@ubuntu-bionic:~/busybox/_install$ mkdir proc
     leava@ubuntu-bionic:~/busybox/_install$ mkdir sys
     leava@ubuntu-bionic:~/busybox/_install$ mkdir -p mnt/newroot
    
  7. initramfsからルートファイルシステムをマウントするようにinitスクリプトを修正する。

     leava@ubuntu-bionic:~/busybox/_install$ cat <<EOF > init
     #!/bin/busybox sh
     echo "Mounting Proc and Sysfs"
     # Mount the /proc and /sys filesystems. 
     mount -t devtmpfs devtempfs /dev
     mount -t proc none /proc
     mount -t sysfs none /sys
     # Mount the root filesystem
     mount -t ext4 /dev/mmcblk0 /mnt/newroot
     # Switch mount point        
     mount -n -o move /sys /mnt/newroot/sys
     mount -n -o move /proc /mnt/newroot/proc
     mount -n -o move /dev /mnt/newroot/dev
     # Execute new mount rootfilesystem
     exec switch_root -c /dev/console /mnt/newroot /sbin/init
     EOF
    
  8. 作成したディレクトリツリーを用いて、initramfsを構築する

     leava@ubuntu-bionic:~/busybox/_install$ find . | cpio -o --format=newc > ../rootfs.img;
    
  9. initramfsを使用するようにQEMUの起動スクリプトを修正する

     leava@ubuntu-bionic:~/busybox/_install$ cd ~/buildroot-2020.02.8
     leava@ubuntu-bionic:~/buildroot-2020.02.8$ sed -e '$d' ./output/images/start-qemu.sh > ./output/images/start-qemu2.sh
     leava@ubuntu-bionic:~/buildroot-2020.02.8$ echo "exec   qemu-system-arm -M vexpress-a9 -smp 1 -m 256 -kernel \${IMAGE_DIR}/zImage -dtb \${IMAGE_DIR}/vexpress-v2p-ca9.dtb -initrd ../rootfs.img -drive file=\${IMAGE_DIR}/rootfs.ext2,if=sd,format=raw -append \"console=ttyAMA0,115200 rootwait\"  -net nic,model=lan9118 -net user  \${EXTRA_ARGS}" >> output/images/start-qemu2.sh
    
  10. initramfsからルートファイルシステムを正常にマウントできるかどうか、QEMUで確認してみる。

     leava@ubuntu-bionic:~/buildroot-2020.02.8$ ./output/images/start-qemu2.sh serial-only
     ...
     Mounting Proc and Sysfs                      # initramfsのinitスクリプトが実行されている
     ...
     buildroot login:                                                  # rootでログイン可能
     # 
    

U-Bootからカーネルをロードする

  1. initramfsを使用するようにQEMUの起動スクリプトを修正する

     leava@ubuntu-bionic:~/buildroot-2020.02.8$ sed -e '$d' ./output/images/start-qemu.sh > ./output/images/start-qemu3.sh
     leava@ubuntu-bionic:~/buildroot-2020.02.8$ echo "exec   qemu-system-arm -M vexpress-a9 -smp 1 -m 256 -kernel \${IMAGE_DIR}/../build/uboot-2020.07/u-boot -device loader,file=\${IMAGE_DIR}/zImage,addr=0x62000000 -device loader,file=\${IMAGE_DIR}/vexpress-v2p-ca9.dtb,addr=0x63000000 -device loader,file=urootfs.img,addr=0x63008000 -drive file=\${IMAGE_DIR}/rootfs.ext2,if=sd,format=raw   -net nic,model=lan9118 -net user  \${EXTRA_ARGS}" >> output/images/start-qemu3.sh
    
  2. U-Bootを起動させる。
    このとき、カーネル0x62000000、device treeを0x63000000、initramfsを0x630080000にロードする。 (これは参考文献より、今回のボードvexpress-a9は0x6000000からSDRAMが実装されているためである)

      leava@ubuntu-bionic:~/buildroot-2020.02.8$ ./output/images/start-qemu3.sh serial-only
    
      U-Boot 2020.07 (Dec 12 2020 - 04:16:22 +0000)
    
      DRAM:  256 MiB
      WARNING: Caches not enabled
      Flash: 128 MiB
      MMC:   MMC: 0
      *** Warning - bad CRC, using default environment
    
      In:    serial
      Out:   serial
      Err:   serial
      Net:   smc911x-0
      Hit any key to stop autoboot:  0 
      => 
    
  3. device treeが展開されているアドレスなどを指定してカーネル起動させる。

     => bootz 0x62000000 0x63008000 0x63000000
     Kernel image @ 0x62000000 [ 0x000000 - 0x46a478 ]                                                        
     ## Loading init Ramdisk from Legacy Image at 63008000 ...                                                
        Image Name:                                                                                           
        Image Type:   ARM Linux RAMDisk Image (gzip compressed)                                               
        Data Size:    1123310 Bytes = 1.1 MiB                                                                 
        Load Address: 00000000                                                                                
        Entry Point:  00000000                                                                                
        Verifying Checksum ... OK                                                                             
     ## Flattened Device Tree blob at 63000000                                                                
        Booting using the fdt blob at 0x63000000                                                              
        Loading Ramdisk to 6fd65000, end 6fe773ee ... OK                                                      
        Loading Device Tree to 6fd5e000, end 6fd6473e ... OK                                                  
    
     Starting kernel ...
     ...
     buildroot login:                                                  # rootでログイン可能
     # 
    

作成ファイル置き場

#!/bin/busybox sh
 echo "Mounting Proc and Sysfs"
 # Mount the /proc and /sys filesystems. 
 mount -t devtmpfs devtempfs /dev
 mount -t proc none /proc
 mount -t sysfs none /sys
 # Mount the root filesystem
 mount -t ext4 /dev/mmcblk0 /mnt/newroot
 # Switch mount point        
 mount -n -o move /sys /mnt/newroot/sys
 mount -n -o move /proc /mnt/newroot/proc
 mount -n -o move /dev /mnt/newroot/dev
 # Execute new mount rootfilesystem
 exec switch_root -c /dev/console /mnt/newroot /sbin/init
#!/bin/sh
IMAGE_DIR="${0%/*}/"

if [ "${1}" = "serial-only" ]; then
    EXTRA_ARGS='-nographic'
else
    EXTRA_ARGS='-serial stdio'
fi

export PATH="/srv/src/buildroot-2020.08/output/host/bin:${PATH}"
exec   qemu-system-arm -M vexpress-a9 -smp 1 -m 256 -kernel ${IMAGE_DIR}/zImage -dtb ${IMAGE_DIR}/vexpress-v2p-ca9.dtb -initrd ../rootfs.img -drive file=${IMAGE_DIR}/rootfs.ext2,if=sd,format=raw -append "console=ttyAMA0,115200 rootwait"  -net nic,model=lan9118 -net user  ${EXTRA_ARGS}
#!/bin/sh
IMAGE_DIR="${0%/*}/"

if [ "${1}" = "serial-only" ]; then
    EXTRA_ARGS='-nographic'
else
    EXTRA_ARGS='-serial stdio'
fi

export PATH="/srv/src/buildroot-2020.08/output/host/bin:${PATH}"
exec   qemu-system-arm -M vexpress-a9 -smp 1 -m 256 -kernel ${IMAGE_DIR}/../build/uboot-2020.07/u-boot -device loader,file=${IMAGE_DIR}/zImage,addr=0x62000000 -device loader,file=${IMAGE_DIR}/vexpress-v2p-ca9.dtb,addr=0x63000000 -device loader,file=urootfs.img,addr=0x63008000 -drive file=${IMAGE_DIR}/rootfs.ext2,if=sd,format=raw   -net nic,model=lan9118 -net user  ${EXTRA_ARGS}

おわりに

本記事では、QEMUでARM用Linuxカーネルを起動させる手順を説明した。
Buildrootで構築した場合、自動でセットアップしてくれるため非常に使いやすく便利である。

また、今回はその恩恵にあやかっていないが、initramfsを使用した起動方法を紹介した。 initramfsを利用しないと実現できない要件もあるので、役割や作り方を一度おさらいしておくとよい。

FAQ

  • initramfs がうまく起動しない。
    • 横着して find _install | cpio -o --format=newc > ../rootfs.img;などとしていませんか?initramfs用のディレクトリ直下で実行しましょう。
  • BusyBoxswitch_rootコマンドで、ルートファイルシステムの変更に失敗する。
    • /sbin/switch_rootでコマンドを実行するとPIDが変わってしまい起動に失敗します。execコマンドを使用しましょう。
  • u-bootで、bootzコマンドを実行後にStarting kernel ...で止まってしまう。
    • メモリマップを確認して、「ロードしたイメージと衝突していないか」と「ロードしたイメージ同士が衝突していないか」を確認しましょう。

参考

Linuxカーネルのファイルアクセスの処理を追いかける (2) System call

はじめに

ユーザプロセスからファイルシステムという機構によって記憶装置上のデータをファイルという形式で書き込み・読み込みすることができる。
本調査では、ユーザプロセスがファイルに書き込み要求を実行したときにLinuxカーネルではどのような処理が実行されるかを読み解いていく。

調査対象や環境などはPart 1: 環境セットアップを参照。

f:id:LeavaTail:20201128233057p:plain
調査対象

本記事では、Syscall interfaceの途中 (SYSCALL_DEFINE)からVFSの関数を呼び出すところまでとする。
ここより上のレイヤー(Application部やsystem call interfaceの上位部)の解説は今回は省略する。

変更履歴

  • 2020/11/29: 記事公開

Application

下記の記事では、ユーザプロセスがfwrite()関数を実行したときのフローを詳細にまとめてある。
特に、glibcまで追いかけているため、一から処理を追いかけたい人におすすめ。

delihiros.jp

下記の記事では、システムコールについて説明されている。 システムコールを一から学習したい人におすすめ。

www.kimullaa.com

System call Interface

writeシステムコールは、SYSCALL_DEFINEマクロによって定義される。

// 667:
SYSCALL_DEFINE3(write, unsigned int, fd, const char __user *, buf,
        size_t, count)
{
    return ksys_write(fd, buf, count);
}

writeシステムコールの実態は、ksys_write()となっている。
ksys_write()の定義は下記のようになっている。

// 647:
ssize_t ksys_write(unsigned int fd, const char __user *buf, size_t count)
{
    struct fd f = fdget_pos(fd);
    ssize_t ret = -EBADF;

    if (f.file) {
        loff_t pos, *ppos = file_ppos(f.file);
        if (ppos) {
            pos = *ppos;
            ppos = &pos;
        }
        ret = vfs_write(f.file, buf, count, ppos);
        if (ret >= 0 && ppos)
            f.file->f_pos = pos;
        fdput_pos(f);
    }

    return ret;
}

ksys_write()には、大きく分けて3つ処理がある。

  1. 現在のプロセスが保有しているファイルディスクリプタテーブルからfd構造体を取得する
  2. ファイルの現在のオフセットを取得する
  3. VFSのwrite処理を呼び出す

ファイルディスクリプタからfd構造体を取得する

ファイル構造体を取得するまでの流れは下記のようなものとなっており、マルチスレッドであった場合に限りファイルディスクリプタテーブルにロックをかけるといった処理がされる。
ちなみに、FMODE_PATHO_PATHフラグを指定してファイルをopenしたときに付与されるフラグであり、その場合は書き込みを失敗させる。

f:id:LeavaTail:20201127235227p:plain
ファイル構造体を取得するまでのフロー

file構造体の取り扱い方については、カーネルドキュメントが用意されているので気になる人はそちらを読むことを推奨する。

脱線してしまったが、ksys_write関数で実行されるfdget_pos関数について定義を確認する。 fdget_pos関数は下記のような定義である。

// 73:
static inline struct fd fdget_pos(int fd)
{
    return __to_fd(__fdget_pos(fd));
}
fd構造体を作成する

fdget_pos関数の処理は単純で下記の2点のみである。

  • file構造体にflagsを付与して、fd構造体を呼び出し元に返す
  • __fdget_pos関数でファイルディスクリプタからfile構造体を取得する

まずは、__to_fd関数の定義から確認する。 __to_fd関数は下記のような定義である。

// 58:
static inline struct fd __to_fd(unsigned long v)
{
    return (struct fd){(struct file *)(v & ~3),v & 3};
}

引数として与えられたvから、下位2bitに格納されたフラグを抽出する。(アドレス空間は4bytes or 8bytesの管理されるので、file構造体のアドレスに影響はない)
上記のフラグと下位2bitをマスクしたfile構造体へのポインタをfd構造体に代入する。

ちなみにfd構造体の定義は下記の通りである。

// 36:
struct fd {
    struct file *file;
    unsigned int flags;
};

ここまでの__to_fd関数の流れのイメージを下記に示す。

f:id:LeavaTail:20201128190451p:plain
file構造体のアドレスとfd構造体の対応

  1. __fdget_pos関数(後述)によって、ファイルディスクリプタからファイル構造体のアドレスを取得する
  2. __fdget_pos関数は、取得したアドレスをunsigned long型の変数vに代入し、下位2bitにフラグを代入する
  3. __to_fd関数は、変数vからfile構造体とflagsを取得して、fd構造体を返す
ファイルディスクリプタからfile構造体を取得する

fdget_pos関数は、 ファイルディスクリプタからfile構造体を取得するために__fdget_pos関数を呼び出す。

// 924:
unsigned long __fdget_pos(unsigned int fd)
{
    unsigned long v = __fdget(fd);
    struct file *file = (struct file *)(v & ~3);

    if (file && (file->f_mode & FMODE_ATOMIC_POS)) {
        if (file_count(file) > 1) {
            v |= FDPUT_POS_UNLOCK;
            mutex_lock(&file->f_pos_lock);
        }
    }
    return v;
}

__fdget_pos関数の処理は下記の2点のみである。

  • __fdget関数でファイルディスクリプタからfile構造体のアドレスを取得する (unsigned long型bに代入していたり、~3とAND演算しているのは前述の理由)
  • 同じファイルが複数openされている場合、オフセットのためのmutex_lockを取得する (取得したロックは、ksys_write関数の最後に実行しているfdput_pos関数で解放する)

まず、__fdget関数とその呼び出し先の__fget_light関数の定義を確認する。

// 913:
unsigned long __fdget(unsigned int fd)
{
    return __fget_light(fd, FMODE_PATH);
}
// 896:
static unsigned long __fget_light(unsigned int fd, fmode_t mask)
{
    struct files_struct *files = current->files;
    struct file *file;

    if (atomic_read(&files->count) == 1) {
        file = __fcheck_files(files, fd);
        if (!file || unlikely(file->f_mode & mask))
            return 0;
        return (unsigned long)file;
    } else {
        file = __fget(fd, mask, 1);
        if (!file)
            return 0;
        return FDPUT_FPUT | (unsigned long)file;
    }
}

__fget_light関数では、現在のプロセスのファイルディスクリプタテーブルを取得して、状況に応じてロックを取得する。

ここで、task_struct構造体からfile構造体までの関係性についておさらいする。

  • current変数は、現在のプロセスのtask_struct構造体を指す
  • task_struct構造体は、現在openしているファイルを管理するためにfiles_struct型のポインタを変数filesで管理する
  • files_struct構造体は、fdtable型の変数fdtでファイルディスクリプタのテーブルを管理する
  • fdtable構造体は、file構造体の配列を管理する。

これらの関係を図示したものが以下である。

f:id:LeavaTail:20201126232954p:plain
ファイルディスクリプタからfile構造体を取得するまでの各構造体の関係性

__fget_light関数でも上記の規則と同様に、current変数からfiles_struct構造体を取得している。
しかし、シングルスレッドかマルチスレッドの場合で、files_struct以降の取り扱いが変わる。

シングルスレッドの場合

シングルスレッドの場合 (つまり、atomic_read(&files->count) == 1) は、対象のファイルディスクリプタテーブルを保有しているは自分のみである。 そのため、それぞれの変数に対して、排他制御を意識する必要はない。

__fcheck_files関数の定義は以下のようになっている。

// 83:
static inline struct file *__fcheck_files(struct files_struct *files, unsigned int fd)
{
    struct fdtable *fdt = rcu_dereference_raw(files->fdt);

    if (fd < fdt->max_fds) {
        fd = array_index_nospec(fd, fdt->max_fds);
        return rcu_dereference_raw(fdt->fd[fd]);
    }
    return NULL;
}

ちなみに、fcheck_files関数についての注意点は、カーネルドキュメントにも記載されている。

4 To look up the file structure given an fd, a reader must use either fcheck() or fcheck_files() APIs.
These take care of barrier requirements due to lock-free lookup.

file構造体を取得した後に、そのファイルのopenフラグを確認する。 FMODE_PATHフラグが立っていた場合、ファイル操作は無効となっているので0を返す必要がある。

マルチスレッドの場合

マルチスレッドの場合 (つまり、atomic_read(&files->count) > 1) は、排他制御を意識しなけらばならない。

__fget関数と呼び出し先の__fget_files関数の定義は以下のようになっている。

// 845:
static inline struct file *__fget(unsigned int fd, fmode_t mask,
                  unsigned int refs)
{
    return __fget_files(current->files, fd, mask, refs);
}
// 822:
static struct file *__fget_files(struct files_struct *files, unsigned int fd,
                 fmode_t mask, unsigned int refs)
{
    struct file *file;

    rcu_read_lock();
loop:
    file = fcheck_files(files, fd);
    if (file) {
        /* File object ref couldn't be taken.
        * dup2() atomicity guarantee is the reason
        * we loop to catch the new file (or NULL pointer)
        */
        if (file->f_mode & mask)
            file = NULL;
        else if (!get_file_rcu_many(file, refs))
            goto loop;
    }
    rcu_read_unlock();

    return file;
}

fcheck_files関数は、__fcheck_files関数(シングルスレッドの場合を参照)のラッパー関数である。

シングルスレッドの場合と大きく異なるのは、rcu_read_lock/rcu_read_unlock命令を発行している点である。

  • rcu_read_lock()はプリエンプションを無効にする命令
  • rcu_read_unlock()はプリエンプションを有効にする命令

コメントにも書いてあるが、dup2()が要因でファイルカウントがおかしくなることがあるため、file構造体の取得をループさせることがある。
file構造体の取得後にプリエンプションが発生して、別にfile構造体を操作されると整合性がとれなくなってしまう。

__fget_files関数ではこの関数内でFMODE_PATHフラグの確認もしている。 シングルスレッドの場合と同様に、上記のフラグが立っていたら0を返す。

また、マルチスレッドの場合にはFDPUT_FPUTフラグを立てる。

ファイルの現在のオフセットを取得する

file構造体には、現在のオフセットをpos変数によって管理している。

file_ppos関数は、file構造体のpos変数を返す関数である。 このとき、stream-likeのファイルの場合は、オフセットを持たないのでNULLを返す必要がある。

// 618:
static inline loff_t *file_ppos(struct file *file)
{
    return file->f_mode & FMODE_STREAM ? NULL : &file->f_pos;
}

VFSのwrite処理を呼び出す

ここまでで、file構造体とオフセットが取得できたのでvfs_write関数でVFSのレイヤに移る(次回説明)。
vfs_write関数による書き込みをした後は、オフセットの更新と後処理(参照カウンタやロックの解放)が必要になる。

// 78:
static inline void fdput_pos(struct fd f)
{
    if (f.flags & FDPUT_POS_UNLOCK)
        __f_unlock_pos(f.file);
    fdput(f);
}

FDPUT_POS_UNLOCKは、__fdget_pos関数内でfile_count(file) > 1)の場合に立てられるフラグである。
このとき、__f_unlock_pos関数でファイルオフセット用のmutexを解放する。

// 938:
void __f_unlock_pos(struct file *f)
{
    mutex_unlock(&f->f_pos_lock);
}

その後、fput関数でファイルの参照カウンタを一つ減らす。

// 43:
void fput(struct file *file)
{
    fput_many(file, 1);
}

実際には、fput_many関数で参照カウンタを減らす操作をしているが、ここでは割愛する。

まとめ

本記事では、writeシステムコールの実態からvfs_write関数を呼び出すまでを解説した。 次回の記事では、vfs_write関数から各ファイルシステムのwrite操作を呼び出すまでの処理を追いかける。

参考

GitHub ActionsでChangeLogからReleasesを自動化する

概要

ソフトウェア開発者 (ユーザ) は、ソフトウェアをリリースする際に幾つかの作業を実施する必要がある。 しかし、それらの作業は、ある程度決められた作業を実施する必要がある。 ヒューマンエラーやユーザの手間を考慮すると、これらの作業を自動化できるとうれしい。

本記事では、GitHubReleases機能を使用しているプロジェクトを対象とする。
Releases機能は、GitHub上にあるリポジトリと紐づけられていて、リポジトリのタグに対して任意のファイルやメッセージを残すことができる。

この機能を使うユースケースとして、下記の作業が考えられる。

  • ユーザは、ChangeLog(または、NEWSなど)に更新点を書き記す。
  • ユーザは、任意のコミットにタグを作成する。
  • ユーザは、GitHubにあるリモートリポジトリにタグをプッシュする。
  • ユーザは、該当リポジトリのパッケージを作成する。
  • ユーザは、Releases 機能からメッセージ(リリースノート)の追加や任意のファイル(パッケージ)を添付する。

本記事では、「パッケージを作成する」作業と「Releases 機能」における作業をGitHub Actionsを利用して自動化を目指す。*1
下記は、本記事を適用したときのソフトウェアのリリースするまでの手順を示したものである。

f:id:LeavaTail:20201024182743p:plain
Releasesするまで手順

  1. ユーザは、ChangeLogに更新点を書き記す。
  2. ユーザは、任意のコミットにタグを作成する。
  3. ユーザは、GitHubにあるリモートリポジトリにタグをプッシュする。
  4. GitHub Actionsは、Change Logからメッセージ(リリースノート)の追加とパッケージを生成し、添付する。

はじめに

本作業は下記のリポジトリのworkflowに導入済みである。本記事では、これを基に説明する。 github.com

リポジトリでは、automakeを使用しているため、下記のmakeターゲットがデフォルトで用意されている。

  • make: ソフトウェアをビルドする
  • make install: 成果物をユーザの環境にインストールする
  • make dist: 配布用パッケージを生成する (パッケージ名-バージョン名.tar.gz)

Git-flowは下記のような形式を採用しており、適当なタイミングでtag(図中の灰色吹き出し)を作成している。
このtagが作成されたタイミングで、そのバージョンにおけるパッケージとChangeLogを残すようにしている。

f:id:LeavaTail:20201024224042p:plain
本プロジェクトのGit-flow

また、図中の白吹き出しはブランチを表している。

  • mainブランチ: 製品として常に安定した状態を保つ。
  • bugfixブランチ: リリース後に、不具合を修正する。
  • developブランチ: 次のリリースに向けた作業をする。
  • featureブランチ: 各機能における開発をする。

自動化のための作業

Releases手順の自動化に向けて、あらかじめ以下の作業を完了させておく必要がある。

  • ChangeLogから変更内容を取得するスクリプトの作成
  • Releases 機能を自動的に設定するworkflowの作成

ChangeLogから変更内容を取得するスクリプトの作成

Releases のリリースノートに記述するための、該当バージョンの更新点をChangeLogから抽出する必要がある。

今回は、下記のようなフォーマットのChangeLogをユーザが作成している場合を考える。

# Changelog
## [1.1.0] - 2020-10-01
### Added
- 新機能C
- 新機能D

### Changed
- 既存機能Zの修正

### Removed
- 既存機能Yの削除

## [1.0.1] - 2020-09-11
### Fixed
- 既存機能Xの修正

## [1.0.0] - 2020-09-01
### Added
- 新機能A
- 新機能B

この形式は、第2レベルの見出しに[バージョン] - 日付が記述されていて、第3レベルの見出しに変更の種類(AddedやChanged)が記述されている。
リリースノートとして必要となるのは、該当バージョンにおける第2レベルの見出し以下の内容となる。

そのため、該当バージョンの第2レベルの見出しの検索とその内容を取得する必要がある。 第2レベルの見出しの検索は^##grepすることで取得できる。

    $ grep -n "^## " ChangeLog.md
    2## [1.1.0] - 2020-10-01
    13:## [1.0.1] - 2020-09-11
    17:## [1.0.0] - 2020-09-01

ここから、awkコマンドでパターンマッチを行い、該当バージョン(VERSION)内容の先頭と末尾の行番号を取得する。

    $ grep -n "^## " ChangeLog.md |\
        awk -F: -v version=${VERSION} '/'"${VERSION}"'/ \
            { start = $1 + 1; getline; end = $1 - 1 } \
            END { print start, end }' )
   sed -n ${sline},${eline}p ${FILE}

例えばv1.1.0の場合、先頭は3行目、末尾12行目が得られる。
ここから、ChangeLogの3行目から12行目を表示することで該当バージョン(v1.1.0)の変更内容を取得できる。

上記の内容を踏まえて、下記のようなChangeLogから変更内容を取得するスクリプトget_changelog.shを用意する。

#!/bin/bash
FILE=CHANGELOG.md
VER=`echo $1 | tr -d "refs/tags/"`    # i.e. v1.0, v20.15.10
VERSION=`echo ${VER} | tr -d v`       # i.e. 1.0, 20.15.10

read sline eline <<< \
    $( grep -n "^## " ${FILE} | \
   awk -F: -v version=${VERSION} '/'"${VERSION}"'/ \
      { start = $1 + 1; getline; end = $1 - 1 } \
      END { print start, end }' )
sed -n ${sline},${eline}p ${FILE}

Releases 機能を自動的に設定するworkflowの作成

タグのプッシュを契機に、Releases 機能を自動的に設定するworkflowの作成する必要がある。

GitHubでは、Releasesを生成するActions と ReleasesにファイルをアップロードするActionsが用意されている。

github.com github.com

create-releaseを利用して、Markdownファイルで記述されたリリースノート(body.md)からReleasesの作成する場合には下記のように記述する。 (Example workflowを参照)

- name: Create Release
  id: create_release
  uses: actions/create-release@v1
  env:
    GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 
  with:
    tag_name: ${{ github.ref }}                      
    release_name: Release ${{ github.ref }}      # Releasesのタイトル (i.e. Release v1.0.0)
    body_path: "body.md"                         # Releasesのリリースノート
    draft: false
    prerelease: false

upload-release-assetを利用して、ファイルをアップロードする場合も同様にExample workflowが用意されているのそれに従って記述する。

ただし、Automakeで生成される配布物パッケージ名はパッケージ名-バージョン名.tar.gzとなっており、可変のファイル名である。
upload-release-assetはアップロードするファイルの名前に正規表現をサポートしていないため、Example workflowに一工夫しなければいけない。

コミュニティで提案されているのは、前のステップにて該当ファイルを環境変数に代入する方法である。

- name: Get Name of Artifact
  run: |
    ARTIFACT_PATHNAME=$(ls debugfatfs-*.tar.gz | head -n 1)
    ARTIFACT_NAME=$(basename $ARTIFACT_PATHNAME)
    echo "ARTIFACT_NAME=${ARTIFACT_NAME}" >> $GITHUB_ENV
    echo "ARTIFACT_PATHNAME=${ARTIFACT_PATHNAME}" >> $GITHUB_ENV
- name: Upload Release Asset
  id: upload-release-asset
  uses: actions/upload-release-asset@v1
  env:
    GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
  with:
    upload_url: ${{ steps.create_release.outputs.upload_url }}
    asset_path: ${{ env.ARTIFACT_PATHNAME }}
    asset_name: ${{ env.ARTIFACT_NAME }}
    asset_content_type: application/gzip

上記のActionsを用いて、Releases 機能を自動的に設定するworkflowを用意する。

name: Create Releases

on:
  push:
    tags:
      - 'v*' # Push events to matching v*, i.e. v1.0, v20.15.10

jobs:
  build:
    runs-on: ubuntu-latest

    steps:
    - uses: actions/checkout@v2
    - name: setup environment           # パッケージ生成のための環境構築
      run: |
        sudo apt-get update
        sudo apt-get install autoconf automake libtool help2man make
    - run:  script/bootstrap.sh
    - run: ./configure
    - run: make
    - run: make dist          # 配布物パッケージの生成 i.e. debugfatfs-0.1.0.tar.gz
      env:
        CI: true
    - run: |                  # 更新内容を一時的にbody.mdとして保存しておく
        ./get_changelog.sh ${{ github.ref }} > body.md
    - name: Create Release
      id: create_release
      uses: actions/create-release@v1
      env:
        GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 
      with:
        tag_name: ${{ github.ref }}                      
        release_name: Release ${{ github.ref }}      # Releasesのタイトル (i.e. Release v1.0.0)
        body_path: "body.md"                         # Releasesのリリースノート
        draft: false
        prerelease: false
    - name: Get Name of Artifact
      run: |
        ARTIFACT_PATHNAME=$(ls debugfatfs-*.tar.gz | head -n 1)      # 正規表現で成果物パッケージのファイル名を取得する
        ARTIFACT_NAME=$(basename $ARTIFACT_PATHNAME)
        echo "ARTIFACT_NAME=${ARTIFACT_NAME}" >> $GITHUB_ENV         # 成果物パッケージのファイル名の環境変数に設定する
        echo "ARTIFACT_PATHNAME=${ARTIFACT_PATHNAME}" >> $GITHUB_ENV
    - name: Upload Release Asset
      id: upload-release-asset
      uses: actions/upload-release-asset@v1
      env:
        GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
      with:
        upload_url: ${{ steps.create_release.outputs.upload_url }}
        asset_path: ${{ env.ARTIFACT_PATHNAME }}
        asset_name: ${{ env.ARTIFACT_NAME }}
        asset_content_type: application/gzip

実行例

# Changelog

## [0.1.0] - 2020-10-24

### Added

- Print main Boot Sector field
- Print any cluster
- Print Directory list
- Backup or restore FAT volume
- Convert into update latter
- Create file
- Remove file
- Change any FAT entry
- Change any allocation bitmap
- Trim deleted directory entry

## Initial Version
  • tag (v0.1.0)を作成し、リモートリポジトリにプッシュする。

     $ git tag v0.1.0
     $ git push origin --tags
    

f:id:LeavaTail:20201025072025p:plain
タグ (v0.1.0)をプッシュした後のリポジトリ

f:id:LeavaTail:20201025072301p:plain
Releases v0.1.0の概要

おわりに

本記事では、ユーザがタグを作成したタイミングでChangeLogからReleasesを自動的に生成するworkflowを作成した。

この手順では、ChangeLogの生成がユーザの手作業で書き記す必要があるが、制度の良いcommitを生成しているのであればこの作業も自動化しても良いだろう。 今回は、正規表現を利用してアップロードファイルを指定しているのでひと手間かかったが、GitHub Actionsは様々なworkflowが用意されているので、任意の作業の自動化が容易である。

このように作業の自動化が容易であるため、定型的な作業がGitHub Actions (などのサービス)を利用して可能な限り自動化していくとよい。

参考

*1:ChangeLogの生成を自動化に関しては「keep a changelog」に則り実施していない。

Linuxカーネルのファイルアクセスの処理を追いかける (1) 環境セットアップ

はじめに

一般的なOSはファイルという形式を通して、ハードディスクやフラッシュメモリといった記憶装置にデータを保存している。
この処理を担うのがファイルシステムと呼ばれる機構である。
一般的な利用者はこのことを意識せずに利用することができるが、ソフトウェアエンジニアは処理を理解していないとディスクIOパフォーマンスが悪化し、システム全体のパフォーマンスに大きく影響を及ぼす恐れがある。
そこで、アプリケーションがファイルを書き込んだ際にLinuxカーネルがどのような処理で記憶装置に読み書きされるかを順を追って説明する。

f:id:LeavaTail:20201122225232p:plain
調査範囲

本記事では、SYSCALL_DEFINE(write)からデバイスドライバまでの処理を対象とする。

変更履歴

背景

一般的なOSでは、さまざまなコンポーネントから成り立っている。ファイルシステムもその一つである。 ファイルの書き込み処理一つとっても、多数のコンポーネントとの関係を持つ。

下記の図は、他サイトで掲載されているLinuxカーネルv4.10の構成図である。(2020年12月現在、Linuxカーネルv5.10.1がリリースされている)

https://www.thomas-krenn.com/de/wikiDE/images/e/e0/Linux-storage-stack-diagram_v4.10.png

Reference: Linux Storage Stack Diagram

このように、Linuxカーネルv4.10の時点でもたくさんのフローからファイルアクセスが成り立っている。(大まかな処理は最新カーネルでも変わらないのでこの図を基に説明を続ける)
ここでは、read(2)とwrite(2)について説明する。
read(2)

  1. VFSは、ファイルに対応するファイルシステムのread処理を呼び出す。
  2. ファイルシステムは、ファイルがキャッシュに載っているか確認する。(あればそれをApplicationに渡して終了する)
  3. ファイルシステムは、Block LayerにBIOを挿入する。
  4. Block Layerは、スケジューラによりBIOを並び替る。
  5. Block Layerは、Device DriverにRequestを発行する。
  6. Device Driverは、Physical devicesにIOを要求する。
  7. Physical devicesは、デバイスファームウェアに則りデータの読み込みをする。
  8. Physical devicesは、カーネルにIO完了通知をする。
  9. (Direct_IOでなければ)カーネルは、読み込みしたデータをPage cacheとしてキャッシュする。
  10. カーネルは、Applicationにデータを渡して終了する。

write(2)

  1. VFSは、ファイルに対応するファイルシステムのwrite処理を呼び出す。
  2. (Direct_IOでなければ)ファイルシステムは、ファイルをキャッシュにしてApplicationに完了を通知する。
  3. ファイルシステムは、Block LayerにBIOを挿入する。
  4. Block Layerは、スケジューラによりBIOを並び替る。
  5. Block Layerは、Device DriverにRequestを発行する。
  6. Device Driverは、Physical devicesにIOを要求する。
  7. Physical devicesは、デバイスファームウェアに則りデータの書き込みをする。
  8. Physical devicesは、カーネルにIO完了通知をする。

このように、概要だけ記載してただけでもこれらを一から追うとなると多大な労力を要する。

一方でLinuxでは、各レイヤにおけるトレース機構が充実している。 本記事では、これらのトレースツールをうまく活用して、Linuxカーネルのファイルアクセスの処理(read/writeを対象とする)を追いかけることにする。

環境構成

本稿では、QEMUを用いて観測対象のLinuxカーネルを起動させる。 QEMUを利用することで、下記のような利点が得られる。

  • 実行環境による違いを緩和することができる
  • ホスト側から任意のタイミングでGDBでアタッチすることができる

ユーザランドの構築にはBuildrootを利用する。
BuildrootはツールチェインやルートファイルシステムLinuxカーネルなどを構築するツールであり、本稿でもこれを利用する。 Buildrootの使い方については、過去の記事に記載しているのでそちらを参考にしてほしい。

leavatail.hatenablog.com

以上を踏まえて、本稿では下記のソフトウェアを用いて処理を確認していく。

QEMUでは、Versatile Express motherboardとCoreTile Express A9x4 daughterboardの組み合わせをvexpress-a9というボードでサポートしている。 それぞれの機器のデータシートはArm Developerに記載されている。

下記は、Arm Developerで記載されている機器のレイアウト図を引用している。
こちらは、Versatile Express motherboardのレイアウト図である。

https://documentation-service.arm.com/static/5e9074b78259fe2368e2acd9?token=

こちらは、CoreTile Express A9x4 daughterboardのレイアウト図である。

https://documentation-service.arm.com/static/5e9db8569931941038de23df?token=

Reference: Documentation – Arm Developer

これらの情報とQEMUの公式サイトに書かれている情報を基に、vexpress-a9の概略図を示す。

f:id:LeavaTail:20210203234353p:plain
vexpress-a9のレイアウト イメージ図

また、Arm Developerなどに記載されているドキュメントを基にこのボードのメモリレイアウトを記す。

f:id:LeavaTail:20210221120656p:plain
ARM Memory Layout

上記の概略図はあくまで著者の理解であり、正確なものではないので注意していただきたい。
ここでは、SDRAMにロードされたinitramfsとSDに接続されているデバイス(調査対象のファイルシステムでフォーマットしておく)を使用して調査を進めていく。

作成手順

開発環境の構築にはBuildrootを用いる。 Buildrootの実行環境として下記のDocker Imageを使用した。 github.com

  1. Buildrootを入手する。

     leava@kbuild:/work$ wget https://git.busybox.net/buildroot/snapshot/buildroot-2020.08.tar.gz
     leava@kbuild:/work$ tar xf buildroot-2020.11.tar.gz
     leava@kbuild:/work$ cd buildroot-2020.11
    
  2. Buildrootのデフォルトの設定を使用する。

     leava@kbuild:/work/buildroot-2020.11$ make qemu_arm_vexpress_defconfig
    
  3. Buildrootの設定を適宜修正する。

     toolchain  --->
       (glibc) C library
       Kernel Headers (Same as kernel being built)  --->
       Custom kernel headers series (5.9.x or later)  --->
       [*] Enable C++ support
       [*] Build cross gdb for the host
         [*]   TUI support
    
     System configuration  --->  
       (root) Root password 
    
     Kernel   --->
       Kernel version (Custom version)  --->
       (5.10) Kernel version
    
     Target packages  --->
       Debugging, profiling and benchmark  --->
         [*] blktrace
         [*] ltrace  
         [*] strace   
         [*] trace-cmd  
       Development tools  --->
         [*] git  
       Filesystem and flash utilities  ---> 
         [*] cpio 
         [*] dosfstools 
           [ ]   fatlabel (NEW)
           [*]   fsck.fat
           [*]   mkfs.fat   
       Hardware handling  --->
          [*] hwloc  
          [*] iostat 
          [*] lshw    
          [*] lsscsi         
          [*] parted   
       Networking applications  --->   
         [*] dropbear 
       Interpreter languages and scripting  --->       
         [*] python3      
    
     Filesystem images  ---> 
       [*] cpio the root filesystem (for use as an initial RAM filesystem)    
    
     Host utilities  ---> 
       [*] host qemu 
          *** Emulators selection ***
         [*]   Enable system emulation
         [*]   Enable Linux user-land emulation
    
  4. Buildrootの設定からユーザランドを構築する。

     leava@kbuild:/work/buildroot-2020.11$ make
    
  5. QEMU用のスクリプトの末尾の行を下記のように修正する。 (-s-gdb tcp::1234と等価)

     exec   qemu-system-arm -M vexpress-a9 -smp 1 -m 256 -kernel ${IMAGE_DIR}/zImage -dtb ${IMAGE_DIR}/vexpress-v2p-ca9.dtb -initrd ${IMAGE_DIR}/rootfs.cpio -drive file=${IMAGE_DIR}/rootfs.ext2,if=sd,format=raw -append "console=ttyAMA0,115200 rootwait root=/dev/ram"  -net nic,model=lan9118 -net user -s ${EXTRA_ARGS}
    
  6. QEMU上でLinuxカーネルを起動する。

     leava@leava-host:/srv$ ./output/images/start-qemu.sh serial-only
    
  7. 別ウインドウからGDBで接続できるか確認する。

     leava2@leava-host:/srv/buildroot-2020.11/output/build/linux-5.10$ sudo ./output/host/bin/arm-buildroot-linux-gnueabihf-gdb ./output/build/linux-5.10.1/vmlinux 
     GNU gdb (GDB) 8.3.1
     Copyright (C) 2019 Free Software Foundation, Inc.
     License GPLv3+: GNU GPL version 3 or later <http://gnu.org/licenses/gpl.html>
     This is free software: you are free to change and redistribute it.
     There is NO WARRANTY, to the extent permitted by law.
     Type "show copying" and "show warranty" for details.
     This GDB was configured as "--host=x86_64-pc-linux-gnu --target=arm-buildroot-linux-gnueabihf".
     Type "show configuration" for configuration details.
     For bug reporting instructions, please see:
     <http://www.gnu.org/software/gdb/bugs/>.
     Find the GDB manual and other documentation resources online at:
         <http://www.gnu.org/software/gdb/documentation/>.
    
     For help, type "help".
     Type "apropos word" to search for commands related to "word"...
     Reading symbols from vmlinux...
     (gdb) target remote :1234
     Remote debugging using :1234
     cpu_v7_do_idle () at arch/arm/mm/proc-v7.S:78
     78              ret     lr
     (gdb)         
    

調査方法

本記事では、上記の環境で下記のコマンドを実行した場合のファイルアクセスの処理を調査する。

# echo "DIRTY" >> /mnt/root/.ash_history 

この時、/mntmount -t ext2 /dev/mmcblk0 /mntによりマウントされているものとする。

まとめ

本記事では、これからLinuxカーネルのファイルアクセスの処理を追いかけるための環境構築をした。
次回の記事では、作成した環境を用いて「writeシステムコールの実態からVFSレイヤまで」の処理を追いかける。

参考

help2man でmanページを自動生成する

はじめに

Linuxではmanコマンドによる利用者にオンラインマニュアルを提供する機構が設けられている。

ソフトウェアパッケージの開発者は、そのソフトウェアの使い方を記したmanページを含めて配布することが望ましい。
しかし、manページはの作成は更新する頻度も多く手間がかかる。

help2manは、ソフトウェアの実行結果から標準的manページを自動生成するツールである。 www.gnu.org

またhelp2manとautomakeを併用することで、configureファイルの生成からmanページの生成までの手順を自動化することができる。

そこで、本記事ではautomakeを使ってhelp2manでmanページを生成する手順を紹介する。

f:id:LeavaTail:20200808171034p:plain
help2manの出力結果

準備

下記のリポジトリを例にhelp2manを使ってmanページを生成する手順と、automakeによりmanページを自動生成する手順をまとめる。

github.com

dumpexfatは、FAT/exFATファイルシステムイメージから情報を取得することができるプログラムである。
このプログラムは複数のオプションの指定を許しており、以下のオプションをサポートしている。

オプション (long option) 概要
-c (--cluster=index) indexで指定したクラスタ番号のデータを出力する
-f (--force) 未確保のクラスタでも出力を許す
-o (--output) 結果を指定したファイルに出力する
-s (--sector=index) indexで指定したセクタ番号のデータを出力する
-v (--verbose) メッセージを冗長に出力する

help2man とは

公式サイトに詳細な説明があるので、詳しくはそちらを参照。

www.gnu.org

help2manはプログラムの--helpオプションと--versionオプションから簡単なmanページを作成するツールである。

f:id:LeavaTail:20200802184937p:plain
help2manの概略図

このとき、--help--versionは、仕様に基づいた形式*1で記述することで精度の高いmanページを生成することができる。

一般的なmanページを作るときに必要となる--helpを下記に示す。

    Usage: <コマンド名> <コマンド書式>              ★ SYNOPSIS に出力される
    <コマンド概要>                                 ★ NAMEに出力される

    <コマンド説明>                                 ★ DESCRIPTIONに出力される

    Options:                                      ★ OPTIONSに出力される
      <オプション>     <オプション概要>

    Examples:                                     ★ EXAMPLEに出力される
      <例>    <実行例の説明>

    Report bugs to: <mailing-address>             ★ REPORTING BUGSに出力される

<>で記述したものに関しては、開発者がそのプログラムにあったものに修正する必要となる。
また、その他にmanページのセクションを追加したい場合は、<セクション名>: で追加することができる。

一般的なmanページを作るときに必要となる--versionを下記に示す。

    <command> <version>

    <著作権表示>                                   ★ COPYRIGHTに出力される

    Written by <作者>                             ★ AUTHORに出力される

manページを生成する

help2manの書式に則って、--help--versionを作成する。

    $ dumpexfat --help
    Usage: dumpexfat [OPTION]... FILE
    dump FAT/exFAT filesystem information.

      -c, --cluster=index   dump the cluster index after dump filesystem information.
      -f, --force   dump the cluster forcibly in spite of the non-allocated.
      -o, --output=file     send output to file rather than stdout.
      -s, --sector=index    dump the sector index after dump filesystem information.
      -v, --verbose Version mode.
      --help        display this help and exit.
      --version     output version information and exit.

    Examples:
      dumpexfat /dev/sda    dump FAT/exFAT filesystem information.
      dumpexfat -c 2 /dev/sda       dump FAT/exFAT filesystem information and cluster #2.

    $ dumpexfat --version
    dumpexfat 0.1

    Written by LeavaTail.

manページの作成に必要なものはこれで完了したので、help2manを実行する。

    $ help2man --no-discard-stderr -N -o dumpexfat.1 dumpexfat    

今回は下記のオプションに指定した。

  • --no-discard-stderr: stderrに出力された内容も使用する
  • -N: manページの末尾にTexinfoの情報を載せないようにする
  • -o: 出力先のファイルを指定する

automakeにhelp2manを組み込む

help2manの更新でも記述されているが、automakeと連携させる場合には、manページの依存関係にソースコードを指定することが望ましい。
そこで、このプロジェクトではMakefile.amに下記のターゲットを追加した。

    dumpexfat.1: dumpexfat$(EXEEXT)
        help2man --no-discard-stderr -N -o dumpexfat.1 ./dumpexfat

これで、automakeを実行することでhelp2manコマンドからmanページを生成できるようなMakefileが生成できるようになった。

おわりに

本記事は、automakeを使ってhelp2manでmanページを生成する手順を書き留めた。

help2manによって生成されたmanページは必要最低限度の情報が記述されているはずなので、とりあえずプロジェクトに組み込むとよいだろう。

*1:cpコマンドを参考にするとよい

GitHub Actionsで既存リポジトリのテストを自動化する

はじめに

GitHubではパブリックリポジトリであれば GitHub Actions という機能が無料に使用することができる

GitHub Actions では、世の中のCI/CDサービスと同様にソフトウェアワークフロー (例えば、Masterブランチにマージする前に用意しておいたテストを流し、テストが通ったらマージを許可するなど) を簡単に自動化することができる。

GitHub Actionsは利用料金がお手軽(プライベートリポジトリでも利用時間が2000分/月)といった点や、GitHubのサービスの一つでもある点から高い統合性がみられる。 また、ソフトウェアワークフローのテンプレートも幅広いパターンが用意されており、取っつきやすいといった点もみられる。

そこで、本記事はソフトウェアワークフローが自動化されていないリポジトリに、GitHub Actionsを導入するまでの手順を書き留めた。

準備

下記のリポジトリを例にGitHub Actionsと連携させる手順をまとめる。

github.com

このプロジェクトは、様々なアーキテクチャ(x86/arm/arm64)用でLinuxカーネルをビルドするためのDockerイメージを管理している。 Dockerイメージの利用方法は、ホスト環境にあるlinuxトップディレクトリ(${PWD}/linux)をコンテナ内のワーキングディレクトリ(/work)と共有化して実行する。

  1. Dockerfileからイメージを生成する。

     $ docker build -t kbuild .
    
  2. Dockerコンテナでカーネルをビルドする。

     $ docker run --rm --name=kbuild -v ${PWD}/linux:/work -it kbuild make
    

このプロジェクトはテストを用意しておらず、その都度実行のたびに問題が見つかっていた。

テストスクリプトの作成

GitHub Actionsと連携するにあたって、手動で実施するテストを設計・実装するところから始める。

このプロジェクトでは、Linuxカーネルのビルド環境を構築するDockerイメージであり、考えられるテストパターンは莫大に存在する。
今回は、LTSのカーネル ((2020年7月現在で 5.4.51, 4.19.132, 4.14.188, 4.9.230, 4.4.230)) のみを対象として、Linuxカーネルがビルドできるかどうかを確認する。

テストの全体像は次に示す。

[f:id:LeavaTail:20200713001155p:plain]
プロジェクトのソフトウェアワークフロー

  1. テストに必要なtarballをkernel.orgからダウンロードする。
  2. プロジェクトが提供しているDockerfileからDockerイメージを作成し、コンテナを起動させる。
  3. コンテナ内でカーネルがビルドできるかどうかテストする。

GitHub Actionsとの連携

プロジェクトのソフトウェアフローが決まったので、GitHub Actionsと連携させていく。

  1. プロジェクトトップページのActionsを選択する。
    [f:id:LeavaTail:20200712222313p:plain]
    GitHub Actionsボタン
  2. 今回はDocker imageのリポジトリなのでDocker imageの「Set up this workflow」を選択する。(自分のリポジトリに似たテンプレートがなければ、「set up a workflow yourself」で一から作成する。)
    [f:id:LeavaTail:20200712223605p:plain]
    workflowsのテンプレートを選択する
  3. templeteからworkflowsのひな形が生成されるので、必要に応じて修正する。
    [f:id:LeavaTail:20200712224759p:plain]
    workflowsのEdit画面

今回のプロジェクトでは、下記のようなworkflowを作成した。

name: Docker Image CI                          # ワークフローの名前。ページに表示される
        
on:                                            # masterブランチへのpush/PRを契機とする
  push:        
    branches: [ master ]        
  pull_request:        
    branches: [ master ]        
        
jobs:        
        
  build:                                       # ジョブのID
        
    runs-on: ubuntu-latest                     # ジョブを実行するマシンの環境
        
    steps:                                     # 一連のタスクとして下記を実行する
    - uses: actions/checkout@v2                # リポジトリからチェックアウトする 
    - name: Obtain LTS kernel                  
      run: ./tests/00_init.sh                  # tarballを取得・展開する
    - name: Execute the Docker Container
      run: ./tests/01_build.sh                 # Dockerコンテナの起動
    - name: Execute the Docker Container
      run: ./tests/10_allnobuild.sh            # カーネルのビルド            

上記のファイルを作成後、Masterブランチにコミットすると、作成したworkflowsが有効になる。
また、今回のworkflowはmasterへのpushを契機としているので、すぐに作成したテストが走る。

f:id:LeavaTail:20200712231800p:plain
workflowsの結果確認

ジョブ(build)の結果を確認する。

f:id:LeavaTail:20200712233236p:plain
ジョブの実行結果を確認する

今回は設定しなかったが、actions/upload-artifact@v1と指定すると成果物を残すことができる。(artifactsで確認可能? )

下記にGitHub Actionsと連携させた後の概要図は下記のようなものになった。

f:id:LeavaTail:20200713001226p:plain
GitHub Actionsと連携後のソフトウェアワークフロー

  1. ユーザが対象のリポジトリに対してPushかPull Requestを投げたとき(on:以下の指定より)、GitHubがあらかじめ用意してあるjobを実行する。
  2. GithubUbuntu仮想マシンを立ち上げ(runs-on: ubuntu-latestより)、下記のタスクを実行していく。
  3. Ubuntu仮想マシン上にリポジトリをチェックアウトする。(actions/checkout@v2)
  4. 指定されたスクリプトを実行し、Docker Imageが適切かどうか検証する。

おわりに

本記事はソフトウェアワークフローが自動化されていないリポジトリGitHub Actionsを導入するところまでを書き留めた。

この記事はあくまで導入のためのフローを書き記したものであり、GitHub Actionsはまだ多彩の機能を兼ね備えている。
ドキュメントを充実しており、導入の敷居も低いのでGitHub Actionsを導入して、プロジェクトの品質を向上させていきたい。

参考

AutotoolsでMakefileを自動生成する

はじめに

Autotoolsはソフトウェアパッケージ開発ツールの一つである。 www.gnu.org 開発者は、configure.acMakefile.amに必要な情報を記載することで、configureと呼ばれるビルドに必要なライブラリのチェックやビルドに必要なファイルを自動生成するスクリプトファイルを生成することができる。

しかし、記述方式や生成方法が少々トリッキーなところもあり、一からautotoolsに必要なファイルを用意するとなると手間がかかる。 *1 幸いにも、Autotoolsを採用しているパッケージは多く存在するので、それらを参考にすることでとりあえず動作させることはできる。

本記事では、備忘録としてAutotoolsを使用するにあたって参考となるサイトの紹介と必要最低限度の手順を紹介する。

準備

今回は小規模ソフトウェアパッケージを想定として、Autotoolsの使い方をおさらいする。 説明を簡単にするために、対象を下記のようなディレクトリ構成から成るソフトウェアを対象とする。

f:id:LeavaTail:20200626233641p:plain
Autotools導入予定のプロジェクトのディレクトリツリー

上記のソフトウェアはGitHub上に公開してあるのでそちらを参照すると理解しやすいと思う。 (Autotoolsを導入するまでの手順とコミットを対応させて説明しているため)

github.com

このソフトウェアは、「プログラムソースコード」と「マニュアルページ」と「多言語翻訳リソース」の三つから構成される。

  • プログラムソースコード: includesrcの配下にCプログラムファイルとヘッダファイルを管理する。 main.cがプログラムのメイン部分となっており、ここからsub.cにある関数を呼び出す。
  • マニュアルページ: manの配下にマニュアルページを管理する。
  • 多言語翻訳リソース: poの配下に翻訳可能な文字列と日本語の対応関係を管理する。 gettextを利用してローカライズする。

本記事では、このソフトウェアが下記の三つの要件を達成することを目標とする。

  1. ソースコードがビルドするためのMakefileを生成できること
  2. マニュアルページをインストールできるMakefileを生成できること
  3. gettextでローカライズが可能なMakefileを生成できること

また、今回は下記ツールのバージョンで確認している。

  • GNU automake 1.16.1
  • GNU gettext-tools 0.19.8.1

手順

CプログラムのソースコードをビルドできるMakefileの生成

github.com

  1. パッケージのトップディレクトリにMakefile.amを作成する。

     bin_PROGRAMS = test
     test_SOURCES = src/main.c src/sub.c
     AM_CPPFLAGS = -I$(top_srcdir)/include -DLOCALEDIR='"$(localedir)"' -DPACKAGE='"test"'
    

    bin_PROGRAMS = ターゲット: コンパイルによって生成されるバイナリファイルを指定する。
    ターゲット_SOURCE = ソースファイル: ターゲットを生成するために必要なファイルを指定する。 AM_CPPFLAGS = オプション: コンパイラに渡すオプションを指定する。

  2. autoscanコマンドを実行して、configure.scanファイルを生成する。

     $ autoscan
    
  3. configure.scanファイルをconfigure.acにリネームする。

     $  mv configure.scan configure.ac
    
  4. configure.acファイルを必要最低限の修正する。(末尾に「★」を付けた行が対象)

     #                                               -*- Autoconf -*-
     # Process this file with autoconf to produce a configure script.
    
     AC_PREREQ([2.69])
     AC_INIT([test], [1.0], [starbow.duster@gmail.com])    ★ パッケージ名, バージョン, バグレポート用アドレスを記入
     AM_INIT_AUTOMAKE([foreign subdir-objects])            ★ 不要ファイルの自動生成を防ぐforeignとサブディレクトリに対応するためのsubdir-objectsを追加
     AC_CONFIG_SRCDIR([config.h.in])                       ★ ソースコードのディレクトリをconfig.h.inに変更しておくとrenameされた場合にも対応できる
     AC_CONFIG_HEADERS([config.h])
    
     # Checks for programs.
     AC_PROG_CC
    
     # Checks for libraries.
    
     # Checks for header files.
     AC_CHECK_HEADERS([libintl.h locale.h])
    
     # Checks for typedefs, structures, and compiler characteristics.
    
     # Checks for library functions.
     AC_CHECK_FUNCS([setlocale])
    
     AC_CONFIG_FILES([Makefile])
     AC_OUTPUT    
    
  5. プリプロセッサ用のヘッダファイルの生成する。

     $ autoheader
    
  6. automake用のm4ファイルを生成する。

     $ aclocal
    
  7. Makefile.inを生成する。

     $ automake --add-missing
    
  8. configureスクリプトを生成する。

     $ autoconf
    
  9. Makefileの生成・ソフトウェアのビルド・パッケージのインストールできることを確認する。

     $ ./configure
     $ make
     $ make install
    

マニュアルページをインストールできるMakefileを生成

github.com

「CプログラムのソースコードをビルドできるMakefileの生成」との差分のみ説明する。

  1. Makefile.amファイルに以下を追記する。

     man_MANS = man/test.1
    

    man_MANS = ターゲット: マニュアルページのパスを指定する。

gettextでローカライズが可能なMakefileを生成

github.com

「CプログラムのソースコードをビルドできるMakefileの生成」との差分のみ説明する。

  1. configure.acファイルを修正する。(末尾に「★」を付けた行が対象)

    #                                               -*- Autoconf -*-
    # Process this file with autoconf to produce a configure script.
    
    AC_PREREQ([2.69])
    AC_INIT([test], [1.0], [starbow.duster@gmail.com])    
    AM_INIT_AUTOMAKE([foreign subdir-objects])            
    AC_CONFIG_SRCDIR([config.h.in])
    AC_CONFIG_HEADERS([config.h])
    AM_GNU_GETTEXT([external])                                ★ 出力ファイルを自動生成する
    AM_GNU_GETTEXT_VERSION(0.19)                              ★ gettextのバージョンを指定する
    
    # Checks for programs.
    AC_PROG_CC
    
    # Checks for libraries.
    
    # Checks for header files.
    AC_CHECK_HEADERS([libintl.h locale.h])
    
    # Checks for typedefs, structures, and compiler characteristics.
    
    # Checks for library functions.
    AC_CHECK_FUNCS([setlocale])
    
    AC_CONFIG_FILES([po/Makefile.in Makefile])               ★ po/Makefile.inも生成する
    AC_OUTPUT    
    
  2. gettextの基礎となるファイルを生成する。

     $ autopoint
    
  3. Makevars.templateをコピーしてMakevarsを作成する。

     $ cp po/Makevars.template po/Makevars
    
  4. 利用可能な言語を記述するファイルpo/LINGUASを作成する。

     # Set of available languages
     ja
    
  5. 翻訳が必要なファイルを記述するファイルpo/POTFILES.inを作成する。

     # List of source files which contain translatable strings.
     src/main.c
    
  6. 翻訳が必要なファイルを記述するファイルMakefile.amを作成する。

     SUBDIRS = po             ★ 対象としてpo以下も適応する
     bin_PROGRAMS = test
     man_MANS = man/test.1
     test_SOURCES = src/main.c src/sub.c include/func.h
     AM_CPPFLAGS = -I$(top_srcdir)/include
    
  7. 下記のコマンドを実行して、Makefileファイルを生成する。

     $ autoheader
     $ aclocal
     $ automake --add-missing
     $ autoconf
     $ ./configure
    
  8. 多言語翻訳用テンプレートのpo/test.potを生成する。(testの部分はバイナリ名と一致する)

     $ make
    
  9. 多言語翻訳用テンプレートのpo/test.potから日本語翻訳ファイルja.poを新規作成する。

     $ cp po/test.pot po/ja.po
    
  10. 日本語翻訳ファイルja.poを修正する。(末尾に「★」を付けた行が対象)

    # SOME DESCRIPTIVE TITLE.
    # Copyright (C) YEAR Free Software Foundation, Inc.
    # This file is distributed under the same license as the test package.
    # FIRST AUTHOR <EMAIL@ADDRESS>, YEAR.
    #
    #, fuzzy
    msgid ""
    msgstr ""
    "Project-Id-Version: test 1.0\n"
    "Report-Msgid-Bugs-To: starbow.duster@gmail.com\n"
    "POT-Creation-Date: 2020-06-27 01:09+0900\n"
    "PO-Revision-Date: 2020-06-27 01:10+0900\n"                      ★ このファイルの更新日時を記入する
    "Last-Translator: LeavaTail <starbow.duster@gmail.com>\n"        ★ このファイルを編集した人を記入する
    "Language-Team: ja\n"                                            ★ 翻訳後の言語担当チームを指定する
    "Language: ja\n"                                                 ★ 翻訳後の言語を指定する
    "MIME-Version: 1.0\n" 
    "Content-Type: text/plain; charset=UTF-8\n"                      ★ 文字コード(UTF-8が妥当)を指定する
    "Content-Transfer-Encoding: 8bit\n"
    
    #: src/main.c:14
    msgid "Hello,World!\n"
    msgstr "こんにちは、世界!\n"                                      ★ 日本語翻訳を記述する
    
  11. 多言語翻訳リソースの更新とコンパイル

    $  make -C po/ update-po
    

注意: make installでパッケージのインストールまでしないと、ローカライズされない。

補足

手順の効率化

autoreconfを使う

ここまで手順を紹介してきたが、その中でたくさんのコマンドが登場してきた。 configure.acMakefile.amからconfigureを生成するのに下記の4つのコマンドが必要になる。

  1. autoheader: configure.acからプリプロセッサ用のヘッダファイルの生成する。
  2. aclocal: configure.acからautomake用のm4を生成する。
  3. automake: Makefile.amなどからMakefile.inを生成する。
  4. autoconf: configure.acなどからconfigureを生成する。

autoreconfはこれらの処理を自動的に実行してくれるコマンドである。

OSSライセンスについて

OSS利用者にとって一番気を付けなければいけないことがOSSライセンスである。

  • GNU automakeはGPLv2+
  • GNU AutoconfはGPLv3
  • GNU gettext-toolsはGPLv3+

調べてみると、このあたりを言及している記事はいくつか見つけることができた。
それらによると、GNU automakeとGNU Autoconfを利用することに関しては、強制的にGPLライセンスにされることはないらしい。

どういうことかというと、autotoolsによって生成されるのは全てテキスト(スクリプト)ファイルであり、そこにはライセンス条項がきちんと記載されている。そう、例のGPL文が載っている…のに続いて、As a special exception...というパラグラフがある。

As a special exception to the GNU General Public License, if you distribute this file as part of a program that contains a configuration script generated by Autoconf, you may include it under the same distribution terms that you use for the rest of that program.

趣旨としては、autoconfで自動生成された*1ものであり、かつ、配布物の一部として含まれる場合は、ライセンスはGPLにしなくてもいいよということみたいだ。これなら、会社とかプロプラな組織でも安心してautotoolsが使えるね!

一方でgettext-toolsを利用することに関しては、強制的にGPLライセンスにされると思われる。
以下のようなGPL違反の問題があげられている。

各国語対応のため,gettextパッケージ(GPL-2ライセンス)のソースコードの一部(libintl)を利用したにもかかわらず,該当の配布物(ライセンスを明確にしていないソースコード・非GPLソースコード,およびバイナリのみ公開のライブラリを含む)がGPLでの配布ではなかった

注意: あくまでどちらも私の理解を記述しているだけなので、利用時にはライセンス表記やライセンサーに問い合わせることを推奨する。

おわりに

本記事では、小規模ソフトウェアパッケージを想定として、Autotoolsの手順を紹介した。

今回は、「ソースコードがビルドできる」「マニュアルページをインストールできる」「gettextでローカライズが可能」を目的としたため、最小限のconfigure.acMakefile.amのパラメータを変更した。 しかし、これらのファイルには多数のパラメータが存在しており、さまざまな状況下でも対応することができる。

実際に中/大規模のプロジェクトでAutotoolsを利用するときには、automakeの公式ページや他プロジェクトを確認しておくとよいだろう。

参考

Automake関連

Gettext関連

OSSライセンス関連

*1:cmakeやMesonなど別のツールに乗り換えるのも手ではあるが