x86架构的CPU加电后的第一条指令到底在哪?
source link: https://xuanxuanblingbling.github.io/ctf/pwn/2020/03/10/bios/
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.
本文讨论的第一条指令的概念是存储于CPU外部的指令,因为真正意义上的第一条指令应当位于CPU内部。这个问题其实看intel的CPU手册就知道了!
0x7c00是什么地址
在学操作系统的时候操,如果是编写一个x86架构下的操作系统,参考书无论是《OrangeS:一个操作系统的实现》还是《一个64位操作系统的设计与实现》,首先都会告诉我们,主引导扇区将会被加载到0x7c00的内存处,所以为了程序运行正确,需要在编译时确定绝对地址,于是在利用nasm汇编写的第一行代码就是org 0x7c00
,这条nasm的伪指令。那么这个0x7c00
这个很奇怪的地址是怎么来的呢?找到了阮一峰的博文:
当我们写的主引导扇区被加载到0x7c00后,我们的代码将会被执行,那也就是说位于0x7c00的代码就是CPU加电后的第一条指令么?如果是,那么从磁盘或者软盘上,把我们的引导扇区搬到内存的0x7c00位置处的功能是怎么实现的呢?当然这个可以硬件实现,不过从上文中我们也可以看到,在加载我们的主引导扇区之前,BIOS就已经运行。所以我们问:那是不是BIOS的把引导扇区搬到0x7c00呢?是的!所以0x7c00是BIOS移交程序控制权的地址,而并不是CPU加电后的第一条地址。那我怎么证明这事呢?看到BIOS的代码就可以了么!
进入BIOS的代码
这里我有两种想法进入BIOS
- int中断例程是BIOS提供的,所以能否通过调试跟入int指令从而进入BIOS代码区呢?
- 在bochs执行程序之前,是否可以通过eip寄存器的值观察到其初始值呢?
那分别来进行试验:
通过调试进入中断例程
在跟着以上两本书学习操作系统的编写时,第一步都是编写主引导扇区的代码,例如《一个64位操作系统的设计与实现》第三章中如下代码:
org 0x7c00
BaseOfStack equ 0x7c00
Label_Start:
mov ax, cs
mov ds, ax
mov es, ax
mov ss, ax
mov sp, BaseOfStack
;======= clear screen
mov ax, 0600h
mov bx, 0700h
mov cx, 0
mov dx, 0184fh
int 10h
;======= set focus
mov ax, 0200h
mov bx, 0000h
mov dx, 0000h
int 10h
;======= display on screen : Start Booting......
mov ax, 1301h
mov bx, 000fh
mov dx, 0000h
mov cx, 10
mov es, dx
mov bp, StartBootMessage
int 10h
;======= reset floppy
xor ah, ah
xor dl, dl
int 13h
jmp $
StartBootMessage: db "Start Xuan"
;======= fill zero until whole sector
times 510 - ($ - $$) db 0
dw 0xaa55
这里使用了int中断指令,进入了BIOS中断服务例程,即BIOS提供的服务,使得我们可以控制屏幕内存等,手册如下:
经过编译,烧写到镜像文件中,bochs启动,我们可以进入到bochs的调试窗口,我们把断点设置在0x7c00处,然后按c断在我们写的第一句上,然后我们观察寄存器:
<bochs:2> b 0x7c00
<bochs:3> c
<bochs:4> r
CPU0:
rax: 00000000_0000aa55 rcx: 00000000_00090000
rdx: 00000000_00000000 rbx: 00000000_00000000
rsp: 00000000_0000ffd6 rbp: 00000000_00000000
rsi: 00000000_000e0000 rdi: 00000000_0000ffac
r8 : 00000000_00000000 r9 : 00000000_00000000
r10: 00000000_00000000 r11: 00000000_00000000
r12: 00000000_00000000 r13: 00000000_00000000
r14: 00000000_00000000 r15: 00000000_00000000
rip: 00000000_00007c00
发现除了rip其他的寄存器也有值,猜测这些就是BIOS运行残余的值。我们反汇编一下我们的代码:
<bochs:5> u cs:eip cs:eip+40
00007c00: ( ): mov ax, cs ; 8cc8
00007c02: ( ): mov ds, ax ; 8ed8
00007c04: ( ): mov es, ax ; 8ec0
00007c06: ( ): mov ss, ax ; 8ed0
00007c08: ( ): mov sp, 0x7c00 ; bc007c
00007c0b: ( ): mov ax, 0x0600 ; b80006
00007c0e: ( ): mov bx, 0x0700 ; bb0007
00007c11: ( ): mov cx, 0x0000 ; b90000
00007c14: ( ): mov dx, 0x184f ; ba4f18
00007c17: ( ): int 0x10 ; cd10
00007c19: ( ): mov ax, 0x0200 ; b80002
00007c1c: ( ): mov bx, 0x0000 ; bb0000
00007c1f: ( ): mov dx, 0x0000 ; ba0000
00007c22: ( ): int 0x10 ; cd10
00007c24: ( ): mov ax, 0x1301 ; b80113
00007c27: ( ): mov bx, 0x000f ; bb0f00
然后将断点打在 0x7c17上,然后按c,断下后按s单步执行:
<bochs:6> b 0x7c17
<bochs:7> c
(0) Breakpoint 2, 0x0000000000007c17 in ?? ()
Next at t=14040253
(0) [0x000000007c17] 0000:7c17 (unk. ctxt): int 0x10 ; cd10
<bochs:8> s
Next at t=14040254
(0) [0x0000000c0152] c000:0152 (unk. ctxt): pushf ; 9c
<bochs:9> u cs:eip cs:eip+40
000c0152: ( ): pushf ; 9c
000c0153: ( ): cmp ah, 0x0f ; 80fc0f
000c0156: ( ): jnz .+6 ; 7506
000c0158: ( ): call .+24805 ; e8e560
000c015b: ( ): jmp .+188 ; e9bc00
000c015e: ( ): cmp ah, 0x1a ; 80fc1a
000c0161: ( ): jnz .+6 ; 7506
000c0163: ( ): call .+28273 ; e8716e
000c0166: ( ): jmp .+177 ; e9b100
000c0169: ( ): cmp ah, 0x0b ; 80fc0b
000c016c: ( ): jnz .+6 ; 7506
000c016e: ( ): call .+22529 ; e80158
000c0171: ( ): jmp .+166 ; e9a600
000c0174: ( ): cmp ax, 0x1103 ; 3d0311
000c0177: ( ): jnz .+6 ; 7506
000c0179: ( ): call .+26559 ; e8bf67
可以看到,我们进入了中断服务例程的代码中,地址位于000c0152,显然这段代码不是我们写的。那这段代码是BIOS么?它又是什么时候被加载到内存中的呢?这里面决定了0x7c00这个引导扇区加载的地址么?这里给出回答:
- 这段代码的确是BIOS,但是这段是显卡的BIOS
- BIOS其实并没有真正的加载到内存条上,其可以通过访问内存的方式访问到的原因是:BIOS所在的ROM和内存RAM是统一编址的
- 这段显卡BIOS并不决定0x7c00这个地址
参考如下:
图片来自于以下两篇文章,让我们按照物理地址的视角,看看实际的内存布局吧!
而且可以通过linux的proc文件系统中的iomem文件来观察地址的映射关系:
➜ ~ sudo cat /proc/iomem
00000000-00000fff : Reserved
00001000-0009e7ff : System RAM
0009e800-0009ffff : Reserved
000a0000-000bffff : PCI Bus 0000:00
000c0000-000c7fff : Video ROM
000ca000-000cafff : Adapter ROM
000cb000-000ccfff : Adapter ROM
直接观察初始EIP
其实在bochs开始的第一行就告诉了当前要执行的第一条指令的地址以及内容:
(0) [0x0000fffffff0] f000:fff0 (unk. ctxt): jmpf 0xf000:e05b ; ea5be000f0
<bochs:1> u cs:eip
000ffff0: ( ): jmpf 0xf000:e05b ; ea5be000f0
那么位于0xffff0的指令是CPU加电后的第一条指令么?这段代码是啥呢?是这段代码把主引导区加载到0x7c00么?答:
- 位于0xffff0是CPU加电后的第一条指令
- 这段代码是bios的入口处
- 是这段代码把主引导区加载到了0x7c00
CPU加电后的第一条指令是可以在intel的手册:英特尔® 64 位和 IA-32 架构开发人员手册:卷 3A中查到。而且通过上面的文章和图,以及linux中的/proc/iomem以及可以猜到了,不过看不到0x7c00这个数我还是不会死心的。
获得BIOS代码
那这个BIOS我们可以怎么看呢?
bochs的BIOS镜像文件
其实在bochs的启动脚本里我们配置的选项romimage所对应的文件,就是BIOS镜像。查看其文件大小为128K,配合上之前的布局图,应该可以猜到这个BIOS的地址应该是0xe0000,读了一下内存进行对比,果然相同,如图:
直接读内存
如果我们不知道这个文件,也可以采取直接读内存的方式把BIOS代码dump出来:
import os
os.system("echo 'x /131072bx 0xe0000' | ./run.sh | grep '>:' | awk '{print $4,$5,$6,$7,$8,$9,$10,$11}' | sed -e 's/0x//g' | tr -d ' \n' > dump")
f = open("dump",'r')
a = f.read()
f.close()
f = open("dump.bin",'wb')
f.write(a.decode('hex'))
f.close
print("done")
注:这里的run.sh就是《一个64位操作系系统的设计与实现》的示例代码中的run.sh,用来按配置文件启动bochs
然后对比BIOS文件:
$diff dump.bin /usr/local/Cellar/bochs/2.6.9_2/share/bochs/BIOS-bochs-latest
发现是的确是相同的,所以其实我们可以写一段汇编去完成这件事情,然后做一个U盘,当从U盘启动时去读这段BIOS地址的内容,然后记录到U盘里的文件系统,这样便可以获得真机的BIOS代码了。
IDA分析
用32位IDA打开dump.bin,发现IDA可以识别出类型为bios_image,可以看到识别出了入口,即加电的第一条指令:
BIOS_F:FFF0 public start
BIOS_F:FFF0 start proc near
BIOS_F:FFF0 jmp far ptr start_0
BIOS_F:FFF0 start endp
BIOS_F:FFF0
一个长跳转,跳转到BIOS主程序处,一堆in out对端口的操作:
BIOS_F:E05B start_0: ; CODE XREF: sub_F53B9+2BC↑J
BIOS_F:E05B ; start↓J
BIOS_F:E05B xor ax, ax
BIOS_F:E05D out 0Dh, al ; DMA controller, 8237A-5.
BIOS_F:E05D ; master clear.
BIOS_F:E05D ; Any OUT clears the ctrlr (must be re-initialized)
BIOS_F:E05F out 0DAh, al
BIOS_F:E061 mov al, 0C0h
BIOS_F:E063 out 0D6h, al
BIOS_F:E065 mov al, 0
BIOS_F:E067 out 0D4h, al
BIOS_F:E069 mov al, 0Fh
BIOS_F:E06B out 70h, al ; CMOS Memory/RTC Index Register:
BIOS_F:E06B ; shutdown status byte
BIOS_F:E06D in al, 71h ; CMOS Memory/RTC Data Register
BIOS_F:E06F mov bl, al
BIOS_F:E071 mov al, 0Fh
BIOS_F:E073 out 70h, al ; CMOS Memory/RTC Index Register:
BIOS_F:E073 ; shutdown status byte
BIOS_F:E075 mov al, 0
BIOS_F:E077 out 71h, al ; CMOS Memory/RTC Data Register
BIOS_F:E079 mov al, bl
BIOS_F:E07B cmp al, 0
BIOS_F:E07D jz short loc_FE0A3
BIOS_F:E07F cmp al, 0Dh
BIOS_F:E081 jnb short loc_FE0A3
BIOS_F:E083 cmp al, 5
BIOS_F:E085 jnz short loc_FE08A
BIOS_F:E087 jmp loc_F9205
那是否应该有0x7c00这个常量呢?经过IDA的搜索,没有找到长跳转到0x7c00的代码。那是分析错了么?
调试技巧之断到跳转之前
我们知道BIOS将会跳转到0x7c00处去执行主引导扇区的代码,但是我们并不知道BIOS是怎么跳过来的,如果用调试器,能否断到0x7c00前面那句呢?我首先想到的是单步执行一直执行过去,用研究看走没走到0x7c00就可以了么!可是经过实践发现这样走的太慢了。于是我去看了bochs的帮助:
<bochs:7> h
h|help - show list of debugger commands
h|help command - show short command description
-*- Debugger control -*-
help, q|quit|exit, set, instrument, show, trace, trace-reg,
trace-mem, u|disasm, ldsym, slist
-*- Execution control -*-
c|cont|continue, s|step, p|n|next, modebp, vmexitbp
-*- Breakpoint management -*-
vb|vbreak, lb|lbreak, pb|pbreak|b|break, sb, sba, blist,
bpe, bpd, d|del|delete, watch, unwatch
-*- CPU and memory contents -*-
x, xp, setpmem, writemem, crc, info,
r|reg|regs|registers, fp|fpu, mmx, sse, sreg, dreg, creg,
page, set, ptime, print-stack, ?|calc
-*- Working with bochs param tree -*-
show "param", restore
也没有具体含义,然后找到:bochs下的debug命令—中文版,有这两条:
- ptime 显示Bochs自本次运行以来执行的指令条数
- sb val 再执行val条指令就中断
于是想到了先执行到0x7c00,看下指令条数,然后利用sb(猜测是step break)参数为到0x7c00所执行的条数减一,应该就可以知道到底是怎么跳过来的了吧:
(0) Breakpoint 1, 0x0000000000007c00 in ?? ()
Next at t=14040244
(0) [0x000000007c00] 0000:7c00 (unk. ctxt): mov ax, cs ; 8cc8
<bochs:3> ptime
ptime: 14040244
发现其实断下的时候已经自动打印出来了执行条数:14040244。这么多,所以步进是不现实的。
<bochs:1> sb 14040243
Time breakpoint inserted. Delta = 14040243
<bochs:2> c
(0) Caught time breakpoint
Next at t=14040243
(0) [0x0000000f89a6] f000:89a6 (unk. ctxt): iret ; cf
<bochs:4> x esp
[bochs]:
0x000000000000ffd0 <bogus+ 0>: 0x00007c00
可以发现断到了iret指令上(可能要多试几次,因为每次指令执行的条数可能不同,不知道原因),原来是用栈保存的0x7c00呀,难怪在指令里找不到长跳转呢!我们用IDA找到这段(f000:89a6):
BIOS_F:8998 B8 55 AA mov ax, 0AA55h
BIOS_F:899B 8A 56 19 mov dl, byte ptr [bp+arg_12+1]
BIOS_F:899E 31 DB xor bx, bx
BIOS_F:89A0 8E DB mov ds, bx
BIOS_F:89A2 assume ds:nothing
BIOS_F:89A2 8E C3 mov es, bx
BIOS_F:89A4 assume es:nothing
BIOS_F:89A4 89 DD mov bp, bx
BIOS_F:89A6 CF iret
还发现了与0xaa55的比较部分,所以我们证实了加载验证主引导记录到0x7c00的代码就是BIOS代码,并且BIOS所在的0xffff0就是CPU上电后执行的第一条指令。
找了好多文章,会发现好多是ucore的大作业,还是清华厉害!
Recommend
About Joyk
Aggregate valuable and interesting links.
Joyk means Joy of geeK