☰
BIOS BDS阶段详解:启动项、设备路径与故障排查
2026/10/2 4:52:05 网站建设 项目流程

有人问我,为什么同一块主板,厂商固件里U盘一插就能认出来,自己照着开源工程编出来的固件怎么插都不认;为什么Setup里能看见U盘,启动菜单里却死活没有;为什么F2键按到手酸也进不去设置界面。这些问题八成都落在同一个地方——BIOS的BDS阶段。BDS(Boot Device Selection)这个名字听着像是"挑个启动项"这么点事,实际上它是整个固件里最像"操作系统"的一段代码:要建控制台、要连驱动、要枚举启动项、要处理热键、要验签、要交棒给OS Loader,任何一环出岔子,表现出来都只是"没反应"三个字。这篇就把BDS从头到尾拆开讲清楚,包括它在整条启动链路里的位置、启动项变量的底层结构、设备路径的匹配规则、常见故障的排查路径,以及平台移植时那些文档里不会写的坑。写过DXE驱动、做过平台移植、或者正被"启动项丢失"折磨的人,应该都能从里面捞到点能直接用的东西。

1. BIOS的BDS阶段在启动链路里的确切位置

1.1 从SEC到DXE,交棒之前固件已经攒下了什么

要理解BDS,得先知道它接手的时候手里有什么牌。上电复位之后,CPU从复位向量开始跑,这是SEC阶段,通常只有几百KB的可用空间,做的是Cache As RAM、建立临时栈、找到固件卷(FV)、然后跳到PEI。PEI阶段干的是最脏最累的活:内存初始化、芯片组早期配置、把关键信息打包成HOB(Hand-Off Block)交给下一棒。这两个阶段基本和"用户能看见的东西"无关,出了问题就是黑屏加蜂鸣,连串口都未必有输出。

到了DXE阶段,世界才开始变得像样。DxeCore从FV里把所有DXE驱动捞出来,按照依赖表达式排队派遣,PCI枚举、内存管理、变量服务、Runtime服务、BDS服务这些Arch Protocol陆续发布。等所有驱动派遣完、APRIORI表也走完了,DxeCore会去检查BDS Arch Protocol有没有就位,就位就调用它的Entry(),控制权正式交给BdsDxe。这个交接点非常关键:在BDS之前,所有驱动是被"发现"的;从BDS开始,所有驱动是被"连接"的。发现和连接是两回事,前者是DxeCore按FV里的顺序把驱动装进内存,后者是用ConnectController把驱动和硬件真正绑在一起,USB、网卡、存储控制器的功能都是在这一步才真正活过来的。

1.2 BDS阶段的四项核心任务

把BDS的工作拆开,大概是四件事,而且顺序不能乱。第一件是建立控制台,让固件有地方输出、有地方收键盘输入,包括ConIn/ConOut/ErrOut三个变量指向的设备路径、Console Splitter的虚拟句柄、串口重定向终端、图形控制台等等。第二件是连接驱动,递归调用ConnectController把所有控制器连一遍,让USB键盘、U盘、NVMe盘、网卡在BDS阶段完成枚举。第三件是启动项决策,读取BootOrder、Boot####、BootNext这些变量,结合平台自己的策略生成启动菜单,处理超时、热键、菜单显示。第四件是加载和交棒,用LoadImage和StartImage把选中的启动项镜像跑起来,也就是把bootmgfw.efi或者grubx64.efi之类的东西拉进内存执行。

第一件和第二件是基础,第三件是策略,第四件是执行。真正麻烦的是第三件,因为它是纯粹的"平台策略",没有任何标准规定你必须把网络启动放最后、也没规定超时必须十五秒、更没规定F12是启动菜单还是F2是Setup。同一个BDS框架,配上不同的PlatformBootManagerLib,行为可以差得离谱。这也是为什么网上那些"BIOS设置图解教程"互相抄来抄去,到了具体机型上还是有人照着按不出来——截图里的按键提示是平台自己画的,不是UEFI规范规定的。

1.3 为什么这个阶段最容易被误判成玄学

BDS出问题的表现有个共同点:不报错。驱动连不上,不报错;启动项被清理掉,不报错;热键没注册,还是不报错。用户看到的就是"菜单里少了一项""按了没反应""重启又进BIOS了"。更麻烦的是,BDS阶段默认几乎不打日志,串口安静得像没上电一样,于是很多人第一反应是怀疑硬件、怀疑电源、怀疑自己编错了DXE驱动,白白浪费掉几天时间。

我的经验是,凡是"设备在Setup里能看到、在启动菜单里看不到",或者"启动菜单里能看到、点进去没反应"这类现象,先把怀疑范围锁死在BDS,然后按"控制台→驱动连接→启动项变量→镜像加载"这条链路顺序排查,不要跳步。后面第5节会把这条排查链路完整展开。

2. 控制台建立与驱动连接:BDS前半段那些看不见的活

2.1 ConIn/ConOut变量和Console Splitter到底在做什么

