开头赞美THU给我们提供了这么棒的资源.难是真的难,好也是真的好.这种广查资料,反复推敲,反复思考从通电后第一条代码搞起来理顺一个操作系统源码的感觉是真的爽.

1. 操作系统镜像文件ucore.img是如何一步一步生成的?

这makefile文件逻辑简略着看都能明白,仔细了瞧却处处有疑问,有的地方还用到了二重展开.对于初学者来讲,细读这东西太痛苦了,还是简略着读吧.

# create kernel target
kernel = $(call totarget,kernel) $(kernel): tools/kernel.ld $(kernel): $(KOBJS)
@echo + ld $@
$(V)$(LD) $(LDFLAGS) -T tools/kernel.ld -o $@ $(KOBJS)
@$(OBJDUMP) -S $@ > $(call asmfile,kernel)
@$(OBJDUMP) -t $@ | $(SED) '1,/SYMBOL TABLE/d; s/ .* / /; /^$$/d' > $(call symfile,kernel) $(call create_target,kernel)

kernel赋值为"bin/kernel"

执行toos/kernel.ld链接脚本

编译kernel\下的所有.s和.c文件

# create bootblock
bootfiles = $(call listf_cc,boot)
$(foreach f,$(bootfiles),$(call cc_compile,$(f),$(CC),$(CFLAGS) -Os -nostdinc))

编译boot\下的所有C文件

bootblock = $(call totarget,bootblock)

$(bootblock): $(call toobj,$(bootfiles)) | $(call totarget,sign)
@echo + ld $@
$(V)$(LD) $(LDFLAGS) -N -e start -Ttext 0x7C00 $^ -o $(call toobj,bootblock)
@$(OBJDUMP) -S $(call objfile,bootblock) > $(call asmfile,bootblock)
@$(OBJCOPY) -S -O binary $(call objfile,bootblock) $(call outfile,bootblock)
@$(call totarget,sign) $(call outfile,bootblock) $(bootblock) $(call create_target,bootblock)

编译boot\下所有文件,并链接bootblock文件

# create 'sign' tools
$(call add_files_host,tools/sign.c,sign,sign)
$(call create_target_host,sign,sign)

编译sign.c文件并调用之

# create ucore.img
UCOREIMG := $(call totarget,ucore.img) $(UCOREIMG): $(kernel) $(bootblock)
$(V)dd if=/dev/zero of=$@ count=10000
$(V)dd if=$(bootblock) of=$@ conv=notrunc
$(V)dd if=$(kernel) of=$@ seek=1 conv=notrunc $(call create_target,ucore.img)

定义变量UCOREIMG,用到了totarget.在本文件中搜不到totarget,但搜索include可得

include tools/function.mk

于是在function.mk中可找到totarget的定义

totarget = $(addprefix $(BINDIR)$(SLASH),$(1))

BINDIR在别处定义为"bin",slash定义为斜线"/",$(1)指代输入参数

所以totarget作用为给输入参数添加前缀"bin/"

回归前文,UCOREIMG则等于"bin/ucore.img"

继续往下看,UCOREIMG依赖项为kernel和bootblock,先不管它们

V定义为at符号"@"

dd为linux命令covert an copy,不过cc已经被用过了,所以改叫dd,用法可以参考【一天一个shell命令】文本操作系列-dd

$@为makefile特殊符号,表示目标文件

这几行命令作用是当kernel和bootblock更新时,把UCOREIMG先写入obt*count的空白数据.其中obt为默认值512,count为10000,生成UCOREIMG大小为5120000字节.然后把bootblock覆写到UCOREIMG上,作为bootloader引导代码.再把kernel覆写到相对于UCOREIMG开头512字节的位置.这里体现了磁盘开头的512字节是属于bootloader的.

最后作一下流程总结

生成kernel
生成bootblock
生成合法的主引导扇区
生成ucore.img

2. 一个被系统认为是符合规范的硬盘主引导扇区的特征是什么?

阅读sign.c可知,大小小于512字节且以0X55AA结尾

3. bootasm.S分析

CPU加电,初始化CS=0XF000,IP=0XFFF0

执行CS:IP处长跳指令,跳转到BIOS起点0XFE05B

BIOS启动,硬件自检与初始化,读取主引导扇区代码到0X7C00处

初始化各种控制寄存器

激活A20地址线控制位,切换保护模式:

等待8042输入缓冲为空

发送写指令

等待8042输入缓冲为空

发送写入的内容

    # Switch from real to protected mode, using a bootstrap GDT
# and segment translation that makes virtual addresses
# identical to physical addresses, so that the
# effective memory map does not change during the switch.
lgdt gdtdesc
movl %cr0, %eax
orl $CR0_PE_ON, %eax
movl %eax, %cr0

GDT信息送入GTDR(高32位:基址 低16位:段界限),然后让CR0寄存保护模式使能位

这里引用一下GDT生成的代码:

	# Bootstrap GDT
.p2align 2 # force 4 byte alignment
gdt:
SEG_NULLASM # null seg
SEG_ASM(STA_X|STA_R, 0x0, 0xffffffff) # code seg for bootloader and kernel
SEG_ASM(STA_W, 0x0, 0xffffffff) # data seg for bootloader and kernel gdtdesc:
.word 0x17 # sizeof(gdt) - 1
.long gdt # address gdt

此时生成的GDT为

{

0: 空段

1: 代码段:base=0x0,limit=ffffffff,即4G内存处,属性可执行可读

2: 数据段:base=0x0,limit=ffffffff,即4G内存处,属性可写

}

搞明白GDT啥样后再回来往下看

    # Jump to next instruction, but in 32-bit code segment.
# Switches processor into 32-bit mode.
ljmp $PROT_MODE_CSEG, $protcseg

注意由于此时已经开启段机制,不能再按\(段基址<<4+偏移\)的方式计算ljmp的位置.现在的PROTMODECSEG为段选择子,protcseg为段偏移

现分析PROTMODECSEG为何取0x8

把PORTMODECSEG按二进制写出

000....00001000 (16bit)

拆分得RPL=0,TI=0,index=1,即对应GDT中的1号段描述符,基址为0,界限为4G

根据段机制下的公式 :

段描述符.基址 + 段偏移 = 线性地址 = 物理地址

因此ljmp最终指向的位置即是protcseg的值

同样我们可以分析出PROT_MODE_DSEG对应GDT的2号位置

接下来几行将各个段寄存器赋值为PROT_MODE_DSEG,将ESP置0,将ESP赋值为$start,即0x7c00,第一条指令处.

因为栈是从高内存往低内存增长,且bootloader代码都在0x7c00后,所以0x7c00前面的空间就暂时留给栈了

最后调用bootmain函数

4. bootmain.c分析:

先从磁盘开始处读取了1页(8个扇区,每个512byte)的数据到内存64K处,再校验头部标识符是否合法.

接着从磁盘中读取每个程序段,并放到虚拟内存对应位置.

最后执行ELF入口程序,将控制权交给kernel

5. 实现函数调用堆栈跟踪函数

考察的是对EBP寄存器的运用.几乎所有本地编译器都会在每个函数体之前插入类似如下的汇编指令:

pushl %ebp
movl %esp,%ebp

这时候EBP的位置就很重要了.EBP总是指向上次压栈的EBP,EBP+4处为上次调用的返回地址,即CALLER的EIP值,EPB+8处为参数1,EPB-4处为局部变量.这些特性就使得可以不断回溯EBP的值来或取函数的调用栈.

| 栈底方向	| 高位地址
| ... |
| 参数3 |
| 参数2 |
| 参数1 |
| 返回地址 |
| 上一层[ebp]| <-------- [ebp]
| 局部变量1 |
| 局部变量2 |
| |低位地址

这题虽然噱头看着挺大,但等着找到填空的地方一看提示都把流程写明了.最大的坑就在于处理指针时括号的使用

int a;
(uint32 *)a+1==(uint32 *)(a+sizeof(int))==(uint32 *)(a+4)

7. 扩展练习 Challenge 1

增加一用户态函数(可执行一特定系统调用:获得时钟计数值),当内核初始完毕后,可从内核态返回到用户态的函数,而用户态的函数又通过系统调用得到内核态的服务

这一部分是真的难,看完LAB2实践课里的讲解再回来理了半天才完全看懂答案的思路.

先捋一遍中断的处理流程:

汇编指令int触发中断,CPU把错误码和中断编号压栈,然后去vector.s里找对应中断例程的入口地址,再调用trapentry.S的__alltraps,在这里各类段寄存器,双字节寄存器,ESP被按顺序压栈,再调用trap.c里的trap函数,进而调用trap_dispatch函数进行中断处理.中断处理完后再把整个过程到着来一遍恢复原状.

再来说说内核态到用户态的转换:会从内核栈切换到用户栈,改变段寄存器特权级,并添加ESP和SS

