将EFI从物理模式切换到虚拟模式efi_enter_virtual_mode
c
void __init efi_enter_virtual_mode(void)
{
efi_memory_desc_t *md;
efi_status_t status;
int i;
efi.systab = NULL;
for (i = 0; i < memmap.nr_map; i++) {
md = &memmap.map[i];
if (md->attribute & EFI_MEMORY_RUNTIME) {
md->virt_addr =
(unsigned long)ioremap(md->phys_addr,
md->num_pages << EFI_PAGE_SHIFT);
if (!(unsigned long)md->virt_addr) {
printk(KERN_ERR PFX "ioremap of 0x%lX failed\n",
(unsigned long)md->phys_addr);
}
if (((unsigned long)md->phys_addr <=
(unsigned long)efi_phys.systab) &&
((unsigned long)efi_phys.systab <
md->phys_addr +
((unsigned long)md->num_pages <<
EFI_PAGE_SHIFT))) {
unsigned long addr;
addr = md->virt_addr - md->phys_addr +
(unsigned long)efi_phys.systab;
efi.systab = (efi_system_table_t *)addr;
}
}
}
if (!efi.systab)
BUG();
status = phys_efi_set_virtual_address_map(
sizeof(efi_memory_desc_t) * memmap.nr_map,
sizeof(efi_memory_desc_t),
memmap.desc_version,
memmap.phys_map);
if (status != EFI_SUCCESS) {
printk (KERN_ALERT "You are screwed! "
"Unable to switch EFI into virtual mode "
"(status=%lx)\n", status);
panic("EFI call to SetVirtualAddressMap() failed!");
}
/*
* Now that EFI is in virtual mode, update the function
* pointers in the runtime service table to the new virtual addresses.
*/
efi.get_time = (efi_get_time_t *) efi.systab->runtime->get_time;
efi.set_time = (efi_set_time_t *) efi.systab->runtime->set_time;
efi.get_wakeup_time = (efi_get_wakeup_time_t *)
efi.systab->runtime->get_wakeup_time;
efi.set_wakeup_time = (efi_set_wakeup_time_t *)
efi.systab->runtime->set_wakeup_time;
efi.get_variable = (efi_get_variable_t *)
efi.systab->runtime->get_variable;
efi.get_next_variable = (efi_get_next_variable_t *)
efi.systab->runtime->get_next_variable;
efi.set_variable = (efi_set_variable_t *)
efi.systab->runtime->set_variable;
efi.get_next_high_mono_count = (efi_get_next_high_mono_count_t *)
efi.systab->runtime->get_next_high_mono_count;
efi.reset_system = (efi_reset_system_t *)
efi.systab->runtime->reset_system;
}
函数功能概述
这个函数将EFI(可扩展固件接口)从物理模式切换到虚拟模式,重新映射EFI运行时服务到内核虚拟地址空间,并更新所有运行时服务函数指针
代码逐行详细解释
第一部分:函数声明和变量初始化
c
void __init efi_enter_virtual_mode(void)
{
efi_memory_desc_t *md;
efi_status_t status;
int i;
efi.systab = NULL;
-
void __init efi_enter_virtual_mode(void)
__init
:初始化函数,完成后内存可释放- 函数名明确表示进入EFI虚拟模式
- 无参数
-
变量声明:
efi_memory_desc_t *md
:EFI内存描述符指针efi_status_t status
:EFI操作状态返回值int i
:循环计数器
-
efi.systab = NULL
:清空EFI系统表指针,准备重新查找
第二部分:内存映射遍历和重新映射
c
for (i = 0; i < memmap.nr_map; i++) {
md = &memmap.map[i];
if (md->attribute & EFI_MEMORY_RUNTIME) {
- 循环条件 :
i < memmap.nr_map
遍历所有内存映射条目 md = &memmap.map[i]
:获取第i个内存描述符if (md->attribute & EFI_MEMORY_RUNTIME)
:- 检查内存区域是否有
EFI_MEMORY_RUNTIME
属性 - 这个标志表示该内存区域需要在运行时被EFI使用
- 检查内存区域是否有
第三部分:物理地址到虚拟地址的映射
c
md->virt_addr =
(unsigned long)ioremap(md->phys_addr,
md->num_pages << EFI_PAGE_SHIFT);
if (!(unsigned long)md->virt_addr) {
printk(KERN_ERR PFX "ioremap of 0x%lX failed\n",
(unsigned long)md->phys_addr);
}
-
ioremap(md->phys_addr, md->num_pages << EFI_PAGE_SHIFT)
:- 将物理地址映射到内核虚拟地址空间
md->num_pages << EFI_PAGE_SHIFT
:计算总字节数EFI_PAGE_SHIFT
通常是12(4KB页面)
-
错误检查 :如果
ioremap
返回NULL,打印错误信息但继续执行
第四部分:定位EFI系统表
c
if (((unsigned long)md->phys_addr <=
(unsigned long)efi_phys.systab) &&
((unsigned long)efi_phys.systab <
md->phys_addr +
((unsigned long)md->num_pages <<
EFI_PAGE_SHIFT))) {
unsigned long addr;
addr = md->virt_addr - md->phys_addr +
(unsigned long)efi_phys.systab;
efi.systab = (efi_system_table_t *)addr;
}
}
}
-
复杂条件判断:检查EFI系统表是否位于当前内存区域内
- 条件1:
md->phys_addr <= efi_phys.systab
(起始地址检查) - 条件2:
efi_phys.systab < md->phys_addr + 区域大小
(结束地址检查)
- 条件1:
-
虚拟地址计算:
-
addr = md->virt_addr - md->phys_addr + (unsigned long)efi_phys.systab
addr = md->virt_addr - md->phys_addr + efi_phys.systab = (md->virt_addr) + (efi_phys.systab - md->phys_addr) = 虚拟基地址 + 物理偏移量
text物理内存布局: 0x0000000000001000 +----------------+ ← md->phys_addr | EFI运行时代码 | 0x0000000000001500 +----------------+ ← efi_phys.systab | EFI系统表 | 0x0000000000001600 +----------------+ | 其他EFI数据 | 0x0000000000002000 +----------------+ 虚拟内存布局: 0xffff000000001000 +----------------+ ← md->virt_addr | 重新映射的 | 0xffff000000001500 +----------------+ ← efi.systab (计算结果) | EFI区域 | 0xffff000000001600 +----------------+ | | 0xffff000000002000 +----------------+
-
第五部分:系统表存在性验证
c
if (!efi.systab)
BUG();
- 关键检查:如果没找到EFI系统表,触发内核BUG
- 这表明EFI系统表是必需的,找不到则系统无法继续
第六部分:切换到虚拟模式
c
status = phys_efi_set_virtual_address_map(
sizeof(efi_memory_desc_t) * memmap.nr_map,
sizeof(efi_memory_desc_t),
memmap.desc_version,
memmap.phys_map);
if (status != EFI_SUCCESS) {
printk (KERN_ALERT "You are screwed! "
"Unable to switch EFI into virtual mode "
"(status=%lx)\n", status);
panic("EFI call to SetVirtualAddressMap() failed!");
}
-
phys_efi_set_virtual_address_map
:调用EFI运行时服务- 参数1:内存映射表总大小
- 参数2:单个描述符大小
- 参数3:描述符版本
- 参数4:物理内存映射表地址
-
错误处理:如果调用失败,打印严重错误并panic
第七部分:更新运行时服务函数指针
c
/*
* Now that EFI is in virtual mode, update the function
* pointers in the runtime service table to the new virtual addresses.
*/
efi.get_time = (efi_get_time_t *) efi.systab->runtime->get_time;
efi.set_time = (efi_set_time_t *) efi.systab->runtime->set_time;
efi.get_wakeup_time = (efi_get_wakeup_time_t *)
efi.systab->runtime->get_wakeup_time;
efi.set_wakeup_time = (efi_set_wakeup_time_t *)
efi.systab->runtime->set_wakeup_time;
efi.get_variable = (efi_get_variable_t *)
efi.systab->runtime->get_variable;
efi.get_next_variable = (efi_get_next_variable_t *)
efi.systab->runtime->get_next_variable;
efi.set_variable = (efi_set_variable_t *)
efi.systab->runtime->set_variable;
efi.get_next_high_mono_count = (efi_get_next_high_mono_count_t *)
efi.systab->runtime->get_next_high_mono_count;
efi.reset_system = (efi_reset_system_t *)
efi.systab->runtime->reset_system;
}
- 现在EFI处于虚拟模式,需要更新函数指针
- 函数指针更新 :将EFI运行时服务表中的函数指针复制到内核的
efi
结构中 - 包含的服务 :
- 时间管理:get_time, set_time, get_wakeup_time, set_wakeup_time
- 变量服务:get_variable, get_next_variable, set_variable
- 系统服务:get_next_high_mono_count, reset_system
关键技术原理
EFI内存映射的重要性
物理模式:EFI服务使用物理地址
虚拟模式:EFI服务使用虚拟地址 ← 切换目标
ioremap
的作用
- 将物理内存映射到内核虚拟地址空间
- 使得内核可以访问EFI运行时服务所需的内存区域
函数指针更新的必要性
- EFI系统表中的函数指针在虚拟模式切换后变为虚拟地址
- 内核需要保存这些指针以便后续调用EFI服务
总结
这个函数完成了EFI从物理模式到虚拟模式的关键切换:
- 内存重映射:将EFI运行时内存映射到虚拟地址空间
- 系统表定位:找到并更新EFI系统表的虚拟地址
- 模式切换:调用EFI服务正式进入虚拟模式
- 服务更新:更新所有运行时服务函数指针
phys_efi_set_virtual_address_map
c
static efi_status_t
phys_efi_set_virtual_address_map(unsigned long memory_map_size,
unsigned long descriptor_size,
u32 descriptor_version,
efi_memory_desc_t *virtual_map)
{
efi_status_t status;
efi_call_phys_prelog();
status = efi_call_phys(efi_phys.set_virtual_address_map,
memory_map_size, descriptor_size,
descriptor_version, virtual_map);
efi_call_phys_epilog();
return status;
}
函数功能概述
这个函数是EFI(可扩展固件接口)运行时服务调用的封装函数,用于在物理模式下调用EFI的SetVirtualAddressMap
服务,将EFI从物理地址模式切换到虚拟地址模式。
代码逐行详细解释
第一部分:函数声明和参数
c
static efi_status_t
phys_efi_set_virtual_address_map(unsigned long memory_map_size,
unsigned long descriptor_size,
u32 descriptor_version,
efi_memory_desc_t *virtual_map)
{
-
static efi_status_t
:static
:函数只在当前文件内可见efi_status_t
:返回值类型,EFI标准定义的状态码
-
函数名称 :
phys_efi_set_virtual_address_map
phys_
:表示在物理模式下调用efi_set_virtual_address_map
:对应的EFI运行时服务名称
-
参数列表:
unsigned long memory_map_size
:内存映射表的总大小(字节)unsigned long descriptor_size
:单个内存描述符的大小(字节)u32 descriptor_version
:描述符版本号efi_memory_desc_t *virtual_map
:虚拟地址映射表指针
第二部分:局部变量声明
c
efi_status_t status;
efi_status_t status
:- 声明一个变量来存储EFI调用的返回状态
efi_status_t
是EFI标准定义的状态类型,通常是64位整数- 可能的值包括:
EFI_SUCCESS
、EFI_INVALID_PARAMETER
等
第三部分:物理模式调用前准备
c
efi_call_phys_prelog();
efi_call_phys_prelog()
:物理模式调用前奏- 这是一个架构特定的宏或函数
- 主要功能包括:
- 保存当前CPU状态
- 切换到EFI调用所需的执行环境
- 可能禁用中断
- 设置特殊的寄存器状态
- 确保CPU处于正确的模式(实模式或保护模式)
第四部分:EFI服务调用
c
status = efi_call_phys(efi_phys.set_virtual_address_map,
memory_map_size, descriptor_size,
descriptor_version, virtual_map);
-
efi_call_phys
:物理模式EFI调用宏- 这是执行实际EFI调用的关键宏
- 处理调用约定和参数传递
-
参数详解:
efi_phys.set_virtual_address_map
:EFI运行时服务函数指针memory_map_size
:传递给EFI服务的内存映射大小descriptor_size
:描述符大小descriptor_version
:版本信息virtual_map
:虚拟地址映射表
-
SetVirtualAddressMap
服务的作用:- 通知EFI固件内核的虚拟内存布局
- 让EFI更新其内部指针为虚拟地址
- 这是EFI从物理模式切换到虚拟模式的关键步骤
第五部分:调用后清理
c
efi_call_phys_epilog();
efi_call_phys_epilog()
:物理模式调用收尾- 与
efi_call_phys_prelog()
对应 - 主要功能包括:
- 恢复之前保存的CPU状态
- 重新启用中断
- 恢复正常的执行环境
- 清理临时设置
- 与
第六部分:返回状态
c
return status;
}
return status
:返回EFI调用的状态码- 调用者根据这个状态判断操作是否成功
efi_call_phys_prelog
c
static void efi_call_phys_prelog(void)
{
unsigned long cr4;
unsigned long temp;
spin_lock(&efi_rt_lock);
local_irq_save(efi_rt_eflags);
/*
* If I don't have PSE, I should just duplicate two entries in page
* directory. If I have PSE, I just need to duplicate one entry in
* page directory.
*/
__asm__ __volatile__("movl %%cr4, %0":"=r"(cr4));
if (cr4 & X86_CR4_PSE) {
efi_bak_pg_dir_pointer[0].pgd =
swapper_pg_dir[pgd_index(0)].pgd;
swapper_pg_dir[0].pgd =
swapper_pg_dir[pgd_index(PAGE_OFFSET)].pgd;
} else {
efi_bak_pg_dir_pointer[0].pgd =
swapper_pg_dir[pgd_index(0)].pgd;
efi_bak_pg_dir_pointer[1].pgd =
swapper_pg_dir[pgd_index(0x400000)].pgd;
swapper_pg_dir[pgd_index(0)].pgd =
swapper_pg_dir[pgd_index(PAGE_OFFSET)].pgd;
temp = PAGE_OFFSET + 0x400000;
swapper_pg_dir[pgd_index(0x400000)].pgd =
swapper_pg_dir[pgd_index(temp)].pgd;
}
/*
* After the lock is released, the original page table is restored.
*/
local_flush_tlb();
cpu_gdt_descr[0].address = __pa(cpu_gdt_descr[0].address);
__asm__ __volatile__("lgdt %0":"=m"
(*(struct Xgt_desc_struct *) __pa(&cpu_gdt_descr[0])));
}
函数功能概述
这个函数为EFI物理模式调用准备执行环境,包括保存状态、修改页表映射、刷新TLB和加载物理地址GDT,确保在物理地址模式下正确调用EFI运行时服务
代码逐行详细解释
第一部分:函数声明和变量定义
c
static void efi_call_phys_prelog(void)
{
unsigned long cr4;
unsigned long temp;
-
static void efi_call_phys_prelog(void)
static
:函数只在当前文件内可见void
:没有返回值- 函数名表示EFI物理调用的前导操作
-
变量声明:
unsigned long cr4
:存储CR4控制寄存器值unsigned long temp
:临时计算变量
第二部分:锁和中断保护
c
spin_lock(&efi_rt_lock);
local_irq_save(efi_rt_eflags);
-
spin_lock(&efi_rt_lock)
:- 获取EFI运行时服务的自旋锁
- 防止多CPU同时进入EFI调用环境
-
local_irq_save(efi_rt_eflags)
:- 保存当前中断标志并禁用中断
- 确保EFI调用期间不被中断打断
第三部分:检查PSE支持
c
__asm__ __volatile__("movl %%cr4, %0":"=r"(cr4));
if (cr4 & X86_CR4_PSE) {
-
内联汇编 :
"movl %%cr4, %0":"=r"(cr4)
- 将CR4控制寄存器的值读取到cr4变量
%%cr4
:CR4寄存器%0
:输出操作数,对应cr4变量"=r"
:约束,表示输出到寄存器
-
PSE检查 :
if (cr4 & X86_CR4_PSE)
X86_CR4_PSE
:Page Size Extension标志位- PSE允许使用4MB大页面,而不仅仅是4KB页面
第四部分:PSE启用时的页表处理
c
efi_bak_pg_dir_pointer[0].pgd =
swapper_pg_dir[pgd_index(0)].pgd;
swapper_pg_dir[0].pgd =
swapper_pg_dir[pgd_index(PAGE_OFFSET)].pgd;
-
备份原始映射:
- 将虚拟地址0对应的页目录项备份到
efi_bak_pg_dir_pointer[0]
pgd_index(0)
:计算虚拟地址0在页目录中的索引
- 将虚拟地址0对应的页目录项备份到
-
建立新映射:
- 将
PAGE_OFFSET
处的内核映射复制到虚拟地址0的位置 - 这样虚拟地址0就指向了内核空间,EFI可以使用物理地址访问
- 将
第五部分:PSE禁用时的页表处理
c
} else {
efi_bak_pg_dir_pointer[0].pgd =
swapper_pg_dir[pgd_index(0)].pgd;
efi_bak_pg_dir_pointer[1].pgd =
swapper_pg_dir[pgd_index(0x400000)].pgd;
swapper_pg_dir[pgd_index(0)].pgd =
swapper_pg_dir[pgd_index(PAGE_OFFSET)].pgd;
temp = PAGE_OFFSET + 0x400000;
swapper_pg_dir[pgd_index(0x400000)].pgd =
swapper_pg_dir[pgd_index(temp)].pgd;
}
-
备份两个页目录项:
- 虚拟地址0和0x400000(4MB)处的映射
- 因为没有PSE,需要处理两个4KB页面目录项
-
建立新映射:
- 将内核空间映射复制到低地址区域
temp = PAGE_OFFSET + 0x400000
:计算对应的内核虚拟地址
第六部分:TLB刷新和GDT处理
c
local_flush_tlb();
cpu_gdt_descr[0].address = __pa(cpu_gdt_descr[0].address);
__asm__ __volatile__("lgdt %0":"=m"
(*(struct Xgt_desc_struct *) __pa(&cpu_gdt_descr[0])));
-
local_flush_tlb()
:- 刷新当前CPU的TLB(转换后备缓冲区)
- 确保页表修改立即生效
-
GDT地址转换:
__pa(cpu_gdt_descr[0].address)
:将GDT描述符地址转换为物理地址- EFI调用需要在物理地址模式下运行
-
加载GDT:
lgdt %0
:加载全局描述符表__pa(&cpu_gdt_descr[0])
:GDT描述符的物理地址- 使用物理地址确保在EFI调用期间GDT可访问
技术原理深度解析
为什么要修改页表映射?
问题:EFI运行时服务期望使用物理地址,但内核运行在虚拟地址空间。
解决方案:建立物理地址到虚拟地址的1:1映射
虚拟地址空间修改前:
0x00000000 +----------------+ ← 用户空间/未映射
| |
PAGE_OFFSET +----------------+ ← 内核空间开始
| 内核映射 |
+----------------+
修改后:
0x00000000 +----------------+ ← 现在映射到内核空间
| 内核映射 | (物理地址0对应虚拟地址0)
PAGE_OFFSET +----------------+ ← 内核空间开始
| 内核映射 |
+----------------+
PSE的影响
有PSE(4MB页面):
- 只需要修改一个页目录项
- 虚拟地址0-4MB映射到内核空间
无PSE(4KB页面):
- 需要修改两个页目录项
- 虚拟地址0-4MB和4MB-8MB都映射到内核空间
GDT处理的重要性
在物理模式调用期间:
- CPU使用物理地址寻址
- GDT描述符必须包含物理地址
- 否则CPU无法正确解析段描述符
内存布局示例
典型的x86内存布局
物理地址空间:
0x00000000 +----------------+ ← 物理内存开始
| |
0x00100000 +----------------+ ← 可能的内核加载位置
| |
虚拟地址空间(修改前):
0x00000000 +----------------+ ← 用户空间
| |
0xC0000000 +----------------+ ← PAGE_OFFSET,内核空间开始
| 内核映射 |
+----------------+
虚拟地址空间(修改后):
0x00000000 +----------------+ ← 现在映射到物理内存
| 物理内存映射 | EFI可以使用物理地址0访问实际内存
0xC0000000 +----------------+ ← 内核空间
| 内核映射 |
+----------------+
efi_call_phys_epilog
c
static void efi_call_phys_epilog(void)
{
unsigned long cr4;
cpu_gdt_descr[0].address =
(unsigned long) __va(cpu_gdt_descr[0].address);
__asm__ __volatile__("lgdt %0":"=m"(cpu_gdt_descr));
__asm__ __volatile__("movl %%cr4, %0":"=r"(cr4));
if (cr4 & X86_CR4_PSE) {
swapper_pg_dir[pgd_index(0)].pgd =
efi_bak_pg_dir_pointer[0].pgd;
} else {
swapper_pg_dir[pgd_index(0)].pgd =
efi_bak_pg_dir_pointer[0].pgd;
swapper_pg_dir[pgd_index(0x400000)].pgd =
efi_bak_pg_dir_pointer[1].pgd;
}
/*
* After the lock is released, the original page table is restored.
*/
local_flush_tlb();
local_irq_restore(efi_rt_eflags);
spin_unlock(&efi_rt_lock);
}
函数功能概述
这个函数是EFI物理模式调用的后处理函数,负责恢复在efi_call_phys_prelog
中修改的系统状态,包括恢复GDT、页表映射,刷新TLB,并释放锁和恢复中断状态
代码逐行详细解释
第一部分:函数声明和变量定义
c
static void efi_call_phys_epilog(void)
{
unsigned long cr4;
-
static void efi_call_phys_epilog(void)
static
:函数只在当前文件内可见void
:没有返回值- 函数名表示EFI物理调用的收尾操作
epilog
与prelog
对应,形成完整的调用包装
-
unsigned long cr4
:存储CR4控制寄存器值,用于检查PSE功能
第二部分:恢复GDT描述符
c
cpu_gdt_descr[0].address =
(unsigned long) __va(cpu_gdt_descr[0].address);
__asm__ __volatile__("lgdt %0":"=m"(cpu_gdt_descr));
-
GDT地址恢复:
__va(cpu_gdt_descr[0].address)
:将物理地址转换回虚拟地址- 在prelog中,GDT地址被转换为物理地址供EFI调用使用
- 现在需要恢复为虚拟地址供内核正常使用
-
重新加载GDT:
lgdt %0
:加载全局描述符表"=m"(cpu_gdt_descr)
:操作数约束,表示内存操作数- 使用恢复后的虚拟地址GDT描述符
第三部分:检查PSE支持
c
__asm__ __volatile__("movl %%cr4, %0":"=r"(cr4));
if (cr4 & X86_CR4_PSE) {
-
读取CR4寄存器 :
"movl %%cr4, %0":"=r"(cr4)
- 内联汇编读取CR4控制寄存器到cr4变量
- 需要检查PSE状态以确定如何恢复页表
-
PSE检查 :
if (cr4 & X86_CR4_PSE)
- 检查CR4寄存器的PSE位是否启用
- 决定恢复页表映射的策略
第四部分:PSE启用时的页表恢复
c
swapper_pg_dir[pgd_index(0)].pgd =
efi_bak_pg_dir_pointer[0].pgd;
- 恢复单个页目录项 :
swapper_pg_dir[pgd_index(0)].pgd
:虚拟地址0对应的页目录项efi_bak_pg_dir_pointer[0].pgd
:之前备份的原始页目录项值- 效果:恢复虚拟地址0的原始映射
第五部分:PSE禁用时的页表恢复
c
} else {
swapper_pg_dir[pgd_index(0)].pgd =
efi_bak_pg_dir_pointer[0].pgd;
swapper_pg_dir[pgd_index(0x400000)].pgd =
efi_bak_pg_dir_pointer[1].pgd;
}
- 恢复两个页目录项 :
- 第一项:恢复虚拟地址
0x00000000
的原始映射 - 第二项:恢复虚拟地址
0x00400000
(4MB)的原始映射 - 对应
prelog
中备份的两个页目录项
- 第一项:恢复虚拟地址
第六部分:TLB刷新和状态恢复
c
local_flush_tlb();
local_irq_restore(efi_rt_eflags);
spin_unlock(&efi_rt_lock);
-
刷新TLB :
local_flush_tlb()
- 使页表修改立即生效
- 清除陈旧的地址转换缓存
-
恢复中断状态 :
local_irq_restore(efi_rt_eflags)
- 恢复之前保存的中断标志
- 重新启用中断(如果之前是启用的)
-
释放锁 :
spin_unlock(&efi_rt_lock)
- 释放EFI运行时服务锁
- 允许其他CPU进行EFI调用