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を自作する際にも参考にしたい工夫である.