UEFI是如何工作的?

23

我在学习关于引导程序时,恰好遇到了UEFI这个术语。我了解一些关于UEFI的知识。但是,使用UEFI启动系统是在哪种模式(Real,Protected,Long)下进行的?如果普通的启动加载程序无法与UEFI配合工作,那么在处理UEFI时有什么替代的启动加载程序呢?除了汇编之外,我是否需要其他编程语言来创建一个启动加载程序?

5个回答

19

UEFI固件在64位平台上以64位长模式运行,在32位平台上以平面模式运行。与BIOS不同,UEFI具有自己的架构,独立于CPU,并拥有自己的设备驱动程序。UEFI可以挂载分区并读取特定的文件系统。

当一台装有UEFI的x86计算机启动时,接口会搜索系统存储器中标记为特定全球唯一标识符(GUID)的分区,该分区被标记为EFI系统分区(ESP)。顺便说一句,Windows不会挂载此分区,您无法在操作系统中看到它。但是有一个技巧,您只需更改VBR中的分区类型(使用HexWorkshop)为常规FAT32代码,就可以将其挂载到操作系统中。

此分区包含为EFI架构编译的应用程序。通常情况下,您无需处理汇编语言即可编写UEFI应用程序/加载程序,它只是普通的C代码。默认情况下,它位于“EFI / BOOT / BOOTX64.EFI”。当选择引导加载程序时,手动或自动,UEFI将其读入内存并将引导过程的控制权交给它。


4
据我理解,UEFI从16位开始启动 https://github.com/tianocore/edk2/blob/master/UefiCpuPkg/ResetVector/Vtf0/Ia16/ResetVectorVtf0.asm#L52 并且这里是它从16位实模式转换为32位保护模式的位置 https://github.com/tianocore/edk2/blob/master/IntelFsp2Pkg/FspSecCore/SecMain.c#L52 - barlop
1
当我问“UEFI之前运行什么?”时,你说“引导加载程序”。但是引导加载程序并不在UEFI之前运行!!Grub是一个引导加载程序,它在UEFI之后运行。(也许引导加载程序在某种程度上是一个模糊的术语) - barlop
5
开机后执行的第一条指令将从CPU中预设的物理地址(复位向量)中获取。此时,CPU处于“非真实”模式,在CS段基址超出低1MiB 的情况下,但其它方面与16位实模式相同。系统设计师确保该地址映射到闪存ROM上。该代码最终会在从磁盘读取任何内容之前切换到64位模式。但在此之前,它必须配置DRAM控制器等等!为此,通常将使用缓存作为RAM(不进行填充)模式的某些时间... - Peter Cordes
@Alex,你能帮我解决这个问题吗?https://stackoverflow.com/questions/66949166/how-to-perform-some-security-verification-before-booting-operating-system - hamed
在Linux上,您可以使用fdisk -l命令轻松找到ESP(EFI系统分区)。在我的计算机上,它是/dev/nvme0n1p1。然后像往常一样挂载它:sudo mount /dev/nvme0n1p1 ~/esp。接下来,您可以检查~/esp中的内容。 - smwikipedia
显示剩余3条评论

