《操作系统真象还原》读书笔记 第0章

0x1 软件访问硬件的方法

软硬件之间的访问是依赖于各种硬件设备,也就是IO接口。接口就是生产硬件的标准,所有硬件必须按照这个标准才能让软件和硬件互通。
硬件在输入输出上分为并行和串行两种方式,相应接口也就是串行和并行接口。串行硬件通过串行接口与CPU通信,CPU通过串行接口与串行设备数据传输。并行同理,只有接口不同的差异。
访问外部硬件的两种方式:
1、将某个外设的内存映射到一定范围的地址空间中,CPU通过地址总线访问该区域时会自动转移到外设的内存中,这种映射让CPU访问外设内存就如通访问主板上的物理内存一样。这种访问外设的例子:显卡,显卡是显示器的适配器,CPU不直接与其进行交互,它只与显卡通信。显卡上有片内存叫显存,它被映射到主机物理内存上的低端1MB的0xB8000~0xBFFFF。CPU访问这片内存就是访问显存。往这片内存上写子就是往屏幕上打印内容。
2、外设是通过IO接口与CPU进行通信的,CPU访问外设,就是访问IO接口,由IO接口将信息传递给另一端的外设。CPU只管向指定外设收发数据,IO接口会根据数据进行处理,将CPU传递的数据处理成外设程序可识别的数据进行响应。IO接口本质就是寄存器。寄存器本质就是一组8位电路。(PS:阅读《0x86从实模式到保护模式》可知)

0x2 应用程序与操作系统是如何配合到一起的

应用程序是需要借助编程语言编写,而语言又依赖于编译器为其抓换成机器码。所以根本没有语言,有的只是编译器。编译器决定怎么解释编程语言的关键字及某种语法。语言只是编译器和大家的约定,只要写入这样的代码,编译其便将其翻译成某种机器指令,翻译成什么样取决于编译器行为,和语言无关。比如:printf函数,如果其他非c/c++编译器进行编译的话,完全可以把这个关键子的内容编译成响铃,换行等机器码,可以不编译成把指定字符显示到屏幕上。

0x2.1 程序语言运行库

编译器提供了一套库函数,可函数中又有封装的系统调用,这样的代码合集称之为运行库。C语言的运行库称为C运行库,就是所谓的CRT。(所以说C语言早于操作系统是不正确的,因为C语言也依赖于操作系统提供的系统调用。Linux下的CRT有Linux的系统调用,Windows下有对应Windows的系统调用。)

0x2.2 什么是应用程序

应用程序应用程序加上操作系统所提供的功能才算是完整的程序。由于有了操作系统的支持,操作系统才能正常运行,我们平时编程写的都是“半成品”,需要调用操作系统提供好的函数才能完整的做成一件事,而这个函数便是系统调用。

0x2.3 什么是用户态和内核态

用户态和内核态是CPU的机制,跟操作系统无关。是指CPU运行在用户态(特权等级3)还是内核态(特权等级0),跟操作系统和应用程序都没关系。
用户态进程陷入内核态是指:用内部或外部中断发生,当前进程被暂时终止执行,其上下文被内核的中断程序保存起来后,开始执行一段内核代码。是内核的代码,不是用户程序在内核的代码,用户程序的代码不可能在内核中。
当应用程序陷入内核后,他自己已经下CPU了,以后发生的事,应用程序完全不知道,它的上下文环境已经被保存到了自己的0特权级栈中了,那时在CPU上运行的程序已经是内核程序了。用户进程永远不会因进入内核态而变身为操作系统。

0x3 为什么称为“陷入”内核

应用程序处于特权级 3,操作系统内核处于特权级 0。当用户程序与访问系统资源时(无论是硬件,还是内核数据结构),它需要进行系统调用。这样CPU便进入了内核态,也称管态。

0x4 内存访问为什么要分段

首先,分段式CPU使用访问内存的机制,只有CPU才关注段。而且分段机制是历史遗留问题。CPU从8086开始的,限制于技术和经济,CPU和寄存器都是16位的。那时的计算机只有物理地址,没有虚拟地址,编译器编译出来的都是绝对物理地址,但是如果编译出来的两个程序的加载地址相同或者有重叠则这两个程序只能有一个运行。为了解决这个问题,有人提出了让CPU采用:段基地址+段内偏移地址的方式访问任意内存,这样即使程序物理地址偏移重复也可以根据修改段基地址进行重定位。这样就可以运行多个程序了。
内存是随机读写设备,也就是说只要给定内存地址就可知直接找到该地址位置,不需要从0开始。如访问内存0xC00,只要将此地址写入地址总线便可。

