简介:一份面向BIOS研发与系统底层开发者的AMI BIOS源代码资料包,源自《BIOS研发技术剖析》配套光盘,适合希望深入理解固件启动流程、中断服务、硬件初始化等机制的工程师。压缩包共1294个文件,大小仅5.61MB,包含519个obj目标文件、201个asm汇编源文件、112个asl与75个ssp等ACPI相关源码、68个inc头文件以及33个mak工程文件,类型覆盖编译中间产物、汇编实现、ACPI表与构建脚本,便于对照分析BIOS模块结构。资源已有1077人学习,属于经典BIOS学习材料。通过阅读源码可掌握POST开机自检流程、中断向量表与INT 10h/INT 13h等调用实现、CMOS配置读写、引导加载器识别机制,并了解传统BIOS向UEFI过渡的代码思路;同时obj与mak文件也有助于理解实际编译链接过程,对从事固件开发、驱动移植或底层系统调试具有直接参考价值。
1. AMI BIOS源代码不是拿来读的,是拿来改的
把AMI BIOS源代码包当成一本书去啃,第一周大概率心情会崩。目录树一大片Pkg、一屏的.inf和.dsc,真正和你需求相关的其实就那么几十个点。这个领域的诉求向来非常具体:想在开机Logo上做文章、想调内存时序、想往Setup菜单里塞一个自己的开关项,甚至是想解掉某个隐藏功能。本文要讲的,就是拿到一份AMI BIOS源代码之后,从认目录、搭环境、编译,到改哪个文件、刷机时怎么保命的全过程。适合三类人:主板固件岗的新人、做定制化终端或工控方案时常和固件打交道的软件工程师,以及喜欢自己折腾BIOS的开源硬件玩家。你会发现这东西并不是黑匣子,它只是入口比较绕,但入口走对了,后面全是流水线操作。
2. 拆解AMI BIOS源码结构:目录分层与启动主线
2.1 先分清平台描述、Flash布局与模块编译:DSC / FDF / INF 三件套
AMI BIOS源码交付的通常是一套基于EDK2移植的UEFI固件工程,哪怕各家改得面目全非,骨架是不动的。整个源码树里最需要优先认识的不是C文件,而是三类文本文件:平台描述文件(.dsc)、Flash布局描述文件(.fdf)和模块编译描述文件(.inf)。
- DSC决定“这个固件由哪些模块组成”,是一个编译入口清单。你会在这里看到一堆“Pcdxxx”、“Component xxx”的声明,二次开发时要把自己的模块挂进来,第一站就是改DSC。
- FDF描述“这些模块在SPI Flash上怎么摆放”,决定固件体积、压缩段位置和最终生成的FD(Firmware Device)映像结构。改错FDF轻则编译出来不能启动,重则直接把Flash分区表搞乱。
- INF描述单个模块怎么编译、链哪些库、依赖哪些头文件。它更像是单个工程的Makefile。
常见的一个误会是:新手只盯INF,在某个驱动源码里翻半天,却想不到问题可能出在DSC没把这个模块编进去,或者FDF没把它放到正确的Flash区域。我自己排过不少这种“翻车现场”:模块代码一切正常,就是DSC里的引入语句被注释掉了。
另外,AMI的源码树里一定会有一个类似“AmiCspPkg”或者“CspLib”的目录,这是AMI给平台方案做封装的核心包。里面大量代码不是让你重写的,而是让你调PCD(可配置平台描述符)的。建议基准做法是:拿到源码先把DSC里所有PCD列表导出成表,很多需求改PCD就能完成,不需要动C代码。
2.2 从Reset Vector推到DXE:看懂BIOS初始化主线的必经文件
UEFI BIOS的启动主线是固定套路:Reset Vector → SEC → PEI → DXE → BDS → RT。AMI源码在这条线上的分工很清楚:
- SEC阶段的代码主要在“SecCore”和“ResetVector”里,负责CPU复位后的第一条指令、临时RAM(Cache as RAM)初始化。这个阶段不要乱碰,改坏了连日志都出不来。
- PEI阶段负责内存初始化,AMI源码中这一块通常封装在“MemoryInit”和“CpuInit”相关模块里。内存时序、容量识别、XMP支持全在这个阶段工作。
- DXE阶段是驱动大杂烩,SMM、ACPI、USB、存储、网卡驱动都在这里加载。
- BDS阶段负责Boot Device Select,也就是“从哪个盘启动”,启动菜单、Boot Manager代码在这里。
实操中怎么用这条主线?我一般会按“编译出的固件里到底有哪些模块”来反推代码结构。修改个Logo都算PEI/DXE两边协作才能完成,因为Logo在PEI显示、但解码驱动可能在DXE才初始化。
有一个值得抓的节点叫“调试串口输出位置”。在AMI源码里,串口初始化代码通常在PEI阶段早期执行,之后所有模块都可以通过DebugLib往串口打日志。所以拿到源码第一件事不是看逻辑,是先把串口日志打通。后面第5章会详细说坑。
2.3 OEM定制层藏在哪:板级独有文件与菜单定义
AMI BIOS源码交付时,不同板卡厂家拿到的是同一套Common Code,但各自手上会有一个板级目录,里面是SDAT(System Design Aid Toolset)之类的配置文件、板载芯片配置和自己定制的Setup页面。这个目录通常命名里带Platform或Board字样,比如“Pkg/Platform/OemBoardPkg”。
定制Setup菜单的数据就存在这一层。菜单的定义文件有几种形式:.vfr(可视表单定义)、.uni(字符串资源),以及VfrDefault默认值定义。实际开机时,你看到的BIOS Setup界面是由HII数据库(Human Interface Infrastructure)动态构建的,并非一张静态图。
需要记住一个关键点:Setup菜单里的每一个可配置项,几乎都对应到一个NVRAM Variable,而不是内存地址。变量的GUID和结构体都在板级目录的“Setup/”相关头文件里定义。所以,想改默认值、加隐藏项、调整下拉选项范围,走的是同一套路径:先找到定义该变量的GUID与结构体,再决定改默认数组、改VFR显示,还是两个一起改。
3. 搭好一套能编译AMI BIOS的环境:工具链选型与最小固件产线
3.1 选对编译链,决定你后面三个月的幸福指数
编译AMI BIOS源码不是一个“一键build”的事,但也没有想象中难。问题多出在环境一致性上。AMI源码通常支持两套主要工具链:一套是微软的VS编译器(历史版本常见是VS2015或VS2019),另一套是GCC。如果你是第一次接触,建议优先选GCC路线。
原因有三:GCC对开源代码的兼容性好,遇到莫名其妙的这类报错少;GCC的命令行参数直观,出问题搜索引擎上能找到大量资料;重点是在同样一套EDK2源码上,GCC路径的库依赖更简单。选VS路线常见会踩“路径带空格导致NMAKE失败”“不同VS版本生成的结构体对齐不一致”这类坑。
另外要确认架构选型:BIOS固件编译通常用IA32和X64混合模式。PEI阶段常常是32位代码,DXE阶段用64位。这个在构建配置里写死,别手动去改。很多人一开是看着“IA32”觉得过时,强行全局切X64,结果PEI跑不起来,卡在SEC阶段连log都没有。
3.2 从源码生成第一版可刷写固件:最小构建命令与产物检查
拿到源码包之后,标准流程是先不要改任何东西,直接把原始代码编译一遍,确认环境没问题的同时,你也得到一份“原厂固件”用来跟后面的修改做对比。下面是一个基于EDK2风格源码树的常见构建流程:
# 进入源码顶层目录,初始化EDK2编译环境 source edksetup.sh # 编译BaseTools:解析DSC/FDF/INF的工具链本体 make -C BaseTools clean make -C BaseTools # 执行平台编译;这里的OemBoard.dsc替换成你板级目录中实际的DSC名 build -p Platform/OemBoard/OemBoard.dsc \ -a IA32 -a X64 \ -t GCC5 \ -b RELEASE \ -n 8落地实践时的说明:
edksetup.sh主要设置环境变量,没有这一步,build命令会找不到BASE_TOOLS路径。BaseTools是整个EDK2编译系统的基础,它解析DSC里的模块依赖、生成EFI的AutoGen文件。很多新人跳过这一步或编译失败没注意,导致后面所有INF解析报错,其实根子就在这里。-a IA32 -a X64表示同时生成两种架构的PE,PEI跑IA32,DXE跑X64,这也是UEFI最常见的混合模式。-t GCC5是工具链标签,它对应Conf/tools_def.txt里定义的实际gcc版本和路径。如果本机gcc版本和tools_def里定义的开关参数不匹配,需要去tools_def.txt里同步修改,这一步最常见报“/usr/bin/ld: unrecognized option”。-b RELEASE表示发布版,关闭了大量跟踪日志。调试阶段建议改成-b DEBUG,但注意DEBUG版固件体积更大、启动更慢,串口日志全开。-n 8是并行编译任务数,8线程能明显缩短编译时间,但内存小于16GB时建议调低到4,否则编译器中途被OOM杀掉会出现一种“无报错但无产物”的奇异状态。
编译完成后,在Build目录下会生成名为“FV”的文件夹,里面FV/下的BIOS.fd,或者BUILD目录中的FLASH.fd,就是最终固件。拿到这个文件第一件事是检查体积,原始固件体积应该和源码里FDF中定义的Firmware Volume Size一致。体积不一致说明时序或者压缩比有问题,先解决体积问题再往板子上刷。
我习惯再用二进制对比工具把原始编译出的固件和板卡出厂固件做一次散列对比。理论上源码交付版和出厂版会有差异(出厂版还包含模块序列号、板卡序列号等),但如果差异集中在某个特定区域,说明那边还有你没看到的配置文件在起作用。
3.3 把自定义模块插入编译产物:DSC挂载与FDF排布的通用手法
当你要加自己的驱动或者小程序时,编译层面的动作是固定的:写INF、挂DSC、排FDF,最后看编译日志确认这个模块真的被编进去了。这里写一个最简单的独立模块挂载流程:
# 1) 在自己模块目录下建立.inf文件,声明模块类型、源文件、库依赖 # 2) 在DSC文件的[Components]区域追加: # OemBoard/MyDriver/MyDriver.inf # 3) 在FDF文件的[FV.DXE_FV]区域追加: # INF OemBoard/MyDriver/MyDriver.inf这三步做完,编译时这个模块就会出现在固件卷里。判断是否真的生效有两个技巧:编译日志里搜索模块路径,确认出现“Generate/Module ... MyDriver”字样;或者在生成固件后用开源固件解包工具把最终.fd文件解开,找到文件GUID对应的模块。
这里有一个值得注意的参数细节:模块放在哪个FV不是随便定的,取决于它想调用哪些协议。如果你的驱动在DXE阶段依赖ACPI协议,就要放在DXE阶段的FV里;而如果放在PEI的FV里,编译能过、运行必挂,因为依赖的协议还没有被安装。这就是FDF排布的真正意义,不仅是空间问题,还是运行阶段匹配问题。
4. 上手改AMI BIOS的三层练习:开机Logo、内存时序与Setup菜单
4.1 替换开机Logo:从图像转换到模块替换
开机Logo是多数人第一个想改的东西,也是能最快建立信心的实验。AMI BIOS对Logo支持有两种常见方式:一种是直接替换Flash里已有的JPEG图像模块,不动代码;另一种是走OEM Logo配置,在PEI阶段画出来。对于后者,常规做法是用Python脚本把图片转换成合适格式:
# 将PNG源图转为可被BIOS Logo模块识别的JPEG from PIL import Image src = Image.open("oem_logo.png").convert("RGB") # 常见开机画面分辨率是1920x1080或1024x768,先按屏幕比例裁切 w, h = src.size target = (1024, 768) if w * 768 // h <= 1024 else (1024, 768) src = src.resize(target, Image.LANCZOS) # JPEG质量不建议超过85:固件压缩率有限,过大的Logo会挤占FV空间 src.save("oem_logo.jpg", quality=85, optimize=True)这里面的重点是:
- 色彩模式必须转成RGB,带Alpha通道的PNG转JPEG后会得到异常黑边,BIOS界面里显示出来就是一圈黑框。
- 分辨率不是越高越好。Logo模块内通常会嵌一个解码缓冲,超过分辨率上限会解不出来,屏幕显示纯色块。
- quality大于85时,体积增长明显但观感提升有限,整个固件体积却可能因此超过FDF限制。
替换动作本身就简单了:把新图转成与源包里Logo相同的格式后,覆盖对应模块里的资源文件,重新编译,验证体积,再刷机。
4.2 调整内存时序参数:改的是默认值而非寄存器
内存时序是继Logo之后最常见的定制点。但先要建立正确认知:AMI BIOS源码里你看到的时序参数,不是直接写寄存器那一套,而是存放在一个特定GUID的NVRAM变量结构体(常见叫Memory Setup Variable)里,MRC(内存参考代码)在每次开机和S3唤醒时才会读取这个变量去配置内存控制器。
所以修改一般的时序选项流程是三步:查结构体、改初始默认值、确认生效路径。
先用文档工具或源码搜索,找到该结构体定义。然后找到该变量所有默认值的赋值数组,修改目标项的默认值。比如想改内存电压(VDDQ)选项,一般就是找到数组索引里对应项,按mV为单位修改。
这里有个关键坑:逻辑上改“默认值”不代表实际生效。生效路径是“变量不存在时用默认值”,也就是说,如果此前已经进过BIOS Setup且保存过,NVRAM里已经有变量实体,你改的默认值不会覆盖已存在的变量。处理方法是在烧录前做一次ME/CMOS清空,或者把代码里清空该变量的逻辑拉出来验证一次。做定制固件时,建议直接把“清CMOS”写进刷机流程。
4.3 往Setup菜单里加自己的开关:VFR、UNI与变量注册
举一个我经常演示的例子:在Setup主界面增加一个“OEM Switch”开关,它绑定到一个现成的保留字节变量。核心操作分三步。
第一步,在板级Setup头文件中定义变量结构体:
// SetupData.h 片段 typedef struct { UINT8 OemSwitch; // 0=Off, 1=On UINT8 OemReserved[3]; // 对齐字段,不可省略 } OEM_SETUP_DATA;对齐字段是一个经常被人漏掉的细节。UEFI Setup变量是要做版本兼容的,你新增字段如果不在结构体尾部,旧NVRAM数据在升级固件解析时就会错位。稳妥法是只在尾部追加字段,并且保留原有的字节偏移。
第二步,在VFR文件里添加一个下拉开关:
// Setup.vfr 片段,oneof就是BIOS里的单选下拉控件 oneof varid = Setup.OemSwitch, prompt = STRING_TOKEN(STR_OEM_SWITCH_PROMPT), help = STRING_TOKEN(STR_OEM_SWITCH_HELP), option text = STRING_TOKEN(STR_DISABLE), value = 0, flags = DEFAULT; option text = STRING_TOKEN(STR_ENABLE), value = 1, flags = RESET_REQUIRED; endoneof;VFR语法本身不复杂,但这个flags = RESET_REQUIRED是关键参数。它的含义是“此项修改后需要重启才生效”,如果漏掉这个标记,用户在Setup里改了值按F10保存,系统不会主动重启,界面显示已保存但运行状态没变,很容易被误判为代码没生效。
第三步,要注意字符串资源。VFR里每个STRING_TOKEN都要在.uni文件里写好对应的语言字符串,否则编译阶段会报“String Not Found”,或者编译过了但Setup界面显示乱码。
这一层做完,你的自定义开关就已经进入Setup菜单了。后续驱动里要读取它,只需要按相同GUID去GetVariable取出该结构体,然后判读OemSwitch字段就行。这里踩过的坑基本都是同一类:GUID在.inf文件里没导出,导致其他模块读不到,之后在第5章细说。
5. AMI BIOS编译与烧录避坑:5个让我返工过的实战问题
5.1 现象:自定义模块链接失败,报“Undefined external symbol”
原因几乎都在于模块的INF文件里没有声明它依赖的库,或者声明的依赖库版本和实际使用的库API不一致。很多新手改C代码半天,最后发现连编译本身都没通过。
解决:不要只看编译错误最后两行。在编译日志里向上翻,搜索“Library instance”和“Arch”。常见做法是找到报错符号名,确认它属于哪个库,再把该库的INF路径加到目标模块INF的[LibraryClasses]或者[Packages]区段里。同时检查该库是否有对应架构的实例,比如某些库只有X64版本,你模块如果声明为IA32,同样会是未定义。
5.2 现象:固件编译成功,刷到板卡后卡在开机第一屏,键盘无响应
原因分两类。第一类:PEI阶段崩溃,但串口日志还没初始化完,属于“静默崩溃”。第二类:FDF里模块排布错误,导致PEI模块需要的某个依赖模块被放到了DXE段,启动流程在等一个永远不会出现的协议。
解决方式:先看电源指示灯是否闪烁、复位是否反复,如果反复复位且间隔极短,基本可以判断PEI崩溃。这时代码层面排查太慢,直接上串口调试线。把板子Debug串口接到电脑,用115200波特率抓日志,PEI阶段崩溃会在日志尾部留下“ASSERT”或者“Error”字样。按我经验,这类问题七成是PCD配置错误,比如内存频率PCD设得超出板卡实际支持范围,MRC初始化直接放弃。
5.3 现象:用SPI烧录器刷完固件,板子彻底变砖,烧录器也识别不到Flash
SPI烧录这块我吃过一次大亏,原因是没区分“整个Flash”和“固件区”。AMI BIOS的Flash布局通常包含多个区域:BIOS区、ME区、GOP驱动区、管理引擎数据。如果你用烧录器把整个Flash内容直接用新固件覆盖,等于把ME区也抹了,就再没有足够的痕迹来让CPU完成启动前的握手协议。
解决:凡是刷BIOS,先用烧录器完整读取原始Flash,保存成完整的备份文件。再只擦除并写入BIOS区域对应的偏移区间,其他区原样保留。分不清区时,宁可先备份、再在软件里按区域操作,也不直接整片快写。这块宁可不升级也不要冒进。
5.4 现象:改了Setup默认值,编译刷机后菜单显示的仍然不是新默认值
原因是上面提到的NVRAM已存在变量覆盖了默认值。这是AMI BIOS定制里最典型的“生效顺序”问题:固件默认值只在NVRAM为空时写入。
解决:并不是每次刷机都会清NVRAM。用SPI烧录器只烧固件区时,NVRAM区还在,旧值当然压过新默认值。标准处理是:刷机后执行一次“Load Defaults”,或者在测试阶段把“清CMOS”逻辑显式触发一次。更间接的做法是升级固件中该变量的版本号(如果结构体内有Version字段),让解析逻辑认为旧数据无效。
5.5 现象:串口日志刷屏,关键信息被淹没,每次开机日志尾部都不一样
这是调试阶段的效率问题。DEBUG固件会输出海量日志,AMI源码里各模块的DebugPrint不是统一分级管理的。有时候你想看的是内存初始化日志,但USB枚举日志、ACPI表加载日志全在同一端口混着输出。
解决:不要靠肉眼盯串口。输出到文件后用脚本过滤关键字,比如只抓带“Mrc”、“MemoryInit”、“Error”的行。另外可以在构建时把单个模块的调试级别提到最高,其他模块调低。操作是在DSC的调试打印开关里精细指定,即给不同模块单独设置PcdDebugPrintErrorLevel。这种做法比整体降级好得多,保住了你想看的主线。
6. 验证改好的BIOS:从串口日志到回归清单的最小闭环
改完固件不等于交付,尤其是改了内存时序或Setup菜单这种影响基础能力的内容。我给自己定的一套最小回归流程如下,不需要昂贵调试卡,一套串口线和一张至少16GB的U盘就够。
先做“串口启动日志基线”。没修改的原始固件刷回去,开机进系统,保存一份完整启动日志,按阶段打上标记:PEI完成为止、DXE完成为止、BDS完成为止、进入操作系统为止。这份日志就是黄金参考。之后每次改动验证,都拿新日志与之对比。
然后是三层验证清单。第一层看启动是否正常,对比日志尾部是否出现异常和卡住标志。第二层看功能完整性,我通常做一个只有几十行的EFI Shell脚本,开机自动往U盘里记录“内存容量、PCI设备数量、SMBIOS版本”等几项关键信息,对比改前改后的差异;第三层才是看业务表现,比如跑一遍内存稳定性测试、反复冷启动20次。这里有个特别容易忽视的地方:S3休眠唤醒路径与冷启动路径走的是不同的代码分支。你改了内存时序之后冷启动没问题,不代表S3唤醒后MRC重新初始化也能过。我坚持S3循环测10次以上才敢确认。
还要提一个实践技巧:用Python脚本自动抓串口日志,卡住时自动保存现场。
import serial, time ser = serial.Serial("/dev/ttyUSB0", 115200, timeout=1) start = time.time() with open("boot_log.txt", "ab") as f: while time.time() - start < 300: # 最长观察5分钟 data = ser.readline() if data: f.write(data) if b"BdsTimeout" in data or b"ASSERT" in data: print("[capture] boot issue found at", time.time() - start) break这个脚本看着简单,价值在“自动停”:日志一旦出现断言或超时,自动结束抓取,并记录出现时间点,配合串口日志的时间戳就能快速定位卡在启动的哪个阶段。如果跑完整流程后日志里没有任何异常,我再做一次CMOS清空重测,排除老变量干扰。
最后说一个个人习惯:每改一个点,就产生一个独立命名的固件版本,绝不在一版固件里同时夹带多个不相关修改。因为BIOS很多问题的复现条件就是“两个看似无关的改动叠加”,一旦出了事,二分法缩小范围会非常痛苦。一旦发生这种无处下手的局面,我会退回上一版干净固件,重新只加一个改动再验证。这个习惯帮我挡掉了不少返工。做BIOS这条线,慢就是快,少改、小步验证、保留退路,才是真正能长久干下去的状态。希望帮到你。
本文还有配套的精品资源,点击获取