我去,又又又被内存坑了!
扫描二维码
随时随地手机看文章
到底现在用的是分段还是分页?段寄存器这个东西现在还在用吗?为什么在讲到虚拟地址翻译的时候,好像跟段又没有关系了呢?
分段式内存管理
早在16位的8086时代,CPU为了能寻址超过16位地址能表示的最大空间(64KB),引入了段寄存器。通过将内存空间划分为若干个段,然后采用段基地址 段内偏移的方式访问内存,这样能访问1MB的内存空间了!在那个时候,段寄存器中存放的是段基地址,注意,是一个地址。在通过ip寄存器读取指令的时候,实际上是cs:ip,通过sp寄存器访问栈的时候,实际上是ss:sp。我看到网络上很多文章介绍分段式内存或者介绍段寄存器的时候就止步于此了,而事实上,进入32位时代后,情况已经发生了翻天覆地的变化,只讲上面这一部分内容实际上会误导很多人。变化1:在32位时代,段寄存器又增加了两个:fs、gs,这两个段寄存器有特殊用途。变化2:段寄存器里面存放的不再是段基地址,而是一个叫段选择子的东西。注意,注意,一切的变化都从这里开始。段寄存器是16位的宽度,原来这16位是个物理内存地址,但现在,它是这样一个结构:
- cs: 代码段
- ds: 数据段
- ss: 栈段
- es:扩展段
分页式内存管理
相比分段式内存管理,可能大家对分页式内存管理要熟悉的多。操作系统将内存空间按照“页”为单位划分了很多页面,这个页的大小默认是4KB(当然可以改的),各进程拥有虚拟的完整的地址空间,进程中使用到的页面会映射到真实的物理内存上,程序中使用的地址是虚拟地址,CPU在运行时自动将其翻译成真实的物理地址。现代操作系统实际情况
学完了这两种内存管理方式,很多人就要懵了:现在操作系统到底用的哪种方式?好像是分页,但为什么段寄存器好像还是有,到底是怎么一回事?先说结论,答案就是:分段 分页相结合的内存管理方式首先要明确一个前提,这一点非常非常重要:无论是分段还是分页,这都是x86架构CPU的内存管理机制,这俩是同时存在的(保护模式下),并不是让操作系统二选一!既然是同时存在的,那为什么现在将内存地址翻译时,都是讲分页,而很少谈到分段呢?这一切的一切,都是因为一个原因:操作系统通过巧妙的设置,‘屏蔽’了段的存在。操作系统怎么做到这一点的,接下来我们就来分析一下,彻底弄清楚背后的猫腻!段寄存器
让我们从段寄存器出发,在Win7 32位系统上,使用调试器(我用的WinDbg)随意调试一个程序,真的,随意,记事本、浏览器、Word,你看上谁就调试谁。在中断的上下文中看一下,程序在执行时,段寄存器里面到底装了啥?PS: 可能不同版本的Windows上面的结果不一样,但这不重要,不影响我们分析问题。只有0x001b和0x0023两个值,前面我们说了,这不是一个地址,而是一个段选择子,按照段选择子的格式展开来看一下这两个值指向的是哪个段描述符:
- cs: 001b
- ds: 0023
- ss: 0023
- es: 0023
十六进制:001b二进制:0000000000011 0 11
- 段序号:3
- 表类型:GDT
- 特权级:Ring3
十六进制:0023二进制:0000000000100 0 11也就是说,cs段指向的是GDT中的第3个表项,其他三个寄存器指向的是GDT中的第4个表项。接下来,我们来看一下这个神秘的GDT里面的内容到底是什么?很多人学了内存管理,可能还从来没看过真实的GDT里面到底是什么数据吧。GDT是位于操作系统内核地址空间中的,在Windows上有两种查看方式,一种是通过Windbg,一种是通过一些ARK工具,我这里选择使用PChunter这个神器进行查看。前面提到过,GDT中的表项是段描述符,这是一个比较复杂的数据格式,好在,这个神器对段描述符进行了解析,使用表格字段的方式进行了展示,让我们看起来轻松多了。废话不多说了,来看一下这个神秘的GDT吧:
- 段序号:4
- 表类型:GDT
- 特权级:Ring3
0x00000000
。再看它们的界限值,都是0x000FFFFF
,注意看这个界限的单位,不是字节,而是Page——页,把这个值乘以页面的大小4KB,就是0xFFFFF000
。也就说这个段的上限到了0xFFFFF000
这个页面,再把这一个页面的大小加进去,就是0xFFFFFFFF
了!所以,重点来了!看到了吗,GDT中的第3个和第4个表项所描述的这两个段,它们的基地址都是0x00000000
,整个段的大小都是0xFFFFFFFF
,这意味着什么?这意味着整个进程的地址空间实际上就是一个段!也就是说:进程的代码段、数据段、栈段、扩展段这四个段全部重合了,而且是整个进程地址空间共计4GB成为了一个段。说起来是分段,实际上等于没分了,再加上段的基地址全部是0,那进行地址翻译的时候,有没有段都没什么区别了。总结一句话:操作系统这样分段,实际上是相当于把段给架空了!以上是Windows的情况,我们再来看一下Linux情况呢。使用GDB随意调试一个ELF32的可执行文件,使用info r命令查看一下寄存器情况:十六进制:0023二进制:0000000000100 0 11
- 段序号:4
- 表类型:GDT
- 特权级:Ring3
十六进制:002B二进制:0000000000101 0 11Linux下我没有找到可以直接用什么命令或者工具查看GDT的方式(如果你知道记得一定告诉我哦),于是去源代码中寻找答案:
- 段序号:5
- 表类型:GDT
- 特权级:Ring3