zoukankan      html  css  js  c++  java
  • 存储器的保护(一)——《x86汇编语言:从实模式到保护模式》读书笔记18

    本文是原书第12章的学习笔记。

    说句题外话,这篇博文是补写的,因为让我误删了,可恶的是CSDN的回收站里找不到!哭泣的脸哭泣的脸 好吧,那就再写一遍,我有坚强的意志。司马迁曰:“文王拘而演《周易》;仲尼厄而作《春秋》;屈原放逐,乃赋《离骚》;左丘失明,厥有《国语》;孙子膑脚,《兵法》修列;不韦迁蜀,世传《吕览》……”好了,不煽情了,进入正题。

    第12章的代码如下。

    1         ;代码清单12-1
    2         ;文件名:c12_mbr.asm
    3         ;文件说明:硬盘主引导扇区代码 
    4         ;创建日期:2011-10-27 22:52
    5
    6         ;设置堆栈段和栈指针 
    7         mov eax,cs      
    8         mov ss,eax
    9         mov sp,0x7c00
    10      
    11         ;计算GDT所在的逻辑段地址
    12         mov eax,[cs:pgdt+0x7c00+0x02]      ;GDT的32位线性基地址 
    13         xor edx,edx
    14         mov ebx,16
    15         div ebx                            ;分解成16位逻辑地址 
    16
    17         mov ds,eax                         ;令DS指向该段以进行操作
    18         mov ebx,edx                        ;段内起始偏移地址 
    19
    20         ;创建0#描述符,它是空描述符,这是处理器的要求
    21         mov dword [ebx+0x00],0x00000000
    22         mov dword [ebx+0x04],0x00000000  
    23
    24         ;创建1#描述符,这是一个数据段,对应0~4GB的线性地址空间
    25         mov dword [ebx+0x08],0x0000ffff    ;基地址为0,段界限为0xfffff
    26         mov dword [ebx+0x0c],0x00cf9200    ;粒度为4KB,存储器段描述符 
    27
    28         ;创建保护模式下初始代码段描述符
    29         mov dword [ebx+0x10],0x7c0001ff    ;基地址为0x00007c00,512字节 
    30         mov dword [ebx+0x14],0x00409800    ;粒度为1个字节,代码段描述符 
    31
    32         ;创建以上代码段的别名描述符
    33         mov dword [ebx+0x18],0x7c0001ff    ;基地址为0x00007c00,512字节
    34         mov dword [ebx+0x1c],0x00409200    ;粒度为1个字节,数据段描述符
    35
    36         mov dword [ebx+0x20],0x7c00fffe
    37         mov dword [ebx+0x24],0x00cf9600
    38         
    39         ;初始化描述符表寄存器GDTR
    40         mov word [cs: pgdt+0x7c00],39      ;描述符表的界限   
    41 
    42         lgdt [cs: pgdt+0x7c00]
    43      
    44         in al,0x92                         ;南桥芯片内的端口 
    45         or al,0000_0010B
    46         out 0x92,al                        ;打开A20
    47
    48         cli                                ;中断机制尚未工作
    49
    50         mov eax,cr0
    51         or eax,1
    52         mov cr0,eax                        ;设置PE位
    53      
    54         ;以下进入保护模式... ...
    55         jmp dword 0x0010:flush             ;16位的描述符选择子:32位偏移
    56                                             
    57         [bits 32]                          
    58  flush:                                     
    59         mov eax,0x0018                      
    60         mov ds,eax
    61      
    62         mov eax,0x0008                     ;加载数据段(0..4GB)选择子
    63         mov es,eax
    64         mov fs,eax
    65         mov gs,eax
    66      
    67         mov eax,0x0020                     ;0000 0000 0010 0000
    68         mov ss,eax
    69         xor esp,esp                        ;ESP <- 0
    70      
    71         mov dword [es:0x0b8000],0x072e0750 ;字符'P'、'.'及其显示属性
    72         mov dword [es:0x0b8004],0x072e074d ;字符'M'、'.'及其显示属性
    73         mov dword [es:0x0b8008],0x07200720 ;两个空白字符及其显示属性
    74         mov dword [es:0x0b800c],0x076b076f ;字符'o'、'k'及其显示属性
    75
    76         ;开始冒泡排序 
    77         mov ecx,pgdt-string-1              ;遍历次数=串长度-1 
    78  @@1:
    79         push ecx                           ;32位模式下的loop使用ecx 
    80         xor bx,bx                          ;32位模式下,偏移量可以是16位,也可以 
    81  @@2:                                      ;是后面的32位 
    82         mov ax,[string+bx] 
    83         cmp ah,al                          ;ah中存放的是源字的高字节 
    84         jge @@3 
    85         xchg al,ah 
    86         mov [string+bx],ax 
    87  @@3:
    88         inc bx 
    89         loop @@2 
    90         pop ecx 
    91         loop @@1
    92      
    93         mov ecx,pgdt-string
    94         xor ebx,ebx                        ;偏移地址是32位的情况 
    95  @@4:                                      ;32位的偏移具有更大的灵活性
    96         mov ah,0x07
    97         mov al,[string+ebx]
    98         mov [es:0xb80a0+ebx*2],ax          ;演示0~4GB寻址。
    99         inc ebx
    100         loop @@4
    101      
    102         hlt 
    103
    104;-------------------------------------------------------------------------------
    105     string           db 's0ke4or92xap3fv8giuzjcy5l1m7hd6bnqtw.'
    106;-------------------------------------------------------------------------------
    107     pgdt             dw 0
    108                      dd 0x00007e00      ;GDT的物理地址
    109;-------------------------------------------------------------------------------                             
    110     times 510-($-$$) db 0
    111                      db 0x55,0xaa

    1.设置堆栈段和栈指针

    6         ;设置堆栈段和栈指针 
    7         mov eax,cs      
    8         mov ss,eax
    9         mov sp,0x7c00

    第7、8两行,你可能觉得有点怪异,但是这么写是可以的。关于原因,作者已经在书中说明了。

    [bits 16]
    mov ds,ax        ;8E D8
    
    [bits 32]
    mov ds,ax        ;66 8E D8
    
    mov ds,eax       ;8E D8

    以上代码每一行的注释是指令编译后产生的机器码。

    对于某些老式的编译器,在编译“mov ds,ax”这条指令时,16位和32位的编译结果是不同的:在32位模式下,会添加前缀0x66(因为编译器认为源操作数AX是16位的,所以要添加0x66以反转默认操作数的大小)。

    但是,如果添加了0x66,处理器在执行时就会多花去一个时钟周期,这样的指令又用得很频繁,所以不管是16位还是32位模式,它们被设计为相同的机器指令,都是8ED8,不需要指令前缀。可是某些编译器太固执了,它们依然会加上指令前缀0x66. 好吧,为了照顾它们,程序员想出了一个办法,就是用这样的形式:

    mov ds,eax

    你别说,还真的有效,果然生成了不加前缀的8ED8!

    说到这里,NASM编译器还是非常优秀的,至少他不会那么固执。不管处理器模式怎么变化,也不管指令形式如何,以下代码编译后都是一个结果:

    [bits 16]
    mov ds,ax       ;8E D8
    mov ds,eax      ;8E D8
    
    [bits 32]
    mov ds,ax        ;8E D8
    mov ds,eax       ;8E D8

    说了这么多,其实我就是把作者讲的内容又讲了一遍。不管你理解了没有,反正我是有点糊涂了。

    因为刚开始的这段代码,是在16位模式下执行的,编译也是按照16位来编译的,所以按照16位的写法就可以了。以下这样写,简单明了。

    7         mov ax,cs      
    8         mov ss,ax

    反汇编后,生成的机器码如下:

    可是,如果按照配书程序,那么反汇编后成了:

    看到了吗,第一行多了前缀0x66,执行时会多用掉一个指令周期。

    我个人认为,写代码用通俗的写法就好,能让人看懂的代码才是好代码。OK,这个问题就到这里,我们继续。

    2.创建GDT

    11         ;计算GDT所在的逻辑段地址
    12         mov eax,[cs:pgdt+0x7c00+0x02]      ;GDT的32位线性基地址 
    13         xor edx,edx
    14         mov ebx,16
    15         div ebx                            ;分解成16位逻辑地址 
    16
    17         mov ds,eax                         ;令DS指向该段以进行操作
    18         mov ebx,edx                        ;段内起始偏移地址
    106;-------------------------------------------------------------------------------
    107     pgdt             dw 0
    108                      dd 0x00007e00      ;GDT的物理地址
    109;-------------------------------------------------------------------------------

    第12行,就是把GDT的物理地址0x7e00传送到EAX,至于为什么给标号pgdt加上(0x7c00+0x02),相信你已经明白了,如果不明白,看看我的图。

     

    第13行到15行,其实是做除法运算,把物理地址分解为段地址和偏移地址: EDX:EAX / 16 = EAX(得到段地址) …EDX(得到偏移地址)

    第17到18行,DS:EBX就指向了GDT的开头。

    20         ;创建0#描述符,它是空描述符,这是处理器的要求
    21         mov dword [ebx+0x00],0x00000000
    22         mov dword [ebx+0x04],0x00000000  
    23
    24         ;创建1#描述符,这是一个数据段,对应0~4GB的线性地址空间
    25         mov dword [ebx+0x08],0x0000ffff    ;基地址为0,段界限为0xfffff
    26         mov dword [ebx+0x0c],0x00cf9200    ;粒度为4KB,存储器段描述符 
    27
    28         ;创建2#描述符,保护模式下初始代码段描述符
    29         mov dword [ebx+0x10],0x7c0001ff    ;基地址为0x00007c00,512字节 
    30         mov dword [ebx+0x14],0x00409800    ;粒度为1个字节,代码段描述符 
    31
    32         ;创建3#描述符,上面代码段的别名描述符
    33         mov dword [ebx+0x18],0x7c0001ff    ;基地址为0x00007c00,512字节
    34         mov dword [ebx+0x1c],0x00409200    ;粒度为1个字节,数据段描述符

    第20~30行分别创建了3个描述符,相信大家都很熟悉了。需要说明的是33~34行,创建了一个代码段的别名描述符。这样做用意何在呢?

    在保护模式下,代码段是不可写入的,所谓不可写入不是说改变了内存的物理性质,使内存写不进去,而是说通过代码段描述符访问对应的内存区域时,处理器不允许向里面写数据或者更改数据。

    但是,如果非要修改代码段,有没有办法呢?有,那就是为该代码段建立一个新描述符,比如说可读可写的数据段描述符,这样,通过这个数据段描述符,我们就可以堂而皇之地修改代码段了。像这样,当两个或以上的描述符都指向同一个段时,把另外的那些描述符就成为别名描述符。

    3.栈操作时的保护

    36         mov dword [ebx+0x20],0x7c00fffe
    37         mov dword [ebx+0x24],0x00cf9600

    第36、37行安装了栈段描述符。用我们的小程序分析一下(参见数据段描述符和代码段描述符(二)——《x86汇编语言:从实模式到保护模式》读书笔记11),结果是:

    -----------------------
    seg_base = 0X7C00
    seg_limit = 0XFFFFE
    S = 1
    DPL = 0
    G = 1
    D/B = 1
    TYPE = 6
    数据段: 向下扩展,可读可写
    ------------------------
    得知,基地址是0x7c00,描述符中的界限值是0xFFFFE,G=1,是向下扩展的可读写数据段(一般作为栈段)。

    有效界限(effective limit)

    段的有效界限取决于G标志。

    G=0:有效界限就是描述符中的界限值

    G=1:有效界限 = 描述符中的段界限值* 0x1000 + 0xFFF

    请牢记这个概念,因为我们会多次用到。

    对于下扩(E=1)数据段,有效界限指定了段中最后一个不允许访问的偏移地址。

    B=0:偏移地址的有效范围是 [有效界限+1,0xFFFF] ,为了叙述方便,这里用闭区间表示。

    B=1:偏移地址的有效范围是 [有效界限+1,0xFFFF_FFFF]

    如果要想访问向下扩展的栈段,那么SP或者ESP的值必须要在偏移地址的有效范围内。

    结合本文的代码,seg_base = 0X7C00,seg_limit = 0XFFFFE,G = 1,于是有效界限是

    0xFFFFE * 0x1000 + 0xFFF = 0xFFFF_EFFF;

    那么偏移地址的有效范围是 [ 0xFFFF_F000, 0xFFFF_FFFF]

    假设ESP的初始值为0,这时候执行 push eax, 请问合法吗?

    分析:ESP先减去4,等于0xFFFF_FFFC,然后(假如合法)EAX的值会被写入 偏移为 0xFFFF_FFFC~0xFFFF_FFFF的四个存储单元,因为这些偏移值在有效范围内,所以没有问题。

    假设ESP的初始值为1,这时候执行push eax, 请问合法吗?

    分析:ESP先减去4,等于0xFFFF_FFFD,然后(假如合法)EAX的值会被写入 偏移为 0xFFFF_FFFD~0xFFFF_FFFF,0x0000_0000的四个存储单元,因为偏移0不在有效范围内,所以会引发异常。
    在Bochs中模拟这种情况,我们发现CPU重启了。

    对于POP指令,也是这个道理。

    假设ESP的初始值为0xFFFF_FFFC,这时候执行 pop eax, 请问合法吗?

    分析:如果合法,那么偏移为 0xFFFF_FFFC~0xFFFF_FFFF的四个存储单元中的内容会传送到eax,之后ESP+4=0;显然0xFFFF_FFFC~0xFFFF_FFFF是有效的偏移,所以允许执行。如下图:

    假设ESP的初始值为0xFFFF_FFFD,这时候执行 pop eax, 请问合法吗?

    分析:如果合法,那么偏移为 0xFFFF_FFFD~0xFFFF_FFFF,0x0000_0000的四个存储单元中的内容会传送到eax,之后ESP+4=1;显然其中0不是有效的偏移,所以不允许执行。如下图:

    再回到我们的代码,因为ESP仅提供偏移地址,真正的物理地址 = 偏移地址 + 段基地址;所以,对于本代码中的栈,结合段基地址= 0x7c00,有效偏移地址= [ 0xFFFF_F000, 0xFFFF_FFFF],所以

    最低端有效物理地址 = 0x7c00 + 0xFFFF_F000 = 0x6c00(进位被丢弃)

    最高端有效物理地址 = 0x7c00 + 0xFFFF_FFFF = 0x7BFF (进位被丢弃)

    也就是说,当前程序定义的栈空间介于物理地址0x6c00~0x7bff 之间。大小为(0x7BFF- 0x6C00 + 0x01 =0x1000 )4KB;

    4.修改段寄存器时的保护

    54         ;以下进入保护模式... ...
    55         jmp dword 0x0010:flush             ;16位的描述符选择子:32位偏移
    57         [bits 32]                          
    58  flush:                                     
    59         mov eax,0x0018                      
    60         mov ds,eax
    61      
    62         mov eax,0x0008                     ;加载数据段(0..4GB)选择子
    63         mov es,eax
    64         mov fs,eax
    65         mov gs,eax
    66      
    67         mov eax,0x0020                     ;0000 0000 0010 0000
    68         mov ss,eax
    69         xor esp,esp                        ;ESP <- 0

    第55行,这条指令会隐式地修改CS;同样,会修改寄存器的指令还出现在58~68行(粗体部分)。

    以上的指令涉及所有的段寄存器,当这些指令执行时,处理器把指令中给出的选择子传送到段寄存器的选择器部分(就是16位可见部分)。但是,处理器的固件在完成传送之前,会进行如下检查:

    (1)检查索引号

    要求:段选择子中的描述符索引 * 8 + 7 <= GDT(或LDT)的界限值

    如果不符合要求,则产生异常13,同时段寄存器中的原值不变。

    (2)检查描述符的类别

    原书表12-1,我在这里绘制一份。

    Y:表示允许

    N:表示不允许

    举例:SS只允许加载可读写的数据段。

    另外,还需要注意:

    • 代码段在任何时候都是不可写的
    • 对于DS,ES,FS,GS,可以向其加载数值为0的选择子(但是访问时会导致异常)
    • 对于CS和SS,不允许向其传送数值为0的选择子

    (3)检查P位

    如果P=0,表示描述符指向的段并不存在于物理内存中。此时,处理器中止处理,引发异常。

    如果P=1,则处理器将段描述符加载到描述符高速缓存寄存器,同时置A位(仅限于当前讨论的存储器段描述符)

    本博文的内容就到这里。第12章余下的内容,请参考存储器的保护(二)——《x86汇编语言:从实模式到保护模式》读书笔记19

  • 相关阅读:
    20210329 3. RocketMQ 高级实战
    20210329 2. RocketMQ 高级特性及原理
    20210329 1. RocketMQ 架构与实战
    20210329 0. RocketMQ 安装
    20210311 java.io.Serializable
    Multi-Agent Actor-Critic for Mixed Cooperative-Competitive Environments
    Reinforcement Learning in Continuous Time and Space
    A Learning Theory for Reward-Modulated Spike-Timing-Dependent Plasticity with Application to Biofeedback
    Functional Requirements for Reward-Modulated Spike-Timing-Dependent Plasticity
    BindsNET学习系列 ——Reward
  • 原文地址:https://www.cnblogs.com/longintchar/p/5224600.html
Copyright © 2011-2022 走看看