浅析linux内核内存管理之分段
作者:李万鹏
<style type="text/css"><!-- @page { margin: 2cm } P { margin-bottom: 0.21cm } --></style>硬件中的分段:
逻辑地址(logicaladdress):
包含在机器语言中用来指定一个操作数或一条指令的地址。每一个逻辑地址都由一个段(segment)和偏移量(offset)组成,偏移量指明了从段开始的地方到实际地址之间的距离。
线性地址(linearaddress)(也称为虚拟地址):
是一个32位无符号整数,可以用来表示高达4GB的地址,范围从0x00000000~0xffffffff。
物理地址(physicaladdress):
用于内存芯片级内存单元的寻址。
内存单元控制(MMU)通过一种称为分段单元(segmentationunit)的硬件电路把一个逻辑地址转换成线性地址;第二个称为分页单元(pagingfault)的硬件电路把线性地址转换成一个物理地址。
<style type="text/css"><!-- @page { margin: 2cm } P { margin-bottom: 0.21cm } --></style>
段选择符和段寄存器:
<style type="text/css"><!-- @page { margin: 2cm } P { margin-bottom: 0.21cm } --></style>
一个逻辑地址由两部分组成:段标识符+偏移量。段标识符是一个16位长的字段,称为段选择符,而偏移量是一个32位长的字段。段选择符的3~15位指定了描述符在gdt或ldt中的位置,第2位指明是在gdt中还是在ldt中,0~1为RPL(requestprivilegelevel),指定特权级,linux只用0级(内核态)和3级(用户态)。所以,如果是访问代码段,RPL位为0,则访问kernelcode,RPL位为1,则访问datacode。为了快速找到段选择符,处理器提供段寄存器,段寄存器的唯一目的是存放段选择符。一共6个段寄存器:cs,ss,ds,es,fs和gs。其中cs(代码段寄存器)还有一个很重要的功能:它含有一个两位的字段,用以指明CPU的当前特权级(CPL)。值为0代表最高优先级,而值为3代表最低优先级。
<style type="text/css"><!-- @page { margin: 2cm } P { margin-bottom: 0.21cm } --></style>
下面来看一下段描述符:
每一个段描述符由8个字节组成。其中BASE字段指明了段的起始位置的线性地址,LIMIT字段指定了段的长度,单位是段占用的页的个数。
Linux中的分段:
与分段相比,linux更喜欢使用分页方式,因为:
当所有的进程使用相同的段寄存器值时,内存管理变得简单,也就是说他们能够共享同样的一组线性地址。
Linux设计目标之一是可以把它移植到绝大多数流行的处理器平台上。然而,RISC体系结构对分段的支持很有限。
Linux内核以有限的方式使用分段,它将所有的段的基地址设为0,也就是说线性地址与逻辑地址的偏移相等,由于基地址为0,所以线性地址与逻辑地址一致(注意不要说成相等,是线性地址与逻辑地址的偏移相等),这样Linux就巧妙的绕过了分段。Linux使用的段只有4个,用户代码段,用户数据段,内核代码段,内核数据段。下图中的DPL指定了特权级,当DPL=3时,使用用户态下的代码段和数据段,当DPL=0时,使用内核态下的代码段和数据段。每个段的段描述符占8个字节。
<style type="text/css"><!-- @page { margin: 2cm } P { margin-bottom: 0.21cm } --></style>
相应的段选择符由宏__USER_CS,__USER_DS,__KERNEL_CS,__KERNEL_DS分别定义。
#define GDT_ENTRY_DEFAULT_USER_CS 14 #define __USER_CS (GDT_ENTRY_DEFAULT_USER_CS * 8 + 3) #define GDT_ENTRY_DEFAULT_USER_DS 15 #define __USER_DS (GDT_ENTRY_DEFAULT_USER_DS * 8 + 3) #define GDT_ENTRY_KERNEL_BASE 12 #define GDT_ENTRY_KERNEL_CS (GDT_ENTRY_KERNEL_BASE + 0) #define __KERNEL_CS (GDT_ENTRY_KERNEL_CS * 8) #define GDT_ENTRY_KERNEL_DS (GDT_ENTRY_KERNEL_BASE + 1) #define __KERNEL_DS (GDT_ENTRY_KERNEL_DS * 8) 注意这里的__USER_CS等都是段选择符,不是段描述符。这里其实就是将index<<3+RPL,因为index是段选择符的3~15位。
把其中的宏替换成数值:
#define __USER_CS 115 0000 0000 0111 0011 #define __USER_DS 123 0000 0000 0111 1011 #define __KERNEL_CS 96 0000 0000 0110 0000 #define __KERNEL_DS 104 0000 0000 0110 1000 计算出index,TI,RPL:
index=14 TI=0 RPL=3 index=15 TI=0 RPL=3 index=12 TI=0 RPL=0 index=13 TI=0 RPL=0<style type="text/css"><!-- @page { margin: 2cm } P { margin-bottom: 0.21cm } --></style>
GDT& LDT:
最后一个话题就是全局描述符表和局部描述符表。这些段描述表存放在全局描述符和局部描述符表中。全局描述符表GDT是每CPU一个,而LDT是INTEL规定每进程一个,但是在Linux中大多数进程共享GDT中的LDT,少数进程除外,比如wine进程就用自己的局部描述符表。所有的gdt都存放在cpu_gdt_table数组中,而所有的gdt的地址和他们的大小存放在cpu_gdt_descr数组中。
<style type="text/css"><!-- @page { margin: 2cm } P { margin-bottom: 0.21cm } --></style>
4个段描述符在gdt中的表项:
ENTRY(cpu_gdt_table) .quad 0x00cf9a000000ffff /* 0x60 kernel 4GB code at 0x00000000 */ .quad 0x00cf92000000ffff /* 0x68 kernel 4GB data at 0x00000000 */ .quad 0x00cffa000000ffff /* 0x73 user 4GB code at 0x00000000 */ .quad 0x00cff2000000ffff /* 0x7b user 4GB data at 0x00000000 */<style type="text/css"><!-- @page { margin: 2cm } P { margin-bottom: 0.21cm } --></style>
按照上边那张描述符的表可以看出BASE字段都为0x00000000。
写了一个helloworld程序,反汇编,在main函数中调用的printf,这里call调用线性地址0x8048318处的代码,访问代码段:
call 8048318 <puts@plt> 画了一张流程图:
<style type="text/css"><!-- @page { margin: 2cm } P { margin-bottom: 0.21cm } --></style>
用gdb调试,info reg部分的内容:
cs 0x73 115 ss 0x7b 123 ds 0x7b 123 es 0x7b 123 fs 0x0 0 gs 0x33 51 可以看到cs为115,00000000 0111 0011,index为14,TI为0,RPL为11。TI为0说明使用的是gdt,RPL为11说明是用态的,index为14,但是想得到相应的段描述符并不是用gdtr中的值(即gdt全局描述符表的起始地址)直接加index,由于一个描述符占8个字节,所以描述符的地址为gdtr中的值+index*8。这样就可以取出描述符中的BASE字段,即段的起始地址,本例子中是用户态的代码段,即0x00000000,加上偏移0x8048318,就可以得到线性地址了。