LinuxにおけるEFI Variableをみてみる
LinuxにおけるEFI Variableをみてみる
この記事は 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つである.
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_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を自作する際にも参考にしたい工夫である.