0x5 代码分段

软件程序段一般是由编译器分配的,也有的是程序员自己进行划分。在多段模型下需要分配多个段,然后不断地切换段寄存器指向的段寄存器指向的段才能访问到不同段中的数据。(其实多段模型是16位汇编设计的,因为访问地址有限才有段基址)
在平坦模式下,段寄存器可以指向4GB空间,就没有必要采取多段模型切换段寄存器内容进行访问数据。所以对于代码是否分段取决于操作系统是否在平坦模式下。
CPU是一个高度集成化的芯片,只要给出CPU第一条指令的起始地址,CPU在执行它指令的同时会自动获取下一条指令。而且被读取的指令必须被要求没有空隙,下条指令的地址是按照前面指令的尺寸大小排下来的,这就是Intel处理器的程序计数器cs:eip能过自动获得下一条指令的原理。
为了让程序内指令接连不断地执行,要把所有指令排在一起,形成一片连续的指令区,这就是代码段。指令是由操作码和操作数组成的,操作数就是程序中的数据。把数据连续地排在一起存储形成的段落,就称为数据端。
数据端和代码段的属性是由CPU、操作系统和编译器共同作用的结果:
1)编译器在编译时挑出不同数据具备的属性。从而根据属性将程序片段分类。
2)操作系统通过设置GDT(全局描述符表)来构建段描述符,在段描述符种指定段的位置、大小及属性(包括S段和TYPE段),这才是真正给段添加属性的地方。
3)CPU中的段寄存器,提前被操作系统赋予了对应的段选择子,从而确定了指向的段。在执行指令时,会根据该段的属性来判断指令的行为,若有返回则发出异常。
内存分段是CPU的访问内存的机制,程序分段是软件种认为逻辑划分的内存区域,它本身也是内存,所以处理器在访问该区域时,也会采用内存分段机制,用段寄存器指向该区域的起始地址。

0x6 物理地址、逻辑地址、有效地址、线性地址、虚拟地址的区别

实模式下,“段基址+段内偏移地址”经过段部件的处理,直接输出的就是物理地址,CPU可以直接使用此地址访问内存。
保护模式下,“段基址+段内偏移地址”称为线性地址,此时的段基址不再是真正的地址,而是叫做段选择子的数据结构。它本身是一个索引,类似于数组下标,通过这个索引便能在GDT种找到对应的段描述符,在找到的描述符种记录了该段的起始、大小等信息,这样便得到了段基址。若没有开启分页功能,此线性地址就直接被当作物理地址使用。若开启,此线性地址是虚拟地址(虚拟地址、线性地址在分页机制下是一个意思)。虚拟地址要经过CPU页部件转换成具体的物理地址,这样CPU才能将其送上地址总线去访问内存。
无论在实模式还是保护模式下,段内地址偏移地址又被称为有效地址,也被称为逻辑地址。

0x7 为什么Linux系统下的应用程序不能在Windows系统下运行

不同操作系统文件格式不同,系统API不同导致两个系统下的应用不互通。

0x8 局部变量和函数参数为什么要放在栈中

局部变量不像static那样属于全局性的。全局变量可以随时访问,而局部变量只是在特定条件下才可以访问,随时可以清理,所以要将局部变量放在堆栈里。(堆栈就是栈,和堆一毛钱关系都没有,就是一种习惯)
在C程序的内存布局中,由于堆和栈的地址空间是接壤的,栈从高地址往低地址发展,堆从低地址向高地址发展,堆和栈早晚会碰头,他们各自大小取决于实际使用的情况,界限并不明朗。可能这就是称为堆栈的原因。
函数放在堆栈的原因:
1)局部性:函数使用的参数是局部变量
2)不确定性:编译器无法预测函数调用的次数,而函数的返回值和参数都需要内存来存储。内存空间不确定。

0x9 为什么说汇编语言比C语言快