15
在接下来的内容中,一些句子是我见过的最好的来源的复制、融合和措辞改进,然后我使用自己的硬件案例研究来改进和纠正它们的未知因素/错误。
引导保护
Intel Boot Guard 是由 Intel 在第四代 Intel Core(Haswell)中引入的一种验证启动过程的技术。这是通过在制造过程中将 BIOS 签名的公钥闪存到可编程场效应器件(FPF)中实现的,这是 Intel ME(位于 PCH 中)内部的一种一次性可编程存储器,在此过程中,它具有 BIOS 的公钥,并且可以在每次后续启动时验证正确的签名。一旦由制造商启用,Intel Boot Guard 就无法再被禁用。
根据 Intel 的说法,Boot Guard 有两种不同的模式。典型的 PC OEM 将其配置为“已验证启动”模式。PC 制造商将其公钥熔丝到硬件本身中。如果 UEFI 固件未经 OEM 签名即未由 OEM 创建,则计算机将停止并拒绝启动。这就是为什么您不能修改 UEFI 固件的原因。还有第二个选项:“测量启动”模式,其中硬件使用 Intel TXT 将关于启动过程的信息(在受信任的平台模块(TPM)中)或 Intel 平台信任技术(PTT)与 SMX 的帮助下进行安全存储。然后,操作系统可以检查此信息,并且-如果存在问题-向用户呈现错误。
安全启动
启用并完全配置安全启动后,安全启动可以帮助计算机抵御恶意软件的攻击和感染。安全启动通过验证数字签名来检测启动加载程序、关键操作系统文件和未经授权的选项 ROM 的篡改。在它们能够攻击或感染系统之前,检测会被阻止运行。UEFI 安全启动假定 OEM 平台固件是一个可信计算基础(TCB)(即已使用 BootGuard 技术进行初始化并隐式信任它)。

验证启动过程

UEFI之前的阶段

当OEM收到PCH时,ME仍处于“制造模式”,运行固件的特殊部分,将“OEM公钥哈希”和“Boot Guard配置文件”策略值从其flash ROM部分复制到可编程熔丝(FPF)中,使它们成为永久且不可更改的值。然后,它设置一个保险丝,指示它已退出制造模式,因此这部分固件将不会再次运行。可以使用英特尔闪存映像工具(FITC)调整这些值,但除非您有一种方法强制ME进入制造模式,否则忽略闪存映像中的值。

Bootguard配置文件如下:

enter image description here

启用保护BIOS环境:如果设置了这个选项,那么ACM可能会将IBB段复制到CPU缓存中,以便它在缓存作为RAM(CAR)模式下运行,并禁用所有DMA以防止设备能够修改它。
一旦有电源可用,ME CPU就会从其芯片上的引导ROM启动,检查一些跳线和保险丝以确定其配置,然后通常将闪存分区表(FPT)从SPI闪存的ME区域(通常位于BIOS区域下方)复制到其芯片内部的SRAM。它使用位于SPI闪存最低地址处的闪存描述符来定位ME区域。

enter image description here

启动ROM使用FPT定位FTPR分区,并将其从SPI闪存中复制到芯片上的SRAM中。然后,它检查分区清单中存储的密钥的SHA-1哈希值是否与其芯片上的ROM中的哈希值匹配,并验证其余分区清单上的RSA签名。分区表包含分区中每个模块的哈希值,允许在将它们复制到芯片上的SRAM以进行执行之后验证这些模块。

enter image description here

当ME引导x86 CPU时,可能在中。首先进行BIST,然后是BSP MP初始化算法。位于CS:FFF0处的传统复位向量(其中CS为0xF000,段描述符缓存包含基址0xFFFF0000)不再是x86 CPU在重置时执行的第一条指令。相反,在芯片上的微码获取0xFFFFFFC0处的FIT指针(使用0xF000:FFC0和非实模式段描述符hack - 因为是寄存器的初始状态),该指针指向SPI闪存BIOS区域中的FIT表。

这张图片展示了16个字节的FIT条目,它们的模式距离结尾3个字节。

#pragma pack (1)
typedef struct {
  UINT64     Address;
  UINT8      Size[3];
  UINT8      Rsvd;
  UINT16     Version;
  UINT8      Type:7;
  UINT8      C_V:1;
  UINT8      Checksum;
} FIRMWARE_INTERFACE_TABLE_ENTRY;

在我的系统中,有一个 FIT 指针位于 FFFFFFC0 到 FFD90100。

FIT表指向微码更新、ACM、BootGuard启动策略清单(其中包含IBBS)和BootGuard密钥清单等。

我的FIT中没有任何0x7条目。记录类型7仅用于传统的Intel® TXT FIT引导,如果后者未使用,则不需要该记录类型。

BootGuard启动策略(IBBM)包含:

