为什么BIOS入口点以WBINVD指令开头?

时间:2019-01-20 06:45:06

标签: assembly x86 boot bios

我正在研究计算机(x86_64 Linux,IvyBridge)中的BIOS代码。我使用以下过程转储BIOS代码:

$ sudo cat /proc/iomem | grep ROM
  000f0000-000fffff : System ROM
$ sudo dd if=/dev/mem of=bios.dump bs=1M count=1

然后我用radare2读取并反汇编二进制转储:

$ r2 -b 16 bios.dump 
[0000:0000]> s 0xffff0
[f000:fff0]> pd 3
        :   f000:fff0      0f09           wbinvd
        `=< f000:fff2      e927f5         jmp 0xff51c
            f000:fff5      0000           add byte [bx + si], al

我知道x86处理器初始化总是从16位8086环境开始,并且要执行的第一条指令在f000:fff0,即0xffff0处。所以我去那个地方并反汇编代码。

令我惊讶的是,第一条指令是WBINVD,其功能是使高速缓存无效,这在处理器加电或重置时似乎无关紧要。我希望第一条指令只是jmp到较低的内存地址。

为什么在WBINVD之前有jmp

我已经搜索了英特尔手册第3卷第9章处理器管理和初始化的相关部分,但未提及WBINVD。我还搜索了一些在线资源,但没有找到任何解释。

编辑以获取更多信息:

遵循jmp的{​​{1}}指令后,代码变得更加有趣;它正在进行自我检查:

0xff51c

总而言之,这个BIOS代码是在[f000:f51c]> pd f000:f51c dbe3 fninit f000:f51e 0f6ec0 movd mm0, eax f000:f521 6631c0 xor eax, eax f000:f524 8ec0 mov es, ax f000:f526 8cc8 mov ax, cs f000:f528 8ed8 mov ds, ax f000:f52a b800f0 mov ax, 0xf000 f000:f52d 8ec0 mov es, ax f000:f52f 6726a0f0ff00. mov al, byte es:[0xfff0] ; [0xfff0:1]=0 f000:f536 3cea cmp al, 0xea ,=< f000:f538 750f jne 0xff549 | f000:f53a b91b00 mov cx, 0x1b | f000:f53d 0f32 rdmsr ; check BSP (Boot Strap Processor) flag, if set, loop back to 0xffff0; otherwise, infinite hlt | f000:f53f f6c401 test ah, 1 ,==< f000:f542 7441 je 0xff585 ,===< f000:f544 eaf0ff00f0 ljmp 0xf000:0xfff0 ||`-> f000:f549 b001 mov al, 1 || f000:f54b e680 out 0x80, al || f000:f54d 66be8cfdffff mov esi, 0xfffffd8c ; 4294966668 || f000:f553 662e0f0114 lgdt cs:[si] || f000:f558 0f20c0 mov eax, cr0 || f000:f55b 6683c803 or eax, 3 || f000:f55f 0f22c0 mov cr0, eax || f000:f562 0f20e0 mov eax, cr4 || f000:f565 660d00060000 or eax, 0x600 || f000:f56b 0f22e0 mov cr4, eax || f000:f56e b81800 mov ax, 0x18 || f000:f571 8ed8 mov ds, ax || f000:f573 8ec0 mov es, ax || f000:f575 8ee0 mov fs, ax || f000:f577 8ee8 mov gs, ax || f000:f579 8ed0 mov ss, ax || f000:f57b 66be92fdffff mov esi, 0xfffffd92 ; 4294966674 || f000:f581 662eff2c ljmp cs:[si] |`.-> f000:f585 fa cli | : f000:f586 f4 hlt | `=< f000:f587 ebfc jmp 0xff585 处读取自身,并将字节与0xffff0进行比较,而这恰恰是一个跳远的操作码:

0xea

如果发现 f000:f52a b800f0 mov ax, 0xf000 f000:f52d 8ec0 mov es, ax f000:f52f 6726a0f0ff00. mov al, byte es:[0xfff0] ; [0xfff0:1]=0 f000:f536 3cea cmp al, 0xea 处的代码有很大的距离,那么它将陷入无限循环。

更准确地说,AP(应用处理器)将在0xffff0指令处无限循环,而BSP(引导处理器)将循环回到起始hlt。由于0xffff0处的代码不会更改,因此可以断定BSP总是会发现字节为0xffff0,并且永远不会跳出循环。

那么这种自我检查的目的是什么?我简直无法相信这是天真地防止修改。

4 个答案:

答案 0 :(得分:4)

尽管很难推理,但请记住,即使mov al, byte es:[0xfff0]设置为es,负载0xf000也不是从BIOS第一条指令读取的。

0xfffffff0中读取第一条指令,PCH在重置时也可能将0xf0000-0xfffff别名为0xffff0000-0xffffffff,因此,在BSP启动时,它将执行您转储的代码。 br /> IIRC,除非明确唤醒,否则AP不会启动。

然后BSP将继续初始化硬件(从转储中判断)。
在某个时候,它将设置0xf0000-0xfffff的属性映射,以引导对内存的读取和写入(或先写入然后读取)。
最终结果是,当处理器(硬件线程)启动时,它将执行闪存中的代码,直到执行远距离跳转为止。
此时,cs基数已按照实模式规则正确计算(非常类似于非实模式),并且将从0xf0000-0xfffff(即从RAM)中提取指令。
cs段值实际上并没有改变。

BSP将在某个时候开始其多处理器初始化例程,在该例程中,它向所有人(包括他自己)广播INIT-SIPI-SIPI,这将导致AP进入睡眠状态,并使BSP进入ljmp 0xf000:0xfff0
这里的技巧是,跳转目标0xf000:0xfff0wbinvd指令的总线地址不同。
那里可能还有其他东西,可能是另一个初始化例程。

在初始化结束时,BIOS可以简单地重置0xf0000-0xfffff的属性以掉落到闪存中(因此可以进行软件重置),从而防止(无意间)转储了中间代码。

这不是很有效,但是BIOS通常不是代码的杰作。

我没有足够的信息来确定正在发生的事情,我的意思是,ljmp 0xf000:0xfff0mov al, byte es:[0xfff0]不必从它们所在的同一区域读取
。 考虑到这一点,所有赌注都关闭了。
只有适当的逆向工程才能证明一切。

关于wbinvd,我在评论中建议它可能与热启动功能有关,而Peter Cordes建议它可能与cache-as-RAM有关。
这是有道理的,但我想永远都不确定。
这也可能是对货物狂热的一种情况,在这种情况下,程序员认为传言是必要的。

答案 1 :(得分:3)

这实际上是标题问题的答案:

Hadi Brais:根据BIOS和系统管理模式内部的幻灯片14所述,wbinv指令存在于UDK2010中,但后来在UDK2012中被删除。也许与安全有关。我不知道确切是什么。

我可以确认从2017年起我的机器上0xfffffff0上不存在该指令。

这里还有一个更棘手的问题,那就是与0xea进行比较意味着什么。

这是我的代码被0xfffffff0处的重置向量跳转到的地方:

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 hard 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; it's not a warm start because BIOS shadow isn't present

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 and the shadow ROM is present, jump to 0xffff0

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.
                                                   //PEI begins at that address

0x8f:  FA                         cli    
0x90:  F4                         hlt    
.
.

我们注意到我的代码与您的代码不同。与0xf000e05b进行了额外的比较,对0xcf9进行了读/写。

edk2源代码中的一个提示here是被跳转到的代码称为'NotWarmStart'。该代码说明一切。解决此问题的关键是通过仔细分析3种不同的实现方式(+您从UEFI旧版引导与UEFI引导中获得的观察结果)。

在我的系统中,如果EA为FFFF0h,则它将检查FFFF1h是否为0xf000e05b。如果存在0xf000e05b,则它检查BSP标志,如果是BSP,则跳转到FFFF0h。如果不存在0xf000e05b,它将跳到16位+ 32位保护模式设置(称为'NotWarmStart),然后跳到32位平面保护模式(edk2称为此PEI,但我会说PEI通常从PEI内核及其跳转到的代码实际上仍然是SEC,因为它使用FSP来设置CAR,如果不存在BootGuard,则可以选择执行微代码更新,并将控制权传递给0x18:0xffffff6e的PEI内核实现。如果不存在EA,它将检查0xcf9的第3位是否为“检查INIT#已断言”。如果置为有效,则它将执行硬重置,将其写入0x6,这将导致PLTRST#,原因是“发出热启动,因为如果仅发出CPU重置,则并非所有MSR都恢复为其默认值”。如果未声明,则跳至“ NotWarmStart”。

有2条建议在起作用,因为事实证明0xffff0包含与复位时0xfffffff0 不同的值。 1)RAM包含数据,并且PAM将0xfffff范围控制在RAM而不是SPI ROM。如果发生某种类型的软复位(例如INIT#),而RAM不受影响,则RAM仅包含数据。 2)UEFI旧式启动会导致Intel ME将PAM设置为默认设置为RAM而不是SPI ROM /禁用LPC或SPI Bridge上的BIOS解码启用位BIOS_LEGACY_F_EN(这对我来说似乎不太可能,也不太详尽,我觉得默认值会在重置向量处为真。

在运行时,对于UEFI引导,您的转储在0xffff0和0xfffffff0处显示相同的代码,而对于UEFI旧式引导则显示不同的代码。在我看来,就像在UEFI模式下一样,RAM中的0xffff0处没有影子ROM。您可能直接访问SPI ROM,因为没有理由要触及该范围(不需要旧选项ROM,而我的UEFI旧式引导系统中也隐藏了旧式选件ROM。在UEFI模式下,将有XROMBAR空间中存在的DXE驱动程序将被代替。

仅查看您的代码就很容易说:检查0xea时说:“如果不存在0xea,则表明它是UEFI引导,因此请跳至32位SEC并确定以后是否预热”。 “如果那里是0xea,那么它是一个热启动,并且先前的引导是旧式引导,因此请跳转到0xffff0的简写实现”。

问题是,我的代码显示了第3个选项,并且它必须存在是有原因的。 0xffff0可以处于3种不同的状态。不包含0xea(跳至32位SEC);包含0xea和0xf000e05b(如果是BSP,则跳转到0xffff0,否则跳转);包含0xea而非0xf000e05b(跳至32位SEC)。

我的猜测是,包含0xea和0xf000e05b意味着它是传统引导和热启动。包含0xea而不包含0xf000e05b表示它是UEFI热启动。不包含0xea表示RAM在这两种模式下均不包含任何有用的内容,如果它实际上是热启动,则如果RAM中不包含任何有用的内容,则需要发出PLTRST#。那是剩下的唯一选择。这使我得到一个理论,即在UEFI BIOS上未进行第三次检查时,您在UEFI模式下看到的代码是相同的,而如果我要启动进入UEFI模式,我认为在0xffff0到0xfffffff0上会看到不同的代码,但是代码与如果我使用UEFI旧版引导时的代码不同。这可能是影子RAM中UEFI热启动的16位简写,在热启动后仍然存在,UEFI将检测并跳转到它/稍后再使用此数据。在您的系统上,未使用该位置的影子RAM,而是将其定向到SPI ROM。也许您的实现方式有所不同,并且对1MiB空间的不同区域进行了阴影处理,并使用了不同的PAM,并且稍后对其进行了检测(因此,无需执行额外的步骤来澄清0xea);它可能假设700MiB范围内的UEFI影子已损坏(因为操作系统可能会覆盖它,但其中某些仍保留;我不确定对此采取的政策)。 1MiB范围可能是影子热启动数据的唯一安全位置,并且无法影子到0xff000000–0xffffffff,因为该范围只能解码到DMI,并且在RAM中通常是从其他位置回收内存。如果假设OS不会覆盖RAM中的UEFI数据,那么您的影子可能根本不在较低的1MiB中,并且进一步检查的可能是检查700MiB区域中的热启动实现。热启动实现将假定已加载服务且设备已枚举,并且可以根据需要选择新的启动设备。

edk2之所以调用例程'NotWarmStart',即使它不像我们的实现那样检查RAM /支持热启动,也是因为我想0xcf9会告诉处理器是否发生了热启动/软复位。系统(即INIT#数据包已发送到处理器:bit3为高但bit2为低,并且当前正在执行的代码隐含在被初始化的处理器上;我只能假定仅通过复位才能使该位变为低使用PLTRST#或向其写入0),因此它仍然可以判断出它是热启动,但是它需要执行(无论RAM是否包含有用的数据)PLTRST#,因为热启动系统状态永远不会发生用于。

在hlt处也没有循环。 Hlt进入HALT状态,并响应INIT#IPI使其进入SIPI等待状态。然后执行将从BSP为AP选择的任何地址开始。

答案 2 :(得分:2)

根据Intel的Pete Dice在2011年撰写的Dr. Dobb's article

由于默认情况下未启用处理器缓存,因此在此步骤中使用WBINV指令刷新缓存并不罕见。较新的处理器不需要WBINV,但不会造成任何伤害。

我不确定WBINVD与默认情况下未启用缓存有关。我以为他可能是想让WBINVD启用缓存,但文档中没有说明有关具有这种效果的指令。我认为第二句话证实了玛格丽特的怀疑,即这是一宗货运邪教案。

答案 3 :(得分:1)

<块引用>

我知道 x86 处理器初始化总是从 16 位 8086 环境开始,要执行的第一条指令在 f000:fff0,即 0xffff0。

没有。在过去的大约 30 年里,段寄存器被分成软件可见部分(例如,由 push cs 压入堆栈的值)和几个包含(32 位)“基地址”的隐藏内部字段。上电或复位时,隐藏的“基址”部分设置为 0xFFFF0000,IP 为 0xFFF0,加起来为 0xFFFFFFF0;并且 CS 中隐藏的“基地址”部分不会改变,直到某些东西将新值加载到 CS 中。然而;通常固件会在此之前切换到保护模式。

虽然在保护模式下固件会做大量的事情 - 配置内存控制器(让 RAM 工作),找出 PCI 设备和设备 ROM,构建 ACPI 表等。其中一部分工作是复制(可能是解压)将一个小的“运行时”blob 放入略低于 1 MiB 的传统区域(其中包括 0xF000:0xFFF0 或 0x000FFFF0 处的指令)并配置内存控制器以假装 RAM 区域是“只写”(以便它的行为就像 ROM,即使它不是 ROM)。换句话说,在 0xF000:0xFFF0 处的“第一条”指令之前,可能会执行数百万条指令。

然而;整个遗留区域的存在是为了与 1980 年代(或更早)的硬壳旧软件向后兼容; 1980 年代的老旧软件可能会尝试通过跳转到 0xF000:0xFFF0 处的指令来重置计算机(在“现代计算机的错误”假设下,它将开始固件对所有内容的重新配置 - 完全重置)。考虑到这一点; 0xF000:0xFFF0 处的指令是代码的开始,它假装重置 1980 年代的任何硬壳旧软件可能知道的东西(无需费心重置/配置硬壳旧软件太老而无法知道的任何东西)。

<块引用>

出乎意料的是,第一条指令是WBINVD,其功能是使缓存失效,在处理器上电或复位时似乎无关紧要。

对 - 如果它用于开机或真正的重置,缓存将被“禁用”并且 wbinvd 将无关紧要。不过,它并不用于真正的重置,而是用于模拟部分重置,以使 1980 年代的硬壳旧软件开心。

但这并不是那么简单 - 老旧固件希望缓存被禁用,因此老旧软件会在跳转到“假装/模拟重置”之前尝试禁用缓存。

对于现代 CPU,您实际上无法禁用缓存。相反,如果您在 CR0 中设置“缓存禁用”位,您实际所做的是将其置于“无填充模式”,其中缓存未命中不会导致数据插入缓存,但缓存命中仍然工作相同(数据来自“未禁用”缓存并且不会从 RAM 中获取)。在此状态下(在 CR0 中设置“缓存禁用”位后)您需要执行 wbinvd 以刷新缓存的旧内容(以便没有缓存命中),然后才能实现“缓存禁用”的行为。 CR0 中的位被称为“缓存禁用”的原因是因为对于旧 CPU,它实际上确实禁用了缓存(如果有)。

现在您开始看到问题所在 - 对于某些 CPU(可能会期待 1980 年代或更早版本的硬壳旧软件 - 主要是 80286 和 80386),该软件可以仅通过设置CR0 中的标志然后跳转到 0xF000:0xFFF0 以开始“重置”(至少在理论上,可能期望缓存被禁用);当同样的老软件在较新的 CPU 上运行时,它会在 CR0 中设置标志(它没有正确禁用缓存),然后需要在缓存实际正确禁用之前执行wbinvd

最终结果应该是显而易见的——“传统的重置仿真实际上并没有重置所有内容”中的第一条指令是实现“缓存禁用”行为的 wbinvd自己做好)。

大多数情况下,我想说所有这些乱七八糟的东西都被 UEFI 取代了有很多很好的理由.. ;-)