这种说法是错误的。因为不管什么语言,想要让其运行必须都要通过CPU。CPU不知道什么是汇编语言、C语言,甚至Java、PHP、Python等,它根本不知道交给它的指令曾经历那么多的解释、编译工序。不管什么语言,编译器最终翻译出来的都是机器指令和C编译器编译出来的机器指令无异。
之所以说汇编语言比C语言快,是因为汇编语言生成的机器指令更少,从而“显得”更快。用汇编语言写程序相当于在直接写机器指令,汇编语言不会添加额外的语句,CPU不会因为多执行一些无关指令而浪费时间,当然会更快。
而C编译器为了让程序员更加方便的编程,它在背后做了大量的工作,不仅如此,出于通用性、易用性的其他方面考虑,C编译器往往会在背后加入额外的代码,然后再由汇编器将汇编代码翻译成机器指令,所以生成的机器指令会产生冗余。

0xA 编译型程序与解释型程序的区别

解释型语言,也成为脚本语言,如JavaScript、Python、Perl、PHP、Shell脚本等。他们本身是文本文件,是某个应用程序的输入,这个应用程序是脚本解释器。
由于只是文本,这些脚本中的代码在脚本解释器看来与字符串无异。也就是说,脚本中的代码从没有正真到CPU上执行过,CPU的cs:ip寄存器从来没有指向它们。在CPU眼里只看得到脚本解释器。脚本的执行本质上是脚本解释器在时时分析这个脚本,动态根据关键字和语法来做出相应行为。因此脚本中若出现错误,先前正确的部分也会正常执行,这和编译型程序有很大区别。

0xB BIOS中断、DOS中断、Linux中断

在计算机系统中,无论是在实模式,还是在保护模式,在任何情况下就会有来自外部或内部的事件发生。如果事件来自于CPU内部就称为异常,即Exception。例如,CPU在计算算法时,发现分母为0,就抛出了除0异常。如果事件来自于外部,也就是该事件由外部设备法出并通知了CPU,这个设备就称为中断。
BIOS和DOS都是存在于实模式下的程序,由他们建立的中断调用都是建立在中断向量表(Interrupt Vector Table,IVT)中的。他们都是通过软中断指令int中断号来调用的。
中断向量表中的每个中断向量大小是4字节。这4字节描述了一个中断处理例程(程序)
中断向量表中每个中断向量大小是4个字节。这4个字节描述了一个中断处理例程(程序)的段基地址和段内偏移地址。因为中断向量表长度为1024个字节,故该表最多容纳256个中断向量处理程序。计算机启动之初,中断向量表中的中断例程是由BIOS建立的,它从物理地址0x0000处初始化并在中断向量表中添加各种处理例程。
BIOS中断调用的主要功能是提供了硬件的访问方法。BIOS只是为了对硬件操作变得简单易行,也可以不用BIOS对硬件进行访问。操作硬件无非是通过in/out指令来读写外设的端口,BIOS中断程序处理就是包含了in/out指令的各种操作。
BIOS填写中断处理例程的原因:
1)给自己用。因为BIOS也是一段程序,是程序就很可能重复执行某段代码,它直接将其写成中断函数,进行直接调用。
2)给后来的程序使用,如加载器或boot loader。它们在调用硬件资源时就不需要自己重写代码了。
BIOS设置中断例程时也需要调用别人的函数例程。BIOS也是软件,也要求于别人。首先硬件厂商为了让自己生产的产品易用,肯定实现写好了一组调用接口,必然是越简单越好,直接给接口函数传递一个参数,硬件就能返回一个输出。
每个外设,包括显卡、键盘、各种控制器等,都有自己的内存(主板也有自己的内存,BIOS就存放在里面),不过这种内存都只是读取ROM。硬件自己的功能调用例程以及初始化代码就放在这ROM中。根据规范,第1个内存单元的内容是0x55,第二个存储单元是0xAA,第三个存储单位是该rom中,以512字节为单位的代码长度。从第4个存储单元起就是实际代码了,知道第三个存储单元所示长度为止。
访问外设有两种方式。
1)内存映射:通过地址总线将外设自己内存映射到某个内存区域(并不是映射到主板上插的内存条中)。
2)端口操作:外设都有自己的控制器,控制器上有寄存器,这些寄存器就是所谓的端口,通过in/out指令读些端口来访问硬件的内存。
DOS是运行在实模式下的,故其建立的中断调用也建立在中断向量表中,只不过其中断向量号和BIOS的不能冲突。0x20~0x27是DOS中断。
DOS中断调用那么多功能是通过先往ah寄存器中写好子功能号,再执行int 0x21。这时在中断向量表中第0x21个表项,即物理地址0x21*4处中的中断处理程序开始根据寄存器ah中的值来调用相应的子功能。
而Linux内核是在进入保护模式后才建立中断例程的,不过在保护模式下,中断向量表已被中断描述符表取代(Interrupt Descriptor Table,IDT)。

