Blog posts by @retrage

mirror of https://retrage.github.io/

LinuxにおけるEFI Variableをみてみる

この記事は Linux Advent Calendar 201820日目の記事として書かれた. ここではLinux kerenlにおけるEFI Variableのコードをみていく.

EFI Variableとは

UEFIでは,EFI varibale(EFI変数)というものが存在する. これは不揮発性メモリ(NVRAM)に値が書き込まれるため, 電源を切っても値が失われることなく保存される. EFI Variableは起動時の起動の順番などが保存される.

UEFIにはEFI Variablesへの読み書きを行うための機能が Runtime Servicesに存在する. このため後述するように,OSの起動後もEFI Variableが Runtime Servicesを通して利用可能となっている. 関連する関数は具体的には以下の4つである.

  • GetVariables
  • GetNextVariablesName
  • SetVariables
  • QueryVairableInfo

EFI Variableはkey-valueの形式になっており, 文字列keyを入力として,GetVariables/SetVariablesにより valueの読み/書きができる. Valueを得るために必要なkeyはGetNextVariableName により得ることができる. QueryVairbaleInfoではEFI Variable全体のについての情報を得ることができる.

Linux kernelにおけるEFI Variable

Linux kernelでは,EFI Variableは 擬似ファイルシステムとして表現され, /sys/firmware/efi/efivarsにマウントされる. (なお,過去にはsysfs経由でのサポートだったようだが, sysfsの制限のため,別実装となったようである)

ここで実際のefivarsを例示してみる.

$ ls /sys/firmware/efi/efivars
# snip
Boot0000-8be4df61-93ca-11d2-aa0d-00e098032b8c
Boot0001-8be4df61-93ca-11d2-aa0d-00e098032b8c
Boot0002-8be4df61-93ca-11d2-aa0d-00e098032b8c
Boot0003-8be4df61-93ca-11d2-aa0d-00e098032b8c
# snip
$ cat /sys/firmware/efi/efivars/Boot0000-8be4df61-93ca-11d2-aa0d-00e098032b8c
bubuntu??H??yC??Y??ä?4\EFI\UBUNTU\SHIMX64.EFI?%

このように,0000には現在起動しているUbuntuについてのパスが保存されていることがわかる.

efivarsの実装

ではここから実際にefivarsの実装をみていく. トップダウンefivarfsの実装から どのようにEFI Variableがファイルにマップされるかを見て, 次にRuntime Servicesがどのように呼ばれるかを見ていく. 参照するLnux kernelはmainlineのv4.20-rc2(ccda4af0f4b92f7b4c308d3acc262f4a7e3affad) である.

efivarfsの初期化

efivarfsの実装はfs/efivarfs/にある. 最初にfs/efivarfs/super.cをみる.

static __init int efivarfs_init(void)
{
    if (!efi_enabled(EFI_RUNTIME_SERVICES))
        return -ENODEV;

    if (!efivars_kobject())
        return -ENODEV;

    return register_filesystem(&efivarfs_type);
}

Linux kernel初期化時にefivarfsの初期化が efivarfs_initにより行われてefivarfsがマウントされる.

static struct file_system_type efivarfs_type = { 
    .owner   = THIS_MODULE,
    .name    = "efivarfs",
    .mount   = efivarfs_mount,
    .kill_sb = efivarfs_kill_sb,
};
static struct dentry *efivarfs_mount(struct file_system_type *fs_type,
                    int flags, const char *dev_name, void *data)
{   
    return mount_single(fs_type, flags, data, efivarfs_fill_super);
}

辿っていくと,efivarfs_fill_supermount_singleにより実行される.

static int efivarfs_fill_super(struct super_block *sb, void *data, int silent)
{   
    struct inode *inode = NULL;
    struct dentry *root;
    int err;
    
    efivarfs_sb = sb;
    
    sb->s_maxbytes          = MAX_LFS_FILESIZE;
    sb->s_blocksize         = PAGE_SIZE;
    sb->s_blocksize_bits    = PAGE_SHIFT;
    sb->s_magic             = EFIVARFS_MAGIC;
    sb->s_op                = &efivarfs_ops;
    sb->s_d_op      = &efivarfs_d_ops;
    sb->s_time_gran         = 1;
    
    inode = efivarfs_get_inode(sb, NULL, S_IFDIR | 0755, 0, true);
    if (!inode)
        return -ENOMEM;
    inode->i_op = &efivarfs_dir_inode_operations;
    
    root = d_make_root(inode);
    sb->s_root = root;
    if (!root) 
        return -ENOMEM;
    
    INIT_LIST_HEAD(&efivarfs_list);
    
    err = efivar_init(efivarfs_callback, (void *)sb, true, &efivarfs_list);
    if (err)
        __efivar_entry_iter(efivarfs_destroy, &efivarfs_list, NULL, NULL);
    
    return err;
}

