この記事は Linux Advent Calendar 2018 の20日目の記事として書かれた. ここではLinux kerenlにおけるEFI Variableのコードをみていく.
EFI Variableとは
UEFIでは,EFI varibale(EFI変数)というものが存在する. これは不揮発性メモリ(NVRAM)に値が書き込まれるため, 電源を切っても値が失われることなく保存される. EFI Variableは起動時の起動の順番などが保存される.
UEFIにはEFI Variablesへの読み書きを行うための機能が Runtime Servicesに存在する. このため後述するように,OSの起動後もEFI Variableが Runtime Servicesを通して利用可能となっている. 関連する関数は具体的には以下の4つである.
GetVariablesGetNextVariablesNameSetVariablesQueryVairableInfo
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_superが
mount_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_listに
EFI 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_readとefivarfs_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を自作する際にも参考にしたい工夫である.