产褥期保健:X86 romInit.s分析

来源:百度文库 编辑:中财网 时间:2024/04/26 00:13:29

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初始化】


.data
■ 开始数据段。以下内容出现在数据段里。
.globl _copyright_wind_river
.long _copyright_wind_river
■ 申明(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:

■ 以下执行的是VxWorks系统的第一条指令。此时CPU还处于实模式,程序只能访问1M内存空间,缺省的指令为16-bit代码。需要尽快将CPU切换到保护模式。
cli
jmp cold
■ 关中断,跳转到cold处。这是段内相对跳转。
.align 4, 0x90

■ 系统热启动从_romWarmHigh或_romWarmLow开始,参考sysLib.c中的sysToMonitor()。请自行分析热启动过程。
_romWarmHigh:
cli
movl 4(%esp), %ebx
jmp warm
.align 4, 0x90

_romWarmLow:
cli
cld
movl $RAM_LOW_ADRS, %esi
movl $ROM_TEXT_ADRS, %edi
movl $ROM_SIZE, %ecx
shrl $2, %ecx
rep
movsl
movl 4(%esp), %ebx
jmp warm

.ascii "Copyright 1984-1996 Wind River System, Inc."
.align 4
■ 以上定义的版权申明字符串出现在代码段中。
cold:
aword
word
lidt %cs:ROM_IDTR
aword
word
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
.byte 0x66
or $0x00000001, %eax
movl %eax, %cr0
jmp romInit1
■ 切换到保护模式。实际上很简单:把CR0寄存器最低位置1即可进入保护模式。
■ .byte 66指令前缀(等效于word;不知道作者为什么不再使用word)指定32位操作数。如果没有这个前缀,CPU在实模式下执行”or $0x00000001, %eax”的效果将是”ax|=0x0001”。
■ jmp为段内相对跳转。切换到保护模式前的指令队列中的内容还是以前CPU预取的指令。利用jmp可以清空它。
romInit1:
.byte 0x66
mov $0x0010, %eax
mov %ax, %ds
mov %ax, %es
mov %ax, %fs
mov %ax, %gs
mov %ax, %ss
.byte 0x66
mov $ROM_STACK, %esp
■ 现在已进入保护模式。然而各个段寄存器的值,以及它们的高速缓存寄存器中的值还是老的。把DS, ES, FS, GS, SS寄存器设为0x0010,即指向GDT的第2项(从0开始),DPL=0。它们都指向一个段。把堆栈指针esp设为ROM_STACK。
■ 由于CS还是以前的值,意味着目前代码段的属性还是16-bit代码。所以使用指令前缀以执行32-bit代码。
aword
word
ljmp $0x08, $ROM_TEXT_ADRS + ROM_INIT2
■ 执行一个远程段间跳转修改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:

.word 0x0000
.word 0x0000
.byte 0x00
.byte 0x00
.byte 0x00
.byte 0x00

.word 0xFFFF
.word 0x0000
.byte 0x00
.byte 0x9A
.byte 0xCF
.byte 0x00

.word 0xFFFF
.word 0x0000
.byte 0x00
.byte 0x92
.byte 0xCF
.byte 0x00

.word 0xFFFF
.word 0x0000
.byte 0x00
.byte 0x9A
.byte 0xCF
.byte 0x00

.word 0xFFFF
.word 0x0000
.byte 0x00
.byte 0x9A
.byte 0xCF
.byte 0x00
■ 以下的代码运行于保护模式,各个段寄存器已包含合适的值。
.align 4,0x90
_romInit2:
cli
mov $ ROM_STACK,%esp
call _romA20on
movl $ BOOT_COLD,%ebx
warm:
movl $_romGdtr,%eax
subl $_romInit,%eax
addl $ ROM_TEXT_ADRS,%eax
pushl %eax
call _romLoadGdt
movl $ STACK_ADRS,%esp
movl $0,%ebp
pushl $0
popfl
pushl %ebx
cld
movl $ ROM_TEXT_ADRS,%esi
movl $_romInit,%edi
movl $_end,%ecx
subl %edi,%ecx
shrl $2,%ecx
rep
movsl
movl $_romStart,%eax
call *%eax

_romInitHlt:
hlt