本文讨论的第一条指令的概念是存储于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的大作业,还是清华厉害!