草庐IT

操作系统是如何保护自己的? CPU与保护模式

陆小风 2023-03-28 原文
​在回答这个问题之前,你可能会首先想:为什么操作系统需要保护自己呢?

操作系统其实就是一个大管家,负责给应用程序搭建舞台,好让程序们过好自己的一生,但偏偏有不听话的程序可能想抢操作系统的戏,显然这会影响所有其它正在程序,因此操作系统必须要有能力保护自己。

在上一篇文章《彻底理解操作系统:CPU与实模式》中,我们从历史的角度了解了x86最开始是没有任何保护机制的,应用程序竟然可以与操作系统平起平坐,操作系统能读写的内存区域应用程序也一样可以读写,操作系统可以控制的硬件应用程序一样不在话下。

应用程序和操作系统这么平等还何谈保护?其实从某种程度讲,保护自己就是限制别人,那么操作系统该怎样限制应用程序呢?

程序也是分三六九等的

核心就在一点:权限。

这一点相信对于打工人都深有体会,在公司里有的文档你无权查看,有的数据库你无权读取,有的门禁你的卡刷不开等等。

这里也是一样的道理,但是操作系统和普通的应用程序都是软件,从本质上讲没有任何区别,在CPU眼里都是机器指令,显然从软件这一层面上看操作系统没有很好的办法能控制应用程序,这就不得不借助硬件的帮忙了,借助谁的帮忙呢?显然是CPU。

我们刚才提到过,不管是操作系统还是应用程序在CPU眼里都是机器指令,CPU闭着眼执行就完事儿,从时间角度上看CPU就是一条又一条的在执行指令:

然而,CPU也不能对此一点都不关心,CPU必须能区分出哪些指令属于操作系统,哪些指令属于普通的应用程序!

该怎么区分呢?很简单,其中一种方法是这样的,我们添加一些特殊的机器指令,假设是指令A和B,CPU执行到该指令A时就知道接下来要执行的指令属于操作系统,当执行完指令B时就知道接下来要执行的属于普通应用程序,这两条指令在x86(32位)中就是int与iret指令,这两个指令对应的背后就是所谓的系统调用。

有了这样的指令,CPU可以清楚的执行什么时候在执行普通程序,什么是在运行操作系统(程序),CPU能区分清楚那么就能给它们赋予不同的权限,这就是所谓用户态与内核态的由来,用户态对应的是普通程序,内核态对应的是操作系统,它们的权限是不一样的。

x86 CPU支持四种权限等级,0,1,2,3,一般的操作系统都使用两种特权0和3,0是最高权限,显然这是操作系统也就是内核态所拥有的权限,而3是普通程序运行的权限,相对较低。

同时,一些指令只有在内核态下才可以执行,这些就是所谓特权指令,当CPU在用户态(普通程序)时是没有办法执行特权指令的,这样就从机器指令这个层面确保了普通程序不能执行某些特权操作。

我们知道程序除了机器指令外还有指令依赖的数据,而数据又是保存在内存中,那么接下来的问题是操作系统该怎样保护自己的内存不会普通程序读写呢?

访问内存也需要权限

答案和我们刚才讲解的机器指令的特权划分是一样的。我们规定操作系统所在的内存区域只有CPU处于内核态时才可以访问,如果位于用户态的程序试图访问内核所在的地址空间那么将立刻被操作系统kill掉。那么接下来的问题就是我们该怎样给一段内存添加上权限信息呢?显然我们需要一张“表”,这张表中记录一段内存区域并且记录下访问这块内存所需要的权限信息,类似这样:

序号 起始地址 长度 所需权限
0 0x7c00 0x1000 0
1 0x9a00 0x2000 3
...
当CPU试图访问这段内存区域时会根据CPU自身所在的权限(内核态或者用户态)与这段内存需要的权限进行比对,只有当CPU所在权限比访问这段内存所需要的权限高或者相等时才能读写这段内存,否则将触发异常。

假设CPU当前正在执行用户态程序,也就是运行在用户态,因此其权限等级为3,此时如果CPU试图访问第0号内存块时发现读写该内存块所需要的权限为0(内核态),这时CPU本身将产生异常,该异常将被操作系统捕获,此时操作系统会发现应用程序试图读写程序不具备权限的内存,因此操作系统手起刀落将该进程kill掉,这样操作系统就保证了自己的内存区域不会被普通程序所读写。