0xC 库函数是用户进程与内核的桥梁

在Linux下C编程时,我们写的程序通常是用户级程序。为了输出文本,我们一般会在文件开始include <stdio.h>,这样程序就可以使用printf这样的函数完成打印输出。这是因为用户程序不具有独立打印字符的功能,他必须借助操作系统的力量才可以。操作系统提供了一套系统调用接口,用户进程直接调用这些接口就行了。所以我们使用的库函数都是这种接口,调用库函数就相当于在使用函数接口。

0xD MBR、EBR、DBR和OBR各是什么

BIOS是主板内存上的小程序,所在空间有限,代码量较少,功能受限,所以必须采取功能接力的形式来移交控制权。BIOS只完成简单的测试初始化工作,然后找机会把处理器使用权移交给MBR中的程序。为了方便OS找到MBR,MBR程序必须存放在指定位置,MBR放置在整个磁盘的第一个扇区内,所以又被称为引导扇区。
MBR是主引导记录,Master或Main Boot Record,它位于整个硬盘最开始的扇区,即0盘0道1扇区。一般情况下扇区大小是512字节,只是一般情况。
MBR引导扇区中内容:
1)446字节的引导程序及参数
2)64字节的分区表
3)2字节结束标记0x55和0xaa
在MBR引导扇区中存储引导程序,为的是从BIOS手中接管系统控制权,也就是处理器的使用权。BIOS从引导扇区数据后或默认将引导扇区数据读取道0x7c00,然后调用cs:ip远跳转指令跳转过去执行MBR里的程序。这就是控制权移交过程。
除了MBR主引导扇区,还有“次”引导。MBR主要任务就是结合当前程序运行情况挑选最好的次引导程序,将系统权限转交给次引导程序。MBR就是帮忙转交没有其他太多作用。MBR除了引导程序外,还有64字节的分区表,里面是分区信息,每个分区信息表项占用16字节。因此MBR的分区表中只能容纳4个表项,转交控制权就是在这4个表项中遍历选择。
通常次引导程序就是通过操作系统加载器的,因此MBR引导任务就是将控制权交给系统加载器。
为了让MBR知道哪有操作系统,我们在分区时,如果像在某个分区中安装操作系统,就用分区工具将该分区设置为活动分区,设置活动分区的本质就是把分区表中该分区对应的分区表项中的活动标记为0x80。0x80表示此分区上有引导程序,0表示没有引导程序,该分区不可引导。此引导程序通常是内核加载器。这个存放内核加载器的扇区也被称为操作系统引导记录OBR,即OS Boot Record,此扇区也被称为OBR引导扇区。
OBR开头的跳转指令跳往的目标地址并不固定,这是由所创建的文件系统决定的,堆与FAT32文件系统来说,此跳转指令会跳转到本扇区偏移0x5A字节的操作系统引导程序处。不管目标地址是多少,总之哪里通常是操作系统的内核加载器。
OBR是DBR遗留下来的。DBR是DOS Boot Record,也就是DOS系统的引导程序,DBR中内容大致是:
1)跳转指令,使MBR跳转到引导代码
2)厂商信息、DOS版本信息
3)BIOS参数快BPB,即BIOS Parameter Block
4)操作系统引导程序
5)结束标记0x55和0xaa
在DOS时代只有4个分区,不存在拓展分区,这4个分区都想与主分区,所以个主分区最开始的扇区称为DBR引导扇区。因为其他操作系统也有这种习惯,而且DOS退出了历史舞台,所以改为了OBR。
当初为了解决分区数量问题才有了拓展分区,EBR是拓展分区为了兼容MBR才提出的概念,主要是兼容MBR的分区表。拓展分区是一个逻辑分区,因此拓展分区中也要由分区表,为拓展分区存储分区表的扇区称为EBR,即Expand Boot Record。兼容的内容是分区表,因此它与MBR结构相同,只是位置不同,EBR位于个子拓展分区中最开始的扇区(注意,各个主分区和逻辑分区中最开始的扇区是操作系统引导扇区,不要把逻辑分区和扇区弄混,是两个完全不一样的概念)
MBR和EBR是分区工具创建的,不属于操作系统管理范围,因此操作系统不可以向其写入内容(这里说的不可以只是操作系统的一种限制,其实操作系统可以访问所有内存地址进行读写),每个子拓展分区只有一个EBR。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章