保护模式-段寄存器

目录

思考

逻辑地址、线性地址、物理地址

什么是段

什么是段寄存器

段寄存器的结构

不可见部分的探测

段基址的探测

段限长的探测

访问权限的探测


思考


以下的汇编指令中的ds:[0x12345678]表示什么?

mov eax, dword ptr ds:[0x12345678]

逻辑地址、线性地址、物理地址


在x86架构中,逻辑地址、线性地址和物理地址之间存在着一定的关系,这些地址的转换关系如下:

  • 逻辑地址(Logical Address):逻辑地址是由程序生成的地址,包括段选择符(Seg.Selector)和偏移量(Offset)两部分。段选择符(Seg.Selector)用于指定段描述符(Seg.Descriptor)在全局描述符表(GDT-Global Descriptor Table)或者局部描述符表(LDT-Local Descriptor Table)中的索引,偏移量(Offset)用于指定段内的偏移地址。由于逻辑地址是相对于段的,所以需要通过段基址和偏移量进行相加才能得到线性地址。如Intel白皮书所示:

  • 线性地址(Linear Address):线性地址是在x86架构中引入的概念,它是指逻辑地址经过分段机制转换后得到的地址。线性地址可以看作是逻辑地址到物理地址转换的中间结果,它是相对于整个虚拟地址空间的。
  • 物理地址(Physical Address):物理地址是指内存中实际的地址,是CPU通过地址总线发送给内存控制器访问内存的地址。线性地址通过页目录表、页表和页内偏移量进行转换得到物理地址,它指向实际的物理存储单元。需要注意的是,在x86-64架构中,由于采用了平坦模型和64位的地址空间,物理地址和线性地址是一一对应的,因此不需要进行分页转换。

ds:[0x12345678]中的ds就是一个段寄存器,它存储了一个段选择符。0x12345678是一个偏移量,它指定了该段内的偏移地址。


什么是段


在x86架构中,内存被划分为多个段(Segment),每个段的大小和属性都不同。段是一段连续的内存区域,它可以包含代码、数据、堆栈等信息。通常,一个段的起始地址和大小通过段描述符来描述,段描述符是一个数据结构,包含了该段的属性信息,如访问权限、基地址、段长等。

什么是段寄存器

在x86架构中,段寄存器用于确定CPU在访问内存时需要操作的段。段寄存器存储着当前正在使用的段选择符,段选择符是一个16位的值,它包含了该段的段描述符在GDT(全局描述符表)或LDT(局部描述符表)中的偏移量,以及该段的特权级。通过段选择符,CPU可以找到对应的段描述符,从而获得该段的基地址和属性信息,之后CPU使用这些信息来访问内存,从而实现内存的保护和隔离。

以下是x86架构中常见的段寄存器:

CS(Code Segment) 代码段寄存器(用于访问程序的指令)。
DS(Data Segment) 数据段寄存器(用于访问全局变量、静态变量、常量、堆等数据结构)。
ES(Extra Segment) 附加段寄存器(在一些字符串操作指令,如MOVS、LODS、STOS、CMPS等中,用于访问目标字符串的数据段)。
SS(Stack Segment) 栈段寄存器(用于访问函数的局部变量、函数的参数、返回地址等)。
FS(F Segment) FS段是为了支持某些操作系统(例如Windows)的特殊需求而增加的。当在Windows用户态下FS段寄存器用于访问前线程的TEB数据结构(Thread Environment Block),而在内核态下用于访问当前线程的KPCR数据结构(Kernel Processor Control Region)。
GS(G Segment) GS段是同样为了支持某些操作系统的特殊需求而增加的,并没有处理器定义的目的,而是由操作系统运行它来实现目的,Windows系统中并没有使用GS段。

段寄存器的结构

每个段寄存器都由“可见(Visible Part)”部分和“不可见(Hidden Part)”部分组成,而这个可见的部分就是前文经常提到的段选择符(Segment Selector)。当段选择符被加载到一个段寄存器的可见部分时,CPU也通过该段选择符所指向的段描述符获取了这个段寄存器的不可见信息:段基址(Base Address)、段限长(Limit)和访问权限(Access Infomation)

段寄存器共有96位,其中可见部分的段选择符占16位,不可见部分的段基址占32位、段限长32位、访问权限16位。

不可见部分的探测

我们使用OD任意附加一个程序,可以看到除了fs段的基址和限长比较特殊外,其余的段基址都为0限长为0xFFFFFFFF。如图所示:

段基址的探测

编写以下测试代码:

#include "stdafx.h"
#include "Windows.h"

int a = 0;
int main(int argc, _TCHAR* argv[])
{
	__asm
	{
		//mov ax,fs;	
		//mov ds,ax;    //让ds等于fs
		mov ebx,dword ptr ds:[0];    //取0偏移处地址的值赋给ebx
		mov ax,es;
		mov ds,ax;    //还原ds(es的值一般与ds的值相同,这里用es帮助ds还原)
		mov dword ptr ds:[a],ebx;    //赋值a等于ebx(0偏移处地址的值)
	}
	printf("%x",a);
	system("pause");
	return 0;
}

众所周知的是,正常情况下当代码运行至mov ebx,dword ptr ds:[0]时是一定会报错的,因为0地址通常是被保留的地址。运行结果如图所示:

可是当我们打开代码中的注释,重新运行时会发现可以正常打印输出一个值,如图所示:

而第二次能够正常执行的原因就是因为fs段与ds段的段基址不同。当我们第一次运行时,由于ds.base + 0 = 0,所以无法正常访问。但是第二次运行时,ds段先被修改为等于fs段,等再执行到mov ebx,dword ptr ds:[0]时,实际上相当于访问了fs.base + 0。由于fs.base不为0,所以能够顺利运行。

段限长的探测

编写以下测试代码:

#include "stdafx.h"
#include "Windows.h"

int a = 0;
int main(int argc, _TCHAR* argv[])
{
	__asm
	{
		mov eax,dword ptr fs:[0xFFC];//0xFFC 0xFFD 0xFFE 0xFFF
		mov eax,dword ptr fs:[0xFFD];//0xFFD 0xFFE 0xFFF 0x1000
	}
	printf("%x",a);
	system("pause");
	return 0;
}

当代码运行至mov eax,dword ptr fs:[0xFFD]时出现内存访问错误,如图所示:

这是因为当在0xFFD偏移处读取4字节的数据时,需要访问至0x1000偏移处,已经超出了fs段的限长,而在0xFFC偏移处读取4字节的数据时,只会访问到0xFFF偏移处,并没有超出0xFFF的段限长。

访问权限的探测

编写以下测试代码:

#include "stdafx.h"
#include "Windows.h"

int a = 0;
int main(int argc, _TCHAR* argv[])
{
	__asm
	{
		mov dword ptr ds:[a],1;    //给a赋值
		mov ax,cs;    
		mov ds,ax;    //让ds等于cs
		mov dword ptr ds:[a],2;    //给a赋值
	}
	printf("%x",a);
	system("pause");
	return 0;
}

 当代码运行至mov dword ptr ds:[a],2时出现内存访问错误,如图所示:

 这是因为cs段不具有写的权限,所以在第二次赋值的过程中出现了错误。

猜你喜欢

转载自blog.csdn.net/weixin_43074760/article/details/131743110