就这样操作系统成功保护了自己的内存数据以及机器指令。

现在是时候总结一下了。

为了将操作系统和普通程序区别开来,我们需要给机器指令赋予权限等级,该权限信息会保存在CPU中,显然CPU中需要特定寄存器来保存该信息,于此同时我们也为内存区域赋予了权限等级,只有当前CPU的权限大于或者等于该内存区域所需权限时才能读写,这就要求有一张“表”来保存内存起始地址、长度、权限等信息,这张表就是所谓的Global Descriptor Table,GDT,以及Local Descriptor Table,LDT。

内核所在内存区域以及一些共享内存区域信息就保存在GDT中,这就是叫做Global的原因,而进程所在的内存区域(私有)信息则保存在LDT中,这就是为什么叫做Local。

具备这些能力的x86 CPU就被称为保护模式,Inter处理器从80286开始引入保护模式,可以看到与x86早前的实模式相比,保护模式开始有了质的飞跃。

从实模式到保护模式

我们在之前的文章中说过,x86是一个有着顽强生命力的物种,其它大部分类型的CPU在计算机不长的历史中逐渐消失了,而x86则历久弥新,也因此x86历史包袱十分沉重,即使是最新款的intel x86处理器也可以运行上世纪编写的古老程序,为做到向后兼容,intel x86程序必须既能运行在实模式下也能运行在保护模式下。

因此x86处理器在加电会首先进入实模式然后切换到保护模式,现代操作系统都运行在保护模式下,正是利用了处理器的一系列特性操作系统才得以保护自己。

算上前一篇《彻底操作系统:CPU与实模式​》以及到目前为止,我们看到的x86内存管理都是基于段式机制,Segmentation来管理内存的,实际上x86处理器在引入保护模式的同时也开始支持页式内存管理(paging),因此现代x86处理器即支持段式内存管理也支持页式内存管理,只不过对于现代操作系统像Linux等实际上几乎不再使用处理器提供的段式内存管理机制而是基于页式内存管理机制。

从这里我们也能看出来,内存管理机制其实是处理器这种硬件提供的,操作系统(软件)只不过这种机制的使用者而已。好啦,这篇文章就先到这里,实际上这里还有很多内容没有讲解完,GDT、LDT长什么样子?怎么使用?具体该怎样从实模式切换到保护模式等等,这些内容将在后续章节中介绍。​