很多人以为BDS第一件事是读启动项,其实它先干的是搭控制台。UEFI里有三个全局变量:ConIn、ConOut、ErrOut,内容是一串设备路径,比如PciRoot(0x0)/Pci(0x1,0x0)/USB(0x3,0x0)/USB(0x1,0x0)后面再接键盘或显示设备的节点。Console Splitter驱动会读这些变量,把路径对应的设备句柄聚合成虚拟的Simple Text Input/Output,这样上层调用者不用关心到底是USB键盘、PS/2键盘还是串口终端,只管往ConIn上注册事件就行。

这里有个特别容易踩的点:ConIn和ConOut是变量,是会被保存的。很多平台出厂时只塞了一个串口或者内置键盘,用户插了USB键盘发现不能用,本质上是ConIn里根本没有USB键盘那条设备路径,而平台在BDS阶段又没有"把新发现的键盘追加进去"的逻辑。EDK II的UefiBootManagerLib里提供了连接默认控制台的接口,平台需要在AfterConsole阶段调用它,才会把新枚举出来的键盘、显示器补进控制台列表。我见过不止一次,有人的固件在串口上一切正常,插上显示器却什么都不显示,就是因为ConOut里只有Terminal那条路径,GraphicsConsole压根没被加进去。

串口重定向本身也值得说一句。Terminal驱动配合SerialPortLib,把1600×900像素的图形界面"翻译"成ANSI字符流,字符宽度得按80×25算,菜单布局全是平台自己画的字符画。如果你在串口上看到Setup界面排版错乱,那通常不是BDS的问题,而是UI画字符的坐标计算没考虑终端宽度。

2.2 ConnectController的递归连接与USB枚举时序

BDS里最容易被低估的是驱动连接。DXE阶段只是把驱动装进内存,控制器和驱动的绑定要等BDS来做。UefiBootManagerLib提供了两个层次的接口:一个是连接全部控制器(内部循环调用ConnectController,递归标志打开),一个是按设备路径连接指定设备。这两个接口什么时候调、调几次,直接决定USB设备什么时候能被认出来。

注意这里的"递归"二字。打开递归之后,ConnectController会把新产生的子句柄继续往下连,USB控制器就是这样一层层连到Hub、再连到U盘的。顺序上大致是:PCI控制器先连上→XHCI驱动挂上去→Root Hub出现→连Root Hub→Port出现→插着的设备被枚举→设备的Block I/O或文件系统协议出现→U盘才能真正出现在启动菜单里。整条链路少一环,U盘就是"不存在"。

我在实际项目里最常遇到的坑是XHCI驱动没进FV。做减法裁固件的时候,为了省空间把USB驱动删了,结果Setup界面里键盘还能用(因为走的是PS/2或者内置EC),U盘启动就彻底废了。还有一种情况是驱动进了FV但依赖不满足,DxeCore在派遣阶段就把它跳过了,日志里只有一行被忽略的提示,不留心根本看不见。所以裁剪固件之后,务必用Shell确认map -r能不能列出U盘的FS句柄,那是对整条USB链路最直接的验证。

2.3 热键检测的窗口期与按键时序

"怎么按都进不去Setup"是BDS相关投诉里的头号问题。它的机制其实不复杂:BDS在进Boot Manager之前,会进入一个等待循环,同时在ConIn的WaitForKey事件和一个定时器事件上等。定时器到了就按默认启动项继续走,按键先到就处理热键。问题出在这个窗口的长度和起点上。

起点问题:热键注册是在控制台建好之后才做的,如果你在USB键盘枚举完成之前就开始计时,用户按的时候按键事件还没人接。超时又设得很短(有些消费级机型为了追求开机速度设到两三秒),等你看到屏幕亮起来再伸手,窗口已经关了。这就是为什么很多人会觉得"新版BIOS启动太快按不进去",本质不是BIOS变快了,是等待窗口被压缩了。

终点问题:等待循环里如果每次都重新查一遍设备连接状态,会拖慢响应。有些平台实现得比较糙,按键回调里做了耗时的设备枚举,结果按一次要等一秒才响应,用户以为自己没按上,又按一次,反而触发了别的功能。

我自己的做法是:平台固件里给热键留一个足够长的窗口(比如五到八秒),并且在平台层加一个"检测到任意按键就把窗口重置一次"的逻辑,避免用户按到一半窗口过期。同时,把等待循环里的事件处理写得足够轻,只做置标志位,真正的处理放到循环外。

2.4 GOP与VBT:屏幕花屏为什么不是BDS的锅

有个现象值得单独提一下:串口日志一切正常,BDS的各种阶段都打出来了,启动项也正确,但屏幕从头到尾是黑的或者花的。这时候别急着怀疑BDS的逻辑,先去查图形输出协议(GOP)驱动。

