Linux在IA-32体系结构下的地址映射
1.概览
2.逻辑地址到线性地址
逻辑地址到线性地址的映射在IA-32体系结构中又被称为段式映射。如上图所示,段式映射我们首先需要获取逻辑地址和段选择符,段选择符用于获取GDT中段的基地址,将逻辑地址作为偏移和段基地址相加获得线性地址。如图为详细的逻辑地址到线性地址的映射过程:
- 根据指令的性质来确定使用哪一个段寄存器;
- 根据段寄存器内容,找到相应的地址段描述符结构,段描述符结构一般放在GDT,LDT,TR或IDT中,描述表的起始地址保存在GDTR,LDTR,TR和IDTR寄存器中;
- 从地址描述结构中找到段的基地址;
- 将指令发出的地址作为位移,与段描述符中规定的段长度比较,看是否越界;
- 根据指令的性质和段描述符中的权限来看权限是否合适;
- 将指令中发出的地址作为位移,与基地址相加得到线性地址;
段选择符在段寄存器中,例如CS,DS。段描述符在内存管理寄存器中,如GDTR,LDTR,IDTR和TR。段选择符内容如下
段描述符内容如下:
在C语言中我们访问一个局部变量的地址将其打印出来,此时这个地址即为逻辑地址,那么这个地址到线性地址的转换过程为什么样的。
#include<stdio.h> int main() { unsigned long x = 0z01234567; printf("the x address is 0x%x\n", &x); return 0; }
上面的程序打印出了逻辑地址,按照逻辑地址到线性地址的转换方式,我们此时要从段寄存器中获取段选择符。我们知道局部变量是存放在桟区的,所以我们可以从堆栈寄存器SS获取段选择符。内核创建一个线程时会先将段寄存器设置好,IA-32架构的实现代码位于arch/x86/kernel/process_32.c:200行处
void start_thread(struct pt_regs *regs, unsigned long new_ip, unsigned long new_sp) { set_user_gs(regs, 0); regs->fs = 0; regs->ds = __USER_DS; regs->es = __USER_DS; regs->ss = __USER_DS; regs->cs = __USER_CS; regs->ip = new_ip; regs->sp = new_sp; regs->flags = X86_EFLAGS_IF; /* * force it to the iret return path by making it look as if there was * some work pending. */ set_thread_flag(TIF_NOTIFY_RESUME); }
从代码中我们可以看到,内核只使用了两个段,分别为代码段(CS)和数据段(DS),并且每个进程的CS和DS都相同,只有EIP和ESP不同。此时从SS段寄存器中获取段选择符,__USER_DS的值定义在arch/x86/include/asm/segment.h中:
#define GDT_ENTRY_DEFAULT_USER_DS 15
#define GDT_ENTRY_DEFAULT_USER_CS 14 #define __USER_DS (GDT_ENTRY_DEFAULT_USER_DS*8+3) #define __USER_CS (GDT_ENTRY_DEFAULT_USER_CS*8+3)
此时SS的二进制为:0000 0000 0111 1011。通过上面的段选择符结构图,高13bit为index,此时index值为15,第3bit为0,表示使用GDT全局描述表。此时我们就能够使用GDT表中索引为15处的地址为段基地址加上偏移地址得到线性地址了。GDT表的位置上面已经说了是由GDTR寄存器存储的,在kernel中GDTR定义在aarch/x86/kernel/cpu/common.c中
DEFINE_PER_CPU_PAGE_ALIGNED(struct gdt_page, gdt_page) = { .gdt = { #ifdef CONFIG_X86_64 /* * We need valid kernel segments for data and code in long mode too * IRET will check the segment types kkeil 2000/10/28 * Also sysret mandates a special GDT layout * * TLS descriptors are currently at a different place compared to i386. * Hopefully nobody expects them at a fixed place (Wine?) */ [GDT_ENTRY_KERNEL32_CS] = GDT_ENTRY_INIT(0xc09b, 0, 0xfffff), [GDT_ENTRY_KERNEL_CS] = GDT_ENTRY_INIT(0xa09b, 0, 0xfffff), [GDT_ENTRY_KERNEL_DS] = GDT_ENTRY_INIT(0xc093, 0, 0xfffff), [GDT_ENTRY_DEFAULT_USER32_CS] = GDT_ENTRY_INIT(0xc0fb, 0, 0xfffff), [GDT_ENTRY_DEFAULT_USER_DS] = GDT_ENTRY_INIT(0xc0f3, 0, 0xfffff), [GDT_ENTRY_DEFAULT_USER_CS] = GDT_ENTRY_INIT(0xa0fb, 0, 0xfffff), #else [GDT_ENTRY_KERNEL_CS] = GDT_ENTRY_INIT(0xc09a, 0, 0xfffff), [GDT_ENTRY_KERNEL_DS] = GDT_ENTRY_INIT(0xc092, 0, 0xfffff), [GDT_ENTRY_DEFAULT_USER_CS] = GDT_ENTRY_INIT(0xc0fa, 0, 0xfffff), [GDT_ENTRY_DEFAULT_USER_DS] = GDT_ENTRY_INIT(0xc0f2, 0, 0xfffff), /* * Segments used for calling PnP BIOS have byte granularity. * They code segments and data segments have fixed 64k limits, * the transfer segment sizes are set at run time. */ /* 32-bit code */ [GDT_ENTRY_PNPBIOS_CS32] = GDT_ENTRY_INIT(0x409a, 0, 0xffff), /* 16-bit code */ [GDT_ENTRY_PNPBIOS_CS16] = GDT_ENTRY_INIT(0x009a, 0, 0xffff), /* 16-bit data */ [GDT_ENTRY_PNPBIOS_DS] = GDT_ENTRY_INIT(0x0092, 0, 0xffff), /* 16-bit data */ [GDT_ENTRY_PNPBIOS_TS1] = GDT_ENTRY_INIT(0x0092, 0, 0), /* 16-bit data */ [GDT_ENTRY_PNPBIOS_TS2] = GDT_ENTRY_INIT(0x0092, 0, 0), /* * The APM segments have byte granularity and their bases * are set at run time. All have 64k limits. */ /* 32-bit code */ [GDT_ENTRY_APMBIOS_BASE] = GDT_ENTRY_INIT(0x409a, 0, 0xffff), /* 16-bit code */ [GDT_ENTRY_APMBIOS_BASE+1] = GDT_ENTRY_INIT(0x009a, 0, 0xffff), /* data */ [GDT_ENTRY_APMBIOS_BASE+2] = GDT_ENTRY_INIT(0x4092, 0, 0xffff), [GDT_ENTRY_ESPFIX_SS] = GDT_ENTRY_INIT(0xc092, 0, 0xfffff), [GDT_ENTRY_PERCPU] = GDT_ENTRY_INIT(0xc092, 0, 0xfffff), GDT_STACK_CANARY_INIT #endif } };
GDT_ENTRY_INIT定义在arch/x86/kernel/cpu/desc_defs.h中
#define GDT_ENTRY_INIT(flags, base, limit) { { { \ .a = ((limit) & 0xffff) | (((base) & 0xffff) << 16), \ .b = (((base) & 0xff0000) >> 16) | (((flags) & 0xf0ff) << 8) | \ ((limit) & 0xf0000) | ((base) & 0xff000000), \ } } }
当GDT_ENTRY_DEFAULT_USER_DS为15时,在GDT表中对应的地址为GDT_ENTRY_INIT(0xc0f2, 0, 0xfffff),此时基地址base为0,segment limit为0xfffff,线性地址等于GDT中的基地址加上逻辑地址,基地址为0,所以在linux kernel中线性地址和逻辑地址是相等的。
3.线性地址到物理地址 待补充
将线性地址最终映射到物理地址的过程称为页式映射。从线性地址到物理地址的映射过程为:
- 从CR3寄存器中获取页面目录的基地址;
- 以线性地址dir位段作为下标,在目录中取得相应页面表的基地址;
- 以线性地址中的page位段作为下标,在所得到的页面目录中获取相应的页面描述项;
- 将页面描述项中给出的页面基地址与线性地址中的offset位段相加得到物理地址;
线性地址到物理地址的映射过程如下图所示:
每个进程都有自己的地址空间,不同的进程就有不同的CR3寄存器,CR3寄存器的值一般保存在进程控制块中,例如task_struct结构体中,32bit时CR3寄存器页面项如图:
从上面描述的过程中可知,我们首先要获得CR3寄存器的值,内核在创建进程时会分配页面目录,页面目录地址保存在task_struct结构体中,task_struct结构体中有一个mm_struct结构体中有一个pgd字段用来存储CR3寄存器的值,此段代码位于kernel/fork.c中
static inline int mm_alloc_pgd(struct mm_struct *mm) { mm->pgd = pgd_alloc(mm); if (unlikely(!mm->pgd)) return -ENOMEM; return 0; }
在进程切换的过程中,会将进程页面目录的基地址加载到CR3寄存器,代码位于arch/x86/include/asm/mmu_context.h中
static inline void switch_mm(struct mm_struct *prev, struct mm_struct *next, struct task_struct *tsk) { unsigned cpu = smp_processor_id(); if (likely(prev != next)) { #ifdef CONFIG_SMP this_cpu_write(cpu_tlbstate.state, TLBSTATE_OK); this_cpu_write(cpu_tlbstate.active_mm, next); #endif cpumask_set_cpu(cpu, mm_cpumask(next)); /* Re-load page tables */ load_cr3(next->pgd); trace_tlb_flush(TLB_FLUSH_ON_TASK_SWITCH, TLB_FLUSH_ALL); /* Stop flush ipis for the previous mm */ cpumask_clear_cpu(cpu, mm_cpumask(prev)); /* Load the LDT, if the LDT is different: */ if (unlikely(prev->context.ldt != next->context.ldt)) load_LDT_nolock(&next->context); } #ifdef CONFIG_SMP else { this_cpu_write(cpu_tlbstate.state, TLBSTATE_OK); BUG_ON(this_cpu_read(cpu_tlbstate.active_mm) != next); if (!cpumask_test_cpu(cpu, mm_cpumask(next))) { /* * On established mms, the mm_cpumask is only changed * from irq context, from ptep_clear_flush() while in * lazy tlb mode, and here. Irqs are blocked during * schedule, protecting us from simultaneous changes. */ cpumask_set_cpu(cpu, mm_cpumask(next)); /* * We were in lazy tlb mode and leave_mm disabled * tlb flush IPI delivery. We must reload CR3 * to make sure to use no freed page tables. */ load_cr3(next->pgd); trace_tlb_flush(TLB_FLUSH_ON_TASK_SWITCH, TLB_FLUSH_ALL); load_LDT_nolock(&next->context); } } #endif }