X86 romInit.s分析

 [转帖]X86 romInit.s分析 by vxFree 【复位向量】
X86 CPU上电后,执行的第一条指令位于何处?
同学们都知道,复位后CPU处于实模式,CS=0xF000,IP=0xFFF0,形成的线性地址为CS<<4+IP=0xFFFF0,也就是1M地址空间的最后16个字节的地方。由于尚未启动分页机制,这个地址就是物理地址。很多书包括一些Linux的什么什么分析都是这样说的,然而这个结论对8086确实是对的,对于80286和386以上的CPU,情况要复杂一些。
对于386以上的处理器(386,486,Pentium),CS寄存器还存在一个48-bit的不可见部分,称为代码段高速缓存寄存器,它包含代码段基地址(Base),段大小(Limit),段属性(Access)等,复位后的初始值如下所示:
EIP 0x0000FFF0
CS Selector = 0xF000
Base = 0xFFFF0000
Limit = 0xFFFF
AR = Present, R/W
Accessed
线性地址的计算方式是
线性地址 = Base + EIP
不管在实模式还是保护模式,都这样计算。但在实模式下一旦修改CS的值,Base的值就会变为CS*16(但初始值不满足这个关系)。
由上面分析可知复位向量为0xFFFF0000+0x0000FFF0=0xFFFFFFF0,即4GB空间的最后16个字节的地方。在没有启动分页机制情况下,这就是CPU形成的物理地址。(有可能是通过主板硬件把它映射到0xF000:0xFFF0,总之PC主板制造者应该会让复位向量指向ROM BIOS。)
注:X86上有3种地址:段/偏移为逻辑地址,经过分段处理(segmentation)变为线性地址;再经过分页(paging)变换为物理地址。一个段就是线性地址空间中的一块,由于段之间可能会发生重叠,所以多个逻辑地址可能对应同一个线性地址。
【A20地址线】
早期的8086只有20根地址线,只能访问1M的地址空间。CPU寻址则按段+偏移的方式进行。16位段+16位偏移的可能的范围是0~0x10FFEF(即0xFFFF0+0xFFFF),即1M+65520字节的范围。由于只有20根地址线,所以在对1M~1M+65520范围进行访问时,会发生“地址回绕”的现象,就是说实际会访问到0~65520的地方。据说某个著名的/臭名昭著的软件利用了这个特点。在80286,386等CPU上,它会失败,因为这些CPU有多于20根的地址线,并不产生“地址回绕”现象。为了保持完全的兼容性,IBM决定在PC AT系统上加个逻辑,来模仿以上的回绕特征。他们的方法就是把A20和键盘控制器的一个输出进行AND,这样来控制A20的打开和关闭。一开始时A20是被屏蔽的(总为0),直到系统软件去打开它。
注意A20而非A20~A31被控制,所以在A20关闭时会发生一些有趣的副作用。就是在访问奇数M地址空间的时候,实际的地址会减少1M。例如访问1M~2M-1时实际访问的是0~1M-1;访问3M~4M-1时为2M~3M-1,等等。
【BIOS】
PC上电后,BIOS从ROM中首先运行。
BIOS启动后会进行一系列的初始化操作,例如POST(Power-On Self-Test),初始化总线控制器和内存控制器,检测内存,初始化PCI设备等等、等等。PC DIY者知道的要比我们多。
BIOS初始化完成后把软盘的第一个扇区(引导扇区,512字节)或者硬盘的第一个扇区(主引导扇区MBR,512字节)读到内存的0x7C00处,然后跳转到0x7C00处执行。这样就将控制权转移到了引导记录。一般情况下,引导记录是安装操作系统时安装的;也可能是其它的工具软件,例如LILO,SystemCommand等,可以有选择地从多个操作系统的某一个启动。
MBR运行后会搜索硬盘的可引导分区(活动分区),加载该分区的引导扇区的内容。引导扇区中的程序叫引导程序。
VxWorks的引导程序叫VxLd,由Tornado工具vxsys.com写入。它运行后会从当前磁盘的根目录下加载BOOTROM.SYS(此文件必须连续存放,因为VxLd很简单,还不认识任何文件系统),加载地址为0x8000。这个步骤通过调用BIOS INT13完成。加载完成后跳转到0x8000执行。VxWorks操作系统的第一条指令就存放于0x8000的地方(注:这是bootrom;对于vxWorks image为0x108000;或者相反)。
注意在PC目标机上,VxWorks和BIOS的关系。VxWorks假定BIOS已经正确地初始化系统硬件,包括内存,PCI总线等,所以pc386,pc486等BSP就省掉了很多工作。然而VxWorks启动后,不会用到BIOS的任何功能,这和DOS不一样。因为VxWorks运行于保护模式,而BIOS功能必须从实模式下调用(也许有人可以给我们说说如何在保护模式下如何调用BIOS功能,当然这是一个高级话题)。实际在一般正常情况下,当VxWorks运行后,BIOS就彻底消失了。

