段寄存器通常有CS DS SS ES,80386后引入了2个额外的段寄存器FS与GS。
大量的书籍上,都描述了段寄存器是16位的,这是一种非常不严谨的说法!
这些段寄存器除了有16位的可见部分,还有不可见的隐藏部分,称为
描述符缓存“descriptor cache”
或隐藏寄存器“shadow register”
[1]。当一个段选择符(segment selector)装入段寄存器的可见部分,处理器同时也把该段描述符的其它数据装入到段寄存器的隐藏部分,这包括段开始的基地址、段长度、访问控制信息等。这些信息缓存到段寄存器中,避免了处理器在转址(translate address)时花费额外的总线周期从段选择符表中读入数据。处理器指令中可以明示使用哪些段寄存器,这将替换掉默认使用的段寄存器。 ————维基百科
通常我们看到的段寄存器只有16位,这16位本质只是一个段选择子,更多的段描述信息需要通过该选择子在GDT全局描述符表中找到对应的描述符表项进而读取段的全部信息。
段寄存器中16位2字节段选择子的结构如下:
GDT表中64位8字节段描述符结构:
对于一个段寄存器描述的一个段,该段具有段属性Attribute、段基址Base以及段限长Limit,段寄存器中能看到的16位段选择子就是为了能够查找上述三个属性。
下面进行三个属性的基本探测,能够证明他们确实存在,而且当进行段寄存器赋值的时候,不止是简单的16位段选择子的改变,同时也涉及段属性、段基址以及段限长的改变。
Attribute
#include <iostream>
using namespace std;
int result;
int main()
{
__asm
{
mov ax, cs //cs是可读 可执行 但是不可写
mov ds, ax
mov dword ptr ds:[result], eax
}
cout << result << endl;
return 0;
}
Base
#include <iostream>
using namespace std;
int result;
int main()
{
__asm
{
mov ax, fs // 如果换成ss就是读取0地址处导致程序运行失败,fs寄存器的段基础不是0,所以能够进行读取
push gs // 保存gs的值
mov gs, ax
mov eax, gs:[0]
mov dword ptr ds:[result], eax
pop gs // 恢复gs的值
}
cout << result << endl;
return 0;
}
Limit
#include <iostream>
using namespace std;
int result;
int main()
{
__asm
{
mov ax, fs // 如果换成ss就是读取0地址处导致程序运行失败,fs寄存器的段基础不是0,所以能够进行读取
push gs // 保存gs的值
mov gs, ax
mov eax, gs:[0xffc] // fs段限长为0xfff
mov dword ptr ds:[result], eax
pop gs // 恢复gs的值
}
cout << result << endl;
return 0;
}
从段描述符的结构中,我们看到描述段限长Limit 低4字节0-15位以及高字节16-19位,但是Base Address被分为了三段低4字节16-31位、高4字节0-7位以及高4字节24-31位,这是由于历史原因,因intelCPU也是一步步发展过来的,为了能够向下兼容,所以出现了这种破碎的场景;剩下的部分都是段的描述属性了。
接下来详细介绍段描述符的各个部分属性!