任务和特权级保护(二)——《x86汇编语言:从实模式到保护模式》读书笔记32

之前做了那么多铺垫,我们终于可以看看第14章的代码了。

对于引导代码和用户程序,依然采用第13章的;对于内核程序(c14_core.asm),编译的时候有几行报错了,只要加上dword即可解决。

1. 为什么要用调用门

在第13章,为了能使用内核提供的例程,用户程序是用call far指令直接转移到内核例程(非一致代码段)。因为CPL=目标代码段描述符的DPL=RPL=0,符合下面表格的条件,所以转移是没有问题的。

但是在本章,用户程序工作在3特权级,而非0特权级,所以是无法直接转移的。不过也不用悲观,我们还是有办法的,可以通过调用门来转移。

2. 什么是调用门

关于调用门的知识,可以参考我的博文:调用门详解

调用门的格式如下图:

3. 调用门的安装

811         ;以下开始安装为整个系统服务的调用门。特权级之间的控制转移必须使用门
812         mov edi,salt                       ;C-SALT表的起始位置
813         mov ecx,salt_items                 ;C-SALT表的条目数量
814   .b3:
815         push ecx
816         mov eax,[edi+256]                  ;该条目入口点的32位偏移地址
817         mov bx,[edi+260]                   ;该条目入口点的段选择子
818         mov cx,1_11_0_1100_000_00000B      ;特权级3的调用门(3以上的特权级才
819                                            ;允许访问),0个参数(因为用寄存器
820                                            ;传递参数,而没有用栈)
821         call sys_routine_seg_sel:make_gate_descriptor
822         call sys_routine_seg_sel:set_up_gdt_descriptor
823         mov [edi+260],cx                   ;将返回的门描述符选择子回填
824         add edi,salt_item_len              ;指向下一个C-SALT条目
825         pop ecx
826         loop .b3

先复习一下过程make_gate_descriptor.

331    make_gate_descriptor:                   ;构造门的描述符(调用门等)
332                                            ;输入:EAX=门代码在段内偏移地址
333                                            ;    BX=门代码所在段的选择子
334                                            ;    CX=段类型及属性等(各属
335                                            ;    性位都在原始位置)
336                                            ;返回:EDX:EAX=完整的描述符

816~821:调用过程make_gate_descriptor构造调用门(请参考调用门的格式),P=1,DPL=3,参数个数=0;

822:调用过程set_up_gdt_descriptor把构造好的调用门安装到GDT中,返回对应的选择子(TI=0,RPL=0);

264  set_up_gdt_descriptor:                    ;在GDT内安装一个新的描述符
265                                            ;输入:EDX:EAX=描述符
266                                            ;输出:CX=描述符的选择子

823:将返回的调用门选择子回填,覆盖原先的段选择子。如下图(下图是内核符号表中一个表项的示意图)所示:

4. 调用门的测试

828         ;对门进行测试
829         mov ebx,message_2
830         call far [salt_1+256]              ;通过门显示信息(偏移量将被忽略) 

表面上,这是一个间接绝对远调用,通过指令中的内存地址,可以间接取得32位偏移量和16位的代码段选择子;但是,处理器在执行这条指令的时候,会用选择子访问GDT,结果发现是一个调用门,所以忽略32位的偏移量(上图中的绿色部分)。

调用门安装完成后,GDT的示意图如下:

不仅间接绝对远调用是这样,直接绝对远调用也是这样,如果选择子指向的是调用门,偏移量也会被忽略。例如

    call 0x0040:0x00001234

结合上图,因为0x40处是调用门,所以偏移0x00001234被忽略。

5. 加载用户程序与创建用户任务

5.1 任务控制块(Task Control Block,TCB)

5.1.1 TCB的格式

加载程序并创建一个任务,需要用到很多数据,比如程序大小、加载位置等等。内核应当为每一个任务创建一个内存区域,来记录任务的信息和状态,这个内存区域就称为任务控制块(Task Control Block,TCB)。

需要说明的是:TCB不是处理器的要求,而是我们为了自己方便而发明的。

关于TCB的结构,如原书图14-12(P264)。为了读者方便,我在这里把图再绘制一遍。

请注意,这个格式是作者发明的,并不是说TCB就必须是这种格式。

5.1.2. TCB链表

为了能够追踪所有的任务,可以把每个TCB串起来,形成一个链表。

在代码的核心数据段中,声明了标号tcb_chain,初始化了一个双字,值为0.

413         ;任务控制块链
414         tcb_chain        dd  0

其实,这相当于一个指针,用来指向第一个任务的TCB。当它为0时,表示没有任务。所有任务都按照被创建的先后顺序链接在一起形成一个无头单向非循环链表。

835         ;创建任务控制块。这不是处理器的要求,而是我们自己为了方便而设立的
836         mov ecx,0x46
837         call sys_routine_seg_sel:allocate_memory
838         call append_to_tcb_link            ;将任务控制块追加到TCB链表 

以上三行用于分配TCB的空间(0x46字节),然后把这个TCB挂到链表上(尾插法)。

5.2. 加载用户任务

