[转载]segmentation 情景分析

用户程序 A 君,是某酒店公寓的新住户,该公寓有若干层,每层有若干寓所,还包括公用的日用百货仓库间,会所等公共设施,是一座自助式的电子化的公寓大厦。

这一天是用户程序 A 君,新入住公寓的日子,管理员 OS 交给了 A 君自己寓所的电子门匙,A 君要凭这条电子门匙在公寓的管理系统里找到自己的寓所。

5.1、寻找 room 的 key:segment registers 和 selectors

----------------------------------------------------------------------------------------
OS 载入用户程序 A,A 从 OS 里接过控制权,A 找到自己的执行代码然后跳转到自己的代码执行,OS 通过 call 跳转到用户程序 A 执行。
----------------------------------------------------------------------------------------

(1)用户程序 A 君从管理员 OS 接过属于自己的 key,A 君用这把电子门匙在公寓管理系统的检索模块进行验证,管理系统正在验证匹配中... ...
电子门匙内部是一块芯片,芯片存储着这条 key 对应的寓所的相关信息。管理系统从这条电子门匙中读取信息... ...

5.1.1、开启 room 的 key:  segment registers(CS、DS、SS、ES、FS & GS)

segment registers 事实上就是这把电子门匙,不过 segment registers 仅仅是个载体,即:segment registers 是存储相关的 descriptors 信息的载体,光有载体而失去里面的 descriptors 信息,这把门匙等于是没用。

segment registers 是个 16 位宽的 registers,segment registers 里装载的是 selector 以及相应的 descriptors 信息。x86 体系使用分段式的内存管理机制,将线性的内存区域分成若干个区域,这个区域的起始地址,区域的大小及相关的属性信息将用一个 descriptors(描述符)的结构存储起来。

5.1.2、 key 里的索引信息: selectors

前面已经提过,光有 segment registers 这个载体是不够的,这个载体首先要存储索引信息,这就叫做 selector。通过 selectors 索引子到出相关的描述段结构的信息 descriptors (描述符)

看一看 selector 的结构:

16 位宽的 selector 分成三个域:

0000000000000  X   XX
---------------  -   ---
       |              |     |
       |              |     +------------>  RPL (Requestor Privilege Level)
       |              |
       |              +------------------> TI ( Table Index)
       |
       +-----------------------------> SI (Selector Index)

RPL (Requestor Privilege Level):实际上 RPL 的作用是构建一个集线器部件。
TI ( Table Index):指引在 GDT 表还是在 LDT 中寻找 descriptors,0 ---  GDT    1 --- LDT
SI (Selector Index):这是真正的索引子,这个域形成一个数值,在相应的 descriptor table 中接这个数值寻找。

情景提示:
x86 的权限级别分为 0 ~ 3 级,共 4  个权限级别。对于 segment 级保护措施来说,0 级为最高权限级别,依次是1、2 级,最低是 3 级。
对于 page 级的保护措施来说,只有 2 个级别:0 ~ 2 属于 supervisor 级别,3 级则属于 user 级。

用户程序通过使用不同的 RPL 来访问不同权限的代码段:
   
                              +--------->  selector A(0x11) -----+
                              |                                                   |
                              |                                                   |
user A ------------>  +---------> selector B (0x10) -----|  ----+
                              |                                                    |        |
                              |                                                    |        |
                              +----------> selector C (0x13) -----+        |
                                                                                             |
                                                                                             |
                                                                                             |
                                                         +------------------------+
                                                         |   
                                                         |
                                                         |
                                                         V
           --------------------------------------------------
                  code 3
           --------------------------------------------------
                  code 2
           --------------------------------------------------
                  code 1
           --------------------------------------------------
                  code 0

selector A 是 0x11,表示: RPL = 1,TI = 0,SI = 2
selector B 是 0x10,表示: RPL = 0,TI = 0,SI = 2
selector C 是 0x13,表示: RPL = 3,TI = 0,SI = 2
code 3 ~ code 0 分别是运行在权限级别为 3 ~ 0 的代码

这 3  个 selector 都是在 GDT 表上索引第 2 个 descriptor,所不同的是各自使用了不同的请求权限。

