AMI BIOS源代码开发实战:从代码结构到安全烧录

发布时间:2026/10/23 22:41:48
AMI BIOS源代码开发实战:从代码结构到安全烧录

简介:AMI BIOS源代码包提取自《BIOS研发技术剖析》配套光盘,面向BIOS研发工程师、系统底层开发人员及希望深入理解计算机启动原理的学习者。资源共包含1294个文件,压缩包仅5.61MB,以asm汇编源码、inc头文件、asl/aml ACPI表、obj目标文件及mak构建脚本为主,辅以bin固件镜像、exe工具与调试日志文件,目录结构清晰。已有1078人学习下载。通过阅读源码,可系统掌握POST开机自检流程、中断向量与INT 10h/INT 13h等BIOS调用实现、PCI设备与I/O端口初始化、CMOS配置读写、引导加载器选择逻辑,以及传统BIOS向UEFI过渡的代码痕迹;同时可学习硬件驱动编写、底层调试手段和ACPI电源管理模块的设计思路,是理解BIOS内部机制和进行定制开发的实用参考资料。

1. AMI BIOS源代码:拿到手别急着烧,先看懂这张代码地图

某开发者接手一块停产主板的定制需求,要改开机Logo、还要调整默认启动顺序。手里只有现成bin文件的时候,每改一次都像开盲盒:烧进去点不亮,返工三回,还得靠编程器刷回原厂才缓过来。直到拿到这份AMI BIOS源代码工程和配套光盘,才第一次觉得是在“开发固件”,不是在赌运气。整套资源以AMI BIOS的实现为主线,覆盖POST流程、内存初始化、ACPI、SMM 与启动管理,光盘里还带了完整的编译工具链和参考文档。适合主板固件工程师、BSP开发者,以及那些想拆开启动黑匣子的嵌入式老手;新手跟着完整编译一遍,也能把PEI、DXE、BDS这几个阶段的概念彻底串起来。

2. 先摸清AMI BIOS的骨架:代码树、模块与构建体系

2.1 顶层代码树:CORE、SDK、OEM 三层不能改混

拿到源码第一件事不是找main函数,BIOS没有main。这套基于AMI Core 8体系的代码树,顶层通常分成三大区域。CORE目录放的是平台无关的EFI核心实现,包含PEI、DXE、BDS三个阶段的调度骨架;SDK目录是芯片组和板卡相关的驱动包,南桥、CPU微码、板载网卡、SATA控制器的初始化全在这一层;OEM目录才是定制的主战场,开机Logo、默认启动项、CMOS默认值、SMBIOS信息都在这层。实际展开后大概长这样。

Source/ ├─ CORE/ │ ├─ CORE_DXE_SERVER/ # DXE核心,驱动分发与协议注册 │ ├─ CORE_PEI_SERVER/ # PEI阶段,CPU/内存早期初始化 │ └─ CORE_BDS/ # 启动设备选择,BootOrder相关逻辑 ├─ SDK/ │ ├─ CHIPSET/ # 南桥/北桥寄存器定义与驱动 │ ├─ CPU/ # 微码加载、Cache与内存控制器 │ └─ DRIVERS/ # 板载设备驱动(网卡、Audio等) └─ OEM/ ├─ LOGO/ # 开机Logo图片与转换脚本 ├─ SETUP/ # Setup界面字符串与默认值 └─ SMBIOS/ # 主板型号、序列号等表数据

这段目录结构对应了BIOS开发里三个明确的责任边界。CORE层一般不动,除非你在修一个跨平台通用的启动Bug;SDK层依赖具体芯片组,换板卡时得整套同步更新;OEM层则是每块主板差异化的地方,定制需求九成在这层完成。拿到代码后先确认你的板卡对应哪个OEM目录,很多人翻车都是因为改错了一份平台目录,编译能过,烧进去却不生效。

