第14章 存储器的保护

第14章 存储器的保护

该章主要介绍了GDT、代码段、数据段、栈段等的访问保护机制。

存储器的保护功能可以禁止程序的非法内存访问。利用存储器的保护功能,也可以实现一些有价值的功能,比如虚拟内存管理。

代码清单14-1

该章节的代码主要实现的功能就是对字符串 ‘s0ke4or92xap3fv8giuzjcy5l1m7hd6bnqtw.’ 进行排序,然后显示在页面上。

image

进入32位保护模式

话说mov ds,ax和mov ds,eax

该章节主要介绍了为什么要用 mov ds,eax 的写法。总结来看:就是可以不用生成操作尺寸反转前缀0x66,可以加快运行效率。

image

段寄存器的值传送:段寄存器(选择器)的值只能用内存单元或者通用寄存器来传送,一般的指令格式为:

mov sreg, r/m16

例如:

mov ds,ax ;寄存器ax的值传送到ds里。
  • 在实模式下,传送到DS中的值是逻辑段地址;
  • 在保护模式下,传送的是段选择子。

段寄存器的值传送机器码:在不同的操作尺寸下,老式的编译器会产生不同的机器代码。

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

有前缀的和没有前缀的相比,处理器在执行时会多花一个额外的时钟周期。

但是如果写成如下形式,那么生成的就是不加前缀的8E D8,执行上就可以少花一个额外的时钟周期了。

mov ds,eax       ;8E D8

NASM编译器: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位的写法不就好了。带着这个疑问,我修改了书中的例子,将eax换成了ax,编译后发现确实没有啥差别,而且用ax的还少了一个反转前缀0x66。

image

难道是nasm编译器优化过了?

创建GDT并安装段描述符

创建GDT:基本和前面类似,就是把16位寄存器换成了32位寄存器。

;计算GDT所在的逻辑段地址
mov eax,[cs:pgdt+0x7c00+0x02]  ;GDT的32位线性基地址 
xor edx,edx                    ;清0,被除数高位为0
mov ebx,16                     ;段地址是16位对齐
div ebx                        ;分解成16位逻辑地址商存储在eax,余数存储在edx。mov ds,eax                     ;令DS指向该段以进行操作
mov ebx,edx                    ;段内起始偏移地址......pgdt             dw 0               ;汇编地址:pgdt+0x00dd 0x00007e00      ;GDT的物理地址,汇编地址:pgdt+0x02

除法的规则:

image

安装空描述符:0#描述符是空描述符,这是处理器的要求。

;创建0#描述符,它是空描述符,这是处理器的要求
mov dword [ebx+0x00],0x00000000
mov dword [ebx+0x04],0x00000000  

创建保护模式下的数据段描述符:数据段,对应0~4GB的线性地址空间。

;创建1#描述符,这是一个数据段,对应0~4GB的线性地址空间
mov dword [ebx+0x08],0x0000ffff    ;基地址为0,段界限为0xfffff
mov dword [ebx+0x0c],0x00cf9200    ;粒度为4KB,存储器段描述符 
  • 线性基地址:0x00000000;
  • 段界限:0xFFFFF;
  • 段粒度:G=1,表示4KB。

段的粒度是以4KB(十进制数4096或十六进制0x1000)为单位,其实际使用的段界限用字节表示为:

(描述符中的段界限值+1)*0x1000-1
= 描述符中的段界限值*0x1000 + 0x1000 -1
= 描述符中的段界限值*0x1000 + 0xFFF
= 0xFFFFF * 0x1000 + 0xFFF = 0xFFFFFFFF

也就是4GB。

创建保护模式下的代码段描述符

;创建保护模式下初始代码段描述符
mov dword [ebx+0x10],0x7c0001ff    ;基地址为0x00007c00,512字节 
mov dword [ebx+0x14],0x00409800    ;粒度为1个字节,代码段描述符 
  • 线性基地址:0x00007c00;
  • 段界限:0x001FF;
  • 粒度为:G=0,表示字节。