Intel BootGuard Boot Policy Manifest found at base FD3C00h
Tag: ACBP Version: 10h HeaderVersion: 01h
PMBPMVersion: 10h PBSVN: 00h ACMSVN: 02h NEMDataStack: 0010h

Initial Boot Block Element found at base FD3C28h
Tag: IBBS       Version: 10h         Unknown: 0Fh
Flags: 00000000h    IbbMchBar: FED10000h VtdBar: 00000000h
PmrlBase: FED90000h PmrlLimit: 00000000h  EntryPoint: 00100000h

Post IBB Hash:
0000000000000000000000000000000000000000000000000000000000000000

IBB Digest:
B9CCC06B77AEACC51768981D07CBE9E43D34DB6795752C4B998312241B26F874

IBB Segments:
Flags: 0000h Address: FFE10000h Size: 001C3C00h
Flags: 0000h Address: FFFD4C00h Size: 00000080h
Flags: 0000h Address: FFFD5C80h Size: 0000A380h
Flags: 0000h Address: FFFE8000h Size: 00018000h

Boot Policy Signature Element found at base FD3CDDh
Tag: PMSG Version: 10h

Boot Policy RSA Public Key (Exponent: 10001h):
....
Boot Policy RSA Public Key Hash:
....
Boot Policy RSA Signature:
....  

关键清单包含:
Intel BootGuard Key Manifest (KEYM) found at base FD4C80h
Tag: KEYM Version: 10h KmVersion: 10h KmSvn: 00h KmId: 0Fh

Key Manifest RSA Public Key Hash:
...
Boot Policy RSA Public Key Hash:
...
Key Manifest RSA Public Key (Exponent: 10001h):
...
Key Manifest RSA Signature:
... //it does actually contain a signature, I just removed these for space

Cache as RAM (CAR)(又称AC-RAM、无填充模式和无驱逐模式)由微码设置。然后,在FIT中搜索与CPU ID匹配的微码更新。当前微码将它们线性地从闪存复制到L3缓存并使用芯片上对称AES密钥进行解密,然后使用(on-die?)RSA密钥进行验证。这些微码更新很可能还包含ACM的密钥哈希。通过写入UCODE MSR应用微码更新,我假设CPU通过正常的内存访问读取它。FIT绝对必须包含一个微码补丁,并将其补丁化微码SRAM以补充已存在的微码ROM。[1] 接下来,微码返回FIT查找启动ACM(又称BIOS或Bootguard ACM),并将其奇怪地复制到L3中(根据该来源,看起来多个超线程正在复制4KB块?)。ACM包含RSA公钥;微码将其与芯片上的密钥或存储在微码更新中的密钥进行比较,如果不匹配,则停止CPU。然后,微码检查ACM上的签名,如果不匹配,则再次停止。
启动ACM完全在L3中运行。ACM通过MSR从ME接收OEM公钥哈希(验证密钥清单的密钥哈希)和Bootguard Profile
ACM从SPI闪存(由FIT指向并由__KEYM__标识)中读取BootGuard密钥清单到L3中,并对其中存储的RSA公钥进行哈希。如果它与OEM公钥哈希不匹配,或者如果密钥清单上的OEM公钥签名不正确,或者存储的KmSvn不正确,则ACM根据Bootguard Profile位采取行动。如果匹配,则在FIT中查找Bootguard Policy(由__ACBP__标识),并将其复制到L3中。然后,ACM计算策略中RSA公钥的哈希值,并将其与密钥清单中存储的SHA256哈希进行比较。如果无法匹配,或者策略上的RSA签名不匹配,则ACM根据Profile设置再次采取行动。
ACM使用现在验证的Bootguard策略结构来读取初始引导块(IBB)段到L3中,并在复制时进行哈希处理。如果计算出的哈希值与策略中的“IBB摘要”不匹配,则ACM根据配置文件设置采取行动。第四个IBB段包含重置向量,因此它会防止其被修改。目前尚不清楚它是否将CPU保留在CAR模式下,或者它只是将它们缓存在L3中但没有禁用驱逐,即未启用CAR模式。但SEC需要(禁用)并设置新的CAR模式。