840         push dword 50                      ;用户程序位于逻辑50扇区
841         push ecx                           ;压入任务控制块起始线性地址
842
843         call load_relocate_program

以上三行用于加载和重定位用户程序。

5.2.1. 使用栈传递参数

464   load_relocate_program:                   ;加载并重定位用户程序
465                                            ;输入: PUSH 逻辑扇区号
466                                            ;      PUSH 任务控制块基地址
467                                            ;输出:无
468         pushad
469
470         push ds
471         push es
472
473         mov ebp,esp                        ;为访问通过堆栈传递的参数做准备

这是过程load_relocate_program开头的几行,执行完第473行后,栈的状态如下图所示:

这里主要是复习如何用栈传递参数。需要说明的是:

1. 用EBP寄存器来寻址的时候,默认使用段寄存器SS;

2. 在32位模式下,栈操作的默认操作数大小是双字;

3. 处理器执行压栈指令的时候,如果发现操作数是段寄存器,则将段寄存器的16位值扩展为32位(高16位全0),然后执行压栈操作;出栈指令执行相反的操作,将32位的值截断,仅保留低16位,并传送到相应的段寄存器;

4. 由于load_relocate_program是通过32位近调用进入的(第843行),所以只压入EIP的内容,没有压入CS;

475         mov ecx,mem_0_4_gb_seg_sel
476         mov es,ecx
477
478         mov esi,[ebp+11*4]                 ;从堆栈中取得TCB的基地址

以上三行执行完后,ES指向0-4GB数据段;ESI指向TCB的基地址。

5.2.2. 在TCB中填写LDT的基地址和初始界限值

GDT一般用于存放全局空间的段描述符。对于任务私有的段描述符,也可以放在GDT中,但是最好放在自己私有的LDT中。

480         ;以下申请创建LDT所需要的内存
481         mov ecx,160                        ;允许安装20个LDT描述符
482         call sys_routine_seg_sel:allocate_memory
483         mov [es:esi+0x0c],ecx              ;登记LDT基地址到TCB中
484         mov word [es:esi+0x0a],0xffff      ;登记LDT初始的界限到TCB中 

5.2.3. 从硬盘加载用户程序到内存

  1. ES指向0-4GB数据段;ESI指向TCB基地址;
  2. 根据头部信息确定用户程序需要多大的内存;
  3. 分配内存,并登记用户程序在内存中的基地址到TCB;
  4. 循环调用read_hard_disk_0加载用户程序;

5.2.4. 在LDT中安装描述符

  1. 安装头部段、代码段、数据段以及固有栈栈段的描述符(DPL=3);
  2. 这些描述符对应的选择子(令其RPL=3)要回填到用户程序头部;在LDT中安装的描述符,通常只由用户程序自己使用,所以请求者是用户程序自己,所以其选择子的RPL=用户程序的CPL=3;
  3. 头部段的选择子(令其RPL=3)还要登记到TCB中。

用户程序的头部的格式和第13章完全相同。

5.2.5. 重定位符号表

这个过程和第13章的基本相同。注意,用户符号表中的调用门选择子,其RPL=3;

5.2.6 创建0、1、2特权级的栈

通过调用门的控制转移,有可能会改变CPL。如果通过调用门把控制转移到了更高特权级的非一致代码段中,那么CPL就会被设置为目标代码段的DPL值,并且会引起堆栈切换。

为此,必须为每个任务定义额外的栈。对于我们的用户任务,需要为它创建特权级0、1、2的栈。而且,这些栈应当在LDT中有对应的段描述符。

这些栈是内核为用户程序动态创建的,而且需要登记在TSS中,以便处理器固件能够自动访问到它们。不过目前我们还没有创建TSS,所以,有必要先将这些栈的信息登记在TCB中暂时保存(如下图)。

创建x(x=0,1,2)特权级的栈的步骤如下:

1. 申请内存,为栈分配空间;

2. 在LDT中创建栈段描述符(DPL=x);

3. 在TCB中登记栈的信息,包括栈的大小、基地址、选择子(RPL=x)以及ESPx的初始值(=0);

我觉得栈的大小和基地址的登记是没有必要的,因为TSS中不需要填写这些字段。

囿于篇幅,本文就到这里。劳逸结合,休息一下…

【未完待续】

时间: 2024-11-02 12:31:29

任务和特权级保护(二)——《x86汇编语言:从实模式到保护模式》读书笔记32的相关文章

《X86汇编语言 从实模式到保护模式》bochs 配置教程(详细)

本文是写给<X86汇编语言 从实模式到保护模式>读者的一份Bochs配置指南. 我们要做的有: 1.下载并安装bochs 2.配置bochs 3.通过bochs调试虚拟硬盘 bochs的官方网址:http://bochs.sourceforge.net/ bochs的下载地址:http://sourceforge.net/projects/bochs/files/bochs/ 本书附带文件下载地址:https://files-cdn.cnblogs.com/files/leec/booktoo

ASM:《X86汇编语言-从实模式到保护模式》第12章:存储器的保护