就是当前执行代码的这个段,总共0x200字节,十进制即512个字节。

创建以上代码段的别名描述符

;创建以上代码段的别名描述符
mov dword [ebx+0x18],0x7c0001ff    ;基地址为0x00007c00,512字节
mov dword [ebx+0x1c],0x00409200    ;粒度为1个字节,数据段描述符
  • 线性基地址:0x00007c00;
  • 段界限:0x001FF;
  • 粒度为:G=0,字节;
  • TYPE位:0010,可读可写。

因为代码段描述符的TYPE位并没有可写标志,所以需要另外定义一个数据段段支持写入。

安装保护模式下的栈段描述符

mov dword [ebx+0x20],0x7c00fffe    ;基地址为0x00007c00,界限为0xffffe
mov dword [ebx+0x24],0x00cf9600    ;粒度为4KB,向下扩展
  • 线性基地址:0x00007C00;
  • 段界限:0xFFFFE;
  • 粒度为:4KB。

书中提到栈段的大小是4KB,为什么是4KB,后续 栈操作时的保护 章节有详细解释了这个问题。

设置GDT的界限:设置GDT的界限为39。因为总共5个描述符,每个描述符占用8字节,总大小40字节,界限值总大小减1为39。

;初始化描述符表寄存器GDTR
mov word [cs:pgdt+0x7c00],39      ;描述符表的界限   

安装GDT表后,内存映像如下。

image

初始化描述符表寄存器GDTR

   lgdt [cs: pgdt+0x7c00]

修改段寄存器时的保护

修改CS段寄存器:这里也是通过jmp指令进行隐式修改,和上一章不同这里增加了一个dword修饰flush标号。

;以下进入保护模式... ...
jmp 0x0010:dword flush  ;16位的描述符选择子:32位偏移

作者在书中解释了加这个 dword 的原因,总结来看就是flush标号变成了32位的,但是在这里完全没有意义。

我尝试对比两种不同的写法编译后的文件,看得就更加清晰了。

image

最后得出结论完全没有必要。

修改其他段寄存器:代码我加了一些注释

mov eax,0x0018                     ;00000000_0001_1000 3号数据段选择子,指向代码段空间
mov ds,eaxmov eax,0x0008                     ;00000000_0000_1000 1号数据段选择子,0~4GB的选择子
mov es,eax
mov fs,eax
mov gs,eaxmov eax,0x0020                     ;00000000_0010_0000 4号栈段选择子
mov ss,eax
xor esp,esp                        ;ESP <- 0

因为选择子的索引号是从第4位(位3)开始的,一开始不容易看出选择子的编号,多看几次就熟悉了。

段描述符边界检查:处理器从GDT中取某个描述符时,就要求描述符的8字节都在GDT边界之内,也就是索引号×8+7小于或等于边界。如果检测到段描述符其位置超过表的边界时,处理器中止处理,产生异常中断13。

判断条件:

索引号*8 + 7 <= 16位界限

参考下图:

image

段描述符的类别检查:载入段寄存器时,还要检查描述符的类别,比如数据段类型就不能载入CS段寄存器。

image

段描述符中的P位检查:P位表示段存在位(Segment Present),用于指示描述符所对应的段是否存在。

  • P=0:表明虽然描述符已被定义,但该段实际上并不存在于物理内存中。此时,处理器中止处理,引发异常中断11。一般来说,应当定义一个中断处理程序,把该描述符所对应的段从硬盘等外部存储器调入内存,然后置P位。中断返回时,处理器将再次尝试刚才的操作。
  • P=1:则处理器将描述符加载到段寄存器的描述符高速缓存器,同时置A位(仅限于当前讨论的存储器的段描述符)。

地址变换时的保护

代码段执行时的保护

段界限容量计算:每个代码段都有自己的段界限,位于其描述符中。实际使用的段界限,其数值和粒度(G)位有关。

  • G=0,表示字节,则实际使用的段界限为:描述符中的段界限值 字节;
  • G=1,表示4KB,则实际使用的段界限为:描述符中的段界限值*0x1000+0xFFF 字节。

