2-段寄存器

从 ds 说起

如果你稍稍懂一点汇编,当你执行下面这行代码的时候,它会把 32 位整数 5 写入到地址 0x0012f000 这个位置处。dword 就表示这是一个 double word 宽度的数,一个 word 是 16 bit.

mov dword ptr ds:[0x0012f000], 5

不要惊讶,不要因为不懂汇编而苦恼,接触多了,就会慢慢熟练。也许上面这行代码就是你在写 C 语言的时候,比如 int x = 5,在你按下 build 按钮那一瞬间,不经意间编译器就替你生成了上面这样的代码。

换过来思考一下,这个 0012f000 其实就是你在调试的时候看到的变量 x 的地址。

cpu 是真的在 0012f000 这个地址里写值吗?

提出上面的问题的时候,你会不会有些难以接受,打破了你的常识?不要着急,耐心读下去。看到那行汇编其中有一个 ds 的字样吗?为什么它缀在了地址前面?有什么作用?

ds 实际上是 CPU 中的一个寄存器(位于CPU内部的一个存储单元)。它里面保存了一些数据,其中有一项,专门保存了一个地址。当你写 ds:[0x0012f000] 的时候,实际上写的地址是把 ds 中的地址加上 0x0012f000 后的值。

简言之,你其实是在 ds.base+0x0012f000 这个地址中写入值。而 ds.base 就是我刚刚说的 ds 中存储的地址。

ds 将打开新的篇章——保护模式。

段寄存器

ds 是 CPU 中的一个寄存器,这种寄存器称为段寄存器,CPU 中还有很多其它段寄存器,他们的结构都是一样的,比如 cs、es、ss、fs、gs 等等。

如果你打开 OllyDbg,随便 open 一个 exe 文件。会看到下面这样的图,注意右侧的窗口,每个段寄存器后面还有数字。


这里写图片描述
图1 OD中观察到的段寄存器

32bit 后面的那个数字,就是我说的段寄存器中保存的那个地址。括号里的数字先不管。

段寄存器结构

  • 段寄存器的大小是 96 位

  • 段寄存器结构可以抽象成以下结构

struct SegMent {
    WORD selector;
    WORD attribute;
    DWORD base;
    DWORD limit;
}
  • selector: 首先可见部分16位对应上面SegMent的selector成员。在 OD 中,可以看到段寄存器后面就跟着一个数字,比如 ds 后面的 0023。而 0023 后面的部分就是,剩余部分不可见部分,不过 OD 也给我们展示出来了。

  • attribute: attribute 属性记录了该段是否有效,是否可读写等权限。如果往一个不可写的段执行写数据,会报异常。

测试用例1:

int main() {
    int var = 0;
    __asm {
        mov ax, ss
        mov ds, ax // 将 ss 段选择子代入 ds 段寄存器
        mov dword ptr ds:[var], eax // 执行成功!
    }
    return 0;
}

测试用例2:

int main() {
    int var = 0;
    __asm {
        mov ax, cs
        mov ds, ax // 将 cs 段选择子代入 ds 段寄存器
        mov dword ptr ds:[var], eax // 执行失败!因为 cs 段是不可写的段
    }
    return 0;
}
  • base: 通常来说,地址0是不可读写的。下面的代码却发现地址0仍然可以读写,原因是gs:[0]的base并不是0,而是0x7ffdf000,这样最终的线性地址为0x7ffdf000,这个地址的内容是可读的。

测试用例:

int main() {
    int var = 0;
    __asm {
        mov ax, fs
        mov gs, ax // 将 fs 段选择子代入 gs 段寄存器。注意不能代入到 ds,否则会编译失败。
        mov eax, gs:[0] // 执行成功!
    }
    return 0;
}
  • limit: limit 表示段界限,如果在超出了段界限进行读写,会报错。下面的代码会报错,因为 fs 段界限是 0xfff,如果尝试去读0x1000位置的数据,会报异常。

测试用例:

int main() {
    int var = 0;
    __asm {
        mov ax, fs
        mov gs, ax // 将 fs 段选择子代入 gs 段寄存器。注意不能代入到 ds,否则会编译失败。
        mov eax, gs:[0x1000] // 执行失败!
    }
    return 0;
}

段寄存器数据来源

我们在执行mov ds, ax这样的指令的时候,明明 ax 只有 16 位,可是,段寄存器却有96位?这又是怎么回事?

剩下的 80 位肯定来自于某个地方。

这里将引入 GDT 表和 LDT 表的概念。下一篇重点讲解。这里暂时先了解下。

GDT 表是全局描述符表,LDT 表是局部描述符表。当我们写段寄存器的时候,只给了16位,剩下80位并未给出,其实这80位的数据将通过查 GDT 表或者 LDT 表来获得。GDT 表和 LDT 表实际上就是一个大数组,数组中的每一项占用 8 个字节。

当填写段寄存器的时候,给出的16位中包含了3部分的信息

  1. 是要查GDT表还是LDT表;
  2. 要查的信息在GDT表或LDT表中的索引号;
  3. RPL,这个暂时不讨论。

当找到我们需要的描述符(GDT或LDT中的某一表项)后,我们把这个描述符拆解成3个部分,分别是 attribute, base 和 limit。

其中 attribute, base 各占用2字节和4字节,共48位。

剩余32位用 limit 填充。

总结

好了,到此为止,你只需要知道,隐藏的那 80 位来源于GDT就够了。

已标记关键词 清除标记
相关推荐
©️2020 CSDN 皮肤主题: 游动-白 设计师:白松林 返回首页