这篇博文是对Lab 1中的Exercise 2的解答~
Lab 1 Exercise 2:
使用GDB的'si'命令,去追踪ROM BIOS几条指令,并且试图去猜测,它是在做什么。但是不需要把每个细节都弄清楚。
答:
在这里我们将尽可能的去分析每一条指令,由于题目中说我们只需要知道BIOS的几条指令在做什么就够了,所以我们也会尽可能的去分析,由于能力有限,这里面有很多问题还没有解决,希望大家谅解。以后有机会会尽可能的把没分析的命令去补全。
首先注意这里是紧接着Lab 1 Part 1.2那篇博文中建立了qemu和gdb调试环境之后进行了,至于如何建立调试环境,大家可以再回到那篇博文中具体查看~
在Lab 1 Part 1.2中我们建立了调试环境后,在运行gdb的窗口中会将显示出PC机启动后运行BIOS的第一条命令,如下:
. 0xffff0: ljmp $0xf000, $0xe05b
这是运行的第一条指令,是一条跳转指令,跳转到0xfe05b地址处。至于要知道这个地址是怎么通过指令中的值计算出来的,我们需要先知道,当PC机启动时,CPU运行在实模式(real mode)下,而当进入操作系统内核后,将会运行在保护模式下(protected mode)。实模式是早期CPU,比如8088处理器的工作模式,这类处理器由于只有20根地址线,所以它们只能访问1MB的内存空间。但是CPU也在不断的发展,之后的80286/80386已经具备32位地址总线,能够访问4GB内存空间,为了能够很好的管理这么大的内存空间,保护模式被研发出来。所以现代处理器都是工作在保护模式下的。但是为了实现向后兼容性,即原来运行在8088处理器上的软件仍旧能在现代处理器上运行,所以现代的CPU都是在启动时运行于实模式,启动完成后运行于保护模式。BIOS就是PC刚启动时运行的软件,所以它必然工作在实模式。
至于这两个模式下的运行原理,可以看这个链接:http://blog.csdn.net/zdwzzu2006/article/details/4030948
这里先简单介绍一下地址的计算方法,在实模式下,指令中出现的地址都是采用
(段基址:段内偏移)
的形式的。其中这两个字段的值,通常是存放寄存器中的。其中段基址必须放在段寄存器中,包括CS(代码段), DS(数据段), SS(堆栈段), ES(扩展段)。不同的段寄存器存放的是你程序不同的段的起始地址。
但是由于8088CPU中寄存器都是16位,而CPU地址总线是20位的,我们怎么通过16位的寄存器去拼接20位的地址呢?
所以我们需要采用下面的方法:把段寄存器中的值左移4位,形成20位段基址,然后和16位段内偏移相加,就得到了真实地址。比如上面的指令中段寄存器的内容为0xf000,所以真实地址为 0xf000<<4+0xe05b = 0xfe05b。
. 0xfe05b: cmpl $0x0, $cs:0x6ac8
下一条指令,把0x0这个立即数和$cs:0x6ac8所代表的内存地址处的值比较,至于为什么这样比较,现在还不是很清楚。其中$cs:0x6ac8就是我们刚刚介绍的在实模式下地址形成的格式,其中$cs就代表CS段寄存器的值。
. 0xfe062: jne 0xfd2e1
jne指令:如果ZF标志位为0的时候跳转,即上一条指令cmpl的结果不是0时跳转,也就是$cs:0x6ac8地址处的值不是0x0时跳转。
. 0xfe066: xor %dx, %dx
下一条指令地址是0xfe066,可见上面的跳转指令并没有跳转。这条指令的功能是把dx寄存器清零。
. 0xfe068: mov %dx %ss
. 0xfe06a: mov $0x7000, %esp
. 0xfe070: mov $0xf34d2, %edx
. 0xfe076: jmp 0xfd15c
. 0xfd15c: mov %eax, %ecx
接下来的这些指令就是设置一些寄存器的值,具体含义现在不明白..
这里要注意第8条指令,进行了绝对跳转。
. 0xfd15f: cli
关闭中断指令。这个比较好理解,启动时的操作是比较关键的,所以肯定是不能被中断的。这个关中断指令用于关闭那些可以屏蔽的中断。比如大部分硬件中断。
. 0xfd160: cld
设置方向标识位为0,表示后续的串操作比如MOVS操作,内存地址的变化方向,如果为0代表从低地址值变为高地址。
具体什么是串操作,可以看这个链接:http://www.tyut.edu.cn/kecheng1/2008/site04/courseware/chapter3/3-3-4.html
. 0xfd161: mov $0x8f, %eax
. 0xfd167: out %al, $0x70
. 0xfd169: in $0x71, %al
这三个操作中涉及到两个新的指令out, in。这两个操作是用于操作IO端口的。这种IO端口的操作我们后面会经常接触到,这里大致说下。
CPU与外部设备通讯时,通常是通过访问,修改设备控制器中的寄存器来实现的。那么这些位于设备控制器当中的寄存器也叫做IO端口。为了方便管理,80x86CPU采用IO端口单独编址的方式,即所有设备的端口都被命名到一个IO端口地址空间中。这个空间是独立于内存地址空间的。所以必须采用和访问内存的指令不一样的指令来访问端口。
所以这里引入in,out操作:
in %al, PortAddress 向端口地址为PortAddress的端口写入值,值为al寄存器中的值
out PortAddres,%al 把端口地址为PortAddress的端口中的值读入寄存器al中
标准规定端口操作必须要用al寄存器作为缓冲。
那么这三条命令就是要操作端口0x70,0x71,它们对应的是什么设备呢?根据下面的链接中所提供的清单(这个连接之后会经常用到,建议大家收藏):http://bochs.sourceforge.net/techspec/PORTS.LST
我们知道了,0x70端口和0x71端口是用于控制系统中一个叫做CMOS的设备,这个设备是一个低功耗的存储设备,它可以用于在计算机关闭时存储一些信息,它是由独立的电池供电的。这个链接有详细介绍http://wiki.osdev.org/CMOS
这个CMOS中可以控制跟PC相关的多个功能,其中最重要的就是时钟设备(Real Time Clock)的 ,它还可以控制是否响应不可屏蔽中断NMI(Non-Maskable Interrupt)。
操作CMOS存储器中的内容需要两个端口,一个是0x70另一个就是0x71。其中0x70可以叫做索引寄存器,这个8位寄存器的最高位是不可屏蔽中断(NMI)使能位。如果你把这个位置1,则NMI不会被响应。低7位用于指定CMOS存储器中的存储单元地址,所以如果你想访问第1号存储单元,并且在访问时,我要使能NMI,那么你就应该向端口0x70里面送入0b10000001 = 0x81。
即mov $0x81, %al
然后对于这个地址单元的操作,比如读或者写就可以由0x71端口完成,比如你现在想从1号存储单元里面读出它的值,在完成上面的两条指令后,就可以输入这条指令
in $0x71, %al
再回到我们的系统,这三条指令可以看出,它首先关闭了NMI中断,并且要访问存储单元0xF的值,并且把值读到al中,但是在后面我们发现这个值并没有被利用,所以可以认为这三条指令是用来关闭NMI中断的。
. 0xfd16b: in $0x92, %al
. 0xfd16d: or $0x2, %al
. 0xfd16f: out %al, $0x92
这三步操作又是在控制端口,此时被控制的端口号为0x92,通过上面那个链接 http://bochs.sourceforge.net/techspec/PORTS.LST
我们可以查看到,它控制的是 PS/2系统控制端口A,而第16,17步的操作明显是在把这个端口的1号bit置为1。这个端口的bit1的功能是
bit 1= 1 indicates A20 active
即A20位,即第21个地址线被使能,了解实模式和保护模式的同学肯定清楚,如果A20地址线被激活,那么系统工作在保护模式下。但是在之后的boot loader程序中,计算机首先要工作在实模式下啊。所以这里的这个操作,根据网上 http://kernelx.weebly.com/a20-address-line.html 所说应该是去测试可用内存空间。在boot loader之前,它肯定还会转换回实模式。
. 0xfd171: lidtw %cs:0x6ab8
lidt指令:加载中断向量表寄存器(IDTR)。这个指令会把从地址0xf6ab8起始的后面6个字节的数据读入到中断向量表寄存器(IDTR)中。中断是操作系统中非常重要的一部分,有了中断操作系统才能真正实现进程。每一种中断都有自己对应的中断处理程序,那么这个中断的处理程序的首地址就叫做这个中断的中断向量。中断向量表自然是存放所有中断向量的表了。关于中断向量表的介绍,大家可以戳这个链接 http://wiki.osdev.org/Interrupt_Descriptor_Table
. 0xfd177: lgdtw %cs:0x6a74
把从0xf6a74为起始地址处的6个字节的值加载到全局描述符表格寄存器中GDTR中。这个表实现保护模式非常重要的一部分,我们在介绍boot loader时会具体介绍它。
. 0xfd17d: mov %cr0, %eax
. 0xfd180: or $0x1, %eax
. 0xfd184: mov %eax, %cr0
计算机中包含CR0~CR3四个控制寄存器,用来控制和确定处理器的操作模式。其中这三个语句的操作明显是要把CR0寄存器的最低位(0bit)置1。CR0寄存器的0bit是PE位,启动保护位,当该位被置1,代表开启了保护模式。但是这里出现了问题,我们刚刚说过BIOS是工作在实模式之下,后面的boot loader开始的时候也是工作在实模式下,所以这里把它切换为保护模式,显然是自相矛盾。所以只能推测它在检测是否机器能工作在保护模式下。
. 0xfd187: ljmpl $0x8, $0xfd18f
. 0xfd18f: mov $0x10, %eax
. 0xfd194: mov %eax, %ds
. 0xfd196: mov %eax, %es
. 0xfd198: mov %eax, %ss
. 0xfd19a: mov %eax, %fs
. 0xfd19c: mov %eax, %gs
修改这些寄存器的值。这些寄存器都是段寄存器。大家可以戳这个链接看一下具体介绍 http://www.eecg.toronto.edu/~amza/www.mindsec.com/files/x86regs.html
这里的23~29步之所以这么做是按照规定来的,https://en.wikibooks.org/wiki/X86_Assembly/Global_Descriptor_Table链接中指出,如果刚刚加载完GDTR寄存器我们必须要重新加载所有的段寄存器的值,而其中CS段寄存器必须通过长跳转指令,即23号指令来进行加载。所以这些步骤是在第19步完成后必须要做的。这样才能是GDTR的值生效。
。。。。。
非常抱歉,到目前为止我只能理解到这里,后面的代码虽然操作上容易,但是它们的目的变得非常难以琢磨,所以我只能把自己比较理解的一部分展示给大家,在之后我会尽力把这部分的知识补全!
综上,我们可以看到BIOS的操作就是在控制,初始化,检测各种底层的设备,比如时钟,GDTR寄存器。以及设置中断向量表。这都和Lab 1 Part 1.2最后两段说的一样。但是作为PC启动后运行的第一段程序,它最重要的功能是把操作系统从磁盘中导入内存,然后再把控制权转交给操作系统。所以BIOS在运行的最后会去检测可以从当前系统的哪个设备中找到操作系统,通常来说是我们的磁盘。也有可能是U盘等等。当BIOS确定了,操作系统位于磁盘中,那么它就会把这个磁盘的第一个扇区,通常把它叫做启动区(boot sector)先加载到内存中,这个启动区中包括一个非常重要的程序--boot loader,它会负责完成整个操作系统从磁盘导入内存的工作,以及一些其他的非常重要的配置工作。最后操作系统才会开始运行。
可见PC启动后的运行顺序为 BIOS --> boot loader --> 操作系统内核
以上就是对Exercise 2的分析,老规矩
zzqwf12345@163.com
欢迎骚扰~