内核栈:             用户栈:(段寄存器的特权级改变)
|栈顶地址 | | |
|各种寄存器|<-栈顶 |各种寄存器|<-栈顶
|..... | |..... |
|err | |err |
|eip | |eip |
|cs | |cs |
|eflags | |eflags |
| | |esp(新加 |(指向内核栈EFLAGS前面的地址)
| | |ss(新加 |

用户态到内核态的转换:产生中断时自动切换到内核栈,即在内核栈内进行操作,改变段寄存器的特权级,去除ESP和SS

内核栈:             内核栈
|栈顶地址 | |栈顶地址 |
|各种寄存器|<-栈顶 |各种寄存器|<-栈顶
|..... | |..... |
|err | |err |
|eip | |eip |
|cs | |cs |
|eflags | |eflags |
|esp(用户程序栈顶) | |
|ss | | |

我最终的实现方法与答案略有不同,这里讲一下

lab1_switch_to_user(void) {
asm volatile ("int %0"::"i"(T_SWITCH_TOU));
//触发转换到用户态的中断
}
lab1_switch_to_kernel(void) {
asm volatile ("int %0"::"i"(T_SWITCH_TOK));
//触发转换到内核态的中断
}

在trap.c中定义一个trapframe全局变量atrapframe做临时栈使用,并在trap_dispatch中完成内核态到用户态的转换

case T_SWITCH_TOU:
if(tf->tf_cs!=USER_CS){ //当前已经为用户态时跳过
atrapfream=*tf; //把中断帧的值赋给临时栈
atrapfream.tf_cs=USER_CS;//更改代码段
atrapfream.tf_ds=atrapfream.tf_es=atrapfream.tf_ss=USER_DS;//更改数据段
atrapfream.tf_esp=(uint32_t)tf+sizeof(struct trapframe)-8;//更改ESP
atrapfream.tf_eflags|=FL_IOPL_MASK;//更改EFLAGS,不然在转换时会发生IO权限异常
*((uint32_t*)tf-1)=&atrapfream;//因为从内核栈切换到用户栈,所以修改栈顶地址
}
break;
case T_SWITCH_TOK:
if(tf->tf_cs!=KERNEL_CS){ //当前已经为内核态时跳过
atrapfream=*tf; //把中断帧的值赋给临时栈
atrapfream.tf_cs=KERNEL_CS; //更改代码段
atrapfream.tf_ds=atrapfream.tf_es=KERNEL_DS; //更改数据段,这次没改SS
atrapfream.tf_eflags&=~FL_IOPL_MASK; //更改ESP
int offset=tf->tf_esp-(sizeof(struct trapframe)-8); //修改后少了ESP和SS,故需要偏移
__memmove(offset,&atrapfream,sizeof(struct trapframe)-8); //把修改好的栈移到目标位置
*((uint32_t*)tf-1)=offset; //重设栈顶地址
}
break;

注意此时我们要在用户态下调用T_SWITCH_TOK部分,所以要在创建IDT里把对应的访问权限设置为USER

SETGATE(idt[T_SWITCH_TOK], 0, GD_KTEXT, __vectors[T_SWITCH_TOK], DPL_USER);

8. 扩展练习 Challenge 2

用键盘实现用户模式内核模式切换。具体目标是:“键盘输入3时切换到用户模式,键盘输入0

时切换到内核模式”。

这就简单了,直接在时间中断处理里加个if-else再把刚写的内写糊上去就行

case IRQ_OFFSET + IRQ_KBD:
c = cons_getc();
cprintf("kbd [%03d] %c\n", c, c);
if(c=='0'){
if(tf->tf_cs!=KERNEL_CS){
cprintf("+++ switch to kernel mode +++\n");
atrapfream=*tf;
atrapfream.tf_cs=KERNEL_CS;
atrapfream.tf_ds=atrapfream.tf_es=KERNEL_DS;
atrapfream.tf_eflags&=~FL_IOPL_MASK;
int offset=tf->tf_esp-(sizeof(struct trapframe)-8);
__memmove(offset,&atrapfream,sizeof(struct trapframe)-8);
*((uint32_t*)tf-1)=offset;
}
}
else if(c=='3'){
if(tf->tf_cs!=USER_CS){
cprintf("+++ switch to user mode +++\n");
atrapfream=*tf;
atrapfream.tf_cs=USER_CS;
atrapfream.tf_ds=atrapfream.tf_es=atrapfream.tf_ss=USER_DS;
atrapfream.tf_esp=(uint32_t)tf+sizeof(struct trapframe)-8;
atrapfream.tf_eflags|=FL_IOPL_MASK;
*((uint32_t*)tf-1)=&atrapfream;
}
}
break;

ucore lab1 操作系统启动过程 学习笔记的更多相关文章

  1. PHP操作XML文件学习笔记

    原文:PHP操作XML文件学习笔记 XML文件属于标签语言,可以通过自定义标签存储数据,其主要作用也是作为存储数据. 对于XML的操作包括遍历,生成,修改,删除等其他类似的操作.PHP对于XML的操作 ...

  2. 基于STM32的USB枚举过程学习笔记

    源:基于STM32的USB枚举过程学习笔记 基于STM32的USB枚举过程学习笔记(一) 基于STM32的USB枚举过程学习笔记(二) 基于STM32的USB枚举过程学习笔记(三) 基于STM32的U ...

  3. 20135202闫佳歆--week 8 进程的切换和系统的一般执行过程--学习笔记

    此为个人笔记存档 week 8 进程的切换和系统的一般执行过程 一.进程调度与进程切换 1.不同的进程有不同的调度需求 第一种分类: I/O密集型(I/O-bound) 频繁的进行I/O 通常会花费很 ...

  4. 【原】无脑操作:ElasticSearch学习笔记(01)

    开篇来自于经典的“保安的哲学三问”(你是谁,在哪儿,要干嘛) 问题一.ElasticSearch是什么?有什么用处? 答:截至2018年12月28日,从ElasticSearch官网(https:// ...

  5. Linux与Windows xp操作系统启动过程

    Linux启动过程: 第一步,加载BIOS,当你打开计算机电源,计算机会首先加载BIOS信息,BIOS信息是如此的重要,以至于计算机必须在最开始就找到它.这是因为BIOS中包含了CPU的相关信息.设备 ...

  6. web进阶之jQuery操作DOM元素&&MySQL记录操作&&PHP面向对象学习笔记

    hi 保持学习数量和质量 1.jQuery操作DOM元素 ----使用attr()方法控制元素的属性 attr()方法的作用是设置或者返回元素的属性,其中attr(属性名)格式是获取元素属性名的值,a ...

  7. 基于STM32的USB枚举过程学习笔记(转)

    之前使用ST官方的库以及网络的资料,完成了使用USB HID类进行STM32和PC机的通讯.由于其他原因并没有深入的分析,虽然实现了功能,但是关于USB设备的枚举,以及具体的通讯方式都没有清晰的概念, ...

  8. oracle储存过程学习笔记

    转载至: https://www.2cto.com/database/201610/559389.htm 1.什么是oracle存储过程 存储过程和函数也是一种PL/SQL块,是存入数据库的PL/SQ ...

  9. Android APK安装过程学习笔记

    1.什么是APK APK,即Android Package,Android安装包.不同平台的安装文件格式都不同,类似于Windows的安装包是二进制的exe格式,Mac的安装包是dmg格式.APK可以 ...

随机推荐

  1. 为什么要使用 kafka,为什么要使用消息队列?

    缓冲和削峰:上游数据时有突发流量,下游可能扛不住,或者下游没有足够多的机器来保证冗余,kafka在中间可以起到一个缓冲的作用,把消息暂存在kafka中,下游服务就可以按照自己的节奏进行慢慢处理. 解耦 ...

  2. spring-boot-learning-事务处理

     事务处理的重要性: 面对高井发场景, 掌握数据库事务机制是至关重要的,它能够帮助我们在一定程度上保证数据的一致性,并且有效提高系统性能,避免系统产生岩机,这对于互联网企业应用的成败是至关重要的. 以 ...

  3. java-面向对象相关

    public class DemoMethodOverload { public static void main(String[] args) { int[] array = new int[]{1 ...

  4. Spring 支持的 ORM?

    Spring 支持以下 ORM:HibernateiBatisJPA (Java Persistence API)TopLinkJDO (Java Data Objects)OJB

  5. 创建Maven web工程

    ---恢复内容开始--- 第一步,启动Eclipse,依次打开菜单[File][New][Other] 找到目录Maven,选择Maven Project, 选择一个Archetype.这里创建Web ...

  6. Mybatis 开发 dao 的方法

    1.分析SqlSession使用范围 1.1.SqlSessionFactoryBuilder 通过 SqlSessionFactoryBuilder 创建会话工厂 SqlSessionFactory ...

  7. 如何设置出IDEA中VCS下的Enable Version Control Intergration

    File–>settings–>Version Control

  8. Citus 分布式 PostgreSQL 集群 - SQL Reference(手动查询传播)

    手动查询传播 当用户发出查询时,Citus coordinator 将其划分为更小的查询片段,其中每个查询片段可以在工作分片上独立运行.这允许 Citus 将每个查询分布在集群中. 但是,将查询划分为 ...

  9. ROS的安装-> rosdep init /update报错2022.02.24实测有效

    ROS的安装-> rosdep init /update报错2022.02.24实测有效   一. 解决rosdep_init问题 正常执行sudo rosdep init会报错,如下: ERR ...

  10. windows本地搭建easy-mock环境

    起因:由于easy-mock官网很不稳定,所以想搭建自己本地的mock环境 1.首先安装node.js 环境 (提供地址:https://nodejs.org/en/) 2.下载mongoDB 地址( ...