若当前 CPL = 2:
(1) 用户代码 A 可以使用 selector A 和 selector B 通过 call/jmp 来访问 code 2 ,效果是完全一样的。
(2) 用户代码 A 可使用 selector A、selector B 以及 selector C 通过 3 级的 gate 来访问 code 0 与 code 1 代码。
(3) 用户代码不能使用这 3 个 selector 来达到访问 code 3 代码。

使用同样的 descriptor index 但使用不同 RPL 的 selector 达到访问不同权限级别的代码。

特别的 selector: NULL selector

当 TI = 0,意味着将 GDT 寻找到 descriptor,当 RPL = 0 以及 SI = 0 时,即 selector 等于 0 时,这是一个特别的 selector,被称为 NULL selector。

NULL Selector:
在 GDT 表的第一个 entry,即索引为 0 的第 1 个 descriptor,这个 descriptor 是个无效的不可用的 descriptor,设计这样一个 selector 的目的,是防止 0 值被加载到 CS 和 SS。
当一个 0 值被加载到 CS 和 SS 时,将会产生 #GP 异常。

实际上,当 SI = 0、TI = 0 时,但 RPL 不等于 0,即 0x00 ~ 0x03 的 descriptors 也是 NULL selectors ,这样的 Null Selector 被加载到 CS 和 SS 同样会产生 #GP 异常。

5.1.3、 descriptor 的载体: segment register

前面已经提到:segment registers 相当于 key,而这把 key 内信息才是最重要的,才是最终的目的。

segment register 的结构:

segment register 在物理上分为 2 部分:用户可见的 selector 部分和用户不可见的隐藏部分,用户可见的 selector 为 16 位宽,这个 selector 就是被加载进 segment register 的 selector。
隐藏部分仅对 processor 可见,这部分存储着被加载的 descriptor 信息,包括:base、limit 以及 attribute 部分。

base 是 64 位宽,但在 x86 下 base 高 32 位不可用,意即为 32 位宽。limit 固定为 32 位宽,在 x64 下 limit 是无效的, limit 仅在是可用的,所以 limit 固定为 32 位。
接下来,attribute 推测为 16 位宽,Intel 和 AMD 都没明显说明 attribute 是多少。

descriptor 的载体:

当 selector 加载进 segment registers,processor 根据 Selector.TI 在 GDT 或者 LDT 搜索对应的 Descriptor,将 Descriptor 的相应部分加载到 segment register 的隐藏部分。

以 CS 为例:
当 selector 0x13 被 load 到 CS,Selector.TI = 0、Selector.SI = 2,那么 processor 在 GDT 中寻找第 2 个 descriptor,当通过了权限检查后,这个 descriptor 的 base、limit 及 attribute 被相应加载到 CS.base、CS.limit 及 CS.attribute 域。CS.selector 保持不变。

情景提示:
在实模式下,相关的保护模式的数据结构不可用,descriptors、GDT 等数据结构是不存在的。当 selector 0x13 被 load 到 CS 时,processor 作如下处理:
CS.base = selector << 4,selector 左移 4 位加载到 base 域,即 base 变成了 0x00000130,Selector.RPL 及 Selector.TI 是不可用的。

这就是实模式分段管理模式的典型内存寻址:段寄存器左移 4 位 + offset。

有一点值得注意的是:
descriptor 里描述的 limit 只有 20 位宽,在加载到 segment register 里时,limit 被扩展到 32 位宽后再加载到 segment register 里。
具体的扩展措施是:descriptor 的 G 标志位代表粒度,G = 1 表示 limit 域的粒度为 4K (4096 bytes),G = 0 表示 limit 域的粒度为 1 byte。因此 G = 1 时:32 位 limit = 4096 * limit + 0xFFF,G = 0 时:32 位 limit = 1 * limit。

5.2、 descriptors 是怎样描述 segment 信息的?

descriptors 描述符,顾名思义就是描述 segment 相关信息的数据结构,定义了 segment 的基址,长度、类型及相关属性。不同类型的 descriptors 有不同的意义。
总体上有 2 种类型的 descriptors:user 描述符和 system 描述符。由 descriptors 结构的 S 标志位指出。user 描述符就是指基本的 segment descriptors,system 描述符包括有 LDT descripts、TSS descriptors 和各种 Gate descriptors。