enter image description here

使用RwEverything获取物理内存转储,或下载适用于您的ME版本的CSME系统工具,然后执行fptw64 -d -me -bios dump.bin将转储闪存(通常只能转储BIOS区域而无法转储ME区域)。现在可以在UEFITool NE Alpha 58中分析该转储,并且FIT在正文中显示ACM的EntryPoint。因此,如果右键单击并将其提取出来,在IDA中作为32位打开,EntryPoint将位于3BB1h + 18h = 3BC9h
3BC9   mov     ax, ds
3BCC   mov     ss, ax
3BCF   mov     es, ax
3BD2   mov     fs, ax
3BD5   mov     gs, ax
3BD8   mov     esp, ebp
3BDA   add     esp, 1000h
3BE0   mov     eax, ebp
3BE2   add     eax, 4C8h
3BE7   lidt    fword ptr [eax]
3BEA   push    ebp
3BEB   call    sub_392A
3BF0   mov     ebx, eax
3BF2   mov     edx, 0
3BF7   mov     eax, 3
3BFC   getsec

这个创业 ACM 总是最终的 ACM,并且它 GETSEC[EXITAC] 到 IBBS 基址 + 入口点地址 (0xFEE90000,位于第一个 IBB 中) 在 bootguard 策略清单中,该清单似乎在 ME 区域中,并且可能包含代码以将 CPU 切换回非真实模式并跳转到第四个 IBB 段的传统复位向量。 GETSEC 只能在保护模式下执行,因此显然 CPU 在启动 ACM 时处于保护模式,因此必须在进入之前由微码启用。 传统复位向量位于 0xFFFFFFF0,对于我的系统来说,它是相对跳转到 FFFFFFF5 - 3BD = FFFFFC38,这是 SEC 核心入口点。

SEC

enter image description here

SEC核心是从 FFFFCA14 到 FFFFCA17 的原始部分,从 FFFFCA18 到 FFFFFFBB 是PE32映像,从 FFFFFFBC 到 FFFFFFBF 是原始部分,从 FFFFFFC0 到 FFFFFFFF 是原始部分(其中包含复位向量)。

在SEC核心中由复位向量跳转到的PE32映像的入口点包含:

0x00:  DB E3                      fninit 
0x02:  0F 6E C0                   movd   mm0, eax   //move BIST value to mm0
0x05:  0F 31                      rdtsc  
0x07:  0F 6E EA                   movd   mm5, edx
0x0a:  0F 6E F0                   movd   mm6, eax  //save tsc
0x0d:  66 33 C0                   xor    eax, eax //clear eax

0x10:  8E C0                      mov    es, ax
0x12:  8C C8                      mov    ax, cs
0x14:  8E D8                      mov    ds, ax
0x16:  B8 00 F0                   mov    ax, 0xf000
0x19:  8E C0                      mov    es, ax
0x1b:  67 26 A0 F0 FF 00 00       mov    al, byte ptr es:[0xfff0]
0x22:  3C EA                      cmp    al, 0xea
0x24:  74 0E                      je     0x34   //if ea is at ffff0h then jump to the 0xf000e05b check 

0x26:  BA F9 0C                   mov    dx, 0xcf9
0x29:  EC                         in     al, dx    //read port 0xcf9
0x2a:  3C 04                      cmp    al, 4    
0x2c:  75 25                      jne    0x53      
0x2e:  BA F9 0C                   mov    dx, 0xcf9 //perform warm reset since if CPU only reset is issued not all MSRs are restored to their defaults
0x31:  B0 06                      mov    al, 6
0x33:  EE                         out    dx, al  

0x34:  67 66 26 A1 F1 FF 00 00    mov    eax, dword ptr es:[0xfff1]
0x3c:  66 3D 5B E0 00 F0          cmp    eax, 0xf000e05b
0x42:  75 0F                      jne    0x53      //if it isn't, move to notwarmstart