这三层之间不是靠随意include来联动的,而是通过Protocol(协议)和PCD(平台配置参数)传递数据。直接跨层引用内部头文件的话,下一次构建会冒出一堆未定义符号,排错最浪费时间的就是这类错误。所以我的习惯是:改代码前先看目录归属,OEM层能解决的绝不去碰CORE层。

2.2 三段执行阶段与入口函数:从PEI到BDS的路标

BIOS从上电到进入操作系统,代码执行分三个阶段,源码里分别对应不同的Server目录,理解这三个阶段的入口,是后续所有排错的地图。PEI阶段在Cache As RAM里运行,负责CPU微码加载、内存控制器训练和主内存探测;DXE阶段从Flash解压驱动,建立Protocol服务,PCI枚举和ACPI表填充都发生在这里;BDS阶段拿到DXE枚举出的设备列表,按BootOrder变量挑一个启动项执行。

找入口函数的方法比较固定:每个模块的.inf文件里声明一个EntryPoint。例如CORE_BDS下面的BdsDxe.inf,入口一般指向BdsEntry。想跟一条完整的启动流程,从这个函数开始单步往下走最直接。我在面对一份不熟悉的代码树时,会先把所有模块的入口函数列成清单,按函数名猜职责,这比在海量C文件里瞎翻快得多。

阶段典型模块入口函数核心职责
PEIPeiMemoryInitMemoryInitEntry内存控制器训练与内存探测
PEICpuPeimCpuPeimEntry微码加载
DXEPciHostBridgePciHostBridgeEntryPCI总线枚举
DXEAcpiTableAcpiInitACPI表生成与发布
BDSBdsDxeBdsEntry启动项选择与引导

这张表是“源码路标”,不是最终目标。真正要改的逻辑往往在入口函数下面第三四层,但入口名能帮你快速避开无关模块,直接锚定到目标区域。比如你只改默认启动顺序,顺着BdsEntry进就行,完全不用碰内存初始化那套东西。

2.3 光盘工具链与路径约束:别自作主张改目录

配套光盘的核心价值之一,是它带了完整的编译依赖。典型内容包括编译器(常见VC或Intel编译器)、Nasm汇编器、GenFw镜像封装工具,以及AMI自己的批处理构建框架。光盘根目录一般有个Setup脚本用于配置环境变量,构建统一从Build.bat入口调用。第一次编译前,先确认工具链装到了哪,路径问题会在后面频繁找你麻烦。

cd C:\AMIBIOS\Source Build.bat /nologo /c /b /v /s

这是典型的全量构建入口。/nologo去掉编译器版权横幅,让日志干净一些;/c指定C编译器后端;/b表示生成最终FD镜像,缺了它你只会得到中间产物,拿不到可烧录的ROM文件;/v输出详细日志,首次构建建议必带;/s跳过交互式确认。参数顺序在AMI的构建框架里不敏感,但/b一定不能漏,这是很多人第一次构建后找不到bin文件的原因。

AMI构建脚本大量使用硬编码相对路径,光盘默认的安装布局是C:\AMIBIOS。装到带空格或中文的目录里,构建脚本会无提示地跳过部分模块,最终镜像缺了东西,烧录后症状千奇百怪。第一次全量构建大概需要半小时到一小时,磁盘预留出5GB比较稳妥。增量编译则只重编改动的模块和依赖项,能压到一两分钟,适合后续频繁调试。

2.4 源码模块与固件二进制的映射:从GUID反查模块

编译产物不是单个大文件,而是几十上百个Module,各自带GUID和FFS头,打包后进入Firmware Volume。运行时日志里报了某个模块出错,想回源码定位,就得靠模块的GUID反查。AMI配套的MMTool工具能做这件事。

MMTool.exe 打开ROM镜像 → 选择Firmware Volume → 按GUID搜索模块 → 记录模块名(如BdsDxe)与文件GUID