有关操作系统是如何保护自己的? CPU与保护模式的更多相关文章

  1. ruby - 如何使用 Nokogiri 的 xpath 和 at_xpath 方法 - 2

    我正在学习如何使用Nokogiri,根据这段代码我遇到了一些问题:require'rubygems'require'mechanize'post_agent=WWW::Mechanize.newpost_page=post_agent.get('http://www.vbulletin.org/forum/showthread.php?t=230708')puts"\nabsolutepathwithtbodygivesnil"putspost_page.parser.xpath('/html/body/div/div/div/div/div/table/tbody/tr/td/div

  2. ruby - 如何从 ruby​​ 中的字符串运行任意对象方法? - 2

    总的来说,我对ruby​​还比较陌生,我正在为我正在创建的对象编写一些rspec测试用例。许多测试用例都非常基础,我只是想确保正确填充和返回值。我想知道是否有办法使用循环结构来执行此操作。不必为我要测试的每个方法都设置一个assertEquals。例如:describeitem,"TestingtheItem"doit"willhaveanullvaluetostart"doitem=Item.new#HereIcoulddotheitem.name.shouldbe_nil#thenIcoulddoitem.category.shouldbe_nilendend但我想要一些方法来使用

  3. python - 如何使用 Ruby 或 Python 创建一系列高音调和低音调的蜂鸣声? - 2

    关闭。这个问题是opinion-based.它目前不接受答案。想要改进这个问题?更新问题,以便editingthispost可以用事实和引用来回答它.关闭4年前。Improvethisquestion我想在固定时间创建一系列低音和高音调的哔哔声。例如:在150毫秒时发出高音调的蜂鸣声在151毫秒时发出低音调的蜂鸣声200毫秒时发出低音调的蜂鸣声250毫秒的高音调蜂鸣声有没有办法在Ruby或Python中做到这一点?我真的不在乎输出编码是什么(.wav、.mp3、.ogg等等),但我确实想创建一个输出文件。

  4. ruby-on-rails - 如何验证 update_all 是否实际在 Rails 中更新 - 2

    给定这段代码defcreate@upgrades=User.update_all(["role=?","upgraded"],:id=>params[:upgrade])redirect_toadmin_upgrades_path,:notice=>"Successfullyupgradeduser."end我如何在该操作中实际验证它们是否已保存或未重定向到适当的页面和消息? 最佳答案 在Rails3中,update_all不返回任何有意义的信息,除了已更新的记录数(这可能取决于您的DBMS是否返回该信息)。http://ar.ru

  5. ruby-on-rails - Rails - 子类化模型的设计模式是什么? - 2

    我有一个模型:classItem项目有一个属性“商店”基于存储的值,我希望Item对象对特定方法具有不同的行为。Rails中是否有针对此的通用设计模式?如果方法中没有大的if-else语句,这是如何干净利落地完成的? 最佳答案 通常通过Single-TableInheritance. 关于ruby-on-rails-Rails-子类化模型的设计模式是什么?,我们在StackOverflow上找到一个类似的问题: https://stackoverflow.co

  6. ruby-on-rails - 'compass watch' 是如何工作的/它是如何与 rails 一起使用的 - 2

    我在我的项目目录中完成了compasscreate.和compassinitrails。几个问题:我已将我的.sass文件放在public/stylesheets中。这是放置它们的正确位置吗?当我运行compasswatch时,它不会自动编译这些.sass文件。我必须手动指定文件:compasswatchpublic/stylesheets/myfile.sass等。如何让它自动运行?文件ie.css、print.css和screen.css已放在stylesheets/compiled。如何在编译后不让它们重新出现的情况下删除它们?我自己编译的.sass文件编译成compiled/t

  7. ruby - 如何将脚本文件的末尾读取为数据文件(Perl 或任何其他语言) - 2

    我正在寻找执行以下操作的正确语法(在Perl、Shell或Ruby中):#variabletoaccessthedatalinesappendedasafileEND_OF_SCRIPT_MARKERrawdatastartshereanditcontinues. 最佳答案 Perl用__DATA__做这个:#!/usr/bin/perlusestrict;usewarnings;while(){print;}__DATA__Texttoprintgoeshere 关于ruby-如何将脚

  8. ruby - 解析 RDFa、微数据等的最佳方式是什么,使用统一的模式/词汇(例如 schema.org)存储和显示信息 - 2

    我主要使用Ruby来执行此操作,但到目前为止我的攻击计划如下:使用gemsrdf、rdf-rdfa和rdf-microdata或mida来解析给定任何URI的数据。我认为最好映射到像schema.org这样的统一模式,例如使用这个yaml文件,它试图描述数据词汇表和opengraph到schema.org之间的转换:#SchemaXtoschema.orgconversion#data-vocabularyDV:name:namestreet-address:streetAddressregion:addressRegionlocality:addressLocalityphoto:i

  9. ruby - 如何指定 Rack 处理程序 - 2

    Rackup通过Rack的默认处理程序成功运行任何Rack应用程序。例如:classRackAppdefcall(environment)['200',{'Content-Type'=>'text/html'},["Helloworld"]]endendrunRackApp.new但是当最后一行更改为使用Rack的内置CGI处理程序时,rackup给出“NoMethodErrorat/undefinedmethod`call'fornil:NilClass”:Rack::Handler::CGI.runRackApp.newRack的其他内置处理程序也提出了同样的反对意见。例如Rack

  10. ruby - 如何每月在 Heroku 运行一次 Scheduler 插件? - 2

    在选择我想要运行操作的频率时,唯一的选项是“每天”、“每小时”和“每10分钟”。谢谢!我想为我的Rails3.1应用程序运行调度程序。 最佳答案 这不是一个优雅的解决方案,但您可以安排它每天运行,并在实际开始工作之前检查日期是否为当月的第一天。 关于ruby-如何每月在Heroku运行一次Scheduler插件?,我们在StackOverflow上找到一个类似的问题: https://stackoverflow.com/questions/8692687/

随机推荐