12章其实是11章的拓展,代码基本不变,就是在保护模式下展开讨论. ★PART1:存储器的保护机制 1. 修改段寄存器的保护 当执行把段选择子传到段寄存器的选择器部分的时候,处理器固件在完成传送之前,要检查和确认选择子是正确的,并且该选择子选择的描述符也是正确的.假如索引号是正确的,也就是说明索引号8+7要小于等于边界.如果超过边界,那么处理器就会终止处理,产生异常中断13,同时段寄存器的原值保持不变. 同时处理器还要对描述符的类别进行检查,如果描述符的类别进行确认,举个例子来说,如果描述符的类

ASM:《X86汇编语言-从实模式到保护模式》5-7章相关知识

第5-7章感觉是这一本书中比较奇怪的章节,可能是作者考虑到读者人群水平的差异,故意由浅入深地讲如何在屏幕上显示字符和使用mov,jmp指令等等,但是这样讲的东西有点重复,而且看了第六,第七章以后,感觉第5章的做法真是太笨了. ★PART1:显卡与显存 1. 显卡与显存              a. 显卡控制显示器的最小单位是像素,一个像素对应着屏幕的一个点,屏幕上通常有数十万乃至更多的像素.而控制这些像素就要用到显存自己内置的一个东西,这个东西叫做显存(Video RAM,VRAM) .显存和

《汇编语言第三版(王爽)》 读书笔记

前言 说起自己读汇编,总有人会在问:读汇编有啥意义?读汇编对我的开发工作有帮助吗?...我觉得读汇编是为了让我们更好地有计算机工作原理方面的知识,不仅仅是一味地高屋建瓴.就拿 IDE 和 Linux gcc 来说,我们为什么要试着用 gcc 去编译链接并生成可执行程序,而不是点一下 Run 程序就能跑起来?原因还是在我们想探究程序运行的本质. 而汇编就是一门帮助我们更好地理解程序如何"跑起来"的语言,它最接近机器语言这门计算机唯一能识别的语言,深入数据在内存单元的存储,直接对内存单元进

任务和特权级保护

本文为<x86汇编语言:从实模式到保护模式> 第14章笔记 任务的隔离和特权级保护 任务, 任务的LDT和TSS 程序是记录在载体上的指令和数据, 总是为了完成某个特定的工作, 其正在执行中的一个副本, 叫做任务(Task). 这句话的意思是说, 如果一个程序有多个副本正在内存中运行, 那么, 它对应着多个任务, 每一个副本都是一个任务. 为了有效的在任务之间实施隔离, 处理器建议每个任务都应当具有自己的描述符表, 称为局部描述符表LDT, 并且把属于自己的那些段放到LDT中. 和GDT一样,

保护模式特权级概述

在IA32的操作系统中,段被分为了4个特权级,分别为0-3级,有时候我们也叫做ring0-ring3,其中,数值越小特权级越高.如下图所示: 图中,核心代码和数据所在的段的特权级都比较高,一般在ring0,而用户程序所在的段的特权级较低,一般在ring3.当低特权级的任务试图在未被允许的情况下访问高特权级的段时,将会产生常规保护错误. 而处理器是如何区分所在段的特权级,进而对其进行保护的呢?这就不得不提到CPL.DPL和RPL三者了.但是在开始之前,我们需要先了解一下一致代码段和非一致代码段.

实模式与保护模式下的分段分页机制

1. 实模式 在实模式下,CPU不会为任务提供任务的保护机制,代码任意运行.8086处理器是学习实模式的常用例子.它内部大致有以下寄存器: 8个16位的通用寄存器: AX (可以拆分成两个AH/AL的8位寄存器) BX (BH,BL) CX (CH,CL) DX (DH,DL) SI (source index, 源索引寄存器) DI (destination index, 目的索引寄存器) BP (base pointer, 基数指针寄存器) SP (stack pointer, 堆栈指针寄存

任务的隔离和特权级保护

任务,任务的LDT和TSS ??程序是记录在载体上的指令和数据,其正在执行的一个副本,叫做任务(Task).如果一个程序有多个副本正在内存中运行,那么他对应多个任务,每一个副本都是一个任务.为了有效地在任务之间进行隔离,处理器建议每个任务都应该具有他自己的描述符表,称为局部描述符表LDT(Local Descriptor Table).LDT和GDT一样也是用来储存描述符的,但是LDT是只属于某个任务的.每个任务是有的段,都应该在LDT中进行描述,和GDT不同的是,LDT的0位也是有效的,也可以

存储器的保护(一)——《x86汇编语言:从实模式到保护模式》读书笔记18

本文是原书第12章的学习笔记. 说句题外话,这篇博文是补写的,因为让我误删了,可恶的是CSDN的回收站里找不到! 好吧,那就再写一遍,我有坚强的意志.司马迁曰:“文王拘而演<周易>:仲尼厄而作<春秋>:屈原放逐,乃赋<离骚>:左丘失明,厥有<国语>:孙子膑脚,<兵法>修列:不韦迁蜀,世传<吕览>……”好了,不煽情了,进入正题. 第12章的代码如下. 1 ;代码清单12-1 2 ;文件名:c12_mbr.asm 3 ;文件说明:硬盘主引