【romInit.s的编译过程】
编译romInit.s时执行的指令为:
cc386 -BD:/Tornado/host/x86-win32/lib/gcc-lib/ -mno-486 -ansi -nostdinc -O -fvolatile -nostdlib -fno-builtin -fno-defer-pop -I/h -I. -ID:/Tornado/target/config/all -ID:/Tornado/target/h -ID:/Tornado/target/src/config -ID:/Tornado/target/src/drv -DCPU=I80386 -P -x assembler-with-cpp -c -o romInit.o romInit.s
可见这个汇编文件也是调用cc进行编译的,使用的选项为-x assembler-with-cpp,即用C预处理器进行预处理。预处理后生成一个“纯”汇编,放在一个临时文件里,cc再调用汇编器对它进行编译。C和汇编进行混合的好处是可以共享一些宏定义并发挥C编译器的灵活性,不好的是难于定位错误。
【VxWorks初始化】
/* romInit.s - PC-386 ROM initialization module */
/* Copyright 1984-1996 Wind River Systems, Inc. */
.data
■ 开始数据段。以下内容出现在数据段里。
.globl _copyright_wind_river
.long _copyright_wind_river /* the first in .data */
■ 申明(declare)全局变量_copyright_wind_river并使用它定义一个新变量。
■ 注意”.globl”是申明而非定义(相当于C的extern)。_copyright_wind_river变量在Tornado的库(对于pc386,为[Tornado]/target/lib/libI80386gnuvx.a)中的一个模块copyright.o中定义。
■ ”.long”定义一个32-bit的全局变量,变量的初始值为_copyright_wind_river的地址。由于在Makefile中规定了romInit.o为第一个链接的模块,所以这个无名变量将出现在数据段的最开始。
#define _ASMLANGUAGE
■ 定义_ASMLANGUAGE宏。GNU编译器cc看到这个定义后,会按照C的语法进行预处理,所以能够认识C头文件中定义的类型和宏。如果不定义_ASMLANGUAGE,以下的#include语句将无法编译。
#include "vxWorks.h"
#include "sysLib.h"
#include "config.h"
■ 包含C的3个头文件。vxWorks.h为系统头文件;sysLib.h为系统提供给BSP的头文件;config.h是BSP的头文件。
.globl _romInit
.globl _sdata
■ 申明全局变量_romInit和_sdata。_romInit实际上是代码的起始位置。
_sdata:
.asciz "start of data"
■ 定义一个以0结尾的字符串”start of data”。这个串出现在数据段的第一个无名变量之后。
.text
.align 4
■ .text开始代码段,以下内容出现在代码段里。.align 4指示编译器调整当前在.text段中的指针为2^4的倍数。编译器进行填充,使得下一条指令出现在能被16整除的地址上。对齐可使CPU取指令快一点。
/****************************************************************
* romInit - entry point for VxWorks in ROM
*/
_romInit:
/* folowing codes are executed as 16-bit code */
■ 以下执行的是VxWorks系统的第一条指令。此时CPU还处于实模式,程序只能访问1M内存空间,缺省的指令为16-bit代码。需要尽快将CPU切换到保护模式。
cli
jmp cold
■ 关中断,跳转到cold处。这是段内相对跳转。
.align 4, 0x90 /* 0x90: nop */
/* following codes are executed in protect mode, as 32-bit code */
■ 系统热启动从_romWarmHigh或_romWarmLow开始,参考sysLib.c中的sysToMonitor()。请自行分析热启动过程。
_romWarmHigh:
cli
movl 4(%esp), %ebx /* ebx = startType */
jmp warm
.align 4, 0x90
/* copy itself from ROM_TEXT_ADRS to RAM_LOW_ADRS, ROM_SIZE bytes */
_romWarmLow:
cli
cld
movl $RAM_LOW_ADRS, %esi /* esi = RAM_LOW_ADRS */
movl $ROM_TEXT_ADRS, %edi /* edi = ROM_TEXT_ADRS */
movl $ROM_SIZE, %ecx /* ecx = ROM_SIZE */
shrl $2, %ecx /* ecx >>= 2 */
rep
movsl
movl 4(%esp), %ebx /* ebx = startType */
jmp warm
/* copyright notice appears at beginning of ROM (in TEXT segment) */
.ascii "Copyright 1984-1996 Wind River System, Inc."
.align 4
■ 以上定义的版权申明字符串出现在代码段中。
cold:
aword /* 32-bit address */
word /* 32-bit operand */
lidt %cs:ROM_IDTR
aword /* 32-bit address */
word /* 32-bit operand */
lgdt %cs:ROM_GDTR
■ 在实模式下的CS,DS,ES,FS,GS,SS等段寄存器的值*16就是段的基地址,而在保护模式下称为selector(16位的段选择子),指向全局描述符表GDT中的项(descriptor,段描述符,48位),段的基地址、大小以及属性从段描述符中取得。在切换到保护模式前,需要准备好GDT。GDT在内存中,由CPU的GDTR寄存器指定其基地址和大小。
所以在保护模式下,使用CS等寄存器作为GDT数组的索引,从表中获得段的基地址。当然这是由硬件自动执行的。
■ 在保护模式下,中断向量表的基地址和大小必须由IDTR寄存器指定。
■ 这两条指令装载IDTR和GDTR寄存器。这两个寄存器都是48位的,前16位是表的大小减1,后32位是表在内存中的基地址。
■ aword和word指令前缀是做什么用的?这两个前缀的机器码分别为0x67和0x66。在32-bit代码前加这样的前缀可以让它变为16-bit代码;在16-bit代码前可以变为32-bit代码。
■ 什么是32-bit代码和16-bit代码?比如说机器码“89H,C3H”是表示“mov %eax, %ebx”还是表示“mov %ax, %bx”?(是的,这两条语句的机器码相同。否则指令数目就太多了!)16-bit还是32-bit,这得看当前代码段的属性是32位的还是16位的。段属性在段描述符中。(一个结论是:保护方式未必是32-bit方式的,因为可以修改段属性。但是在实模式下,并没有段描述符,段寄存器中直接包含段值。Intel的手册说缺省的操作数宽度和地址宽度总是16-bit的。)
操作数宽度属性:指定操作数的宽度是32-bit的(例如%eax),还是16-bit的(例如%ax)。
地址宽度属性:如果一条指令产生对存储器的寻址,则指定地址宽度。
■ GNU编译器缺省工作于32-bit汇编模式,就是说它假定当前指令为32-bit代码。然而此时CPU还处于实模式,CPU缺省地认为当前指令为16-bit指令。可以让CPU在实模式下执行32-bit的指令,方法就是加指令前缀。
■ 还有其它的指令前缀,参考CPU手册。指令前缀只影响下一条指令。
■ ROM_IDTR和ROM_GDTR在pc.h中分别定义为0xAF和0xB5,分别指定要加载的IDTR和GDTR寄存器的值在当前代码段中存放的位置。当然这是手工计算的结果。也许可以替换为_romIdtr - _romStart和_romGdtr - _romStart。
movl %cr0, %eax /* eax = cr0 */
.byte 0x66 /* word */
or $0x00000001, %eax /* eax |= 0x00000001 */
movl %eax, %cr0 /* cr0 = eax */
jmp romInit1 /* near jump to flush instruction Q*/
■ 切换到保护模式。实际上很简单:把CR0寄存器最低位置1即可进入保护模式。
■ .byte 66指令前缀(等效于word;不知道作者为什么不再使用word)指定32位操作数。如果没有这个前缀,CPU在实模式下执行”or $0x00000001, %eax”的效果将是”ax|=0x0001”。
■ jmp为段内相对跳转。切换到保护模式前的指令队列中的内容还是以前CPU预取的指令。利用jmp可以清空它。
romInit1:
.byte 0x66 /* word */
mov $0x0010, %eax /* eax = 0x10 */
mov %ax, %ds /* ds = ax */
mov %ax, %es /* es = ax */
mov %ax, %fs /* fs = ax */
mov %ax, %gs /* gs = ax */
mov %ax, %ss /* ss = ax */
.byte 0x66 /* word */
mov $ROM_STACK, %esp /* esp = ROM_STACK */
■ 现在已进入保护模式。然而各个段寄存器的值,以及它们的高速缓存寄存器中的值还是老的。把DS, ES, FS, GS, SS寄存器设为0x0010,即指向GDT的第2项(从0开始),DPL=0。它们都指向一个段。把堆栈指针esp设为ROM_STACK。
■ 由于CS还是以前的值,意味着目前代码段的属性还是16-bit代码。所以使用指令前缀以执行32-bit代码。
aword /* 32-bit address */
word /* 32-bit operand */
ljmp $0x08, $ROM_TEXT_ADRS + ROM_INIT2 /* far inter-segment jump */
■ 执行一个远程段间跳转修改CS。CS的新值为0x08,即GDT的第1项,DPL=0。修改CS时它的高速缓存寄存器也会自动更新。以下将进入到32-bit代码模式。
■ ROM_INIT2在pc.h中定义为0xF0,也应该是手工计算的结果,指示_romInit2相对于当前代码段的偏移。也许可以替换为_romInit2 - _romStart + ROM_TEXT_ADRS。
_romIdtr:
.word 0x0000
.long 0x000000000
■ IDT(Interrupt Description Table),中断描述符表,空的。
_romGdtr:
.word 0x0027
.long ROM_TEXT_ADRS + ROM_GDT
■ 将要加载的GDTR寄存器的值。ROM_GDT在pc.h中定义为0xC0。也许可以这样写:
.word _romGdtEnd - _romGdt - 1
.long _romGdt - _romStart + ROM_TEXT_ADDR
让编译器给我们计算,这样更灵活一些。
.align 4, 0
_romGdt:
/* 0 (selector = 0x0000): null descriptor */
.word 0x0000
.word 0x0000
.byte 0x00
.byte 0x00
.byte 0x00
.byte 0x00
/* 1 (selector = 0x0008): code descriptor */
.word 0xFFFF /* limit: FFFF */
.word 0x0000 /* base : ***x0000 */
.byte 0x00 /* base : xx00***x */
.byte 0x9A /* P,DPL0,DT,e/r */
.byte 0xCF /* G,D,limit: F***x */
.byte 0x00 /* base : 00****** */
/* 2 (selector = 0x0010): data descriptor */
.word 0xFFFF /* limit: FFFF */
.word 0x0000 /* base : ***x0000 */
.byte 0x00 /* base : xx00***x */
.byte 0x92 /* P,DPL0,DT,r/w */
.byte 0xCF /* G,D,limit: F***x */
.byte 0x00 /* base : 00****** */
/* 3 (selector = 0x0018): code descriptor, for the nesting interrupt */
.word 0xFFFF /* limit: FFFF */
.word 0x0000 /* base : ***x0000 */
.byte 0x00 /* base : xx00***x */
.byte 0x9A /* P,DPL0,DT,e/r */
.byte 0xCF /* G,D,limit: F***x */
.byte 0x00 /* base : 00****** */
/* 4 (selector = 0x0020): code descriptor, for the nesting interrupt */
.word 0xFFFF /* limit: FFFF */
.word 0x0000 /* base : ***x0000 */
.byte 0x00 /* base : xx00***x */
.byte 0x9A /* P,DPL0,DT,e/r */
.byte 0xCF /* G,D,limit: F***x */
.byte 0x00 /* base : 00****** */
■ 以下的代码运行于保护模式,各个段寄存器已包含合适的值。
.align 4,0x90
_romInit2:
cli /* LOCK INTERRUPT */
mov $ ROM_STACK,%esp /* set a stack pointer */
call _romA20on /* enable A20 */
movl $ BOOT_COLD,%ebx /* %ebx has the startType */
warm:
movl $_romGdtr,%eax /* load the original GDT */
subl $_romInit,%eax
addl $ ROM_TEXT_ADRS,%eax
pushl %eax
call _romLoadGdt
movl $ STACK_ADRS,%esp /* initialise the stack pointer */
movl $0,%ebp /* initialise the frame pointer */
pushl $0 /* initialise the %eflags */
popfl
pushl %ebx /* push the startType */
cld /* copy itself to the entry point */
movl $ ROM_TEXT_ADRS,%esi
movl $_romInit,%edi
movl $_end,%ecx
subl %edi,%ecx
shrl $2,%ecx
rep
movsl
movl $_romStart,%eax /* far jump to romStart */
call *%eax
/* just in case, if there's a problem in usrInit */
_romInitHlt:
hlt

 

  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值