作者:张华 发表于:2016-03-01
版权声明:可以任意转载,转载时请务必以超链接形式标明文章原始出处和作者信息及本版权声明
( http://blog.csdn.net/quqi99 )
- 分段,即逻辑地址转换为线性地址,段基址+段偏移=线程地址。通过LDT中的段选择符找到段描述符(GDT)然后找到段基址。CPU为了方便找到LDT与GDT又在CPU中做了两个寄存器GDTR与LDTR。GDT中有limit字段,相当于段尾址,从硬件上可以确保段不会越界。一个程序是分段的,如代码放在代码段(CS),局部变量与函数指针放在栈段(SS),数据放在堆段等(DS, ES, FS, GS)。在64位,因为偏移量为64位就够大,所以分段机制基本被禁用,处理器将CS,DS,ES,SS的段基址视为0, 64位模式不进行段长度检查。
- 分页,即线性地址转换为物理地址,先查TLB,地址不在TLB中就由MMU(Memory Management Unit)硬件中的CR3向CPU提供页目录基址。
- 分段启用,分页未启用:逻辑地址 -> 线性地址 = 物理地址
- 分段启用,分页也启用:逻辑地址 -> 线性地址 -> 物理地址
- 端口I/O, 通过I/O端口访问设备寄存器,x86有65536(0x0-0xFFFF, 64K)个8位的I/O端口
- 内存映射I/O(Memory Map I/O, MMIO),将设备寄存器或设备RAM映射到物理地址空间的某段地址,访问此段地址即像访问设备。MMIO也需要线性地址到物理地址的转换,但这个过程不需要TLB了。目前很多CPU架构都没有Port I/O,采用统一的MMIO方式。
实模式与段
8080拥有16根地址线(寄存器是16位,2的16次方=64K), 8086拥有20根地址线(2的20次方=1M=16×64K,但它的寄存器仍然是16位),为了兼容8080,Intel仍然让程序只使用1M里的64K字节段, 这叫内存的实模式。8088, 8086, 80286的寄存器都仍然是16位,它是使用两个16位寄存器来寻址20位,一个16位寄存器用来存放段地址,一个16位寄存器用来存放段偏移。段可以是在1M内任意以16为倍数的段地址开始,接着最大可达64K的界限。0001:0019=0001H段开始的第0019H个字节处,我们知道,一个段可能开始于实内存中所有1M字节里的任意一个16字节处,所以0001:0019等于0000:0029, 也等于0002:0009。
1M=2的20次方=2的10次方×1K,20位16进制是5位,所以1M的地址是00000H~0FFFFFH
16位实模式:段基址为16的整数倍(对于1M空间的20位寻地址采用两个16位寄存器,将其中一个左偏移4-bit加另一个寄存器构成16位,所以段基址必须是16的整数倍),最大偏移(Limit)为64KB。
32位保护模式:段基址为32位所表示的任何值,Limit可以被设为32-bit所能表示的以2^12=4K为倍数的任何值)在保护模式下各进程对段的描述包括3方面(段基址,段限长,特权级),它们被放在一个64-bit的数据结构中,被称为段描述符,这需要64位段寄存器去访问它,但没有64位段寄存器(为兼容仍然。于是将64-bit放在一个数组中,而将段寄存器的值作为下标来引用,这个数组便是GDT。
注意:CS在实模式中是代码段基址,而在保护模式中指的是在GDT中的代码段选择符。例如jmpi 0,8指令代码在段间跳转,8意为1000,第1个1代表选择GDT的第2个记录, 然后CS寄存器会自动设置成第2个GDT记录里前8位所指的段寄存器基址。在保护模式下除了CS是操作系统自动设置的,其他的段寄存器可以使用(movl $0x10, %eax && mov %ax, %ds)设置,这里的0x10的分析和jmpi指令里的8的含义相同。
64位长模式:
段寄存器
这这种实模式下,一个段只有64K大小,一个程序不够用,所以一般通过划分CS, DS, SS, ES等段的方式来扩大大小。
8088, 8086, 80286有4个用于存放段地址的段寄器,80386增加了2个。
CS(Code Segment):代码段,用于存放机器指令。注意:那么CS就不再是代码段基址,只由Linux根据jmpi指令自动设置将第几项GDT记录里的基地址记录设置进CS寄存器
DS(Data Segment): 数据段,用于存放变量和其他数据。可能会有很多数据段,但CPU一次只能使用一个。
SS(Stack Segment): 堆栈段, 一个单独的程序只能有一个堆栈。注意:在保护模式下,
ES(Extra Segment): 附加段寄存器(结合DS使用意味着可以同时访问两个数据段),用于指定内存中某一位置的备用段。
FS和GS:是ES的克隆,命令按ES往后的F,G排列,只存在于80386(寄存位为32位)及后来的x86 CPU中。
汇编中的Push-y指令
push ax
push [ax]
pushf #将16位寄存器标志值压入堆栈
pushfd #将EFLAGS寄存器的全部32位值压入堆栈
pusha #将8个通用寄存器压入堆栈
通用寄存器
在32位世界中,通用寄存器分为三个一般类:16位通用寄存器、32位扩展寄存器和8位的半寄存器(实际上,16位和8位寄存器只是32位寄存器内部的一块区域而已)。
有8个16位通用寄存器:AX,BX,CX,DX,BP,SI,DI和SP用于存放16位的或更少位的值(BX与BP常用于基址寻址,SI与DI常用于变址寻址)。在实模式下它可以是结合段寄存器这样指定一个完整的20位地址使用:
SS:SP #SP(Stack pointer)常被用作栈顶指针
SS:BP #BP(base pointer)常被用作维护过程的stack frame结构,
ES:DI
DS:SI
CS:BX
后来x86体系结构将其扩展为32位时,在原有的名称前加了前缀E(EAX, EBX, ECX, EDX, EBP, ESI, EDI和ESP, 低16位仍然可以用老式不加前缀E的叫法,但不幸的是,寄存器的高16位根本没有自己的名字)。
64位系统,在原有的名称前加了前缀R(RAX, RBX, RCX, RBP, RSI, RDI, RSP)。另外添加了8个全新的64位寄存器(R8到R15)。x86-64也增加了8个128位的SSE寄存器到IA-32的8个同类寄存器中,一共有16个SSE寄存器。
半寄存器
上面的4个通用寄存器(EAX, EBX, ECX, EDX)的低16位(AX, BX, CX, DX)又被划分为8位的半寄存器,高位后加H,低位后加L,如:在BX中有BH和BL两个半寄存器,依此类推。
指令指针寄存器
16位中叫IP, 32位中叫EIP,存放当前代码段(一个程序可能包含多个代码段)中下一段即将执行的机器指令的偏移地址。CS和IP一起,保存下一条即将执行的指令的完整地址(在实模式下,CS与IP一起工作带来20位的地址,CS由操作系统设置,IP可以跟踪64K内存段;在保护模式下,32位系统的IP可以跟踪4G内存段)。IP寄存器是唯一不能读入与写出的寄存器,它只能使用跳转指令移动。Linux内核里的GDT(Global Descritor Table)是唯一存放段寄存器的数组,配合各进行在保护模式下的段寻址,它在进程切换中具有重要的意义,可理解为所有进程的总目录表,其中存放着每一个任务(task)局部描述表( LDT, Local Descriptor Table)地址和任务状态段(TSS, Task Structure Segment)地址,用于完成进程中各段的寻址、现场保护与现场恢复。而GDTR是GDT的入口地址寄存器,在内核对GDT的初始化完成后,可以用LGDT(Load GDT)指令将GDT基地址加载至GDTR。类似地还有IDTR(Interrupt Descriptor Table Register)寄存器存放着IDT中断描述表的入口地址,IDT是保护模式下所有中断服务程序的入口地址,类似于实模式下的中断向量表。
标志寄存器
16位中叫FLAGS,32位中叫EFLAGS,每一位都有特殊的含义,也有单独的名字,如CF, DF, OF等。
保护模式
实模式因为寻址只有64K不够用,所以有段的概念。但在32位系统中,可以寻址4G的内存空间,就不需要分段了。但传统的段寄存器依然存在,只是你不能读或写它们,完全交由操作系统来做。操作系统有一个虚拟地址空间可以非常大,但32位系统的物理寻址空间最大只能是4G,操作系统从虚拟地址空间中找一块4G的内存空间作为内存寻址,那么段寄存器就被操作系统设置为这个虚拟空间下的基址。所以说,Linux没有实模式“遗留问题”需要处理,自从1992年第一次出现以来,它就一直运行在保护模式下。只有BIOS需要运行在实模式下(Linux提供软中断80H去访问BIOS)。如果多个程序同时访问一块内存,可能造成混乱,像DOS是单任务程序无此问题,多个程序通过驱动访问(驱动能隔离程序到某块内存的访问)也无此问题,保护模式可以让多个程序在同一时刻运行。
64位长模式
x86-64定义了三种一般模式:实模式,保护模式和长模式。实模式是兼容模式兼容16位系统,保护模式也是一种兼容模式兼容32位系统,长模式。由于64位地址过于巨大(10亿GB),今天的x86-64 CPU一般只支持48位虚拟内存和40位物理内存地址(1000GB)。
中断向量表-
所有中断实际上是由位图描述的,每个位图类似于寄存器指向一个内存地址偏移向量(即中断服务程序地址,一个向量由4个字节组成,共有256个向量,由Linux向中断向量表中写入正确的这些地址),也叫中断向量表。x86有一条软中断指令80H专门用于操作系统找中断程序(保护模式下是禁止的):
move eax, 4 #要调用的中断号
move ebx,1 #中断的参数传入到了寄存器中,而不是堆栈中
int 80H #进行软中断调用,然后Linux会通过eax里的中断号在中断向量中找到中断服务程序地址
next_order #下一条指令,在执行上面行的中断之前,会先将这下一条指令压入堆栈,这样中断返回之后就知道继续从这里执行了。
汇编
计算机只能运行二进制,程序员不好记,所以芯片厂商发明了一些助记符,也就是汇编语言,用助记符写的指令可以很容易的翻译成二进制代码。各家的汇编格式可能不一样,例如gcc的汇编器使用的是AT&T文法,常见的汇编语言可以使用nasm编译。汇编编译器assembler编译目标代码二进制文件(nasm -f elf -g -F stabs *.asm),连接器linker(ld -o bin_file *.o)除了把目标代码组合成一个单个的块,还要确保模块以外的函数调用能够指向正确的内存引用(连接器必须建立一个索引,也就是符号表,里面存放的是它连接的每一个目标模块中的每一个已命名项,其中存放着一些关于哪个名字或叫符号指向模块内部哪个位置的信息)。
立即数,内置在机器指令内部,它不是存放在寄存器中,也不是存放在位于某个指令之外的内存中。
1, 寄存器打中括号代表寄存器的内存地址中的内存数据。例:
mov eax,[ebx+16]。
2, 在汇编中,变量名代表的地址,不是数据。例:
Msg: "Hello World"
mov ecx,Msg #复制Msg地址到ecx寄存器,而不是数据
mov edx,[Msg] #复制数据,而不是地址
MsgLen: equ $-Msg #$代表末尾,长度=末尾位置减开始位置
计算机启动
1, CPU硬件在加电时为兼容运行在保护模式下,并且CPU硬件逻辑强制将CS寄存器值设置为0xFFFF, IP值设置为0x0000, 这样CS:IP就指向了0xFFF0这个地址位置,这个地址便为BIOS的入口。2, BIOS程序被固化在主板上一块很小的ROM芯片里 (0xFE000 ~ 0xFFFF),它除了硬件自检,还在内存中建立中断向量表和中断服务程序(因为CPU寄存器是一个地址值,相当于位图,需要在一定的偏移处存放中断服务程序入口地址,这便是中断向量表, BIOS使用0x0000 ~ 0x003FF共1K的内存创建中断向量表,每个中断向量占4个字节,两个是CS寄存器的值,两个是IP寄存器的值,所以1K内存只能创建最多256个中断向量, 所以接着的256字节用于构建BIOS的数据区,再接着加载了8K左右的若干中断服务程序)。
3, BIOS通过BIOS的一个中断(int 0x19,它是BIOS的中断,不是Linux的中断)将硬盘的第一扇区的512字节的引导程序boot.s(也就是MBR)加载到内存的指定位置0x07C00处。
4, boot.s(MBR)对内存有如下划分:
第一步, boot.s要先规划上面的内存使用计划,然后将mbr代码从BOOTSEG复制到INITSEG处然后并跳转至该处执行MBR的代码并设置堆栈SS:SP指针让堆栈能用;
第二步, 然后MBR在主分区表中搜索标志为活动的分区接着使用Linux的int 0x13中断(注意:int 0x13是Linux的中断,上面的int 0x19是BIOS的中断)将活动分区的头512B复制到BOOTSEG然后再复制到SETUPSEG存放并跳转执行;
第三步,同样的方法,使用Linux的int 0x13中断将内核加载到 SYSSEG。
第四步,Linux调用BIOS提供的中断从设备上提取内核运行所需的机器系统数据(如光标位置,扩展内存数,显示页面,显示模式,显示内存,显示状态,硬盘参数表1,硬盘参数表2,根设备号等)加载至INITSEG,这样便覆盖了之前存放的MBR的代码(0x90000~0x901FD, 共510字节,原来的MBR只有2字节未被覆盖)。
SETUPLEN=4
BOOTSEG=0x07c0
INITSEG=0x9000 #先存放MBR代码, Linux加载之后被覆盖存放从BIOS提取的一些硬件数据
SYSSEG=0x1000 #存放内核代码, 先后再移动到0x0000将BIOS的中断向量表和数据区完全覆盖
SETUPSEG=0x9020 #存放活动分区下的512B
ENDSEG=SYSEG+SYSSIZE
5, setup.s(活动分区头512B)接着要打开32/64位寻址空间,打开保护模式,建立保护模式下的中断响应机制等,建立内存的分页机制,最后做好调用main.c:start_kerner的准备。
第一步,关中断(EFLAGS寄存器的IF标志位置0即可),将内核从SYSSET移动到内存起始位置0x0000, 它会将BIOS的中断向量表和数据区完全覆盖。
第二步,设置两个寄存器(中断描述符寄存器IDTR与全局描述符寄存器GDTR)。IDTR指向IDT(Interrupt Descriptor, 中断描述表,32/64位保护模式下的所有中断服务程序的入口地址,类似于16位实模式下的中断向量表. 实模式的中断向量表的起始地址是固定在0x0000处,保护模式下的中断机制用的是IDT,位置是不固定的,可由操作系统根据要求灵活设置,由IDTR来锁定其位置),GDTR指向GDT( Global Descriptor Table, 全局描述表, 保护模式下的各进程的段基址段偏移及访问控制等信息,完成进程中各段的寻址,现场保护与恢复)。IDT与GDT是存放在镜像中的静态数据(这时候初了内核一个进程还没有其他进程,所以初始的GDT创建的第一项为空,第二项为内核代码段描述符,第三项为内核数据段描述符,其余项皆为空)。
第三步,打开32/64位寻址。
第四步,对可编程中断控制器重新编程,将CPU保护模式保留的int 0x00 - int 0x1F重新编程为int 0x20 - int 0x2F。
第五步,打开处理器的保护模式(将CR0寄存器的0号Protected Mode位置1)。注意:保护模式下只有一个段,那么CS就不再是代码段基址,而是代码段选择符
第六步,使用jmpi指令从SETUPSEG跳转到0x0000处即开始执行head.s
6, head.s(内核的开始代码)的汇编代码汇编成的目标代码(25KB+184B)与C语言的内核代码编译成的目标代码是链接成一个system模块的。
第一步,head.s从0x000000处建立页目录表(0x0000~0x4FFFF,20KB),页表,缓冲,GDT, IDT,并将head程序已经执行过的代码所占内存空间覆盖。
第二步,在保护模式下设置GDT,用GDT表里的段地址去设置相应的DS,ES,FS,GS段寄存器。也设置SS段寄存器让堆栈在保护模式下可用。
第三步,在保护模式下设置IDT
第四步,head.s将 main函数压入堆栈,这样今后head程序执行完后通过ret指令(ret,用栈中的数据修改IP寄存器)就可以直接执行main函数, 因为main函数本来就是最底层的代码故不能使用call返回式的调用。
第五步,head.s调用setup_paging创建分页机制, 从0x000000开始使用5个页(每页4K)覆盖head.s内存的自身创建一个页目录表及4个页表(内核专属的页表)
第六步,设置CR3页目录表基址寄存器(高20位存放页目录表的基地址),再将CR0寄存器设置的最高低(31位,分页机制控制位)置为1。
第7步,ret跳入main函数执行。此时仍然处于关闭中断的状态。
7, main.c
第一步,根据上面boot.s在0x90000~0x901FD处存处的硬件信息设置硬盘相关(根设备与硬盘)。
第二步,设置内存相关(主机与外设交互的缓冲区,设置虚拟盘并初始化,进程代码运行的主内存,内存管理结构mem_map初始化)。
第三步,异常(除零,单步调试,不可屏蔽中断,溢出,边界检查错误,无效指令,无效设备,双故障,缺页产,栈异常等等)处理中断服务程序trap_init()挂接到IDT上。
第四步,初始化块设备请求项结构request[32]用于块设备与缓冲区之间的联系
第五步,设置tty设备(人机交互界面,如两个串口,显示器,键盘等)与中断服务程序的挂接
第六步,读CMOS时钟设置启动时间
第七步,初始化进程0(如systemd等),通过TR任务寄存器或LDTR寄存器能找到TSS0(TSS存放进程的一些寄存器信息和状态信息)与LDT0(内核的代码段和数据段直接在GDT里,但各个进程的代码段与数据段又是放在LDT里,然后在GDT里通过指针指的)。接着设置时钟中断,设置system_call软中断。
第八步,初始化缓存区管理结构hash_table[NR_HASH], buffer_head
第九步,挂接硬盘中断服务程序hd_interrupt()到IDT
第十步,挂接软盘中断服务程序到IDT
第十一步,保护模式下开启中断
第十二步,进程0由0特权级翻转到3特权级,成为真正的进程。