从 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部分的信息
- 是要查GDT表还是LDT表;
- 要查的信息在GDT表或LDT表中的索引号;
- RPL,这个暂时不讨论。
当找到我们需要的描述符(GDT或LDT中的某一表项)后,我们把这个描述符拆解成3个部分,分别是 attribute, base 和 limit。
其中 attribute, base 各占用2字节和4字节,共48位。
剩余32位用 limit 填充。
总结
好了,到此为止,你只需要知道,隐藏的那 80 位来源于GDT就够了。