搜到GUID后,去源码树里搜索同名字段,就能定位到具体C文件。这套映射关系是排障的基础,没有它,日志报错和源码之间就是断的。之前某次调试卡在内存初始化,日志里反复出现PeiMemoryInit模块名,靠GUID反查到SDK层,才发现是一个Cache变量配置和CORE层的预期不符。

没有MMTool时,也可以靠构建日志反查。每次构建会把生成的模块名和GUID写进Build.log,用文本工具检索模块名,就能确认它有没有被编进镜像。

findstr /i "BdsDxe" Build.log

这条命令在Windows下检索日志里的模块名,只看一件事:目标模块是否真的参与了构建。很多情况是优化编译时,链接器把未被引用的模块整个裁掉了,源码里完全看不出删除痕迹,唯一依据就是构建日志里有没有对应模块名,以及fdf文件里有没有对应的FV引用行。养成看日志的习惯后,这类“改了不生效”的玄学问题会减少大半。

3. 搭一个最小可编译工程:dsc、fdf 与首次烧录验证

3.1 dsc与fdf各管什么:先读文件头再改代码

构建的配置核心是两对关键文件。dsc决定“编译什么”,fdf决定“烧录在Flash的哪里”。拿到一套代码,先别急着改功能,把这两份文件头几十行读明白,能避开后面一半的坑。很多定制需求最后出问题,不是C代码写错,而是配置文件和代码不一致。

关键配置项集中在这几个字段,先对着自己板卡的实际情况核对一遍。

Platform.dsc PLATFORM_NAME = 与OEM目录名保持一致 FLASH_SIZE = 主板SPI Flash实际大小(如8MB/16MB) BOOT_DEVICE_DEFAULT = 默认启动设备优先级 Platform.fdf FV_MAIN_SIZE = 主Firmware Volume大小 BIOS_SIZE_OFFSET = 镜像在Flash中的起始偏移

FLASH_SIZE填小,程序没有超容量的机会,构建期直接报错;填大却编译能过,生成镜像可能覆盖SPI Flash上的其他区域,例如管理引擎固件区。某开发者曾把16MB Flash配成32MB,编译顺利,烧录后主板直接停在CPU早期初始化,最后用编程器刷回原厂备份才救回来。这类错误最隐蔽,因为二进制工具看不到任何异常提示。

配置项所在文件典型值参考错误后果
FLASH_SIZEdsc0x1000000(16MB)填大可能覆盖ME区域导致点不亮
FV_MAIN_SIZEfdf0x200000过小导致模块链接失败
BOOT_DEVICE_DEFAULTdsc与启动顺序表一致默认启动项异常
BIOS_SIZE_OFFSETfdf0x0偏移错误导致镜像解压失败

改配置文件前,我习惯先做一次主板原厂备份的完整解析,确认Flash总大小和实际分区布局,然后把dsc里的FLASH_SIZE按原厂值照抄,一步到位。不要凭感觉填数值,这是后续所有烧录风险的第一道闸门。

3.2 编译参数与增量编译:首跑全量,后续应增量

首次构建建议用完整参数跑一遍,把环境彻底验证通,之后再增量编译才靠谱。命令本身和第二章的入口一致,但首次运行时我更建议打开详细日志输出,并且把日志存下来,防止终端缓冲不够丢信息。

cd C:\AMIBIOS\Source Build.bat /nologo /c /b /v /s > build_log.txt 2>&1

这里> build_log.txt是把所有输出重定向到文件,2>&1把错误流合并进去。首次构建阶段,日志文件比屏幕输出更可靠,搜错也方便。构建系统是按依赖关系走的,某些模块报错会连锁触发其他模块失败,日志文件能让你从第一个真正的错误开始排查,而不是盯着最后几屏红色输出头疼。

增量编译更像提前买的后悔药:只重编改动模块和依赖项,几分钟就能拿到新镜像。注意增量编译要求命令参数和全量时保持一致,换一个参数组合,构建系统会认为配置已变,重新走整体编译,反而更慢。所以我的做法是,把这行命令存成Build.bat,每次只改源码不动命令。

一个额外经验:构建过程中如果出现假死现象,先看是不是杀毒软件在扫描中间文件,把源码目录加入白名单能省很多时间。BIOS构建会产生大量临时文件,实时扫描的开销比想象中大。

3.3 生成镜像后的第一轮验证:备份、对比、再写入

镜像生成后,不要直接进烧录器。先把三个验证动作做完:确认输出文件大小和预期一致;用MMTool打开镜像,确认关键模块GUID都在;最后才是烧录。这样能拦截绝大多数低级配置错误。

# 假设原厂备份为 factory.bin,新镜像为 new.bin fc /b factory.bin new.bin

fc /b是Windows下的二进制文件对比命令,输出第一处差异的字节偏移和内容。这里主要关心的不是全部差异,而是镜像头部几百字节内有没有意外变化——那些区域通常属于平台描述和压缩表头,一旦跑偏,烧进去大概率点不亮。差异出现在尾部几十字节往往来自NVRAM变量区,属正常现象。

三道门槛都过了,再拿刷新工具写入。第一次烧录前,把原厂镜像完整备份到两个不同介质上,是我现在的条件反射。备份文件命名里带上主板型号和日期,防止几个月后认不出是哪块板的固件。

这里补充一个容易踩的点:刷新工具读Flash时,默认可能只读BIOS区域,而不是整个SPI Flash。备份时如果工具问区域范围,选择Full或Entire Device,确保把管理引擎区域一起读出来。只备份BIOS区域的“半份镜像”,在回滚时常常不够用。

4. 把一处启动逻辑改明白:BootOrder、NVRAM 与回滚方案

4.1 定位启动项管理模块:从BdsEntry进入BootOrder

BDS阶段的启动逻辑集中在BdsDxe模块,入口函数BdsEntry。它做的事分两层:从NVRAM读BootOrder变量,确定设备排序;然后按顺序尝试启动每一个BootOption。想改默认启动顺序,最直接的位置是BootOrder变量的初始构造逻辑,而不是运行时设备匹配那部分。

