第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…

Qt QSerialPort数据发送和接收DataComm

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

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

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

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 抽象工厂&…

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

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

【Google Chrome Windows 64 version及 WebDriver 版本】

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

STL相关简介

string 看到这个词&#xff0c;相信大家一定都很好奇什么是string&#xff0c;它有什么作用呢&#xff1f;今天&#xff0c;就让我们一起来了解一下关于string的简介吧~ 目录 string 1. 什么是STL 2. STL的版本 3. STL的六大组件 4. STL的重要性 5. 如何学习STL 6.STL的…

Unity实战案例全解析 :PVZ 植物脚本分析

植物都继承了Pants脚本&#xff0c;但是我因为没注意听讲&#xff0c;把Pants也挂在植物上了&#xff0c;所以子类的PlantEnableUpdate和PlantDisableUpdate抢不过父类&#xff0c;无法正确触发动画&#xff0c;我还找不到哪里出了问题&#xff0c;所以就使用了携程加while强行…

bpf的了解以及bpftrace的简单练习

最近接触到bpf的概念&#xff0c;简单做一些练习&#xff0c;做以下整理&#xff0c;学习笔记。 0&#xff1a;总结 使用ebpf可以实现的功能打开新世界&#xff0c;可以不改变源码的情况下&#xff0c;实现内存&#xff0c;网络&#xff0c;对应接口等各种期望内容的监控。 …

王者荣耀改重复名(java源码)

王者荣耀改重复名 项目简介 “王者荣耀改重复名”是一个基于 Spring Boot 的应用程序&#xff0c;用于生成王者荣耀游戏中的唯一名称。通过简单的接口和前端页面&#xff0c;用户可以输入旧名称并获得一个新的、不重复的名称。 功能特点 生成新名称&#xff1a;提供一个接口…

C++基础知识7 list

list 1. list的介绍及使用1.1 list的介绍1.2 list的使用1.2.1 list的构造1.2.2 list iterator的使用1.2.3 list capacity1.2.4 list element access1.2.5 list modifiers1.2.6 list的迭代器失效 2.1 模拟实现list 1. list的介绍及使用 1.1 list的介绍 1.2 list的使用 1.2.1 l…

aspcms 获取webshell漏洞复现

1.通过访问/admin_aspcms/login.asp来到后台 使用admin 123456 登录 2.点击扩展功能-幻灯片设置-保存&#xff0c;同时进行抓包 3.修改数据包中的slideTextStatus字段&#xff0c;将其更改为 1%25><%25Eval(Request (chr(65)))%25><%25 密码为a 4.访问木马的地…

面试官:讲一讲Spring MVC源码解析

好看的皮囊千篇一律、有趣的灵魂万里挑一 文章持续更新&#xff0c;可以微信搜索【小奇JAVA面试】第一时间阅读&#xff0c;回复【资料】获取福利&#xff0c;回复【项目】获取项目源码&#xff0c;回复【简历模板】获取简历模板&#xff0c;回复【学习路线图】获取学习路线图。…

wopop靶场漏洞挖掘练习

1、SQL注入漏洞 1、在搜索框输入-1 union select 1,2,3# 2、输入-1 union select 1,2,database()# &#xff0c;可以得出数据库名 3、输入-1 union select 1,2,group_concat(table_name) from information_schema.tables where table_schematest#&#xff0c;可以得出数据库中…

C++ | Leetcode C++题解之第405题数字转换为十六进制数

题目&#xff1a; 题解&#xff1a; class Solution { public:string toHex(int num) {if (num 0) {return "0";}string sb;for (int i 7; i > 0; i --) {int val (num >> (4 * i)) & 0xf;if (sb.length() > 0 || val > 0) {char digit val …

JDBC实现对单表数据增、删、改、查

文章目录 API介绍获取 Statement 对象Statement的API介绍使用步骤案例代码 JDBC实现对单表数据查询ResultSet的原理ResultSet获取数据的API使用JDBC查询数据库中的数据的步骤案例代码 API介绍 获取 Statement 对象 在java.sql.Connection接口中有如下方法获取到Statement对象…

汽车电子笔记之-013:旋变硬解码ADI芯片AD2S1210使用记录(从零开始到软件实现)

目录 1、概述 2、技术规格 3、芯片引脚 4、旋变信号格式 5、使用过程只是要点分析 5.1、程序注意点分析 5.1.1、SPI配置时序 5.1.2、问题一&#xff1a;SPI时序问题 5.1.3、问题二&#xff1a;SPI读取时序&#xff08;配置模式&#xff09; 5.1.4、问题三&#xff1a…

削峰+限流:秒杀场景下的高并发写请求解决方案

我是小米,一个喜欢分享技术的29岁程序员。如果你喜欢我的文章,欢迎关注我的微信公众号“软件求生”,获取更多技术干货! 哈喽,大家好!我是小米,一个29岁、活泼积极、热衷分享技术的码农。今天和大家聊一聊应对高并发的写请求这个主题,尤其是在大促、秒杀这种场景下,系统…