操作系统(3)实验相关原理——bootloader启动uCore

2021-10-15 16:35:01 浏览数 (1)

x86启动顺序

CS EIP决定启动地址。

CS部分后面又4个0,相当于是左移了4位。总之就是要让CS左移4位之后加上EIP来得到要跳转的地址。

0x7c00地方开始的512字节的内容就是bootloader。这么做的原因是BIOS只能加载一个扇区,所以只能通过bootloader来加载系统。

段机制

这边uCore没有实现段机制,因为可以通过页机制来很方便地实现。

但是还是绕不开段模式,只要启了保护模式段就enable了(而且页机制基于段基址实现),所以还是要建立好段机制。下面这种映射关系近似于对等:

上图中Offset就是EIP,Seg Selector可以是CS也可以是别的段寄存器。段描述符表用来查找段描述符,index用的是段寄存器的内容,段描述符最终就相当于是一个起始地址。上图中线性地址就等同于物理地址(因为还没启动页机制)。如果base设为0,那么EIP就是对应物理基址。

段描述符表相当于是一个数组,这个数组由操作系统生成,我们称之为GDT,全局描述符表。GDT由Bootloader建立。CPU内部的GDTR这个寄存器用来保存GDT。uCore中基址都设定为零,段长度都设定为4G。

上图中段寄存器的这个标记表明了当前执行程序的时候所处的特权级(0代表系统级,数字越大越没特权)。TI一般设置为0,忽略掉,因为这里没有用到LDT(本地描述符表)。

最后要使能保护模式,最终进入保护模式。搞这么多,GDT就是为了保证段机制在进入保护模式之后还是可以正常工作。

解决了之后就可以开始加载uCore OS。

C函数调用

uCore得到控制权之后还需要了解函数调用关系(用处例子:当程序出错的时候就可以定位到错在哪里)。

下面是一个例子,用来说明C函数调用的实现。

注意栈中高低位的位置,以及push和pop的顺序。

上图这部分就是调用的过程,先push保护寄存器的内容,并且压栈返回地址,然后跳转到被调用函数的地址,调用完返回之后再pop恢复现场。

注意上图中返回地址入栈。

开始调用函数,注意红框部分:

其实是这样的步骤:

上图是push �p的结果,即将EBP指向的地址入栈,ESP上移指向已经入栈了的内容的位置。

然后movl %esp, �p来让EBP指向ESP指向的位置(此时ESP和EBP指向同一个位置)。这样就形成了所谓的调用栈的链。这样就可以把更深层次的调用关系给表述出来。

在这期间可以看到还压入了一些调用函数时候需要用到的参数(实参的传递)。

最后面popl �p,让EBP重新指向之前保存的地址,于是就变回了这样:

最后做ret的时候会根据ESP指的return address来跳转。

所以后面会从上面的call foo下面的代码开始执行。

最后,运行3个popl把最开始的时候压入的参数给弹回去,恢复现场。

GCC内联汇编

内联汇编的作用:使得C语言可以和汇编语言混在一起使用。

在C里面插入汇编代码的例子:

大概的格式:

上图中clobber可以暂时忽略。这三个部分是在你想要添加约束,例如限定某个寄存器的时候才写上去的,三个部分都是可选的,即可以都不写。

一个例子:

上面的例子就是用来给cr0第一位置1,首先将cr0寄存器的内容读取到%0寄存器里面去,并且最终cr0寄存器的内容会被赋给cr0内存变量(注意cr0的区别,一个是寄存器,一个是内存变量。此外注意右侧的=r和下面的:"r")。然后对cr0变量进行操作(或操作使得第一位置1)。最后就是将cr0变量的内容写回到cr0寄存器(首先将变量cr0给一个寄存器,然后将寄存器的值给到cr0寄存器)。下面的就是生成的对应的汇编代码。

下面是一些关键词的解释:

另一个例子:

右下角是关键词对应的寄存器。这段代码相当于是给某些特定的寄存器赋值,然后调用0x80函数,最终赋值给__res

x86中断处理过程

软中断的例子:之前提到的int 80,软件就是可以通过软中断来调用系统提供的服务。

IDT中每一项称为中断门或者陷阱门(和之前的全局描述符表类似,也是个数组),通过中断号来选中IDT中的陷阱门,通过这个陷阱门/中断门可以获得陷阱门/中断门相关的段的选择子(类似段机制的选择子和段类偏移),最后就可以得到终端服务例程的地址。表的起始地址在IDTR里面,这个起始地址由操作系统指定。

上图为陷阱门/中断门的信息,可以看到每一项包含了段选择子和偏移。通过这两个东西可以确定例程的起始地址。

上图表示了怎么通过IDT和GDT/LDT来确定中断服务例程的确切地址,首先中断向量进来,变成index在IDT中选择相应的陷阱门/中断门,提取出对应的偏移和段选择子,最后通过段选择子在GDT中选中段描述符,最后提取段描述符里面的基地址(base address)。最后的最后,基地址和偏移结合,得出最终的中断例程的地址(中断例程也是操作系统要实现的)。CPU会自动根据这两个表来进行处理,所以操作系统只需要构建这两个表和例程就行。以上就是中断处理初始化的过程。

中断发生之后会打断当前执行的程序并跳转执行中断例程去(如果此时使能了中断的话),执行完中断程序之后才会返回来继续执行当前执行的程序。所以这里就涉及到保存现场和恢复现场的过程。但是要注意在不同的特权级会对应不同的处理方式,特权级在段描述符里面,例如CS的低两位,0为内核态,3是用户态。用户态的终端可能会跳到内核态。特权级变化和没变化对应不同的处理方式:

首先是没有变化的情况:

此时中断前后都是使用同一个栈。

油用户态切换到内核态变化的时候:

注意红框中的SS和ESP,这是用来指明用户态下使用的栈的地址,即中断前后不是使用同一个栈。

中断结束后,没有改变特权方式的时候,iret会弹出CS、EIP来跳回到打断的地方继续执行,同时还会弹出EFLAGS恢复标志位。ret只弹出EIP,跳到当时调用指令的下一条指令去执行,retf要弹出CS和EIP,用来实现远程跳转。特权变化的时候就要弹出图中所示的栈内的内容。

系统调用

小结

上图中GCC内联汇编就是在C里面嵌入汇编代码,没什么特殊含义。

后面会有一篇关于实验的博文,今天就写到这里。

0 人点赞