5.2.1、理解 descriptors 结构是很容易的,基本上 descriptors 结构描述的无非就是以下几个信息:

(1) segment 的基址,即 base
(2) segment 的长度,即 limit
(3) segment 的类型,即 S(System 标志位)。
(4) segment 的具体类型,在确定 descriptors 是 System 还是 user 后,还要定义具体的类型,如:code segment / date segment、LDT / TSS 还是 Gate(什么类型的 gate)。
(5) segment 的属性,接下来就是 descriptors 相关的属性,如:G(粒度标志位)、D/B(缺省 operand)、L(long mode)等
(6) 最后就是访问该 segment 的权限,即 DPL(Descriptor Privlilege-Level)

下面是 x86 下的 segment descriptor 结构:

例 5.2.1.1

struct x86_descriptor_struct {

/**** descriptor 的前 4 字节 ****/
        int limit_lo : 16;                 /* low 2 bytes of segment limit */
        int base_lo : 16;           /* low 2 bytes of segment base */

/**** descriptor 的后 4 字节 ****/
        int base_3rd : 8;          /* 3rd byte of segment base */
        int type : 4 ;                 /* descriptor type */
        int S : 1;                       /* System flag bit */
        int DPL : 3;                   /* Descriptor Privilege Level */
        int P : 1;                       /* persent bit */
        int AVL : 1;                   /* avallable bit */
        int limit_hi : 4;              /* hight one byte of segment limit */
        int L : 1;                     /* x86: reserved bit,  x64 : L bit : long bit */
        int D : 1;                       /* Default bit */
        int G : 1;                       /* Granularlty bit */
        int base_hi : 8              /* hight one byte of segment base */
};

5.2.2、descriptor 结构的 size

这个也很好理解:
(1) base 是 32 位
(2) limit 是 20 位(前面已经说过 descriptors 结构里的 limit 域是 20 位)
(3) S 标志位占 1 位 加上相关的属性及类型

相信已经道结果了:x86 下的 descriptors 结构是 8 个字节,即 64 位,因为 base 已经是 4 个字节了,加上 limit  及其它域就是  8 个字节。这样 descriptors 结构是 8 bytes 边界对齐的。

那么,在 x64 的 long mode 下情况稍有些复杂:

(1)由于 long mode 下采用了单一内存管理模式,有效地屏蔽了 segmentation 分段机制的内存管理模式,base、limit 及相关的属性都是无效的,因此,在 long mode 下,user 描述符,即 segment descriptors 结构依旧是 8 bytes 长。

(2)对于 System 描述符,由于 base 需要指出具体值,因此,long mode 下,System 描述符是 16 bytes 长的。典型的如:Call Gate descriptors 结构,需要指出具体 64 位的 offset 值 及 16 位的 selector 值,因此它是 16 bytes 边界对齐的。

5.2.3、 segment 的 base
x86 下的 descriptor 描述 segments base 是 4 bytes 32 位地址。前面提到过 x86 下 descriptors 结构是 8 bytes 64 位结构。
由例 5.2.1 可知,4 bytes 的 segment base 分布在 descriptor 的 8 bytes 空间里的第 3 字节 ~ 第 5 字节以及第 8 个字节,两部分组成 4 个字节。

在 x64 long 模式下的 segment descriptor 它的 base 是无效的,因此,它不描述的 segment base。

5.2.4、 segment 的 limit
同样,segment limit 仅在 x86 & x64 的 32 位环境中才有效,在 long mode 是无效的。前面已经提到:limit 在 descriptor 结构是 20 bit,它如何描述 segment 的 32 位长度呢?

(1)G =1: limit = 4096 * limit + 0xFFF
(2)G = 0: limit = 1 * limit

最终,形成的 32 位的 limit 会被加载到 segment register 的 limit 域里。

5.2.4.1、processor 是怎样检查 segment 的 limit

对于 CS 来说,它描述的 segment 是 Expand-Up 类型的。那么,对于 Expand-Up 类型的 segment 来说,检查 limit 是否超限相对简单。

情景提示:
当 G = 0 时:segment 的范围是:base ~ base + limit
当 G = 1 时:segment 的范围是:base ~ base + limit * 4096 + 0xFFF

这里有 2 个比较特殊的情况:
(1)、当 G = 0 时,若 limit = 0,那么 segment 的宽度是 1 byte,segment 范围是:base ~ base + 1
(2)、当 G = 1 时,若 limit = 0,那么 segment 的宽度是 0xFFF, segment 范围是:base ~ base + 0xFFF

对于描述 Expand-Up 类型的 segment descriptor,它描述的 segment 的宽度是在粒度内,因此:当 G = 0 时,segment 的宽度 1 byte,而 G =1,segment 宽度是 4095,也就是 0xFFF。

5.2.4.2、 下面解释一下为什么 G = 1 时要加上 0xFFF 这个值

以 descriptor 20 位的 limit 饱和时:limit = FFFFF 为例,若 G = 1,那么此时,它是描述饱和的 32 位空间,即 4G 空间。

看一看:
limit = 4096 * limit = 1000h * limit,也就是 limit << 12  结果是:FFFFF000,此时的还不足描述 4G 空间,因此必须加上 0xFFF 这个值,类似补码的特征。
此时:FFFFF000 + FFF = FFFFFFFF 这就能表示完整的 4G 空间。

5.3、 descriptors 存放的地方:descriptor table

没错!若干个 descriptors 存放在一个连续的空间,形成数组形式的结构,这个数组就被称为 descriptor table。以前面例 5.2.1.1 定义的 descriptor 结构来说,那么,descritptor table 结构就是这样的:

descriptor table 结构:

struct x86_descriptor_struct  dtable[n];      /* descriptor table 结构*/

上面定义了 descriptor table 结构。

5.3.1、 descriptor table 的类型

有三种类型的的 descriptor table:GDT(Global Descriptor Table)、LDT(Local Descriptor Table)以及 IDT(Interrupt Descriptor Table)。这三种 descriptor table 能容纳的 descriptors 类型是不同的。
GDT 容纳所有类型的 descriptors,包括:segment descriptor(code segment & data segment)、system descriptors(LDT descriptor、TSS descritpor、所有的 gate descriptor)。
LDT 容纳 segment descriptor 以及 gate descriptor。
IDT 仅容纳 interrupt gate descriptor、trap gate descriptor 和 task gate descriptor,但不包含 call gate descriptor。

5.3.2、 descriptor table 的容量

descriptor table 的 base 存储在相应的 descriptor table register 中:

(1) GDT base 存放在 GDTR 中
(2) LDT base 存放在 LDTR 中
(3) IDT base 存放在 IDTR 中

同时,descriptor table register 还存储着 descriptor table 的 limit 值。descriptor table 的 limit 值是 16 位,即值为:FFFFh。

情景提示:
(1)selector 中有 13 位是表示 index 值,1111111111111b = 1FFFh = 8191,也就是说:index 的范围是 0 ~ 8191,共可以寻址 8192 个 descriptors。

(2)另一个角度来看:descriptor table register 中的 limit 是 16 位,值为 FFFFh,这是一个字节值。在 x86 下 descriptor 的 size 是 8 bytes,那么它容纳的 descriptors 数是: FFFFh / 8 = 1FFFh = 8191,也就是 FFFFh >> 3 = 1FFFh 刚刚好是:8191,这和 selector 中的最大可容纳的 index 值是一样的。

(3)在 x64 的 long 模式下,system descriptor 和 gate descriptor 是 16 bytes。那么,它能容纳的 descriptors 数是:FFFFh / 16 = FFFF >> 4 = FFFh = 4095,selector 仅能索引到 4095,若是 selector 的 index 为 4096 时将产生越界,引发 #GP 异常。

以 GDT 为例,加载 base 到 GDTR 的指令是 LGDT,指令形式是 LGDT dword ptr [dtr]。这个 dtr 地址处存放着 descriptor table 的 base 和 limit 值。低 16 位是 limit,高 32 位是 base 值。

(2)... ... 管理系统从 key 中读取出信息,用户程序 A 君使用的 key 是 0x1b 号,管理系统需要在数据库里检索房号为 0x1b 的 room 的具体位置。 管理系统正在检索中... ...

-------------------------------------------------------------------------------

GDTR 与 GDT 构建成公寓的数据库系统,descriptors 是数据库里的数据信息。管理系统就是从 GDTR 与 GDT 里检索 seleoctor 为 0x1b 的 descriptor 数据信息。

在用户程序中使用了指令 call 0x1b:0x00401000 进行接收控制权,执行自己的任务... ...

5.4、 查找 descriptor

selector 0x1b 的结构是:

0000000000011  0  11
---------------  -  --
           |          |   |
           |          |   |
           |          |   +-------> DPL = 3
           |          |
           |          +----------> TI = 0
           |
           +------------------> SI = 3

使用这个 selector 的结果是:以 DPL = 3 的权限,在 GDT 中寻找编号为 3 的 descriptor,

gdtr.base = 0x800f3000   gdtr.limit = 0x3ff

这是一段截取 xp 启动时瞬间情况。

5.4.1、 查找 descriptor 的规则。

下面是 C 代码表示:

1、获取 descriptor table base

/*
*  假如 selector 的 TI 为 0 时,在 gdt 中索引。
*  假始 selector 的 TI 为 1 时,则在 LDT 中索引。
*/

if (selector.TI  == 0)
        base = gdtr.base;
else
        base = ldtr.base;

2、索引 descriptor 地址

/*
*  根据 selector 的 SI (索引值)在 base 索引。
*  索引因子是 sizeof(descriptor),x86/x64 的 segment descriptor 是 8 个字节的
*  所以,等于乘上 8
*/

descriptor = base + selector.SI * sizeof(descriptor);      /* base + SI * 8  */

从更高级的 C 语言层面来描述是:

struct x86_descriptor_struct      descriptor_table[gdtr.limit];     /* 定义 descriptor table 结构 */

descriptor = descriptor_table[selector.SI];                               /* 获取 descriptor 结构 */

5.4.2、 加载 descriptor

当获得相应的 descriptor 后,processor 会将 descirptor 信息加载到相应的 segment registers 中。

以下是默认的 descriptor 加载到相应的 segment registers 列表。

code segment descriptor     ----->  cs
data segment descriptor     ----->  ds
data segment descriptor     ----->  ss
data segment descriptor     ----->  ss
data segment descriptor     ----->  fs
data segment descriptor     ----->  gs

前面已经提过:segment registers 分部两部分,一个是可见的 selector 部分,另一个是仅对 processor 可见的隐藏部分。因此 descriptor 被找到之后,通过仅限及相关的检查之后,会被 processor 加载到 segment registers 的隐藏部分。

正如所料:
1、descriptor 的 selector 是 segment register 的 selector 域。
2、descriptor 的 base 被加载到 segment register 的 base 域。
3、descriptor 的 limit 被扩屋到 32 位后,加载到 segment register 的 limit 域。
4、最后 descriptor 的相关 attribute 加载到 segment register 的 attribute 域。

segment register 的 base、limit 以及 attribute 域构成 segment register 的隐藏部分。这部分信息在下一次加载之前是保持恒值的。使用同样的 selector 来获取数据,无须做多次的 segmentation 转换过程。

下面是 descriptor 的寻扯过程示意图:

base+SI*8
selector ---------->  GDT  -------------> descritptor  -------> segment register
            |   TI = 0            ^  
            |                        |
            |                        |
            +--------> LDT  --+
               TI = 1

5.5、  Linear Address 的产生

selector : offset  这种形式的地址被 Logic Address(逻辑地址)。Logic Address(逻辑地址)和 Linear Adress(线性地址)都应该被称为 Virtual Address(虚拟地址)。Logic Address 经过 segmentation(分段机制)转换为 Linear Address。
逻辑地址中的 offset 值实际上也被称为 Effective Address(有效地址)。

Linear Address 的形式是:LA = base + offset    或表达为: LA = base + EA。 在 32 位的地址空间中,linear address 的表达范围是:0x00000000 ~ 0xFFFFFFFF 共 4G 的地址空间。在 64 位地址空间范围是:0x00000000_00000000 ~ 0xFFFFFFFF_FFFFFFFF。

如前所述:linear address 的 base 地址是由 selector 寻址 descriptor 获取。

因此:
Logic Address 形式中的 selector 索引到相应的 descriptor,从而获得 base 地址值,再加上 Logic Address 形式中的 offset 值,就产生了 Linear Address 。

下面是 Linear Address 产生的示例图:

selector : offset
-------    ------
    |           |
    |           +------------------------------+--------------> linear address
    |                                                       |
    |                                                       |
    +--------> descriptor ------> base ------+

当第 1 次加载 descriptor 后,在 selector 不变的情况下,linear address 的产生已经成为 cs.base + offset 或 ds.base + offset ...

时间: 2024-11-02 04:53:25

[转载]segmentation 情景分析的相关文章

Linux内核源代码情景分析-内存管理之slab-回收

在上一篇文章Linux内核源代码情景分析-内存管理之slab-分配与释放,最后形成了如下图的结构: 图 1 我们看到空闲slab块占用的若干页面,不会自己释放:我们是通过kmem_cache_reap和kmem_cache_shrink来回收的.他们的区别是: 1.我们先看kmem_cache_shrink,代码如下: int kmem_cache_shrink(kmem_cache_t *cachep) { if (!cachep || in_interrupt() || !is_chaine

Linux源代码情景分析读书笔记 物理页面的分配

函数 alloc_pages流程图 Linux源代码情景分析读书笔记 物理页面的分配,布布扣,bubuko.com

Linux内核源码情景分析-系统调用

一.系统调用初始化 void __init trap_init(void) { ...... set_system_gate(SYSCALL_VECTOR,&system_call);//0x80 ...... } 对0x80中断向量.设置了系统调用的总入口system_call. static void __init set_system_gate(unsigned int n, void *addr) { _set_gate(idt_table+n,15,3,addr); } 在IDT中设置

《Android系统源代码情景分析》连载回忆录:灵感之源

上个月,在花了一年半时间之后,写了55篇文章,分析完成了Chromium在Android上的实现,以及Android基于Chromium实现的WebView.学到了很多东西,不过也挺累的,平均不到两个星期一篇文章.本来想休息一段时间后,再继续分析Chromium使用的JS引擎V8.不过某天晚上,躺在床上睡不着,鬼使神差想着去创建一个个人站点,用来连载<Android系统源代码情景分析>一书的内容. 事情是这样的,躺在床上睡不着,就去申请了一个域名,0xcc0xcd.com.域名申请到了,总不能

Linux内核源代码情景分析-系统调用mknod

普通文件可以用open或者create创建,FIFO文件可以用pipe创建,mknod主要用于设备文件的创建. 在内核中,mknod是由sys_mknod实现的,代码如下: asmlinkage long sys_mknod(const char * filename, int mode, dev_t dev) //比如filename为/tmp/server_socket,dev是设备号 { int error = 0; char * tmp; struct dentry * dentry;

Linux内核源代码情景分析-文件系统的安装

执行sudo mount -t ext2 /dev/sdb1 /mnt/sdb,将文件系统挂在到/mnt/sdb上.系统调用mount,映射到内核层执行的是sys_mount.假设/dev/sdb1和/mnt/sdb都位于ext2文件系统中. asmlinkage long sys_mount(char * dev_name, char * dir_name, char * type, unsigned long flags, void * data)//dev_name指向了"/dev/sdb

Linux内核源代码情景分析-访问权限与文件安全性

在Linux内核源代码情景分析-从路径名到目标节点,一文中path_walk代码中,err = permission(inode, MAY_EXEC)当前进程是否可以访问这个节点,代码如下: int permission(struct inode * inode,int mask) { if (inode->i_op && inode->i_op->permission) { int retval; lock_kernel(); retval = inode->i_

Linux内核源代码情景分析-系统调用

一.系统调用初始化 void __init trap_init(void) { ...... set_system_gate(SYSCALL_VECTOR,&system_call);//0x80 ...... } 对0x80中断向量,设置了系统调用的总入口system_call. static void __init set_system_gate(unsigned int n, void *addr) { _set_gate(idt_table+n,15,3,addr); } 在IDT中设置

Linux内核源代码情景分析-文件系统安装后的访问

在Linux内核源代码情景分析-文件系统的安装,一文中,已经调用sudo mount -t ext2 /dev/sdb1 /mnt/sdb,在/mnt/sdb节点上挂载了文件系统,那么我们接下来访问/mnt/sdb/hello.c节点.我们来看一下path_walk的执行有什么不同? int path_walk(const char * name, struct nameidata *nd) { struct dentry *dentry; struct inode *inode; int er