GOP驱动要靠平台提供的显示配置块(Intel平台上就是VBT)来初始化显示控制器,里面包含面板时序、背光参数、接口类型这些信息。VBT不对,显示控制器初始化就是错的,GOP句柄要么出不来,要么出来了但分辨率、时序全是乱的。BDS会照常调GraphicsConsole输出,只是输出到了一块"没配好"的屏幕上。所以碰到"串口有、屏幕没有",先确认GOP句柄存不存在(Shell里可以用dh -p GraphicsOutput之类的命令看),再去改VBT,别在BDS里绕圈子。

3. 启动项从哪来:变量结构与设备路径匹配规则

3.1 Boot####的结构:一个被严重低估的变量

BDS的启动项都存在变量里,命名规则是Boot0000到BootFFFF,另外还有BootOrder、BootNext、BootCurrent三个全局变量。每条Boot####的内容是一个EFI_LOAD_OPTION结构,字段顺序是:UINT32的Attributes、UINT16的FilePathListLength、以空字符结尾的Description字符串、设备路径列表、以及可选的OptionalData。这个结构非常直白,直白到很多人以为它没什么可研究的,实际上坑全在Attributes和设备路径这两块。

Attributes里,最低位是LOAD_OPTION_ACTIVE,表示这条启动项是否有效;第3位是LOAD_OPTION_HIDDEN,置位之后这条启动项在菜单里就不显示了,但依然存在、依然可以被BootNext指向。这就解释了那种"启动项明明存在,菜单里却看不到"的现象——大概率是被标了HIDDEN。还有一个LOAD_OPTION_FORCE_RECONNECT位,用于可移动介质,提示BDS在加载这条启动项之前先重新连一次对应设备。另外Attributes的高位里还有category字段,用来区分"启动项"和"应用项",部分平台会把Setup、Device Manager这类东西也做成一条Boot####,用category区分,不进正常启动列表。

变量属性也是坑。Boot####、BootOrder、BootNext这些必须是NV+BS+RT三种属性齐全:NV保证掉电不丢,BS保证BDS阶段能读写,RT保证OS起来之后还能改(Windows的bcdedit和bcfg命令能改启动项,靠的就是RT属性)。如果平台移植时变量驱动写得不对,刷完ROM之后启动项变成了只有RT属性,一次重启就全丢了,表现出来就是"改完启动顺序,重启又变回去了"。

3.2 短格式设备路径:U盘换口还能启动的秘密

设备路径是理解BDS行为的关键。一条完整的启动项设备路径,写起来大概是这样的:

PciRoot(0x0)/Pci(0x14,0x0)/USB(0x5,0x0)/USB(0x1,0x0)/HD(1,GPT,3F2504E0-4F89-11D3-9A0C-0305E82C3301,0x800,0x100000)

这里面既有物理位置(哪个PCI、哪个USB端口),也有分区信息(分区类型、分区表类型、分区GUID、起止扇区)。完整格式的好处是精确,坏处是太精确——你把U盘从第二个口换到第三个口,路径就匹配不上了,启动项直接失效。这也是很多人"换了个USB口就启动不了"的真实原因。

于是UEFI引入了"短格式设备路径"的概念:只保留到控制器那一级的节点,比如只写到PciRoot(0x0)/Pci(0x14,0x0)/USB(0x5,0x0),后面靠BDS在启动时按前缀去匹配当前枚举出来的所有子设备,找到第一个能匹配上的再补全成完整路径。可移动介质的启动项通常就用这种形式创建,所以U盘换个口、换台机器,只要控制器层级还能对上,BDS就能重新匹配出来。

这里有个推论很有用:启动项失效的原因,八成就写在设备路径的哪一段失配了。硬盘换过之后分区GUID变了,HD那段就失配;主板走线改了导致PCI设备号变了,Pci那段就失配。用Shell里的bcfg boot dump -v把完整路径打出来,跟当前设备树对一下,问题基本一眼就能看出来,比在BDS代码里加一堆日志高效得多。

3.3 启动项自动消失与自动增加的真相

很多人抱怨"启动项会自己消失"或者"插过的U盘启动项一直在"。这两个现象的根源是同一个:BDS在做启动项刷新。UefiBootManagerLib提供的刷新接口会把当前枚举到的所有设备和已有启动项做交叉比对,规则大致是——当前存在且合理的启动项保留,指向已不存在设备的启动项删除,新发现的可启动设备补一条进去。这个逻辑本身是合理的,问题在于它依赖枚举结果。

如果刷新发生在设备还没连上之前,枚举结果是空的,已有启动项就会被全部判为"无效"而清掉。这就是"开机进一次BIOS,启动顺序就乱了"的经典成因。正确的做法是在驱动连接完成之后、菜单生成之前做刷新,而且在平台层要保证连接动作真的把存储设备都连上再往下走。

另一个来源是默认变量。固件里通常会烧一份出厂默认变量数据(一份打包好的二进制,第一次开机或者恢复默认设置时灌入)。如果这份默认数据里带着一堆Boot####,那恢复默认设置之后,启动项就会"复活"成出厂状态。做平台的时候,默认变量数据最好只保留必要项,别把开发机上的启动项一起打包进去。

3.4 BootNext、Timeout与一次性语义的陷阱

BootNext是个UINT16,存的是下一

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询