0x44:  B9 1B 00                   mov    cx, 0x1b //if it is equal, read bsp bit from apic_base msr
0x47:  0F 32                      rdmsr  
0x49:  F6 C4 01                   test   ah, 1
0x4c:  74 41                      je     0x8f   //if the and operation with 00000001b produces a zero result i.e. it's an AP then jump to cli, hlt

0x4e:  EA F0 FF 00 F0             ljmp   0xf000:0xfff0 //if it's the BSP, exit unreal mode by far jumping to 0xffff0 which reloads the segment descriptor cache with a 0 base

notwarmstart:
0x53:  B0 01                      mov    al, 1
0x55:  E6 80                      out    0x80, al  //send 1 as a debug POST code
0x57:  66 BE 68 FF FF FF          mov    esi, 0xffffff68
0x5d:  66 2E 0F 01 14             lgdt   cs:[si] //loads 32&16 GDT pointer (not 16&6, due to 66 prefix) at 16bit address fff68 in si into GDTR (base:ffffff28 limit:003f); will be accessing alias and not shadow ROM

//enter 16 bit protected mode//
0x62:  0F 20 C0                   mov    eax, cr0
0x65:  66 83 C8 03                or     eax, 3   //Set PE bit (bit #0) & MP bit (bit #1)
0x69:  0F 22 C0                   mov    cr0, eax  //Activate protected mode
0x6c:  0F 20 E0                   mov    eax, cr4 
0x6f:  66 0D 00 06 00 00          or     eax, 0x600 //Set OSFXSR bit (bit #9) & OSXMMEXCPT bit (bit #10)
0x75:  0F 22 E0                   mov    cr4, eax

//set up selectors for 32 bit protected mode entry
0x78:  B8 18 00                   mov    ax, 0x18 //segment descriptor at 0x18 in GDT is (raw): 00cf93000000ffff
0x7b:  8E D8                      mov    ds, ax
0x7d:  8E C0                      mov    es, ax
0x7f:  8E E0                      mov    fs, ax
0x81:  8E E8                      mov    gs, ax
0x83:  8E D0                      mov    ss, ax
0x85:  66 BE 6E FF FF FF          mov    esi, 0xffffff6e
0x8b:  66 2E FF 2C                ljmp   cs:[si]   //transition to flat 32 bit protected mode and jump to address at 0x0:0xffffff6e aka. 0xffffff6e which is fffffcd8. CS contains 0 remember (it's the base that is 0xffff) so it will load the first entry.  This address is also in the SEC Core PE32 Image
                                                  
0x8f:  FA                         cli    
0x90:  F4                         hlt    
.
.
.

根据此 来源,FPF位于PCH而不是CPU中。另一来源 表示:“自2013年起,与选择的Intel平台一起出货的新安全和管理引擎支持名为可编程场效应晶体管的功能。”由于Intel ME也驻留在PCH中,这似乎也证实了FPF位于那里。 - wmjdgla
@wmjdgla,它们在PCH中(如果PCH像一些新的笔记本电脑架构一样被整合到CPU中,则在CPU中)。我总有一天会回答这个问题的。我被分心了。 - Lewis Kelsey

8

以下是这个问题的优秀答案

其他现代的64位计算机有新的EFI固件。它们根本不从磁盘的第0扇区加载引导程序。它们通过EFI启动管理器加载和运行EFI引导加载程序进行引导。这些程序在保护模式下运行。这就是EFI引导过程。

一般情况下,EFI固件会在处理器复位后的几条指令内切换到保护模式。切换到保护模式早在EFI固件初始化的“SEC阶段”中就完成了。从技术上讲,32位及以上的x86处理器甚至没有真正进入实模式,而是进入了俗称的虚拟模式。(CS寄存器的初始段描述符并不描述传统的实模式映射,这使得它“虚拟”了。)

因此,当原生引导到EFI引导加载程序时(即在不使用兼容性支持模块的情况下),可以说这些EFI系统根本没有进入真正的实模式,因为它们直接从虚拟模式切换到保护模式,并从那时起一直处于保护模式。