假设当前代码段的粒度是4KB,那么,因为描述符中的段界限值是0x001FF,故实际使用的段界限是:

0x1FF * 0x1000 + 0xFFF = 0x001FFFFF

代码段越界检查:代码段是向上(高地址方向)扩展的,要执行的那条指令,其长度减1后,与EIP寄存器的值相加,结果必须小于或等于实际使用的段界限,否则引发处理器异常。即:

0 <= (EIP+指令长度-1) <= 实际使用的段界限

数据访问时的保护

数据段分类:数据段分为向上扩展的数据段和向下扩展的数据段。

  • 向上扩展的数据段可以是一般的数据段,也可用做栈段;
  • 向下扩展的数据段总是用作栈段。

向上扩展的数据段,代码段的检查规则同样适用于数据段,只是将代码段的指令长度换成操作数长度。

例如:

mov [0x2000],edx ;将寄存器edx的值写入内存0x2000处。

检查规则:

0 <= (EA + 操作数大小 -1) <= 实际使用的段界限
  • EA表示:Effective Address 有效地址。

屏幕显示字符:es指向0~4GB内存空间,加上0xb8000,正好是屏幕显示缓冲区所在的区域。另外这次显示字符是一行代码显示两个字符,原理都是类似的。

mov dword [es:0x0b8000],0x072e0750 ;字符'P'、'.'及其显示属性
mov dword [es:0x0b8004],0x072e074d ;字符'M'、'.'及其显示属性
mov dword [es:0x0b8008],0x07200720 ;两个空白字符及其显示属性
mov dword [es:0x0b800c],0x076b076f ;字符'o'、'k'及其显示属性

一次写入两个字符:

image

栈操作时的保护

栈段段界限:栈段也是数据段,可以向上扩展,也可以向下扩展。

  • 向上扩展:段界限的最小值等于0;最大值在段描述符中指定。
  • 向下扩展:段界限的最大值是固定的,最小值需要在段描述符中指定。

向下扩展的栈段段界限最大值:取决于D/B位,对于代码段来说是D位,对于向下扩展的栈段来说是B位。

  • B=0:表示段界限的最大值是0xFFFF;
  • B=1:表示段界限的最大值是0xFFFFFFFF。

栈段栈指针:不管是向下扩展,还是向上扩展的段都符合如下规则。

  • B=0:表示栈操作时使用栈指针寄存器SP;
  • B=1:表示栈操作时使用栈指针寄存器ESP。

实际使用的段界限:在栈段中,实际使用的段界限也和粒度(G)位相关,

  • G=0:实际使用的段界限就是描述符中记载的段界限;
  • G=1:则实际使用的段界限:描述符中的段界限值*0x1000+0xFFF。

检测规则:

  • B=0:实际使用的段界限+1 <= (SP的内容-操作数的长度) <= 0xFFFF;
  • B=1:实际使用的段界限+1 <= (ESP的内容-操作数的长度) <= 0xFFFFFFFF。

安装栈段描述符

mov dword [ebx+0x20],0x7c00fffe    ;基地址为0x00007c00,界限为0xffffe
mov dword [ebx+0x24],0x00cf9600    ;粒度为4KB,向下扩展
  • 线性基地址:0x00007C00;
  • 段界限:0xFFFFE;
  • 上下:向下扩展的段(TYPE中E=1);
  • 粒度:4KB(G=1);
  • 栈指针:ESP(B=1)
  • 段界限的最大值:0xFFFFFFFF(B=1)

计算实际使用的段界限:

0xFFFFE*0x1000 + 0xFFF = 0xFFFFEFFF

因为ESP的最大值是0xFFFFFFFF,处理器的检查规则:

0xFFFFEFFF+1 <= (ESP的内容-操作数的长度) <= 0xFFFFFFFF= 0xFFFFF000 <= (ESP的内容-操作数的长度) <= 0xFFFFFFFF

栈指针寄存器ESP的内容仅仅是在压栈和出栈时提供有效地址,操作数的物理地址要用段寄存器的描述符高速缓存器中的段基址和ESP的内容相加得到。

该栈最低端的有效物理地址是:

0x00007C00 + 0xFFFFF000 = 0x00006C00

最高端的有效地址:

0x00007C00 + 0xFFFFFFFF = 0x00007BFF

也就是说,当前程序所定义的栈空间介于地址为0x00006C00~0x00007BFF之间,大小是4KB。

如果单纯算栈段空间大小,用偏移量的最大值和最小值相减即可:

0xFFFFFFFF - 0xFFFFF000 = 0xFFF

设置栈段寄存器和栈指针

mov eax,0x0020 ;0000 0000 0010 0000, 索引号是4
mov ss,eax
xor esp,esp ;ESP <- 0,最大值 0xFFFFFFFF + 1 

处理器检查过程实例:一开始压入一个双字。

push ecx ; 压入一个双字,4字节

因为压栈操作是先减ESP,然后再访问栈,故ESP的新值:

0-4 = 0xFFFFFFFC (-1:F -2:E -3:D -4:C)

符合段界限的范围:0xFFFFF000 ~ 0xFFFFFFFF,压入的双字,其线性地址:

0x00007c00 + 0xFFFFFFFC = 0x00007BFC

image

该双字的4个字节分别占据以下线性地址:0x00007BFC、0x00007BFD、0x00007BFE和0x00007BFF。

使用别名访问代码段对字符排序

该章节就是对字符串进行排序。

字符串定义:通过string别名可以访问到字符串。

string db 's0ke4or92xap3fv8giuzjcy5l1m7hd6bnqtw.'

使用代码段别名描述符:代码段是不可以更改内容,所以用代码段的别名描述符,就是如下这个。

mov eax,0x0018  ;00000000_0001_1000 3号数据段选择子,指向代码段空间
mov ds,eax

排序逻辑:思路如下。

  • 使用冒泡排序;
  • 冒泡排序需要两个循环,都需要ECX,可以利用栈进行保存;
  • 一次读入相邻的两个字符到ax,比较ah和al,看是否交换ah和al。

image

代码我做了一些注释和说明,更加清晰。

image

其中xchg是交换指令,用于交换两个操作数的内容。

xchg r/m8, r8
xchg r/m16, r16
xchg r/m32, r32
xchg r8,m8
xchg r16,m16
xchg r32,m32

我用JavaScript编写冒泡排序,汇编的排序逻辑类似。

// JavaScript编写的冒泡排序
let arr = [11, 8, 5, 3, 5, 2, 9, 20, 1];
for (let i = arr.length - 1; i >= 0; i--) { // 外层循环 length-1 ~ 0for (let j = 0; j <= i; j++) {          // 内层循环 0 ~ iif (arr[j] > arr[i]) {let temp = arr[j];arr[j] = arr[i];arr[i] = temp;}}
}

显示最终的排序结果

     mov ecx,pgdt-string          ;循环次数就是字符串的长度xor ebx,ebx                  ;偏移地址是32位的情况 
@@4:                              ;32位的偏移具有更大的灵活性mov ah,0x07                  ;字符显示属性mov al,[string+ebx]          ;字符mov [es:0xb80a0+ebx*2],ax    ;es是0~4GB寻址,从第2行第1列开始写入inc ebx                      ;增加ebx,显示下一个字符loop @@4

为什么是从第2行第1列?

因为每行可以已显示80个字符,每个字符都有一个属性字节,所以每行总共160个字节。

所以显示第一行就是:0xb80000, 0xb8001, …, 0xb809F

第二行就是:0xb80a0, 0xb80a1, …, 0xb813F

偏移量:0xa0=160,所以就是第2行第1列了。

程序的编译和运行

image

完。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/diannao/54163.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

详解Transformer位置编码Positional Encoding

提到 Transformer&#xff0c;大家就会联想到位置编码、注意力机制、编码器-解码器结构&#xff0c;本系列教程将探索 Transformer 的不同模块在故障诊断等信号分类任务中扮演什么样角色&#xff0c;到底哪些模块起作用&#xff1f; 前言 本期基于凯斯西储大学&#xff08;CWR…

微搭低代码实战:构建个性化点餐小程序

目录 前言书籍目录第1章&#xff1a;从零开始&#xff0c;认识微搭低代码平台第2章&#xff1a;用户认证与登录体验优化第3章&#xff1a;点餐小程序功能实现&#xff0c;包括创建应用、页面布局、组件使用等。第4章&#xff1a;变量定义与初始化&#xff0c;讲解低代码开发中变…

Docker部署Joplin Server教程

Joplin Server 是 Joplin 应用的后端服务,提供笔记和待办事项的同步功能。它允许用户在不同设备之间同步笔记,同时支持多用户和协作功能。Joplin Server使用现代技术栈,数据库使用的是 PostgreSQL 。 主要功能 同步:在桌面、移动设备和网页应用之间同步笔记。多用户支持:允…

Qt QSerialPort数据发送和接收DataComm

文章目录 Qt QSerialPort数据发送和接收DataComm2.添加 Qt Serial Port 模块3.实例源码 Qt QSerialPort数据发送和接收DataComm Qt 框架的Qt Serial Port 模块提供了访问串口的基本功能&#xff0c;包括串口通信参数配置和数据读写&#xff0c;使用 Qt Serial Port 模块就可以…

在react中 使用redux

1.安装redux npm install reduxjs/toolkit react-redux 2.创建切片模块化数据 在Src目录下创建store目录&#xff0c;创建moude目录 创建tab.js import { createSlice } from reduxjs/toolkit; const tabSlice createSlice({name: tab,initialState: {Collapse: false,},re…

C# WinForm 中 DataGridView 实现单元格cell 能进编辑状态但是不能修改单元格的效果

在Windows Forms&#xff08;WinForms&#xff09;开发中&#xff0c;DataGridView 控件是一个功能强大的组件&#xff0c; 用于显示和管理表格数据。无论是展示大量数据&#xff0c;还是实现交互式的数据操作&#xff0c; DataGridView 都能提供多样的功能支持&#xff0c;比如…

CLUSTERDOWN Hash slot not served问题复现

1、一主两从 172.31.4.18:6381> cluster nodes f5e774bd5cd8f5bcba53f1297015f3245dd3f18c 172.31.4.20:638316383 master - 0 1726278342891 10 connected 5461-10922 9a79febbfb1d7a8f7a6ba3fb11e86b0f122f71e7 172.31.4.21:638616386 slave 401730ba6e1c2cd8b60243990f0…

C++设计模式(更新中)

文章目录 1、创建型模式1.1 简单工厂&#xff08;Simple Factory&#xff09;&#xff08;1&#xff09;示例&#xff08;2&#xff09;总结 1.2 工厂方法&#xff08;Factory Method&#xff09;&#xff08;1&#xff09;示例&#xff08;2&#xff09;总结 1.3 抽象工厂&…

PL/SQL程序设计入门

PL/SQL程序设计 PL/SQL起步鼻祖&#xff1a;hello World语法分析声明部分举例 应用举例 PL/SQL 起步鼻祖&#xff1a;hello World 先举个例子&#xff0c;用PL/SQL打印输出hello world declarev_string varchar2(20); beginv_string:hello world;dbms_output.put_line(v_str…

Sass实现文字两侧横线及Sass常用方案

Sass常用方案及Sass实现文字两侧横线 1.Sass实现文字两侧横线2.用Sass简化媒体查询3.使用继承占位符实现样式复用4.Sass 模块化5.lighten 和 darken 自我记录 1.Sass实现文字两侧横线 mixin 的基本作用&#xff1a; 代码复用&#xff1a;把常用的样式封装在一起&#xff0c;…

Java如何扫描指定包下所有类?

Java如何扫描指定包下所有类&#xff1f; Java8、jdk8、idea、反射、class背景 每次写算法题时&#xff0c;总觉得测试代码写起来又没营养又很麻烦&#xff0c;即便是借助junit测试框架也很麻烦&#xff0c;太重了。 正好在学习spring过程中接触到注解&#xff0c;研究其原理…

SpringCloud-04 OpenFeign服务调用与负载均衡

OpenFeign是一个声明式、模板化的HTTP客户端&#xff0c;它简化了在Java应用程序中调用RESTful API的过程。OpenFeign是Netflix开发的一个开源项目&#xff0c;它构建在Feign的基础上&#xff0c;为开发者提供了更加简单、灵活的方式来实现HTTP请求。OpenFeign的特点包括&#…

地平线秋招2025

【地平线秋招】 中秋卷起来&#xff01;&#xff01;&#xff01; 内推码 kbrfck 内推码 kbrfck 内推码 kbrfck 投递链接&#xff1a;https://wecruit.hotjob.cn/SU62d915040dcad43c775ec12c/mc/position/campus?acotycoCodekbrfck&recruitType1&isLimitShowPostScope…

深入剖析:C++类对象的内存布局与优化

深入剖析&#xff1a;C类对象的内存布局与优化 引言 在C编程中&#xff0c;理解类对象的内存布局对于优化内存使用和提高程序性能至关重要。本文将详细介绍C类对象的内存布局&#xff0c;包括数据成员、虚函数表指针以及静态变量和静态方法在内存中的位置。通过这些知识&…

【Google Chrome Windows 64 version及 WebDriver 版本】

最近升级到最新版本Chrome后发现页面居然显示错乱实在无语, 打算退回原来的版本, 又发现官方只提供最新的版本下载, 为了解决这个问题所有收集了Chrome历史版本的下载地址分享给大家. Google Chrome Windows version 64 位 VersionSize下载地址Date104.0.5112.10282.76 MBhtt…

MySQL从入门到精通二

第1章&#xff1a;MySQL基础 1.1运算符 1.1.1 算术运算符 算术运算符是MySQL中最常用的一类运算符。MySQL支持的算术运算符包括加、减、乘、除、求余。 算术运算符 符号作用符号作用加法运算%求余运算-减法运算DIV除法运算*乘法运算MOD求余运算&#xff0c;返回余数/除法运算…

Python|基于Kimi大模型,删除已上传的“指定文档”或“全部文档”(6)

前言 本文是该专栏的第6篇,后面会持续分享AI大模型干货知识,记得关注。 在本专栏上一篇《Python|基于Kimi大模型,实现上传文档并进行对话(5)》中,笔者有详细介绍“基于kimi大模型,上传指定文档并结合prompt,获取目标文本数据”。对此感兴趣的同学,可以直接点击翻阅查…

【洛谷】P3743 小鸟的设备 的题解

【洛谷】P3743 小鸟的设备 的题解 题目传送门 题解 水一道二分 qaq 刚开始考虑的是动态规划&#xff0c;但是动态规划并不能维护题目所要求的东西。所以我们将思路转向另一种求最值问题的方法&#xff1a;二分答案。 首先&#xff0c;如果一个设备在 t t t 的时间内消耗的…

Redis事件循环与网络通信机制详解

探索 Redis 底层设计:事件循环与网络通信机制 一、引言 Redis 作为一款高性能的内存数据库,其底层设计中的事件循环与网络通信机制起着至关重要的作用。这些机制使得 Redis 能够高效地处理大量的并发连接,快速响应客户端请求,并在不同的操作系统上实现良好的性能表现。 …

Java虚拟机:类的加载机制

大家好,我是栗筝i,这篇文章是我的 “栗筝i 的 Java 技术栈” 专栏的第 034 篇文章,在 “栗筝i 的 Java 技术栈” 这个专栏中我会持续为大家更新 Java 技术相关全套技术栈内容。专栏的主要目标是已经有一定 Java 开发经验,并希望进一步完善自己对整个 Java 技术体系来充实自…