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
}
时间: 2024-08-06 03:23:57

Linux在IA-32体系结构下的地址映射的相关文章

Linux(Red Hat 6 32位) 下安装Mysql5.6.30

1. 下载MySQL 5.6 下载页面:http://dev.mysql.com/downloads/mysql/ 此处选择"Red Hat Enterprise Linux 6 / Oracle Linux 6 (x86, 32-bit), RPM Bundle"下载,下载至/root/fuxian/目录下,下载文件名为"MySQL-5.6.30-1.el6.i686.rpm-bundle.tar" 2. 解压tar包 cd /fuxian/Downloads/

32位Windows7上8G内存使用感受+xp 32位下使用8G内存 (转)

32位Windows7上8G内存使用感受+xp 32位下使用8G内存 博客分类: Windows XPWindowsIE企业应用软件测试 我推荐做开发的朋友:赶快加入8G的行列吧....呵呵..超爽...速度超快...基本没有等待的概念...深有体会... 为什么要使用8G内存?在国内外各大论坛上,这都是一个有争议的问题.问题的反方论据非常充分: 除了少数专业领域,大多数应用程序不会需要超过1G的内存. 游戏使用的内存最多也是2G而已. 8G内存不便宜,不如花在显卡上. 升级到8G后没有什么明显

基于Linux 2.6.32的进程分析

前言 Linux是一套免费使用和自由传播的类Unix操作系统,是一个基于POSIX和UNIX的多用户.多任务.支持多线程和多CPU的操作系统. 本文的分析全部基于Linux Kernel 2.6.32,源代码的链接地址:https://elixir.bootlin.com/linux/v2.6.32/source/fs 具体内容分为: 进程的概念 进程的建立 进程的转换 进程的调度 对于进程的理解 一.进程的概念 1.1什么是进程 大众对进程的理解基本上基于打开任务管理器所看到的正在执行的软件等

Red Hat Enterprise Linux 5.10在vmware10下的安装

Red Hat Enterprise Linux 5.10在vmware10下的安装 1.启动"新建虚拟机"向导程序.如下图,选择"自定义",点击"下一步" 2.选择虚拟机硬件兼容性,你可以根据自己需求选择,这里选择默认,如下图.单击"下一步" 3.指定虚拟机系统的安装方式.选择"稍后安装操作系统",如果选择"安装盘镜像文件(iso)",虚拟机启动后会自动执行快速安装,不方便用户控制安装

Tomcat Can&#39;t load AMD 64-bit .dll on a IA 32

1.下载64位的tcnative-1.dll替换tomcat中bin目录下的tcnative-1.dll就解决了 2.tcnative-1.dll下载地址 请点击 这里 Tomcat Can't load AMD 64-bit .dll on a IA 32

第一次作业:Linux 2.6.32的进程模型与调度器分析

1.前言 本文分析的是Linux 2.6.32版的进程模型以及调度器分析.在线查看 源码下载 本文主要讨论以下几个问题: 什么是进程?进程是如何产生的?进程都有那些? 在操作系统中,进程是如何被管理以及它们是怎样被调用的? 2.进程模型 2.1进程的概念 在我的理解中,一个程序就相当于一个进程,程序的启动意味着产生了一个新的进程,程序的关闭也就意味着一个进程的消亡. 那么专业定义应该是: 在计算中,进程是正在执行的计算机程序的一个实例. 它包含程序代码及其当前活动. 根据操作系统(OS),一个进

如何查看linux系统是32位还是64位

1.#uname -a 如果有x86_64就是64位的,没有就是32位的 # uname -a  Linux desktop 2.6.35-23-generic #37-Ubuntu SMP Fri Nov 5 19:17:11 UTC 2010 i686 GNU/Linux 这是32位的  #uname -a  Linux backup 2.6.9-67.ELsmp #1 SMP Wed Nov 7 13:58:04 EST 2007 i686 i686 i386 GNU/Linux  注意:

使用cygwin移植Linux的项目到Windows下之总结(转)

使用cygwin移植Linux的项目到Windows下之总结(转) 原文 http://my.oschina.net/michaelyuanyuan/blog/68615?p=1 一.why 接到一个任务,把公司的某个在Linux下开发的项目(也就是一个程序啦)移植到Windows下,使得其可以在Windows下运行,并且运行的结果当然要是正确的啦,何谓正确,当然就是和Linux运行结果比对,一样就行. 二.难在何处 难就难在如何在Windows下编译通过.假设你在Linux下开发了一个这样的程

[Linux] 批量转换整个目录下的文件编码为UTF-8;

[Linux] 批量转换整个目录下的文件编码为UTF-8: #!/bin/bash - #===============================================================================# #          FILE: conv.sh#  #         USAGE: ./conv.sh #  #   DESCRIPTION: 一个支持把整个目录递归转换GB2312为UTF-8的脚本: #  #       OPTIONS: