9

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

 2 years ago
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.
neoserver,ios ssh client

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

参考如下:

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

而且可以通过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,读了一下内存进行对比,果然相同,如图:

直接读内存

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


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK