开头赞美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. 什么是 NetflixFeign?它的优点是什么?

    Feign 是受到 Retrofit,JAXRS-2.0 和 WebSocket 启发的 java 客户端联编程序.Feign 的第一个目标是将约束分母的复杂性统一到 http apis,而不考虑其稳定 ...

  2. consumer 是推还是拉?

    Kafka 最初考虑的问题是,customer 应该从 brokes 拉取消息还是 brokers 将消 息推送到 consumer,也就是 pull 还 push.在这方面,Kafka 遵循了一种大 ...

  3. vue单文件组件data选项的函数体获取vue实例对象

    因配置的关系,导致 vue的data选项中存在事件.而事件无法获取 vue 的实例对象:项目是单文件形式的,以下代码只是例子 new Vue({ el:..., data:{ a: { onevent ...

  4. Effective Java —— 多字段下考虑使用建造者模式构建实例

    本文参考 本篇文章参考自<Effective Java>第三版第二条"Consider a builder when faced with many constructor pa ...

  5. Thoughtworks Technology Radar #26 技术雷达26期

    Thoughtworks Technology Radar #26 Techniques Adopt Four key metrics Google Cloud's DevOps Research a ...

  6. 从零开始画自己的DAG作业依赖图(四)--节点连线优化版

    概述 上个版本简单的连线在一些复杂场景,尤其层级比较多,连线跨层级比较多的情况下,会出现线条会穿过矩形的情况,这一讲就是在这个基础上,去优化这个连线. 场景分析 在下面几种情况下,简单版本的画法已经没 ...

  7. Mpvue 小程序转 Web 实践总结

    介绍 Mpvue 是一个使用 Vue.js 开发小程序的前端框架.框架基于 Vue.js 核心,修改了 Vue.js 的 runtime 和 compiler 实现,使其可以运行在小程序环境中,从而为 ...

  8. sqlite的Query方法操作和参数详解

    query()方法实际上是把select语句拆分成了若干个组成部分,然后作为方法的输入参数: SQLiteDatabase db = databaseHelper.getWritableDatabas ...

  9. 前端面试题整理——HTML/CSS

    如何理解语义化: 对应的内容是用相应意思的标签,增加开发者和机器爬虫对代码的可读性. 块状元素和内联元素: 块状元素有:display:block/table:有div h1 h2 table ul  ...

  10. java中finally有什么意义呢,在现实中?举例

    马克-to-win: finally有什么意义呢,在现实中?比如你开了一个流处理文件,可能没开成功,或开成功了,但后面的操作失败了,但不管你怎么样,你必须在一个地儿把它关闭,那就是finally块儿. ...