x86架构的CPU加电后的第一条指令到底在哪?

本文讨论的第一条指令的概念是存储于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

  1. int中断例程是BIOS提供的,所以能否通过调试跟入int指令从而进入BIOS代码区呢?
  2. 在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这个引导扇区加载的地址么?这里给出回答:

  1. 这段代码的确是BIOS,但是这段是显卡的BIOS
  2. BIOS其实并没有真正的加载到内存条上,其可以通过访问内存的方式访问到的原因是:BIOS所在的ROM和内存RAM是统一编址的
  3. 这段显卡BIOS并不决定0x7c00这个地址

参考如下:

image

图片来自于以下两篇文章,让我们按照物理地址的视角,看看实际的内存布局吧!

而且可以通过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么?答:

  1. 位于0xffff0是CPU加电后的第一条指令
  2. 这段代码是bios的入口处
  3. 是这段代码把主引导区加载到了0x7c00

CPU加电后的第一条指令是可以在intel的手册:英特尔® 64 位和 IA-32 架构开发人员手册:卷 3A中查到。而且通过上面的文章和图,以及linux中的/proc/iomem以及可以猜到了,不过看不到0x7c00这个数我还是不会死心的。

获得BIOS代码

那这个BIOS我们可以怎么看呢?

bochs的BIOS镜像文件

其实在bochs的启动脚本里我们配置的选项romimage所对应的文件,就是BIOS镜像。查看其文件大小为128K,配合上之前的布局图,应该可以猜到这个BIOS的地址应该是0xe0000,读了一下内存进行对比,果然相同,如图:

image

直接读内存

如果我们不知道这个文件,也可以采取直接读内存的方式把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+2BCJ
BIOS_F:E05B                                         ; startJ
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的大作业,还是清华厉害!

扩展阅读