EFI_STATUS BdsEntry ( IN EFI_BDS_PHASE_PROTOCOL *This ) { // 1. 从NVRAM读取BootOrder变量 Status = GetVariable(L"BootOrder", &BootOrder); // 2. 遍历BootOption并按顺序启动 for (Index = 0; Index < BootOrderCount; Index++) { BdsConnectDevice(BootOption[Index]); Status = BdsStartBootImage(BootOption[Index]); } return EFI_SUCCESS; }

上面的代码是BDS阶段的主干流程:先读BootOrder,再逐项连接设备并尝试引导。这里的逻辑说明两点:一是BootOrder是NVRAM里的持久变量,可能已存在并覆盖代码里的默认值;二是启动项的描述字符串(比如“USB Hard Drive”“Windows Boot Manager”)在BootOption里定义,跟BootOrder的顺序解耦。只改代码但不清NVRAM,行为可能不变,这是最常见的“改了没用”根源。

4.2 修改默认启动顺序的代码路径与PCD联动

源码里BootOrder的初值,通常在一个名叫BootManager或DefaultBootOrder的模块里按设备类型拼接。常见做法是定义一张静态顺序表,例如USB设备、SATA硬盘、NVMe设备、PXE网卡,然后由构建系统把这张表编成默认的BootOrder变量。

STATIC UINTN mDefaultBootOrder[] = { BOOT_DEVICE_USB, // U盘优先 BOOT_DEVICE_SATA, // SATA硬盘 BOOT_DEVICE_NVME, // NVMe SSD BOOT_DEVICE_PXE, // 网卡PXE启动 };

改这个数组的顺序即可调整出厂默认启动优先级。但要注意两点:第一,某些平台会同时用PCD控制首启设备,改代码时要同步检查dsc里的BOOT_DEVICE_DEFAULT值,两边不一致时,构建系统可能以PCD为准,你的代码改动等于白写;第二,NVRAM里如果已有旧BootOrder变量,新默认值只在清空CMOS后才生效,也就是进入Setup把设置恢复默认,或短接CMOS跳线。

这块板卡当时就遇到了类似情况:代码里的顺序表已经改成U盘优先,但烧录后启动还是从SATA硬盘走。排查到最后发现NVRAM残留了旧变量。从那以后,我每次改启动逻辑都顺手记录一条:清CMOS前后各测一次,行为差异一旦出现,立刻能锁定是变量覆盖还是代码没编进去。

4.3 烧录与回滚:备份为什么不是可选项

改启动顺序这类改动,风险不在代码逻辑本身,而在烧录环节。SPI Flash是分区域的,BIOS区域之外还有管理引擎和GOP驱动区域。用不完整的镜像或错误参数写入,可能连带清掉其他区域。回滚方案只有一个:拿到源代码那一刻,就立刻从主板上完整读出原厂镜像。

# 使用主板原厂工具读取整个SPI Flash afuwin64.exe -D factory_full.bin

afuwin是常见AMI刷新工具族里的程序,-D参数表示Dump整个Flash到文件。如果工具不支持当前芯片组,就得用编程器夹子离线读取。备份文件存两份放在不同位置,之后所有调试都以这份文件作为基线。回滚时同样用完整镜像写回,只写BIOS区域而不带其他分区,点不亮的概率很高。

这个习惯救过我不止一次。某次调试ACPI表时把DSDT写坏,系统卡在POST后面黑屏,当时原厂备份就在手边,五分钟内刷回,完全没有返工焦虑。从那以后,刷写类操作在我这里永远遵循一个顺序:先读备份,再改代码,最后烧录。备份不是安全建议,是强制前置步骤。

5. BIOS开发常踩的坑:现象、原因与解决

这一章整理的是多次构建和烧录实战中反复遇到的高频问题,每条都按“现象→原因→解决”展开。源码光盘里的工程相对完整,但环境的差异会让同一套代码在不同机器上跑出完全不同的结果。

5.1 编译报错:头文件路径不匹配

现象:构建推进到某个模块时,终端打出cannot open include file或No such file or directory,附带路径指向一个不存在的目录。

原因:两个高发点。一是拖动代码时改了目录名,但dsc里的include路径没有同步,缺哪一个头文件会有明确提示;二是工具链安装路径带了空格或中文,报错看起来像随机故障,时而编过时而缺头文件,实际是脚本拼接出的路径在某条命令行下被拆断。

解决:先全局搜索所有.dsc和.fdf里的include路径,把目录名同步为实际值。工具链路径统一改成英文字符,并保持相对路径写法。我习惯在一开始就把所有路径改成$(PLATFORM)宏开头的相对表达,避免换一台机器、换一个盘符后全盘爆炸。改完后重新构建,确认错误是否从第一个真正失败的模块开始报,而不是后续连锁错误。

5.2 烧录后点不亮

现象:刷新完固件重启,主板无显示,诊断卡或DEBUG卡停在某个早期代码,常见是CPU初始化阶段不再往前走。

原因:三个高频来源。FLASH_SIZE配大覆盖了管理引擎区域;fdf里的FV偏移不对,镜像写在Flash里但解压地址错误;部分模块被链接器裁掉,DXE阶段缺了必要驱动,导致启动流程中断。

解决:先回到第3章的验证动作,比对镜像头部的关键字节,并用MMTool确认关键模块GUID都在。如果配置无误,再翻构建日志,查被裁掉模块的依赖关系,恢复引用。最稳妥的后悔药还是原厂备份,出问题直接写回去,不要在现场反复换参数试烧,那只会放大损坏范围。

5.3 ACPI表尺寸超限引发启动变慢

现象:系统能启动但明显变慢,Windows事件日志出现ACPI错误,MCTable或DSDT表校验失败。

原因:往ACPI表里加了自定义DSDT内容,却没有同步调整fdf中专用于ACPI表的区域尺寸。ACPI区域在Flash里是独立分配的,数据超限后表内容被截断,操作系统解析时读到的表不完整,启动阶段就会反复重试。

解决:在fdf中找到ACPI区域定义,按新增内容体积调大尺寸,同时确保总Flash布局不溢出。改完重新构建,用MMTool打开镜像核对DSDT和SSDT是否完整。给表格扩容后,启动慢和ACPI报错通常会同时消失,这两个症状往往指向同一个根因。

5.4 调试串口无输出

现象:代码里加了一堆DebugPrint,串口终端却什么都没有,改动看起来完全没有执行。

原因:三个排查方向。串口重定向没有在dsc里打开,常见是调试输出宏被置位关闭;串口的I/O基地址与主板实际UART不一致;打印点在串口驱动加载前就执行了,数据被直接丢弃。

解决:先确认dsc里的串口配置开关和基地址与主板原理图一致,再在BdsEntry之前留一个基准打印点做对照。把打印语句挪到串口驱动加载之后,很多莫名消失的输出就回来了。对于PEI阶段的早期输出,则要看平台是否支持SPI调试或ITP这类硬件调试方案。

5.5 OEM目录改动不生效

现象:OEM目录下某个模块改了,编译成功,烧录后行为和原来一模一样。

原因:最常见的是模块根本没被编进镜像。优化编译会裁掉未引用的模块;或者fdf中模块对应的FV路径写错,实际加载的是另一份旧版本。另一个常见原因是NVRAM变量残留,覆盖了代码里的默认值。

解决:先用findstr在Build.log里确认目标模块名是否出现,再核对fdf里对应模块的行是否指向正确路径。两者都没问题,就清空CMOS后重新测试。这套排查顺序,从构建产物到运行数据一层层排除,基本能定位所有“改了不生效”的情况。

6. 进阶技巧:用源码级打印与诊断卡验证你的改动

6.1 在BDS阶段插入自定义输出

验证启动顺序改动,最好的手段是在BdsEntry里加一条DebugPrint,把每次尝试启动的BootOption路径打出来,串口就能看到实际走了哪个设备,不用再猜。

DEBUG((DEBUG_INFO, "Attempt boot: %s\n", BootOption[Index]->Description));

这条打印把设备的描述字符串输出到调试串口。烧录后如果看到USB设备优先于SATA设备,说明顺序表改对了;如果顺序还是原样,则按第5章的排查路径去查NVRAM残留和模块裁剪。

6.2 用诊断卡和逻辑分析仪交叉验证

DebugPrint能覆盖DXE和BDS阶段,但更早的PEI阶段可能需要诊断卡或硬件调试器配合。手边放一块POST诊断卡,主板停在哪一步会显示对应代码,和日志里的模块序号能对上。逻辑分析仪则用于验证SPI Flash的片选和读时序是否异常,配置错误时能看到读写地址跳变,这比纯看日志直观得多。

6.3 从源码注释反推芯片组寄存器意图

源码的价值不只在改,还在于它是芯片组手册的导读。很多寄存器配置代码的注释会直接写设计意图,例如某段Cache配置旁边标注了功耗与延迟的取舍。把注释和数据手册对照起来读,能更快定位“某个值为什么不推荐却默认使用”这类问题。

从那以后,我每次拿到新板卡,都强制先走一遍原厂备份、全量构建、日志检查、烧录验证这四步,再开始碰功能代码。哪怕只是改一个启动顺序这样的小改动,也按全流程走。回头来看,前期多花的几小时换来了后续调试的大量返工时间。如果你手里恰好有这套带光盘的AMI BIOS源代码资源,建议先按这条路径走一遍再动代码,省下来的时间远比预想的多。希望帮到你。

本文还有配套的精品资源,点击获取