1
UEFI没有保护模式!UEFI在64位长模式下工作!一些平台可能使用32位平模式。术语本身就是错误的,这就是为什么它会让很多人感到困惑。早期,所有不属于x86模式的东西都被称为保护模式。但这并不完全正确。 - Alex D
@Alex,我已经给你提供了UEFI从16位模式开始并转移到32位模式的链接。你可能认为这是某种引导加载程序,但事实上它在16位模式下启动的位置是UEFI的一部分。 - barlop
4
@barlop说,从16位模式开始是x86早期引导的一部分。 直到固件准备好从磁盘加载内容并向其加载的代码提供标准化配置和引导API之前,它不是EFI / UEFI(或传统BIOS)。 在此之前,固件必须配置特定系统的内存控制器等内容,并且该部分完全私有,不受任何标准管辖。 16位不是UEFI的一部分,因此,一个UEFI固件必须在跳转到从磁盘加载的任何代码之前切换出该模式并提供该ABI。 - Peter Cordes

2
“当你问‘一个拥有UEFI的系统以什么模式(实模式、保护模式、长模式)启动?’时,你的意思是什么?处理器会以类似于过去80386的模式开始执行。但是你真正关心的不是处理器的模式,而是当操作系统加载器代码被控制时提供了哪些服务。这个环境在UEFI规范中有定义。UEFI规范的最新版本。至于使用哪种语言,汇编语言是一个好的起点。一段时间后使用C或其他高级语言可能更容易。其他背景:这里有很多术语,我们并不总是小心地正确使用。当处理器重置时执行的代码是系统固件,它会对系统中的各种硬件进行大量初始化。”
在UEFI论坛成立之前的x86 PC系统早期,系统固件被称为BIOS。在那个时代,BIOS会执行所有初始化代码,然后从软盘或硬盘加载一些代码并跳转到该代码。BIOS还提供了一些接口来帮助操作系统与硬件隔离,以解决硬件差异问题。但是没有标准化。唯一的标准是使用BIOS接口的操作系统和应用程序级软件。如果操作系统和应用程序能够正常运行,则认为BIOS是正确的。但只有通过缺乏故障来证明正确性。因此,新的操作系统或应用程序可能会在一个正确的系统上工作,但在另一个正确的系统上失败。
如今,我们已经尝试对这些接口进行一些实际的标准化。它们由UEFI论坛定义。现在,我可以证明我的系统符合UEFI规范的要求,从而证明其正确性。
当人们说“UEFI”之类的东西时,通常指的是在操作系统开始执行之前执行的实际系统固件。但是,我们中的许多人仍然像倒掉字母汤一样随意使用这些术语。
引导加载程序实际上是操作系统拥有的代码,由系统固件加载到内存中,并将硬件控制权交给引导加载程序。可以说,系统固件的结尾就是UEFI引导加载程序。或者您可以说BDS使用系统策略来查找操作系统。但仍然可能会有人对这些话持不同意见。

1
UEFI引导程序这个术语并不是一个好的术语。引导程序,例如GRUB,并不是UEFI的一部分。它在UEFI之后运行。有为UEFI编写的引导程序。https://superuser.com/questions/1112090/what-is-a-uefi-bootloader - barlop

2
如果您想了解更多关于UEFI如何工作的信息,那么强烈推荐阅读Adam Williamson的文章UEFI boot: how does that actually work, then?。他回答了您的问题,这是一篇很好的文章:

现在让我们来看看UEFI系统上的引导过程。即使您不理解本文的细节,也请理解:它完全不同。与BIOS引导方式完全不同。您不能将任何关于BIOS引导方式的理解应用于本机UEFI引导。您不能对专为BIOS引导世界设计的系统进行微小的调整,并将其应用于本机UEFI引导。您需要了解这是一个完全不同的世界。

此外,Wikipedia页面Unified Extensible Firmware Interface也是一个有用的资源。

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接