您的位置:首页 > 移动开发 > IOS开发

文章标题

2015-11-21 23:02 351 查看


物理内存的0x000A0000 -0x00100000为VGA显示存储、BIOS ROM以及扩展ROM

物理内存的高端0xefffffff-0xffffffff往往被PCI设备的外设I/O所占据。

JOS 中的 Boot Loader

Boot Loader的源程序是由一个叫做的boot.S的AT&T汇编程序与一个叫做main.c的C程序组成的。

boot.S主要是将处理器从实模式转换到32位的保护模式,这是因为只有在保护模式中我们才能访问到物理内存高于1MB的空间;

main.c的主要作用是将内核的可执行代码从硬盘镜像中读入到内存中。

理解boot.S,和main.c

首先boot程序会进行初始化,将代码段选择子和数据段选择子及保护模式启动标志设为常量

关中断,将寄存器清零

开A20地址线 默认情况下第20根数据线一直为0(

Intel 8088中地址线只有20 根(A0 – A19)。在当时内存RAM 只有几百KB 或不到1MB 时,20根地址线已足够用来寻址这些内存。其所能寻址的最高地址0xffff:0xffff,也即0x10ffef。(微机内有16位寄存器)对于超出0x100000(1MB)的寻址地址将默认地环绕到0x0ffef。当IBM公司于1985 年引入AT 机时,使用的是Intel 80286 CPU,具有24 根地址线,最高可寻址16MB,并且有一个与8088 完全兼容的实模式运行方式。然而,在寻址值超过1MB 时它却不能象8088那样实现地址寻址的环绕。但是当时已经有一些程序是利用这种地址环绕机制进行工作的。为了实现完全的兼容性,IBM公司发明了使用一个开关来开启或禁止0x100000地址比特位。由于在当时的8042键盘控制器上恰好有空闲的端口引脚(输出端口P2,引脚P21),于是便使用了该引脚来作为与门控制这个地址比特位。该信号即被称为A20。如果它为零,则比特20 及以上地址都被清除。从而实现了兼容性。 由于在机器启动时,默认条件下,A20 地址线是禁止的,所以操作系统必须使用适当的方法来开启它。但是由于各种兼容机所使用的芯片集不同,要做到这一点却是非常的麻烦。 对A20 信号线进行控制的常用方法是通过设置键盘控制器的端口值。对于其它一些兼容微机还可以使用其它方式来做到对A20 线的控制。有些操作系统将A20的开启和禁止作为实模式与保护运行模式之间进行转换的标准过程中的一部分。由于键盘的控制器速度很慢,因此就不能使用键盘控制器对A20 线来进行操作。为此引进了一个A20 快速门选项(Fast Gate A20),它使用I/O 端口0x92 来处理A20信号线,避免了使用慢速的键盘控制器操作方式。对于不含键盘控制器的系统就只能使用0x92端口来控制,但是该端口也有可能被其它兼容微机上的设备(如显示芯片)所使用,从而造成系统错误的操作。还有一种方式是通过读0xee端口来开启A20 信号线,写该端口则会禁止A20 信号线。

处理器从BIOS进入bootloader后,在boot/boot.S中第48行到第51行代码,bootloader将寄存器cr0的末位更改为1,使得处理器从实模式更改到保护模式

lgdt gdtdesc //装入定义好的gdt,gdtdesc是紧接gdt定义的gdt地址

movl %cr0, %eax //接下来cr0寄存器的PE位,使能32位保护模式

orl $CR0_PE_ON, %eax

movl %eax, %cr0



lgdt gdtdesc将GDT表的首地址加载到GDTR

在Real Mode下,我们对一个内存地址的访问是通过Segment:Offset的方式来进行的,其中Segment是一个段的Base Address,一个Segment的最大长度是64 KB,这是16-bit系统所能表示的最大长度。而Offset则是相对于此Segment Base Address的偏移量。Base Address+Offset就是一个内存绝对地址。对一个内存地址的访问需要指出:使用哪个段以及相对于这个段Base Address的Offset,这个Offset应该小于此段的Limit。当然对于16-bit系统,Limit不要指定,默认为最大长度64KB。我们在实际编程的时候,使用16-bit段寄存器CS,DS,SS指定Segment

到了Protected Mode,内存的管理模式分为段模式和页模式,其中页模式也是基于段模式的,段模式必不可少,页模式可选。Protected Mode的内存管理模式事实上是:纯段模式和段页式。对于段模式来讲,访问一个内存地址仍用Segment:Offset的方式。由于 Protected Mode运行在32-bit系统上,那么Segment的两个因素:Base Address和Limit也都是32位的。IA-32允许将一个段的Base Address设为32-bit所能表示的任何值(Limit则可以被设为32-bit所能表示的,以2^12为倍数的任何值 ),而不像Real Mode下,一个段的Base Address只能是16的倍数(因为其低4-bit是通过左移运算得来的,只能为0,从而达到使用16-bit段寄存器表示20-bit Base Address的目的),而一个段的Limit只能为固定值64 KB。另外,Protected Mode一个段的描述符需要规定对自身的访问权限(Access)。所以,在Protected Mode下,对一个段的描述则包括3方面因素:[Base Address, Limit, Access],它们加在一起被放在一个64-bit长的数据结构中,被称为段描述符。如果我们直接通过一个64-bit段描述符来引用一个段的时候,就需用一个64-bit长的段积存器装入这个段描述符。但Intel为了保持向后兼容,将段积存器仍然规定为16-bit(尽管每个段寄存器事实上有一个64-bit长的不可见部分,但对于程序员来说,段寄存器就是16-bit的)。解决的方法就是把这些长度为64-bit的段描述符放入一个数组中,而将段寄存器中的值作为下标索引来间接引用(事实上,是将段寄存器中的高13位的内容作为索引)。这个全局的数组就是GDT。事实上,在GDT中存放的不仅仅是段描述符,还有其它描述符,它们都是64-bit长,我们随后再讨论。

GDT可以被放在内存的任何位置,那么当程序员通过段寄存器来引用一个段描述符时,CPU必须知道GDT的入口,也就是基地址放在哪里,所以 Intel的设计者门提供了一个寄存器GDTR用来存放GDT的入口地址,程序员将GDT设定在内存中某个位置之后,可以通过LGDT指令将GDT的入口地址装入此寄存器,以后,CPU就根据此寄存器中的内容作为GDT的入口来访问GDT了。

GDT(Global Descriptor Table)是Protected Mode所必须的数据结构,是唯一的。另外,对任何一个任务而言它都是全局可见的。

除了GDT,IA-32还允许程序员构建与GDT类似的数据结构,它们被称作LDT(Local Descriptor Table),但与GDT不同的是,LDT在系统中可以存在多个,并且不是全局可见的,它们只对引用它们的任务可见,每个任务最多可以有一个LDT。另外,每一个LDT自身作为一个段存在,它们的段描述符被放在GDT中。

IA-32为LDT的入口地址也提供了一个寄存器LDTR,因为在任何时刻只能有一个任务在运行,所以LDT寄存器全局也只有一个。如果一个任务拥有自身的LDT,当它需要引用自身的LDT时,它需要通过LLDT将其LDT的段描述符装入此寄存器。LLDT指令与LGDT指令不同的是,LGDT指令的操作数是一个32-bit的内存地址,这个内存地址处存放的是一个32-bit GDT的入口地址,以及16-bit的GDT Limit。而LLDT指令的操作数是一个16-bit的选择子,这个选择子主要内容是:被装入的LDT的段描述符在GDT中的索引值——这一点和刚才所讨论的通过段寄存器引用段的模式是一样的

开启保护模式之后,基址:偏移这种寻址方式就变成了段选择子:偏移这种方式,而所谓的段选择子就是段表中的索引,因此为了正确的进行段式地址变换,还需要加载段表。这就是在装在cr0之前需要先使用指令lgdt gdtdesc加载段表的原因。

之后是段表的内容,也就是符号gdtdesc的位置

可以发现gdt里面有3个段

第一个段为空段(查相关资料才知道,这是x86中的规定,第一个段均为空段)

第二个和第三个段的定义使用了SEG(ype,base,limit)和SEG_NULL两个宏,定义于inc/mmu.h中。

所以我们可知定义的第二个和第三个段均是基质为0,长度是4G的段,也就是整个32位地址空间。

可以看出,jos
4000
并没有使用x86的段式地址变换来进行内存管理(起码在lab1里没有用),加载段表只是为了能正确的访问32位地址空间而已。

之后boot.S 设置一些寄存器的值,然后就call bootmain,跳转到boot/main.c这个文件里执行了。

运行地址<—>链接地址 ,加载地址<—>存储地址各是等价的,也是两种不同的说法。即见LMA,VMA

运行地址:程序在SRAM、SDRAM中执行时的地址。就是执行这条指令时,PC应该等于这个地址,换句话说,PC等于这个地址时,这条指令应该保存在这个地址内。

bootloader执行的最后一条指令为将内核ELF文件载入内存后,调用内核入口点

ELF文件由4部分组成,分别是ELF头(ELF header)、程序头表(Program header table)、节(Section)和节头表(Section header table)。实际上,一个文件中不一定包含全部内容,而且他们的位置也未必如同所示这样安排,只有ELF头的位置是固定的,其余各部分的位置、大小等信息有ELF头中的各项值来决定。

ELF文件头

.text节:可执行指令的部分

.rodata节:只读全局变量部分

.stab节:符号表部分

.stabstr节:符号表字符串部分

.data节:可读可写的全局变量部分

.bss节:未初始化的全局变量部分,这一部分不会在磁盘有存储空间,因为这些变量并没有被初始化,因此全部默认为0,于是在将这节装入到内存的时候程序需要为其分配相应大小的初始值为0的内存空间

.comment节:注释部分,这一部分不会被加载到内存

ELF文件头的数据结构

struct Elf {

uint32_t e_magic; // 标识文件是否ELF文件

uint8_t e_elf[12]; // 魔数和相关信息

uint16_t e_type; // 文件类型

uint16_t e_machine;// 针对体系结构

uint32_t e_version; // 版本信息

uint32_t e_entry; // Entry point 程序入口点

uint32_t e_phoff; // 程序头表偏移量

uint32_t e_shoff; // 节头表偏移量

uint32_t e_flags; // 处理器特定标志

uint16_t e_ehsize; // 文件头长度

uint16_t e_phentsize;// 程序头部长度

uint16_t e_phnum; // 程序头部个数

uint16_t e_shentsize;// 节头部长度

uint16_t e_shnum; // 节头部个数

uint16_t e_shstrndx; // 节头部字符索引

};

e_entry是可执行程序的入口地址

e_phoff和e_phnum可以用来找到所有的程序头表项

e_phoff是程序头表的第一项相对于ELF文件的开始位置的偏移

e_phnum则是表项的个数

e_ shoff和e_ shnum可以用来找到所有的节头表项

根据查询objdump-xobj/kern/kernel的结果可以得知内核ELF的入口 地址为0xf010000c,但是boot/main.c在载入内核时做了一次手动的地址转换,将高位的f去掉了,所以事实上在运行中内核是被加载到了0x10000c的内存地址上
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签:  bios