ここでファイルシステムの中身を作成している. 最初に/sys/firmware/efi/efivarが作成され, efivar_initによりefivarfs_listEFI Variableのエントリが追加される.

/**
 * efivar_init - build the initial list of EFI variables
 * @func: callback function to invoke for every variable
 * @data: function-specific data to pass to @func
 * @atomic: do we need to execute the @func-loop atomically?
 * @duplicates: error if we encounter duplicates on @head?
 * @head: initialised head of variable list
 *
 * Get every EFI variable from the firmware and invoke @func. @func
 * should call efivar_entry_add() to build the list of variables.
 *
 * Returns 0 on success, or a kernel error code on failure.
 */
int efivar_init(int (*func)(efi_char16_t *, efi_guid_t, unsigned long, void *),
        void *data, bool duplicates, struct list_head *head)
{
    const struct efivar_operations *ops = __efivars->ops;
    unsigned long variable_name_size = 1024; 
    efi_char16_t *variable_name;
    efi_status_t status;
    efi_guid_t vendor_guid;
    int err = 0;
    /* snip */
    do {
        variable_name_size = 1024;

        status = ops->get_next_variable(&variable_name_size,
                        variable_name,
                        &vendor_guid);
        switch (status) {
        case EFI_SUCCESS:
            if (duplicates)
                up(&efivars_lock);

            variable_name_size = var_name_strnsize(variable_name,
                                   variable_name_size);

            /*
             * Some firmware implementations return the
             * same variable name on multiple calls to
             * get_next_variable(). Terminate the loop
             * immediately as there is no guarantee that
             * we'll ever see a different variable name,
             * and may end up looping here forever.
             */
            if (duplicates &&
                variable_is_present(variable_name, &vendor_guid,
                        head)) {
                dup_variable_bug(variable_name, &vendor_guid,
                         variable_name_size);
                status = EFI_NOT_FOUND;
            } else {
                err = func(variable_name, vendor_guid,
                       variable_name_size, data);
                if (err)
                    status = EFI_NOT_FOUND;
            }

            if (duplicates) {
                if (down_interruptible(&efivars_lock)) {
                    err = -EINTR;
                    goto free;
                }
            }

            break;

efivar_initでは, ops->get_next_variableにより 実際にRuntimeServices->GetNextVariableが呼ばれ, EFI Variableが読み出される. なお,コメントにあるように,いくつかのUEFI Firmwareの実装では 複数のGetNextVariableの呼び出しにより同じEFI Variableが得られてしまう というバグがあるため,重複があった場合,dup_variable_bugにより これを処理し,do-whileを抜けるようになっているようである.

さて,問題がない場合は,引数として渡されるfuncを呼び出す. これはefivarfs_callbackとなっている.

static int efivarfs_callback(efi_char16_t *name16, efi_guid_t vendor,
                 unsigned long name_size, void *data)
{
    struct super_block *sb = (struct super_block *)data;
    struct efivar_entry *entry;
    struct inode *inode = NULL;
    struct dentry *dentry, *root = sb->s_root;
    unsigned long size = 0;
    char *name;
    int len;
    int err = -ENOMEM;
    bool is_removable = false;
    /* snip */
    inode = efivarfs_get_inode(sb, d_inode(root), S_IFREG | 0644, 0,
                   is_removable);
    if (!inode)
        goto fail_name;

    dentry = efivarfs_alloc_dentry(root, name);
    if (IS_ERR(dentry)) {
        err = PTR_ERR(dentry);
        goto fail_inode;
    }

    efivar_entry_size(entry, &size);
    err = efivar_entry_add(entry, &efivarfs_list);
    if (err)
        goto fail_inode;

efivarfs_callbackでは, 得られたEFI Variableに対応するファイルを作成して追加する.

以上がefivarfsの初期化の流れとなっている.

efivarfsのread/write

次にefivarfsでのread/write時の動作をみてみる.

const struct file_operations efivarfs_file_operations = {
    .open   = simple_open,
    .read   = efivarfs_file_read,
    .write  = efivarfs_file_write,
    .llseek = no_llseek,
    .unlocked_ioctl = efivarfs_file_ioctl,
}

より,efivarfs_file_readefivarfs_file_writeに集約されている.

read時

static ssize_t efivarfs_file_read(struct file *file, char __user *userbuf,
        size_t count, loff_t *ppos)
{
    struct efivar_entry *var = file->private_data;
    unsigned long datasize = 0;
    u32 attributes;
    void *data;
    ssize_t size = 0;
    int err;
    /* snip */
    size = efivar_entry_get(var, &attributes, &datasize,
                data + sizeof(attributes));
int efivar_entry_get(struct efivar_entry *entry, u32 *attributes,
             unsigned long *size, void *data)
{
    const struct efivar_operations *ops = __efivars->ops;
    efi_status_t status;

    if (down_interruptible(&efivars_lock))
        return -EINTR;
    status = ops->get_variable(entry->var.VariableName,
                   &entry->var.VendorGuid,
                   attributes, size, data);
    up(&efivars_lock);

    return efi_status_to_err(status);
}

このように, efivarfs_file_read->efivar_entry_get->get_variable によりread時にRuntimeServices->GetVariableを呼び出していることがわかる.

write時

static ssize_t efivarfs_file_write(struct file *file,
        const char __user *userbuf, size_t count, loff_t *ppos)
{
    struct efivar_entry *var = file->private_data;
    void *data;
    u32 attributes;
    struct inode *inode = file->f_mapping->host;
    unsigned long datasize = count - sizeof(attributes);
    ssize_t bytes;
    bool set = false;
    /* snip */
    bytes = efivar_entry_set_get_size(var, attributes, &datasize,
                      data, &set);
    /* snip */
    if (bytes == -ENOENT) {
        drop_nlink(inode);
        d_delete(file->f_path.dentry);
        dput(file->f_path.dentry);
    } else {
        inode_lock(inode);
        i_size_write(inode, datasize + sizeof(attributes));
        inode_unlock(inode);
    } 
int efivar_entry_set_get_size(struct efivar_entry *entry, u32 attributes,
                  unsigned long *size, void *data, bool *set)
{
    const struct efivar_operations *ops = __efivars->ops;
    efi_char16_t *name = entry->var.VariableName;
    efi_guid_t *vendor = &entry->var.VendorGuid;
    efi_status_t status;
    int err;
    /* snip */
    status = ops->set_variable(name, vendor, attributes, *size, data);
    if (status != EFI_SUCCESS) {
        err = efi_status_to_err(status);
        goto out;
    }
    *size = 0;

    /*
     * Writing to the variable may have caused a change in size (which
     * could either be an append or an overwrite), or the variable to be
     * deleted. Perform a GetVariable() so we can tell what actually
     * happened.
     */
    status = ops->get_variable(entry->var.VariableName,
                   &entry->var.VendorGuid,
                   NULL, size, NULL);

    if (status == EFI_NOT_FOUND)
        efivar_entry_list_del_unlock(entry);
    else
        up(&efivars_lock);

write時も同様にして efivarfs_file_write->efivar_entry_set_get_size->set_variable により与えられたデータをEFI Variableとして書き込んでいる. コメントにあるように,RuntimeServices->GetVariableを実行し, データの追加や上書き,削除のどれがなされたかをsizeにより判定する. efivar_entry_set_get_sizeの返り値bytesにより, ファイルを削除するか,サイズを変更するなどの操作を行う.

Runtime Servicesの呼び出しに関するTips

static efi_status_t virt_efi_get_variable(efi_char16_t *name,
                      efi_guid_t *vendor,
                      u32 *attr,
                      unsigned long *data_size,
                      void *data)
{
    efi_status_t status;

    if (down_interruptible(&efi_runtime_lock))
        return EFI_ABORTED;
    status = efi_queue_work(GET_VARIABLE, name, vendor, attr, data_size,
                data);
    up(&efi_runtime_lock);
    return status;
}

Linux kernelにおいて, UEFI Runtime Servicesを呼び出す場合には, efi_runtime_lockセマフォをdownしてから 実行すべきUEFIサービスをefi_queue_workキューに挿入して 実行させ,最後にセマフォをupさせている. (例外もあり,nonblockingな呼び出しも存在する)

OSを自作する際にも参考にしたい工夫である.

EFI stubなArch Linuxのインストール

QEMU上の仮想マシンEFI stubなArch Linuxをインストールする。 ここでのホストはUbuntu 16.04.4 TLSとする。 なお[1][2][3][4]を参考にした。

OVMFのダウンロードとArch Linuxのインストールディスクのダウンロード

最初にOVMFをダウンロードして解凍する。 OVMFはTianoCoreをベースにした仮想マシン向けのUEFI実装である。

$ wget https://www.kraxel.org/repos/jenkins/edk2/edk2.git-ovmf-x64-0-20180508.84.g7cd8a57599.noarch.rpm
$ rpm2cpio edk2.git-ovmf-x64-0-20180508.84.g7cd8a57599.noarch.rpm | cpio -idmv
$ cp usr/share/edk2.git/ovmf-x64/OVMF_CODE-pure-efi.fd .
$ cp usr/share/edk2.git/ovmf-x64/OVMF_VARS-pure-efi.fd .

Arch Linuxのインストールディスクをダウンロードする。

$ wget http://ftp.jaist.ac.jp/pub/Linux/ArchLinux/iso/2018.05.01/archlinux-2018.05.01-x86_64.iso

インストール先のディスクイメージを作成する。

$ qemu-img create -f raw arch-amd64.img 32G

仮想マシンの起動と設定

インストールディスクから起動する。 これ以降は仮想マシンでの操作となる。

$ qemu-system-x86_64 -enable-kvm -k ja -m 2048 -localtime -drive if=pflash,format=raw,readonly,file=OVMF_CODE-pure-efi.fd -drive if=pflash,format=raw,file=OVMF_VARS-pure-efi.fd -hda arch-amd64.img -net nic,model=e1000 -net user -cdrom archlinux-2018.05.01-x86_64.iso -boot d

キーボードレイアウトを設定。

# loadkeys jp106

gdiskパーティションを作成。 ここでは/dev/sda1を512MBのEFIパーティション、 /dev/sda2を残りのすべての容量としたLinuxパーティションとした。

# gdisk /dev/sda
Command (? for help):o
Command (? for help):n
Permission number: 1
First sector     : 
Last sector      : +512M
Hex code or GUID : ef00
Command (? for help):n
Permission number: 2
First sector     : 
Last sector      :  
Hex code or GUID : 8300

最後に書き込みを行う。

Command (? for help):w

パーティションをフォーマット

# mkfs.vfat -F32 /dev/sda1
# mkfs.ext4 /dev/sda2

それぞれをマウント。

# mount /dev/sda2
# mkdir -p /mnt/boot
# mount /dev/sda1 /mnt/boot

ダウンロードのミラーサイトを設定。

# nano /etc/pacman.d/mirrorlist

システムのインストール。

# pacstrap -i /mnt base base-devel

fstabの生成。

# genfstab -U -p /mnt >> /mnt/etc/fstab

localeの設定。

# locale-gen
# echo LANG=en_US.UTF-8 > /etc/locale.conf
# export LANG=en_US.UTF-8

キーボードマップの設定。

# echo KEYMAP=jp106 > /etc/vconsole.conf

localtimeの設定。

# rm /etc/localtime
# ln -s /usr/share/zoneinfo/Asia/Tokyo /etc/localtime
# hwclock -u -w

ホスト名の設定。

# echo "arch-vm" > /etc/hostname

DHCPクライアントの設定。

# systemctl enable dhcpcd.service

パスワードの設定。

# passwd

GRUB2の代わりにefibootmgrEFI Variableを設定。

# efibootmgr -d /dev/sda -p 1 -c -L "Arch Linux" -l /vmlinuz-linux -u "root=dev/sda2 rw initrd=initramfs-linux.img"
# efibootmgr -v

exitしてhalt。

# exit
# halt

以下で正しく起動することを確認。

$ qemu-system-x86_64 -enable-kvm -k ja -m 2048 -localtime -drive if=pflash,format=raw,readonly,file=OVMF_CODE-pure-efi.fd -drive if=pflash,format=raw,file=OVMF_VARS-pure-efi.fd -hda arch-amd64.img -net nic,model=e1000 -net user

EFI stubなLinux kernelのビルド

先の説明でインストールしたArch LinuxEFI stubとなっている。 ここでは、ホストでLinux kernelをビルドしそれを仮想マシンにインストールしてみる。

Linuxカーネルのビルド

カーネルをダウンロードする。

$ git clone https://kernel.googlesource.com/pub/scm/linux/kernel/git/torvalds/linux.git --branch v4.17 --depth 1

ホストのconfigをコピーして多少の修正を行い、ホスト上でビルドする。 make menuconfigCONFIG_EFI_STUB=yとなっていることを確認する。 makeするときのjob数はホストのCPUのコア数に応じて変更する。

$ cp /boot/config-4.15.0-33-generic .config
$ make menuconfig
$ make -j9

Linux kernelの仮想マシンへのインストール

仮想マシンのディスクイメージをマウントする。

# /sbin/kpartx -av arch-amd64.img
# mount /dev/mapper/loop0p2 /mnt

ホストでビルドしたカーネル仮想マシンにインストールする。

# make INSTALL_MOD_PATH=/mnt modules_install
# mkdir -p /mnt/var/tmp
# cp System.map /mnt/var/tmp
# cp arch/x86_64/boot/bzImage /mnt/var/tmp
# umount /mnt

最後に仮想マシン上でホストでビルドしたbzImageとSystem.mapをコピーし、 initramfsを生成する。

# cp -f /var/tmp/bzImage /boot/vmlinuz-linux
# cp -f /var/tmp/System.map /boot/System.map
# mkinitcpio -c /etc/mkinitcpio.conf -g /boot/initramfs-linux.img

最後に再起動し、ビルドしたバージョンのカーネルであることを確認する。

参考文献

技術書典5で「UEFI読本 基礎編 Linux編」を頒布

2018年10月8日に池袋サンシャインシティで開催される 技術書典5において「海洋軟件」として 「UEFI読本 基礎編 Linux編」を頒布する. 配置先は「お09」である.

UEFI読本 基礎編 Linux

以下,本書「はじめに」より抜粋.

UEFIが2005年に登場してから10年以上が経過した。 現在のx86系CPUを搭載したコンピュータのほとんどが UEFIに対応しており、かつOSも当然のように対応している。 しかし、UEFIやそこからの起動を解説をした文書は非常に少なく、 規格書かソースコードを読み解くしかなかった。 本書はそのような現状に応えるべく企画された。

本書は「UEFI読本 基礎編 Linux編」というUEFIについての解説書である。 「基礎編」と「Linux編」の2部構成となっており、 「基礎編」ではUEFIでのOSの起動を知るためのUEFIの基礎について解説を行い、 「Linux編」ではLinux kernelのソースコードを実際に追跡してみていく。

LKL.js: Linux kernelを直接JavaScript上で動かす

Linux kernelを直接JavaScript上で動かした. つまり,JSLinuxのようにEmulatorをJavaScriptで作成し, その上でLinuxを動かすのではなく, JavaScriptで書かれたLinuxを生成し,それを動かす,ということである.

f:id:retrage01:20180721131907p:plain
LKL.js Architecture

リポジトリは以下の通り.

なお lkl.js Demo にデモを用意した. SharedArrayBufferを有効にして試してみてほしい.

続きを読む

HiFive1でZephyr

HiFive1とは,SiFiveによって開発されたRISC-V搭載のArduino互換ボードである. 一方,Zephyrは,Linux Foundationにより開発が進められている組み込み向けOSである. upstreamのZephyrはHiFive1に対応している. ここでは,ZephyrをHiFive1向けにビルドしてみる.

続きを読む

RustでBrainfuck処理系を高速化して遊んでみる

Brainfuckとは><+-.,[]の8つの命令からなるプログラミング言語である. 実装が簡単であるために,すでに多くの言語によって実装がなされている. ここでは, Adventures in JIT compilation: Part 1 - an interpreter を参考にC++の実装をRustに移植し,そのパフォーマンスを計測し,比較をして遊んでみる.

Brainfuckの実装

実装したBrainfuck

  • simpleinterp
  • optinterp
  • optinterp2
  • optinterp3

の4通りがある.

simpleinterpは素朴な実装であり,高速化はなされていない. optinterpは[]の対応表を実行時に作成することにより最適化を図っている. optinterp2は8つの命令に加えて高水準な命令を定義し, 局所的に実行される命令をそれらの命令に置き換えることで最適化を行う. optinterp3はさらに実行されるループに使われるパターンを高水準な命令に置き換え,最適化を行う.

実装の概要については BrainF*ckの高速化 において解説している.

ベンチマークと計測方法

先に挙げたブログ内では mandelbrotfactorの2つのスクリプトベンチマークに用いている. ここでもこれらのスクリプトを用いることとする.

実行速度の計測にはtimeを用いる. 計測時には--releaseオプションを指定し,最適化がなされるようにした.

実行環境は

MacBook Air (11-inch, Early 2014)
CPU: 1.7 GHz Intel Core i7
RAM: 8GB 1600 MHz DDR3
OS: macOS High Sierra 10.13.5
cargo: 1.26.0
rustc: 1.26.2

である.

mandelbrot

mandelbrotの場合の比較.

simpleinterp: cargo run --release mandelbrot.bf  47.78s user 0.14s system 99% cpu 48.037 total
optinterp:    cargo run --release mandelbrot.bf  25.90s user 0.10s system 99% cpu 26.080 total
optinterp2:   cargo run --release mandelbrot.bf  8.30s user 0.08s system 99% cpu 8.443 total
optinterp3:   cargo run --release mandelbrot.bf  6.46s user 0.09s system 99% cpu 6.617 total

f:id:retrage01:20180618225424p:plain
Mandelbrot Performance

factor

factorの場合の比較

simpleinterp: cargo run --release factor.bf < prime  17.16s user 0.10s system 99% cpu 17.321 total
optinterp:    cargo run --release factor.bf < prime  9.64s user 0.09s system 99% cpu 9.790 total
optinterp2:   cargo run --release factor.bf < prime  3.45s user 0.09s system 98% cpu 3.611 total
optinterp3:   cargo run --release factor.bf < prime  2.93s user 0.07s system 98% cpu 3.039 total

f:id:retrage01:20180618225420p:plain
Factor Performance

まとめ

いずれのスクリプトの場合でも simpleinterp > optinterp > optinterp2 > optinterp3 の順の実行時間であった.これは元記事とも一致する. さらに元記事のようにJITなどを用いて最適化することも考えられる.

参考文献

8cc in Lazy K

本日は4月1日で,エイプリルフールの日である. ただ,書いている現在は午後9時で,ちょっと嘘をつくには遅すぎる時間である.そこで,今回は何にも役に立たないものを作ってみようと思った. そこで,表題の通り,Lazy Kで書かれた8ccを生成して遊んでみた.

生成

  1. ELVMで生成された8cc.bf をダウンロード
  2. bf2lazy.c をダウンロード

  3. 2.で1.をLazy Kに変換

gcc bf2lazy.c -o bf2lazy
./bf2lazy < 8cc.bf > 8cc.bf.lazy
  1. 生成されたLazy Kのコード
$ du -h 8cc.bf.lazy
 12G    8cc.bf.lazy

バカみたいにでかくなる.

検証

おそらくは正しく動くはずであるが,8cc.bfだけで検証するのが相当かかるようなので,8cc.bf.lazyではさらに検証に時間がかかるはずなので,やらないこととする.

まとめ

適当にやったらLazy Kのコードがバカでかくなった. 生成されたコードは無用の長物なので,どこにもあげないが,簡単に生成できるので,暇な人はやってみよう.

Google V8 JavaScript EngineでのWebAssemblyのi32.addの実装を見てみる

WebAssembly(以下,wasm)については,既に多くの解説記事が存在するため, wasmについての説明は割愛する. ここでは,wasmにおいてとある命令i32.addがどのように実行されるのかを見ていく. 参照する実装はGoogle V8 JavaScript Engineの 1b254a25163fd84a7696ff62e87cb6dcde7e13f2である.

続きを読む