• 操作系统学习笔记1 | 初识操作系统


    本部分主要记录了计算机开机过程中操作系统的工作流程,并以此理解操作系统的代码结构。

    参考资料:


    1. 什么是操作系统

    操作系统是计算机硬件和应用软件之间的一层软件,方便我们使用硬件(比如显存)、高效地使用硬件(如打开多个终端和窗口):

    管理的硬件:

    • CPU管理、内存管理
    • 终端管理、磁盘管理
    • 文件管理、网络管理
    • 电源管理、多核管理

    而组成一个操作系统最基本的是前五个。

    学习操作系统可以有很多层次:

    大部分人停留在第一层,即使用操作系统的接口。而计算机专业学生应当能够掌控计算机系统,真正理解操作系统的工作原理。

    2. 计算机工作原理

    探讨一个问题。打开计算机电源后,计算机的开机过程中发生了什么?

    这也是实验一的内容。

    要了解这个问题,首先要了解计算机的工作原理。

    计算机是如何工作的?

    首先是 图灵机。之前做过记录:计算机系统3-> 现代计算机基石 | 图灵机理论 - climerecho - 博客园 (cnblogs.com)

    但是这样的图灵机还是太菜啦,一个图灵机只能做特定的一件事(因为控制逻辑是写死了的)

    通用图灵机 可以看碟下菜,成为大厨。纸带上对控制器的控制逻辑进行编码,而控制器识别这样的编码,就能够完成我们需要执行的操作。

    通用图灵机的功能就已经很像一个应用程序(程序)了。

    接下来的冯诺依曼 存储程序 思想,将程序存入内存,按照需求将程序载入CPU(上图中的控制器)进行解释执行。

    经典的 “取值执行”。

    这样一个计算机就算搭建完成了,就像是大厨能够按照客人需求选择菜谱进行烹饪。

    3. 开机过程理解

    再回到开机过程的理解,计算机的工作归结于 “取指执行”,而所有的程序(包括操作系统),在开机前都放在磁盘上,如何取指执行呢?

    • 打开电源,计算机执行的第一句指令是什么?即第一条指令对应的PC寄存器里的地址是多少?

    以×86 PC 为例,

    • 刚开机,会执行 BIOS 中的代码

      BIOS,ROM BIOS映射区,是Basic Input Output System 的缩写。意思是计算机的内存里总要有一个基本的输入输出程序,否则内存空白一片,就无法开启冯诺依曼的"取值执行"。

    • 而这段代码固化在 0xFFFF0 处

    • 开机时,CS = 0xFFFF, IP= 0x0000

      和保护模式对应,实模式的寻址CS:IP(CS左移4位+IP),这样 CS << 4 + IP 就正好是 0xFFFF0,正是内存刚上电时唯一有代码的地方,接着进行取值执行。

    • 这段代码主要用于 检查 RAM,键盘,显示器,软硬磁盘

      如果这段代码过不去,表示硬件出问题了。

    • 将磁盘0磁道0扇区读入内存0x7c00

      1扇区 512字节

      0磁道0扇区就正是操作系统的引导扇区,这个扇区中存放操作系统的第一段代码

      开机时按住相关热键(不同设备不同)即可进入启动设备设置界面(俗称BIOS界面),可以设置为光盘启动,也可以从U盘等设备进入某个操作系统。

      启动时设备信息被设置在 CMOS 中,CMOS是互补金属氧化物半导体64~128B,用来存储实时钟和硬件配置信息。

    • 设置 CS=0x07c0, ip=0x0000,进行实模式寻址后跳到引导扇区的代码执行。

      引导扇区的代码做了什么事情呢?

    4. bootsect.s 代码理解

    bootsect.s 就是上面所说的引导扇区的代码,是汇编代码。

    因为高级语言(如C)无法具体指定硬件,特别是内存位置;而汇编则可以对硬件进行完整的控制。

    这段代码经过汇编后得到机器代码,放在引导扇区。

    划重点:所以bootsect.s的起始位置是0x7c00。后续理解会用到。

    首先是固化的bootsect,需要把后续的代码引导出来。

    mov ax, #BOOTSEG 
    mov ds, ax
    # 将ds置为 07c0
    mov ax, #INITSEG
    mov es, ax
    # 将es置为 9000
    # 这是两个段寄存器,还需要偏移才能寻址
    mov cx, #256
    
    sub si, si 
    sub di, di
    # 加入偏移,偏移通过自减产生,为0
    # 根据上面提到过的实模式的CS:IP寻址
    # 此时ds:si=7c00,es:di= 90000
    
    rep movw
    # rep:重复执行,直到cx=0
    # 意思是移动字,一共移动256个字(cx处有说明,也正好是512字节)
    # movw: 将DS:SI内容复制到ES:DI中即从7c00
    # DS和ES一个是源数据段寄存器,另一个是目的数据段寄存器
    jmpi go, INITSEG
    # 段间跳转指令,cs=INITSEG,IP=go
    # go是一个标记,替代一个具体的地址,编译后就会分配到我们指定的地址
    # 这一点具体计算机组成原理有提到过,但我还没整理出来,就是从汇编代码起始的地址,到这个标号处,标号标记了此处的地址。比如说到go这个标签处,go是200地址
    # INITSEG 上面提到过,是0x9000
    # 这样根据寻址,90000+200
    # bootsect.s现在就挪到了 900200,在这里相当于顺序向下执行。
    # 但是必须写这句话,因为代码在那个地方。
    
    

    这段代码要决定接下来setup的读入情况。

    # 接下来的代码略讲,看一些重点的
    #
    # 这段代码是用于读入setup区的(分区图见上图)
    go:mov ax,cs  
       #cs是0x9000
       mov es,ax
       mov ss,ax
       mov sp, #0xff00
    # 为call准备(具体后面会使用这一块的内容)
    
    load_setup:
    	mov dx,#0x0000
    	mov cx,#0x0002
    	mov bx,#0x0200
    	
    	mov ax,#0x0200+SETUPLEN
    	
    	# 0x13是BIOS读磁盘扇区的中断:ah=0x02-读磁盘,al=扇区数量(SETUPLEN=4) ch=柱面号,dh=磁头号,dl=驱动器号,es:bx=内存地址
    	
    	int 0x13
    	# 现在只是读入了引导扇区用十三号中断读入操作系统其他的内容
    	# 需要知道从哪里读:cl开始扇区,即mov cx,#0x0002,读取cx的低8位是2。
    	# 理解一下,boot扇区占了第1个扇区,所以从第2个扇区读。
    	# 需要知道读多少扇区:ax,0x0200,高八位作为ah,低八位作为al
    	# 所以是从第二个扇区开始读4个扇区
    	# 需要直到读到哪里,es:bx告知读到哪里
    	# 从go标签处得知,cs赋值给了ax,ax赋值给了es,cs是0x9000,而bx是0200
    	# 所以基址是0x9000,偏移是0x0200.意思就是把setup的四个扇区读进来
    	jnc ok_load_setup
    	mov dx,#0x0000
    	mov ax,#0x0000
    	int 0x13
    	j load_setup
    
    

    ok_load_setup:
    	mov dl,#0x00
    	mov ax,#0x0800
    	
    	int 0x13
    	mov ch,#0x00
    	mov sectors,cx
    	
    	mov ah,#0x03
    	xor bh,bh
    	int ox10
    	# 这句是这段代码的关键,进行BIOS的10号中断,是一个显示字符的BIOS中断,用于在屏幕上输出。
    	# 具体参数不再介绍,回头单独介绍吧。
    	
    	mov cx,#24
    	# 显示的字符数为24
    	mov bx,#0x0007
    	# 7是显示属性
    	
    	mov bp,#msg1
    	#msg1是用于显示的内容所在的内存地址,见下面的data段
    	# 意思就是把下面的msg1段显示到光标位置
    	# Windows开机时的logo就是这一段代码的作用(好看了一些)
    	
    	mov ax,#1301
    	int 0x10
    	
    	mov ax,#SYSSEG
    	mov es,ax
    	call read_it
    	# 读入 system 模块
    	jmpi 0,SETUPSEG
    	# 转入0x9020:0x0000,接下来执行setup.s
    

    bootsect.s 中的data段/数据:

    sectors: .word 0
    msg1:.byte 13,10
         .asscii "Loading system"
         .byte 13,10,13,10
         
    # 根据这一段就可以修改开机显示的内容,比如改为:"CliviaOS is loading"
    # 不过要记得修改显示的字符长度,在上面的mov cx,#24的地方修改一下
    # 修改之后这个系统重新编译,再开机就可以看到更改效果
    
    

    这也是实验二的内容,回头会把实验二整理出来。

    下面读入 system 模块。

    read_it: mov ax,es
    		 cmp ax,#ENDSEG
    		 #ENDSEG=SYSSEG+SYSSIZE,
    		 #SYSSIZE=0x8000,这个变量可以根据image的大小设定(编译操作系统的时候)
    		 jb ok1_read
    		 ret
    
    ok1_read:
    	mov ax,sectors
    	sub ax,sread
    	# sread是当前磁道已读扇区数,ax是未读扇区数
    	call read_track
    
    • 值得注意的是,除了函数read_it,读入 system 模块为什么还要定义一个函数ok1_read

      因为 system 模块可能很大,要跨越磁道,所以要处理这个问题。

    • 在引导扇区的末尾,BIOS需要这段代码识别引导扇区

      .org 510
      	.word oxAA55 
      #扇区的最后两个字节,否则会打出非引导设备
      
    • 接下来需要将控制权交给 setup.s,怎么交接呢?

      使用跳转,即修改PC;

      setup模块放在 0x90200 处,所以cs=9020,ip=0

      SETUPSEG在上面的图中就正是9020

      这样就实现了跳转

      jmpi 0,SETUPSEG
      # ip=0,cs=SETUPSEG,
      

    综上,开机的图标背后做了什么事情,大概上理解一下:

    • 打出Logo
    • 把 setup 和system 区的代码读进来。
    • 一些别的事情...

    5. bootsect.s 补充解释

    其实不同系统不同版本的bootsect.s都会有差别,所以上面的代码不必死记,但是这是我接触到的与操作系统相关的第一段代码,所以认真整理了下。

    摘自一些我觉得有用的弹幕。

    可以参考《Linux内核完全注释V3.0》203页,有详细注释。

    老师没讲但很重要的几个点结合linux0.11 和教材解释一下

    1. 为什么要从07c0 转移到90000:因为如果不转移,system 数据从10000-90000转移到00000的时候会覆盖07c0的程序,司令命令部队把自己司令部给趟平了,所以提前搬走

      这一点看了视频L3以及下面setup.s的代码理解就可以明白。

    2. 为什么system 数据要开始放在10000,因为bios 中断程序在00000开始存放,把system从磁盘读到内存之后的命令还是要用bios的中断的,所以要移动

    3. bios的终端每次启动都会又BIO rom的程序初始化一次,不用担心下次启动的时候没有

    6. setup.s 代码理解

    setup模块依然是setup.s汇编代码,依然对启动过程进行精细控制,相当于编程中的初始化,底层硬件的参数初始化操作系统。

    6.1 初始化操作系统并移动

    start:mov ax,#INITSEG
    	  mov ds,ax
    	  mov ah,#0x03
    	  
    	  xor bh,bh
    	  int 0x10
    	  mov [0],dx
    	  
    	  mov ah,#0x88
    	  int 0x15
    	  # 本段代码重点,是一个BIOS中断,获取物理内存的大小
    	  # 使用#0x88作为参数,获取的值放入ax中,ax 赋给 [2]
    	  mov [2],ax
    	  # 这是间接寻址,段寄存器左移 4 位再 +2,即0x90002
    	  # 而段寄存器现在指向9000
    	  # 将 ax 中内容传递至内存地址 ds:[2] 处 即 0x90002 处, 
    	  # ax 中保存的值为调用 int15 中断后获取的扩展内存大小
    	  # 操作系统是要管理内存的,所以有必要知道内存的大小。
    	  # 这就是setup的意义,要让操作系统知道计算机底层的模样。
    	  # 操作系统会形成很多数据结构来管理上图表格中这些参数。
    	  cli 
    	  # 不允许中断
    	  
    	  mov ax,#0x0000
    	  cld
    
    #########################重点提醒###########################
    # 下面还是做一个移动
    # 移动system模块到0x0000的位置,共计0x8000的地址空间,将来操作系统的代码将一直放在这个位置
    # 此前 5.bootsect.s 补充解释中提到的也是这里
    # 回顾前一小节中,bootsect代码首先会将自身从0x07c0:0x0000处移动到0x9000:0x0000处,接下来读入的setup模块也紧跟在移动后的bootsect代码后,这么做就是为了给此时将system放在0x0000~0x8000腾出空间
    
    #########################重点提醒###########################
    
    
    do_move:mov es,ax
    		#ax=0,赋值给了es
    		add ax,#0x1000
    		cmp ax,#0x9000
    		jz end_move
    		mov ds,ax
    		sub di,di
    		sub si,si
    		mov cx,#0x8000
    		rep
    		movsw
    		jmp do_move
    		
    # 此后,内存从0开始的地址存放的都是操作系统,在此之上的是应用程序。	
    

    扩展内存:

    扩展内存是 ram 中高于 1MB 的部分。Intel 刚出来的时候是1MB,后来把大于这部分的内存都称为扩展内存。

    拓展阅读:《Linux0.11内核剖析》,能够对操作系统的全貌有所了解。

    setup.s 到此做了两件事,1 是把操作系统进行挪动,2 是初始化操作系统,使其能够管理底层硬件。

    6.2 进入保护模式

    接下来,操作系统应当继续向下执行,setup 还做了一件重要的事:

    • 进入保护模式(此前是实模式)

    call empty_8042
    mov al,#0xD1
    out #0x64,a1
    
    call empty_8042
    mov a1,#0xDF
    out #ox60,a1
    mov ax,#0x0001
    mov cr0,ax
    jmpi 0,8
     
    
    empty_8042:
      .word 0x00eb,0x00eb
      in al,#0x64
      test al,#2
      jnz empty_8042
      jnz empty_8042
      ret
    

    来看这一句:

    jmpi 0,8
    
    • 这句代码是重点,也是操作系统中的理解重点
    • 如果还按照上面的寻址模式,则会跳转到0x80,属于system模块,会死机
    • 所以实际上并不是上面的寻址模式,cs<<4+ip,cs和ip都是16位寄存器,这种寻址最多只能达到20位地址 = 1M,不适用动辄内存4G的计算机。
    • 所以需要从16/20位(1M)切换到32位(4G),后者就是保护模式
    • 那么如何做到这种切换呢?即切换寻址模式,切换CPU的解释方式,切换为另一条电路?

    这两句:

    mov ax,#0x0001
    mov cr0,ax
    
    • 提到了一个寄存器cr0,这个寄存器的最后一位PE,如果是0,则为实模式;如果是1,则为保护模式。

    • 可见,这两句代码就是把末位赋1,走了另一条电路

    • 而这个电路如何解释执行,涉及一个著名概念 gdt.

      这个功能由硬件实现,目的就是快。

    • cs此时被称为 选择子(selector) ,存放查表的索引,真正的地址放在表项里。

      cs=8是要选择表中的项,再从表中取出基址,与ip相加得到地址,这时得到的就是32位地址。

      这个表就是著名的 gdt表(global description table)

    • 表中的内容从何而来呢?是由setup.s来做的。

    #这段代码简单讲
    end_move:mov ax,#SETUPSEG
    		 mov ds,ax
    		 lidt idt_48
    		 lgdt gdt 48
    		 
    idt_48:.word 0 
    	   .word 0,0
    gdt_48:.word 0x800
    	   .word 512+gdt,0x9
    	   
    ## 这一段就是初始化表
    gdt:.word 0,0,0,0
    	# 注意,一个word16位,所以一行作为一个表项就是64位
    	.word 0x07FF,0x0000,0x9A00,0x00C0
    	# 寻址的时候以字节做索引,1个字节8位,所以cs=8就是这个第二行起始处
    	.word ox07FF,0x0000,0x9200,0x00C0
    

    通过上面代码几个指令的组合(相对固定),就能得到这个gdt表,再结合上面那一段更改寻址方式的指令,就能够实现jmpi 0,8

    现在来谈中断,中断处理也与上面类似。

    也是以上面的方式去寻找中断函数的入口地址。

    这一点再下一部分操作系统接口会再提到。

    6.3 gdt 查表方式理解

    上面我们得知,jmpi 0,8使用gdt 查表,查到的是下面代码中的第2行word,而怎么解释这个表项则是由硬件规定的。

    .word的四个地址是如何体现在GDT表项中的呢?

    • 注意看GDT表项的四个段标注
      • 段基址31...24对应0x00C0的高位00
      • 段基址23...16对应0x9A00的低位00
      • 段基址15...0对应0x0000
      • 段限长15...0对应0x07FF
    • 合起来,段基址就是全零。
    • 所以这个表项的意思是,jmp 到内存0x0000处,接下来去0地址处执行,也就是前面移动过的system模块。

    也就是大端寻址。

    这里存放的不连续是因为硬件设计的历史原因。

    前面提到过的bootsect模块和setup模块都是由其相应的.s文件编译过来的,而system模块一定有很多文件,我们要保证接下来进行的是system的第一段代码;也就是head.s

    • 操作系统的代码最后必须是:boot、setup、system这样的过程
    • 这些过程的严丝合缝,才能保证操作系统顺利开机,否则就死机了。

    所以我们要编写编译操作系统的控制代码——Makefile

    6.4 Makefile

    Makefile可以控制最终生成的代码的组织结构,然后按照前述的顺序放在硬盘的前面几个扇区中。

    我们通常把操作系统编译后的样子称为 image,image 中就是上面所说的代码结构,指定放在0磁道0扇区。

    • Makefile是一种树状结构

    • image 是依赖于上图中的 boot/bootsect tools/system tools/build ... 产生

      相当于父节点依赖于子节点,每个子结点完成了,最终整个树才能建立。

    • 而image 的这些子节点还会依赖于它的子节点,比如tools/system依赖于boot/head.o init/main,o $(DRIVERS) 等等

    • 上面提到过的这些子节点还会依赖于 head.s等文件。

    • 当所有子节点完成后,通过(LD)boot/head.o init/main.o $DRIVERS ... -o tools/system 链接起来,来构建父节点

    • 再向上创建 image

      使用build,具体参见 Linux 源码

    • 而在整个树中,head.s 是第一个。

      这就达到了目的。

    1. 每个子节点的依赖关系在下方会像 tools/system这样写出来,通过这种书写方式建立一整个树。

    2. 数据结构的后根遍历。

    3. system模块的第一部分代码是head.s,head.s执行完后再执行main.c

    7. head.s 代码理解

    • 再次初始化IDT和GDT表

      之前的IDT 和 GDT 被建立起来只是为了临时完成jmpi 0,8 这条指令,而之后操作系统要开始真正工作。

    • 其他如开启20号地址线 ,就不再探讨细节。

    • 上图中,head.s的代码和之前看到的bootsect.s和setup的代码不太一样,多了很多%eax,而不再是ax。

      这是因为head.s是运行在保护模式(32位模式)下的,是32位的汇编代码,而bootsect.s和setup的代码是16位的汇编代码。

      image.png

    1. as86汇编:16位的Intel 8086汇编
    2. GNU as汇编:产生32位代码,采用 AT&T系统V语法。
    3. 另外在c代码中,可以内嵌汇编,达到精细控制的目的,这又是另外一种汇编

    接下来会跳转到 main.c。从汇编跳到C,如何做到?

    image.png

    after_page_tables:
    	#压栈
    	push1 $0
    	push1 $0
    	push1 $0
    	push1 $L6
    	push1 $_main
    	#压栈结束后跳转到set_paging
    	jmp set_paging
    	
    L6: jmp L6
    setup_paging:#设置页表代码#
    			 ret
    ## 设置页面setup_paging的具体代码这里省略,后面再讲
    
    • 可见head.s也是在压栈,所以从head.s跳转到main.c实际上很简单,就是把参数和main.c的地址压入栈中
    • 在设置页表的代码(即setup_paging模块)执行完后,会执行 ret返回指令,那么就把栈中 main.c 的地址作为返回地址,达到了跳转到main.c的效果
    • 接下来就是执行上面压栈的内容,即执行main.c。
    • 如果main.c执行结束,会跳转到L6,L6是一个死循环;实际上,正常情况下,main.c就会一直运行下去,不会执行结束,如果main.c结束了,就会跳到L6,表现的结果就是计算机死机了

    总结一下head.s功能:

    1. 初始化idt以及gdt,表示用各种数据结构管理硬件参数
    2. 向后交接给main.c

    8. main.c 代码理解

    下面就开始C语言程序了。

    image.png

    • 传给main函数的三个形参(上一部分的p1~p3),分别是envp、argv、argc,但是main函数并没有使用;main如果返回,就会跳转到L6,但操作系统正常情况下一直在工作,永远不会退出;
    • main的工作就是xx_init:内存、中断、设备、时钟、CPU等内容的初始化,这里就可以看到熟悉的chr_dev_init()tty_init()等等

    init 函数举例mem_init()函数:

    • mem_init()就是内存的初始化,如下图

    image.png

    • 按照 4K 为单位对内存进行划分区域(页),mem_map数组是表示内存区域是否被使用的一个表格;

      2的12次方也就是4K,这就是 的初始化

    • end_mem其实就是总内存大小,那这个参数是从哪里来的呢?我们之前讲setup的时候,说了会读取内存大小放在0x90002的位置,就是从这里来的。

      妙蛙。

    • 下面的代码,首先将mem_map全部初始化为USED,然后将start_mem到end_mem之间的内存区域设置为0,即未被使用

    • mem_map前面的部分是USED,这是操作系统代码和一些管理硬件的数据结构所占用的内存

    9. 总结

    • 前面代码部分,分析了bootsect.s、setup、head.s、main.c、mem_init()

      bootsect.s 将操作系统从磁盘读入,setup.s 获得参数,启动保护模式,head.s 初始化页表,main.c 初始化硬件管理器。

    • 笼统的说,这些步骤,就是做了两件事情

      1. 把操作系统代码读到内存中,读到内存中,CPU才可以取指执行
      2. 初始化工作,准备一些用于管理硬件设备的数据结构
    • 后面我们还会回过头来看这里准备的这些数据结构

  • 相关阅读:
    vue中路由传参
    Session使用细节 [JavaWeb][Servlet]
    废水除氟技术,高盐废水除氟有什么好的方法?
    python环境迁移:从联网笔记本到离线服务器
    基于bootstrap,企业内部管理系统后台框架前端源码,响应式布局
    利用Landsat8 TIRS反演地表温度实例
    [数据集][目标检测]野生动物检测数据集VOC+YOLO格式1054张4类别
    C++版本的OpenCV实现二维图像的卷积定理(通过傅里叶变换实现二维图像的卷积过程,附代码!!)
    【计算机图形学】光线追踪 Ray Tracing
    chrome Driver 使用教程
  • 原文地址:https://www.cnblogs.com/Roboduster/p/16582703.html