這篇文章想探究一下 CPU 上電后的 BIOS 執(zhí)行過程卤唉。
CPU 有一個(gè) RESET 引腳,電腦上電后該引腳給信號(hào)后(要等供電等穩(wěn)定才能給信號(hào)),CPU 才開始運(yùn)行噪窘,CPU 從固定內(nèi)存地址讀取指令并執(zhí)行吏廉,而這個(gè)固定的內(nèi)存地址是映射到 BIOS 芯片的泞遗,因此,RESET 后席覆,CPU 是直接從 BIOS 的 ROM 中讀取指令并執(zhí)行的史辙,一般 BIOS 程序最開始會(huì)先初始化內(nèi)存控制器(然后才可以用內(nèi)存),然后將 ROM 后面壓縮的 BIOS 代碼解壓到內(nèi)存中佩伤,再 JMP 到內(nèi)存去運(yùn)行解壓后的 BIOS 程序聊倔,這么做的原因是直接從 ROM 讀取指令太慢。
80386 及以后的 x86 處理器是從地址 0xfffffff0 開始運(yùn)行的生巡,這個(gè)地址一般叫 reset vector方库,相關(guān)資料可以查閱 Intel 的軟件開發(fā)手冊(cè)第三卷 9.1.4 節(jié)。
以前誤以為上電后是由主板的某電路將 BIOS 的 ROM 內(nèi)容先載入到內(nèi)存障斋,然后 CPU 再從內(nèi)存開始讀取指令并執(zhí)行的纵潦。實(shí)際上并不是這樣徐鹤,即使是內(nèi)存,它的內(nèi)存控制器也是需要初始化才能用的邀层,上電后要初始化內(nèi)存控制器后才能用返敬。初始化內(nèi)存控制器是 BIOS 程序做的。
既然第一條指令是從 0xfffffff0 開始執(zhí)行寥院,那就去這個(gè)地方把內(nèi)存 dump 下來分析劲赠,查看內(nèi)存布局 cat /proc/iomem
發(fā)現(xiàn),整個(gè) 0xff000000 - 0xffffffff 是一個(gè)整體秸谢,干脆把這個(gè) 16M 的內(nèi)存塊都 dump 下來了:
dd if=/dev/mem of=ffmem.dump bs=16M ibs=16M skip=255 count=1
dump 下來后用神器 radare2 分析凛澎,由于 CPU 啟動(dòng)時(shí)處于「真實(shí)地址模式」,指令操作數(shù)以及地址默認(rèn)都是 16 位的(可以用指令前綴明確指示該指令用 32 位地址)估蹄,所以 radare2 需要用參數(shù) -b 16 運(yùn)行塑煎,為了分析指令時(shí)更方便,還需要將基地址設(shè)定為 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 地址的計(jì)算方法:這是一條段內(nèi)跳轉(zhuǎn)地址臭蚁,下一條指令段內(nèi)地址 0xfff5 + 跳轉(zhuǎn)指令操作數(shù) 0xf923 = 0x1f918最铁,超過段內(nèi)地址位數(shù)的部分抹去得到 0xf918,即段內(nèi)(f000)0xf918 的地址垮兑。
由于我們指定了 16 位模式冷尉,所以段基地址被截?cái)喑闪?f000,實(shí)際上我們應(yīng)該在段 ffff000 內(nèi)系枪。
關(guān)于為什么 CPU 啟動(dòng)時(shí)是「真實(shí)地址模式」雀哨,但卻從 0xfffffff0 開始執(zhí)行,原因是這樣的:
我們知道 CS 寄存器是 16 位的私爷,其實(shí)那只是暴露出來的部分雾棺,CS 還有一個(gè)隱藏的部分,長(zhǎng) 32 位当犯,用來放段基地址的垢村,什么?段基地址不是用 CS 左移 4 位計(jì)算出來的么嚎卫?是的嘉栓!段基地址是計(jì)算出來的,計(jì)算出來后就放到隱藏的那部分了拓诸,然后算地址的時(shí)候直接從隱藏部分取出地址來加上偏移就是目標(biāo)地址了侵佃,所以其實(shí)尋址跟 CS 那 16 位是個(gè)間接的關(guān)系,與隱藏的 32 位的部分才是直接的關(guān)系奠支。而 CPU 上電 RESET 后馋辈,CS 的初始值為0xf000,然而那 32 位也有初始值 0xffff0000倍谜,于是尋址時(shí)直接用這個(gè)段基地址去計(jì)算目的地址了迈螟,只要不去寫 CS 寄存器叉抡,段基地址就一直會(huì)是 0xffff0000。IP 寄存器的初始值是 0xfff0答毫,于是 CPU 的第一條指令地址就是 0xffff0000 + 0xfff0 = 0xfffffff0了褥民。
這里還有一個(gè)問題,8086 CPU 尋址時(shí)洗搂,算出來的地址如果超過 20 位消返,默認(rèn) 20 位以上會(huì)被 mask 掉,這樣耘拇,就只能訪問 1M 以下的內(nèi)存了撵颊,有一個(gè) A20 線可以控制這個(gè)行為,現(xiàn)在的 CPU 在「真實(shí)地址模式」下不會(huì) mask惫叛,所以在這個(gè)模式下訪問 1M 以上的內(nèi)存才成為可能倡勇。
接著分析匯編代碼,去 jmp 的地方看看:
[ffff000:fff0]> s 0xfffff918
[ffff000:f918]> pd 64
? f000:f918 dbe3 fninit // 初始化浮點(diǎn)單元挣棕,應(yīng)該是用來判斷 CPU 的译隘,不支持這個(gè)指令的就是老 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 // 檢查通過跳轉(zhuǎn)到 legacy BIOS 區(qū)
└─> f000:f930 66bbc4faffff mov ebx, 0xfffffac4 // 不為 ea 會(huì)跳到這里亲桥,懷疑這個(gè) ea 標(biāo)至是用來區(qū)別 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 程序會(huì)被映射到內(nèi)存地址空間 0xe0000 - 0xfffff题篷,注意词身,這 128K 不會(huì)載入到內(nèi)存,此時(shí)番枚,內(nèi)存控制器甚至還未初始化法严,內(nèi)存根本不能訪問,這 128K 的地址是直接電路到 BIOS 的 ROM葫笼,CPU 此時(shí)取數(shù)或者取指令都是直接從 ROM 取的深啤,所以會(huì)比較慢。
把 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路星!
好溯街,檢查通過后會(huì)執(zhí)行 ljmp 0xf000:0xe05b
,這里要注意了洋丐,這是個(gè)長(zhǎng)跳轉(zhuǎn)指令呈昔,會(huì)載入 CS 寄存器,因此友绝,段基地址會(huì)變成 0xf000堤尾,所以這條指令跳轉(zhuǎn)到了 BIOS 程序中,地址為 0xfe05b迁客,接著分析 bios.rom:
[f000:fff0]> s 0xfe05b
[f000:e05b]> pd 1
└─< f000:e05b e9e259 jmp 0x3a40
開頭就是一個(gè) 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 // 測(cè)試是否開啟保護(hù)模式
┌─< f000:3a47 7504 jne 0xf3a4d // 開啟則跳轉(zhuǎn)
│ f000:3a49 8cc8 mov ax, cs
│ f000:3a4b 8ed0 mov ss, ax // 棧跟代碼用一個(gè)段
┌└─> f000:3a4d e90000 jmp 0xf3a50
└┌─< f000:3a50 e99201 jmp 0xf3be5 // 從這里跳走
│ f000:3a53 b08f mov al, 0x8f // 關(guān) 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郭宝,這個(gè)值應(yīng)該是上一次關(guān)機(jī)時(shí)設(shè)置的辞槐,相當(dāng)于一個(gè) 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,寫了個(gè)0(Power on or soft reset)進(jìn)去
│ 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 // 內(nèi)存不可用粘室,模擬call指令催蝗,該地址的值是 0x3a98
││ ┌──< f000:3a93 e99a00 jmp 0xf3b30 // 跳轉(zhuǎn)到硬件初始化,隨后跳到3a9b,si=4|5
┌─< f000:3a98 e95d01 jmp 0xf3bf8 // 跳轉(zhuǎn)到 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 的調(diào)用地址
si = 0, jmp 0x3ad0 // Power on or soft reset育特,沒走通
si = 1, jmp 0x3e7a // Memory size pass丙号,有可能是這個(gè)
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 // 寫了個(gè) 2 到 debug
││ f000:3adc e87407 call 0xf4253
───> f000:3adf ebfe jmp 0xf3adf // 死循環(huán),所以上面函數(shù)調(diào)用后不應(yīng)返回
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 // 這里死循環(huán)停機(jī)棉浸,所以怀薛,0xf0425 如果返回的話,肯定不對(duì)
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 設(shè)為 e000
│ f000:043b 837ef600 cmp word [bp - 0xa], 0 // 第一個(gè)局部變量迷郑,貌似并沒有用
┌──< f000:043f 7419 je 0xf045a
││ f000:0441 2eff7444 push word cs:[si + 0x44] // 0xf0054 處為 0x8008
││ f000:0445 5e pop si // si 設(shè)為 0x8008
││ f000:0446 8b44fe mov ax, word [si - 2] // 0xe8006 處為0xe82d
││ f000:0449 874604 xchg word [bp + 4], ax // bp+4為入?yún)?
┌───> f000:044c 833cff cmp word [si], 0xffff // 最終會(huì)在0xe800c處找到0xffff
┌────< f000:044f 7409 je 0xf045a
│?││ f000:0451 3904 cmp word [si], ax // 此時(shí) 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 // 根據(jù)下面的推測(cè)枝恋,這個(gè)函數(shù)也不應(yīng)該返回
f000:415d 9d popf
┌─< f000:415e 7503 jne 0xf4163 // 上一個(gè) je 指令為假,所以這一個(gè)指令會(huì)跳轉(zhuǎn)嗡害,但跳轉(zhuǎn)的話函數(shù)就返回了焚碌,也不對(duì)
│ 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 實(shí)現(xiàn),實(shí)現(xiàn)了 x86 平臺(tái)標(biāo)準(zhǔn) BIOS 調(diào)用接口霸妹,qemu 的默認(rèn) BIOS十电。
https://www.seabios.org/SeaBIOS
coreboot
開源的 boot firmware膛虫,支持多種硬件睁搭,CPU 的第一條指令從 coreboot 開始運(yùn)行,隨后控制權(quán)被 coreboot 交給 payload存筏,payload 可以是上面提到的 SeaBIOS罢绽。
https://www.coreboot.org/
TianoCore
UEFI 的開源實(shí)現(xiàn)畏线。
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