这篇文章想探究一下 CPU 上电后的 BIOS 执行过程。
CPU 有一个 RESET 引脚,电脑上电后该引脚给信号后(要等供电等稳定才能给信号),CPU 才开始运行,CPU 从固定内存地址读取指令并执行,而这个固定的内存地址是映射到 BIOS 芯片的,因此,RESET 后,CPU 是直接从 BIOS 的 ROM 中读取指令并执行的,一般 BIOS 程序最开始会先初始化内存控制器(然后才可以用内存),然后将 ROM 后面压缩的 BIOS 代码解压到内存中,再 JMP 到内存去运行解压后的 BIOS 程序,这么做的原因是直接从 ROM 读取指令太慢。
80386 及以后的 x86 处理器是从地址 0xfffffff0 开始运行的,这个地址一般叫 reset vector,相关资料可以查阅 Intel 的软件开发手册第三卷 9.1.4 节。
以前误以为上电后是由主板的某电路将 BIOS 的 ROM 内容先载入到内存,然后 CPU 再从内存开始读取指令并执行的。实际上并不是这样,即使是内存,它的内存控制器也是需要初始化才能用的,上电后要初始化内存控制器后才能用。初始化内存控制器是 BIOS 程序做的。
既然第一条指令是从 0xfffffff0 开始执行,那就去这个地方把内存 dump 下来分析,查看内存布局 cat /proc/iomem
发现,整个 0xff000000 - 0xffffffff 是一个整体,干脆把这个 16M 的内存块都 dump 下来了:
dd if=/dev/mem of=ffmem.dump bs=16M ibs=16M skip=255 count=1
dump 下来后用神器 radare2 分析,由于 CPU 启动时处于「真实地址模式」,指令操作数以及地址默认都是 16 位的(可以用指令前缀明确指示该指令用 32 位地址),所以 radare2 需要用参数 -b 16 运行,为了分析指令时更方便,还需要将基地址设定为 0xff000000:
r2 -b 16 -m0xff000000 ffmem.dump
[ff00000:0000]> s 0xfffffff0
[ffff000:fff0]> pD 16
╎ f000:fff0 90 nop
╎ f000:fff1 90 nop
└─< f000:fff2 e923f9 jmp 0xf918
插一嘴 jmp 地址的计算方法:这是一条段内跳转地址,下一条指令段内地址 0xfff5 + 跳转指令操作数 0xf923 = 0x1f918,超过段内地址位数的部分抹去得到 0xf918,即段内(f000)0xf918 的地址。
由于我们指定了 16 位模式,所以段基地址被截断成了 f000,实际上我们应该在段 ffff000 内。
关于为什么 CPU 启动时是「真实地址模式」,但却从 0xfffffff0 开始执行,原因是这样的:
我们知道 CS 寄存器是 16 位的,其实那只是暴露出来的部分,CS 还有一个隐藏的部分,长 32 位,用来放段基地址的,什么?段基地址不是用 CS 左移 4 位计算出来的么?是的!段基地址是计算出来的,计算出来后就放到隐藏的那部分了,然后算地址的时候直接从隐藏部分取出地址来加上偏移就是目标地址了,所以其实寻址跟 CS 那 16 位是个间接的关系,与隐藏的 32 位的部分才是直接的关系。而 CPU 上电 RESET 后,CS 的初始值为0xf000,然而那 32 位也有初始值 0xffff0000,于是寻址时直接用这个段基地址去计算目的地址了,只要不去写 CS 寄存器,段基地址就一直会是 0xffff0000。IP 寄存器的初始值是 0xfff0,于是 CPU 的第一条指令地址就是 0xffff0000 + 0xfff0 = 0xfffffff0了。
这里还有一个问题,8086 CPU 寻址时,算出来的地址如果超过 20 位,默认 20 位以上会被 mask 掉,这样,就只能访问 1M 以下的内存了,有一个 A20 线可以控制这个行为,现在的 CPU 在「真实地址模式」下不会 mask,所以在这个模式下访问 1M 以上的内存才成为可能。
接着分析汇编代码,去 jmp 的地方看看:
[ffff000:fff0]> s 0xfffff918
[ffff000:f918]> pd 64
╎ f000:f918 dbe3 fninit // 初始化浮点单元,应该是用来判断 CPU 的,不支持这个指令的就是老 CPU
╎ f000:f91a 0f6ec0 movd mm0, eax
╎ f000:f91d fa cli
╎ f000:f91e b800f0 mov ax, 0xf000
╎ f000:f921 8ed8 mov ds, ax
╎ f000:f923 bef0ff mov si, 0xfff0
╎ f000:f926 803cea cmp byte [si], 0xea // 检查 0xffff0 处值是否为 0xea
╎┌─< f000:f929 7505 jne 0xfffff930
└──< f000:f92b ea5be000f0 ljmp 0xf000:0xe05b // 检查通过跳转到 legacy BIOS 区
└─> f000:f930 66bbc4faffff mov ebx, 0xfffffac4 // 不为 ea 会跳到这里,怀疑这个 ea 标至是用来区别 new BIOS 和 legacy BIOS 的
f000:f936 662e0f0117 lgdt cs:[bx]
f000:f93b 0f20c0 mov eax, cr0
f000:f93e 0c01 or al, 1
f000:f940 0f22c0 mov cr0, eax
f000:f943 fc cld
f000:f944 b80800 mov ax, 8
f000:f947 8ed8 mov ds, ax
f000:f949 8ec0 mov es, ax
f000:f94b 8ed0 mov ss, ax
f000:f94d 8ee0 mov fs, ax
f000:f94f 8ee8 mov gs, ax
┌─< f000:f951 66ea59f9ffff. ljmp 0x10:0xfffff959
0xffff0 是哪里呢?就是 BIOS 程序,BIOS 的 128K 程序会被映射到内存地址空间 0xe0000 - 0xfffff,注意,这 128K 不会载入到内存,此时,内存控制器甚至还未初始化,内存根本不能访问,这 128K 的地址是直接电路到 BIOS 的 ROM,CPU 此时取数或者取指令都是直接从 ROM 取的,所以会比较慢。
把 BIOS 程序 dump 下来:
dd if=/dev/mem of=bios.rom bs=64k ibs=64k skip=14 count=2
然后看下 0xffff0 的值:r2 -b 16 bios.rom
[f000:fff0]> px 1
- offset - 0 1 2 3 4 5 6 7 8 9 A B C D E F 0123456789ABCDEF
f000:fff0 ea .
果然是 0xea!
好,检查通过后会执行 ljmp 0xf000:0xe05b
,这里要注意了,这是个长跳转指令,会载入 CS 寄存器,因此,段基地址会变成 0xf000,所以这条指令跳转到了 BIOS 程序中,地址为 0xfe05b,接着分析 bios.rom:
[f000:fff0]> s 0xfe05b
[f000:e05b]> pd 1
└─< f000:e05b e9e259 jmp 0x3a40
开头就是一个 jmp
[f000:e05b]> s 0xf3a40
[f000:3a40]> pd 100
f000:3a40 fa cli
f000:3a41 fc cld
f000:3a42 0f01e0 smsw ax // 取 cr0 低 16 位
f000:3a45 a801 test al, 1 // 测试是否开启保护模式
┌─< f000:3a47 7504 jne 0xf3a4d // 开启则跳转
│ f000:3a49 8cc8 mov ax, cs
│ f000:3a4b 8ed0 mov ss, ax // 栈跟代码用一个段
┌└─> f000:3a4d e90000 jmp 0xf3a50
└┌─< f000:3a50 e99201 jmp 0xf3be5 // 从这里跳走
│ f000:3a53 b08f mov al, 0x8f // 关 NMI
│ f000:3a55 e670 out 0x70, al
┌──< f000:3a57 e300 jcxz 0xf3a59
┌└──> f000:3a59 e300 jcxz 0xf3a5b
└┌──< f000:3a5b e300 jcxz 0xf3a5d
┌└──> f000:3a5d e300 jcxz 0xf3a5f
└───> f000:3a5f e471 in al, 0x71 // 读 CMOS Shutdown Status,这个值应该是上一次关机时设置的,相当于一个 checkpoint
│ f000:3a61 b400 mov ah, 0
│ f000:3a63 8bf0 mov si, ax
│ f000:3a65 b08f mov al, 0x8f
┌──< f000:3a67 e300 jcxz 0xf3a69
┌└──> f000:3a69 e300 jcxz 0xf3a6b
└───> f000:3a6b e670 out 0x70, al
│ f000:3a6d b000 mov al, 0
┌──< f000:3a6f e300 jcxz 0xf3a71
┌└──> f000:3a71 e300 jcxz 0xf3a73
└┌──< f000:3a73 e300 jcxz 0xf3a75
┌└──> f000:3a75 e300 jcxz 0xf3a77
└───> f000:3a77 e671 out 0x71, al // 写 CMOS Shutdown Status,写了个0(Power on or soft reset)进去
│ f000:3a79 8cc8 mov ax, cs
│ f000:3a7b 8ed0 mov ss, ax
│ f000:3a7d 8bc6 mov ax, si
│ f000:3a7f 3c04 cmp al, 4 ; 4
┌──< f000:3a81 740a je 0xf3a8d
││ f000:3a83 3c05 cmp al, 5 ; 5
┌───< f000:3a85 7406 je 0xf3a8d
│││ f000:3a87 3c0a cmp al, 0xa ; 10
┌────< f000:3a89 760d jbe 0xf3a98
┌─────< f000:3a8b eb43 jmp 0xf3ad0
││└└──> f000:3a8d bb7008 mov bx, 0x870
││ │ f000:3a90 bc963a mov sp, 0x3a96 // 内存不可用,模拟call指令,该地址的值是 0x3a98
││ ┌──< f000:3a93 e99a00 jmp 0xf3b30 // 跳转到硬件初始化,随后跳到3a9b,si=4|5
┌─< f000:3a98 e95d01 jmp 0xf3bf8 // 跳转到 3a9b,si<=a && si != 4 && si !=5
│ f000:3a9b b84000 mov ax, 0x40
│ f000:3a9e 8ed8 mov ds, ax // ds = 0x40
│ f000:3aa0 33c0 xor ax, ax
│ f000:3aa2 8ec0 mov es, ax // es = 0
│ f000:3aa4 b030 mov al, 0x30
│ f000:3aa6 8ed0 mov ss, ax // ss = 0x30
│ f000:3aa8 bc0001 mov sp, 0x100
│ f000:3aab d1e6 shl si, 1
│ f000:3aad 2effa4b23a jmp word cs:[si + 0x3ab2]
[f000:3ab2]> pxh
f000:3ab2 0x3ad0 0x3e7a 0xeffb 0x3ac8 0xf001 0xf009 0x3aca 0x3bf2 .:z>...:.....:.;
f000:3ac2 0x3bf5 0x3eaf 0xf011
// 这是从 si = 0 到 si = a 的调用地址
si = 0, jmp 0x3ad0 // Power on or soft reset,没走通
si = 1, jmp 0x3e7a // Memory size pass,有可能是这个
si = 2, jmp 0xeffb // Memory test pass
si = 3, jmp 0x3ac8 // Memory test fail
si = 4, jmp 0xf001 // POST complete; boot system,没走通
si = 5, jmp 0xf009 // JMP double word pointer with EOI
si = 6, jmp 0x3aca // Protected mode tests pass
si = 7, jmp 0x3bf2 // protected mode tests fail
si = 8, jmp 0x3bf5 // Memory size fail
si = 9, jmp 0x3eaf // Int 15h block move
si = a, jmp 0xf011 // JMP double word pointer without EOI
si = b, jmp 0x3ad0 // Used by 80386,没走通
┌───< f000:3ac8 eb06 jmp 0xf3ad0
┌────< f000:3aca eb04 jmp 0xf3ad0
┌─────< f000:3acc eb02 jmp 0xf3ad0
┌──────< f000:3ace eb00 jmp 0xf3ad0
└└└└───> f000:3ad0 b83000 mov ax, 0x30 // si=0,跳到这里
││ f000:3ad3 8ed0 mov ss, ax
││ f000:3ad5 bc0001 mov sp, 0x100
││ f000:3ad8 b002 mov al, 2
││ f000:3ada e680 out 0x80, al // 写了个 2 到 debug
││ f000:3adc e87407 call 0xf4253
───> f000:3adf ebfe jmp 0xf3adf // 死循环,所以上面函数调用后不应返回
f000:3b30 b011 mov al, 0x11 ; 17
f000:3b32 e6a0 out 0xa0, al
f000:3b34 50 push ax
f000:3b35 e461 in al, 0x61
f000:3b37 58 pop ax
f000:3b38 50 push ax
f000:3b39 e461 in al, 0x61
f000:3b3b 58 pop ax
f000:3b3c e620 out 0x20, al
f000:3b3e 50 push ax
f000:3b3f e461 in al, 0x61
f000:3b41 58 pop ax
f000:3b42 50 push ax
f000:3b43 e461 in al, 0x61
f000:3b45 58 pop ax
f000:3b46 8ac3 mov al, bl
f000:3b48 e6a1 out 0xa1, al
f000:3b4a 50 push ax
f000:3b4b e461 in al, 0x61
f000:3b4d 58 pop ax
f000:3b4e 50 push ax
f000:3b4f e461 in al, 0x61
f000:3b51 58 pop ax
f000:3b52 8ac7 mov al, bh
f000:3b54 e621 out 0x21, al
f000:3b56 50 push ax
f000:3b57 e461 in al, 0x61
f000:3b59 58 pop ax
f000:3b5a 50 push ax
f000:3b5b e461 in al, 0x61
f000:3b5d 58 pop ax
f000:3b5e b002 mov al, 2
f000:3b60 e6a1 out 0xa1, al
f000:3b62 50 push ax
f000:3b63 e461 in al, 0x61
f000:3b65 58 pop ax
f000:3b66 50 push ax
f000:3b67 e461 in al, 0x61
f000:3b69 58 pop ax
f000:3b6a b004 mov al, 4
f000:3b6c e621 out 0x21, al
f000:3b6e 50 push ax
f000:3b6f e461 in al, 0x61
f000:3b71 58 pop ax
f000:3b72 50 push ax
f000:3b73 e461 in al, 0x61
f000:3b75 58 pop ax
f000:3b76 b001 mov al, 1
f000:3b78 e6a1 out 0xa1, al
f000:3b7a 50 push ax
f000:3b7b e461 in al, 0x61
f000:3b7d 58 pop ax
f000:3b7e 50 push ax
f000:3b7f e461 in al, 0x61
f000:3b81 58 pop ax
f000:3b82 e621 out 0x21, al
f000:3b84 50 push ax
f000:3b85 e461 in al, 0x61
f000:3b87 58 pop ax
f000:3b88 50 push ax
f000:3b89 e461 in al, 0x61
f000:3b8b 58 pop ax
f000:3b8c b0ff mov al, 0xff
f000:3b8e e6a1 out 0xa1, al
f000:3b90 50 push ax
f000:3b91 e461 in al, 0x61
f000:3b93 58 pop ax
f000:3b94 50 push ax
f000:3b95 e461 in al, 0x61
f000:3b97 58 pop ax
f000:3b98 e621 out 0x21, al
f000:3b9a c3 ret
╎╎╎╎ f000:3be5 0f01e0 smsw ax
╎╎╎╎ f000:3be8 a801 test al, 1 ; 1
┌─────< f000:3bea 7403 je 0xf3bef
┌──────< f000:3bec e96406 jmp 0xf4253
│└───└─< f000:3bef e961fe jmp 0xf3a53
│ ╎└───< f000:3bf2 e9d7fe jmp 0xf3acc
│ └────< f000:3bf5 e9d6fe jmp 0xf3ace
│ └──< f000:3bf8 e9a0fe jmp 0xf3a9b
[f000:3ac6]> s 0xf4253
[f000:4253]> pd100
f000:4253 6a01 push 1 ; 1
f000:4255 e8cdc1 call 0xf0425
f000:4258 baf90c mov dx, 0xcf9 ; 3321
f000:425b ec in al, dx
f000:425c 0c02 or al, 2
f000:425e ee out dx, al
f000:425f 50 push ax
f000:4260 e461 in al, 0x61
f000:4262 58 pop ax
f000:4263 0c04 or al, 4
f000:4265 ee out dx, al
┌─> f000:4266 f4 hlt
└─< f000:4267 ebfd jmp 0xf4266 // 这里死循环停机,所以,0xf0425 如果返回的话,肯定不对
f000:0425 55 push bp
f000:0426 8bec mov bp, sp
f000:0428 50 push ax
f000:0429 56 push si
f000:042a 1e push ds
f000:042b 9c pushf
f000:042c be1000 mov si, 0x10 ; 16
f000:042f 2e837c4200 cmp word cs:[si + 0x42], 0 // 0xf0052 处为 e000
┌─< f000:0434 7424 je 0xf045a
│ f000:0436 2eff7442 push word cs:[si + 0x42]
│ f000:043a 1f pop ds // ds 设为 e000
│ f000:043b 837ef600 cmp word [bp - 0xa], 0 // 第一个局部变量,貌似并没有用
┌──< f000:043f 7419 je 0xf045a
││ f000:0441 2eff7444 push word cs:[si + 0x44] // 0xf0054 处为 0x8008
││ f000:0445 5e pop si // si 设为 0x8008
││ f000:0446 8b44fe mov ax, word [si - 2] // 0xe8006 处为0xe82d
││ f000:0449 874604 xchg word [bp + 4], ax // bp+4为入参1
┌───> f000:044c 833cff cmp word [si], 0xffff // 最终会在0xe800c处找到0xffff
┌────< f000:044f 7409 je 0xf045a
│╎││ f000:0451 3904 cmp word [si], ax // 此时 ax 为 1
┌─────< f000:0453 740d je 0xf0462 // 找到 1 就跳
┌──────> f000:0455 83c604 add si, 4
╎││└───< f000:0458 ebf2 jmp 0xf044c
╎│└─└└─> f000:045a 9d popf
╎│ f000:045b 1f pop ds
╎│ f000:045c 5e pop si
╎│ f000:045d 58 pop ax
╎│ f000:045e 5d pop bp
╎│ f000:045f c20200 ret 2
╎└─────> f000:0462 1e push ds
╎ f000:0463 06 push es
╎ f000:0464 0fa0 push fs
╎ f000:0466 0fa8 push gs
╎ f000:0468 6660 pushal
╎ f000:046a 0e push cs
╎ f000:046b 687504 push 0x475 ; 1141
╎ f000:046e ff7604 push word [bp + 4]
╎ f000:0471 ff7402 push word [si + 2]
╎ f000:0474 cb retf
╎ f000:0475 6661 popal
╎ f000:0477 0fa9 pop gs
╎ f000:0479 0fa1 pop fs
╎ f000:047b 07 pop es
╎ f000:047c 1f pop ds
└──────< f000:047d ebd6 jmp 0xf0455
[e000:8006]> pxh
e000:8006 0xe82d 0x0007 0x0000 0xffff 0x0000 0xf859 0xf000 0xfc80 -.........Y.....
[f000:3ab2]> s 0xf3af4
[f000:3af4]> pd 100
f000:3af4 66c1e902 shr ecx, 2 // ecx=0
f000:3af8 6633c0 xor eax, eax // eax=0
f000:3afb f36766ab rep stosd dword es:[edi], eax // 写入 ecx=0 次
f000:3aff e85106 call 0xf4153
f000:3b02 6633c0 xor eax, eax
f000:3b05 6633db xor ebx, ebx
f000:3b08 6633c9 xor ecx, ecx
f000:3b0b 6633d2 xor edx, edx
f000:3b0e 6633f6 xor esi, esi
f000:3b11 6633ff xor edi, edi
f000:3b14 6633ed xor ebp, ebp
f000:3b17 6681e4ffff00. and esp, 0xffff
f000:3b1e cb retf
[f000:3af4]> s 0xf4153
[f000:4153]> pd 100
f000:4153 6660 pushal
f000:4155 fa cli
f000:4156 9c pushf
f000:4157 ba1000 mov dx, 0x10 ; 16
f000:415a e80d00 call 0xf416a // 根据下面的推测,这个函数也不应该返回
f000:415d 9d popf
┌─< f000:415e 7503 jne 0xf4163 // 上一个 je 指令为假,所以这一个指令会跳转,但跳转的话函数就返回了,也不对
│ f000:4160 e8affa call 0xf3c12
└─> f000:4163 6661 popal
f000:4165 c3 ret
f000:4166 e8eaff call 0xf4153
f000:4169 cb retf
其他资料
SeaBIOS
开源的 16bit x86 BIOS 实现,实现了 x86 平台标准 BIOS 调用接口,qemu 的默认 BIOS。
https://www.seabios.org/SeaBIOS
coreboot
开源的 boot firmware,支持多种硬件,CPU 的第一条指令从 coreboot 开始运行,随后控制权被 coreboot 交给 payload,payload 可以是上面提到的 SeaBIOS。
https://www.coreboot.org/
TianoCore
UEFI 的开源实现。
https://www.tianocore.org/
参考资料:
https://jin-yang.github.io/reference/linux/kernel/CPU_Reset.pdf
http://ece-research.unm.edu/jimp/310/slides/8086_memory2.html
http://bochs.sourceforge.net/techspec/CMOS-reference.txt
https://stackoverflow.com/questions/42593957/bios-reads-twice-from-different-port-to-the-same-register-in-a-row
http://www.bioscentral.com/misc/cmosmap.htm
http://kernelx.weebly.com/cmos.html
http://bochs.sourceforge.net/techspec/PORTS.LST
https://sites.google.com/site/pinczakko/pinczakko-s-guide-to-award-bios-reverse-engineering#Bootblock
http://stanislavs.org/helppc/bios_data_area.html
https://www.matrix-bios.nl/system/cmos.html