Linux 地址映射全过程(分段机制过程在Linux中不起作用)

地址映射的全过程

 

Linux 内核采用页式存储管理。虚拟地址空间划分成固定大小的“页面”,由 MMU 在运行时将虚拟地址“映射”成某个物理内存中的地址。与段式存储管理相比,页式存储管理有很多好处。首先,页面都是固定大小的,便于管理。更重要的是,当要将一部分物理空间的内容换出到磁盘上的时候,在段式存储管理中要将整个段 ( 通常很大 ) 都换出,面在页式存储管理中则是按页进行,效率显然要高得多。由于 i386 系列的历史演变过程,它对页式存储管理的支持是在其段式存储管理已经存在了相当长的时间以后才发展起来的。所以,不管程序是怎样写的, i386 微处理器一律对程序中的地址先进行段式映射,然后才能进行页式映射。而 Linux 所采用的方法实际上使段式映射的过程中不起什么作用。

下面通过一个简单的程序来看看 Linux 下的地址映射的全过程:

  #include 
  greeting()
  {
      printf(“Hello world!
”);
  }
  main()
  {
      greeing();
  }

该程序在主函数中调用 greeting 来显示 “Hello world!” ,经过编译和反汇编(% objdump -d hello),我们得到了它的反汇编的结果。


08048568:<greeting>:
8048568: 55                push1 %ebp
8048856b:89 e5            mov1 %esp,%ebp
804856b: 68 04 94 04 08    push1 $0x8048404
8048570: e8 ff fe ff ff    call 8048474 
8048575: 83 c4 04          add1 $0x4,%esp
8048578: c9                leave
8048579: c3                ret
804857a: 89 f6            mov1 %esi,%esi
0804857c :
804857c: 55                push1 %ebp
804857d: 89 e5            mov1 %esp,%ebp
804857f: e8 e4 ff ff ff    call 8048568 
8048584: c9                leave
8048585: c3                ret
8048586: 90                nop
8048587: 90                nop

从上面可以看出, greeting() 的地址为 0x8048568 。在 elf 格式的可执行代码中,总是在 0x8000000 开始安排程序的 “ 代码段” ,对每个程序都是这样。

当程序在 main 中执行到了 “call 8048568” 这条指令,要转移到虚拟地址 8048568 去。

 首先是段式映射阶段。地址 8048568 是一个程序的入口,更重要的是在执行的过程中有 CPU 的 EIP 所指向的,所以在代码段中。I386cpu 使用 CS 的当前值作为段式映射的“选择码”,也就是用它作为在段描述表中的下标。哪一个段描述表呢》是全局段描述表GDT 还是局部段描述表 LDT ?那就要看 CS 中的内容了。

内核在建立一个进程时都要将其段寄存器设置好,有关代码在 include/asm-i386/processor.h 中:

 # define       start_thread(regs, new_eip, new_dsp)       do   {     \

      __asm__(“movl       %0,%%fs; movl       %0,%%gs”:     :”r”  (0));       \

              set_fs(user_DS);

              regs->xds = __USER_DS;

              regs->xes = __USER_DS;

              regs->xss = __USER_DS;

              regs->xcs = __USER_CS;

              regs->eip = new_eip;

              regs->esp = new_esp;

              }       while (0)

 

这里把 DS 、 ES 、 SS 都设置成 _USER_DS, 而把 CS 设置成 _USER_CS, 这也就是说,虽然 Intel 的意图是将一个进程的映象分成代码段、数据段和堆栈段,但在 Linux 内核中堆栈段和代码段是不分的。

再来看看 USER_CS 和 USER_DS 是什么。那是在 include/asm-i386/segment.h 中定义的:

                        Index          TI DPL
#define_KERNEL_CS 0x10  0000 0000 0001 0|0|00  
#define_KERNEL_DS 0x18  0000 0000 0001 1|0|00  
#define_USER_CS 0x23         0000 0000 0010 0|0|11
#define_USER_DS 0x2B         0000 0000 0010 1|0|11
_KERNEL_CS:        index=2,TI=0,DPL=0
_KERNEL_DS:        index=3,TI=0,DPL=0    
_USERL_CS:          index=4,TI=0,DPL=3
_USERL_DS:          index=5,TI=0,DPL=3

TI 全都是 0 ,都使用全局描述表。LDT在Linux中没有使用,只有在Linux模拟运行Windows软件和DOS软件时才会使用。内核的 DPL 都为 0 ,最高级别;用户的 DPL 都是 3 ,最低级别。 _USER_CS 在 GDT 表中是第 4 项,初始化 GDT 内容是在 arch/i386/kernel/head.S 中定义的,其主要内容在运行中并不改变。代码如下:


  ENTRY(gdt-table)
      .quad 0x0000000000000000  /* NULL descriptor */
      .quad 0x0000000000000000  /* not used */
      .quad 0x00cf9a00000ffff    /* 0x10 kernel 4GB code at 0x00000000 */
      .quad 0x00cf9200000ffff    /* 0x18 kernel 4GB data at 0x00000000 */
      .quad 0x00cffa00000ffff    /* 0x23 user 4GB code at 0x00000000 */
      .quad 0x00cff200000ffff    /* 0x2b user 4GB data at 0x00000000 */

  GDT 表中第一、二项不用,第三至第五项共四项对应于前面的四个段寄存器的数值。 
将这四个段描述项的内容展开:


  K_CS:  0000 0000 1100 1111 1001 1010 0000 0000
        0000 0000 0000 0000 1111 1111 1111 1111
  K_DS:  0000 0000 1100 1111 1001 0010 0000 0000
        0000 0000 0000 0000 1111 1111 1111 1111
  U_CS:  0000 0000 1100 1111 11111 1010 0000 0000
        0000 0000 0000 0000 1111 1111 1111 1111
  U_DS:  0000 0000 1100 1111 1111 0010 0000 0000
        0000 0000 0000 0000 1111 1111 1111 1111

这四个段描述项的下列内容都是相同的。

      
    ·BO-B15/B16-B31 都是 0    基地址全为 0
    ·LO-L15 、 L16-L19 都是 1    段的上限全是 0xfffff
    ·G 位都是 1                段长均为 4KB
    ·D 位都是 1                32 位指令 
    ·P 位都是 1                四个段都在内存中

结论:每个段都是从 0 地址开始的整个 4GB 虚存空间,虚地址到线性地址的映射保持原值不变。可以看到段基址相同,虚地址到线性地址的映射保持不变。段式映射机制把地址0x08048368映射到了其自身,作为线性地址。因此,讨论或理解 Linux 内核的页式映射时,可以直接将线性地址当作虚拟地址,二者完全一致。

不同之处在于权限级别不同,内核的为 0 级,用户的为 3 级。另一个是段的类型,或为代码,或为数据。这两项都是 CPU 在映射过程中要加以检查核对的。如果 DPL 为 0 级,而段寄存器 CS 中的 DPL 为 3 级,那就不允许了,因为那说明 CPU 的当前运行级别比想要访问的区段要低。或者,如果段描述项说是数据段,而程序中通过 CS 来访问,那也不允许。实际上,这里所作的检查比对在页式映射的过程中还要进行,所以既然用了页式映射,这里的检查比对就是多余的。

再回到 greeting 的程序中来,通过段式映射把地址 8048568 映射到自身,得到了线性地址。现在 8048568 是作为线性地址出现了,下面才进入页式映射的过程。

与段式映射过程中所有的进程全都共用一个 GDT 不一样,现在可是动真格的了,每个进程都有自身的页目录 PGD ,指向这个目录的指针保持在每个进程的 mm_struct 数据结构中。每当调度一个进程进入运行时,内核都要为即将运行的进程设置好控制寄存器 CR3 ,而MMU 硬件总是从 CR3 中取得当前进程的页目录指针。不过, CPU 在执行程序时使用的是虚存地址,而 MMU 硬件在进行映射时所使用的则是物理地址。这是在 inline 函数 switch_mm() 中完成的,其代码见 include/asm-i386/mmu_context.h 。这里关心的只是其中最关键的一行:

28                    static inline void switch_mm(struct mm_struct *prev, struct mm_struct *next, struct task_struct *tsk, unsigned cpu)

29                    {

……

44                 asm volatitle(“movl              %0,%%cr3”:   :”r” ( __pa(next->pgd) ) );

……

59      }

这里 __pa() 的用途是将下一个进程的页面目录 PGD 的物理地址装入寄存器 %%cr3, 也即 CR3 。这时可能有疑问:这样,在这一行以前和以后 CR3 的值不一样,也就是使用不同的倒买倒卖目录,不会使程序的执行不能连续了吗?答案是,这是在内核中。不管什么进程,一旦进入内核就进了系统空间,都有相同的页面映射,所以不会出问题。

当程序要转到地址 0x8048568 去的时候,进程正在运行中, CR3 已经设置好了,指向本进程的页目录了。

    8048568 : 0000 1000 0000 0100 1000 0101 0110 1000

按照线性地址的格式,最高 10 位 0000100000 ,十进制的 32 ,所以 i386CPU( 确切地说是 CPU 中的 MMU ,下同 ) 就以下标32 去页目录表中找其页目录项。这个页目录项的高 20 位指向一个页面表。 CPU 在这 20 位后面添上 12 个 0 就得到该页面表的指针。前面讲过,每个页面表占一个页面,所以自然就是 4K 字节边界对齐的,其起始地址的低 12 位一定是 0. 正因如此,才可以把 32 位目录项中的低 12 位挪着它用,其中的最低位为 P 标志柆,为 1 时表示该页面在内存中。

找到页表后,再看线性地址的中间 10 位 001001000 ,十进制的 72 。就以 72 为下标在找到的页表中找到相应的表项。与目录项相似,当页面表项的 P 标志位为 1 时表示所映射的页面在内存中。 32 位的页面表项中的高指向一个物理内存页面,在后边添上 12 个 0 就得到了物理内存页面的起始地址。所不同的是,这一次指向的不再是一个中间结构,而是映射的目标页面了。在其起始地址上加上线性地址中的最低 12 位就得到了最终的物理内存地址。这时这个线性地址的最低 12 位为 0x568 。所以,如果目标页面的起始地址为0x740000 的话 ( 具体取决于内核中的动态分配 ) ,那么 greeting() 入口的物理地址就是 0x740568,greeting() 的执行代码就存储在这里。

在页式映射的过程中, CPU 要访问内存三次,第一次是页面目录,第二次是页面表,第三次才是真正要访问的目标。这样,把原来不用分页机制一次访问内存就能得到的目标,变为三次访问内存才能得到,明显执行分页机制在效率上的牺牲太大了。



                             页目录与页表示意图

为了减少这种开销,最近被执行过的地址转换结果会被保留在 MMU 的转换后备缓存( TLB )中。虽然在第一次用到具体的页面目录和页面表时要到内存中读取,但一旦装入了 TLB 中,就不需要再到内存中去读取了,而且这些都是由硬件完成的,因此速度很快。

TLB 对应权限大于 0 级的程序来说是不可见的,只有处于系统 0 层的程序才能对其进行操作。

当 CR3 的内容变化时, TLB 中的所有内容会被自动变为无效。 Linux 中的 _flush_tlb 宏就是利用这点工作的。 _flush_tlb 只是两条汇编指令,把 CR3 的值保存在临时变量 tmpreg 里,然后立刻把 tmpreg 的值拷贝回 CR3 ,这样就将 TLB 中的全部内容置为无效。除了无效所有的 TLB 中的内容,还能有选择的无效 TLB 中某条记录,这就要用到 INVLPG 指令。

<think>好的,用户现在想了解物理地址和逻辑地址的区别及关系。之前我已经给出了一个比较详细的回答,但现在需要根据用户提供的引用内容来调整和补充。首先,我需要仔细阅读用户提供的四个引用内容,确保准确理解每个引用的重点,并将它们整合到回答中。 引用[1]提到了物理地址、虚拟地址、逻辑地址以及内存分段和分页的管理方式,还有地址转换功能。这说明在回答中需要解释这些概念之间的关系,特别是逻辑地址到物理地址的转换机制,比如分段和分页的区别。 引用[2]以Linux x86 32位系统为例,说明了逻辑地址转换为物理地址的具体计算方法,即物理地址等于逻辑地址减去0xc0000000。这里需要注意这个例子是否具有普遍性,或者是否只在特定模式下有效。同时,需要指出不同系统或模式下转换方式可能不同。 引用[3]提到了逻辑地址到物理地址的转换过程中涉及页面大小,需要模运算和除法操作来分离页号和页内地址,然后通过页表找到对应的物理块号。这涉及到分页机制的具体步骤,需要详细说明这个过程。 引用[4]详细解释了逻辑地址和物理地址的定义,特别指出逻辑地址是由程序产生的内偏移地址,在实模式下两者相等,而在保护模式下需要转换。还提到应用程序员通常只处理逻辑地址,转换对他们是透明的。这一点需要强调,区分不同模式下的差异以及程序员视角和系统视角的不同。 接下来,用户的问题集中在物理地址和逻辑地址的区别及关系上。需要将上述引用内容整合起来,结构清晰地分点回答。可能的结构包括:定义区别、转换机制、应用场景、硬件支持等。 需要注意用户之前的问题中提到内存是主存(RAM),而这里讨论的是地址空间的概念,可能需要简要关联到内存管理,但重点在地址转换。 另外,用户可能对实模式和保护模式的区别不太清楚,需要简明解释,尤其是在地址转换方面的不同。同时,引用[2]中的例子是32位Linux的情况,需要指出这可能属于保护模式下的分页机制,而引用[4]提到实模式下两者相等,所以需要说明不同模式的影响。 还要注意引用[3]中的分页转换步骤,可能需要用数学公式表达,如使用模运算和除法,以及页表查找的过程。例如,逻辑地址除以页面大小得到页号,模运算得到页内偏移,然后通过页表找到物理块号,组合成物理地址。 需要检查是否有冲突的信息,例如引用[2]中的线性转换是否适用于分页机制,或者是否属于分段的情况。根据知识,Linux在保护模式下使用分页而非分段,所以引用[2]中的例子可能是一种线性偏移,但需要确认是否正确。可能这里存在误解,因为通常逻辑地址需要先通过分段转换为线性地址,再通过分页转换为物理地址。但引用[2]中的描述可能简化了步骤,或者系统配置为平坦模式,使得分段基址为0,从而逻辑地址直接减去偏移得到物理地址。需要澄清这一点,避免混淆用户。 此外,引用[4]提到逻辑地址是相关的偏移地址,在保护模式下需要转换,而分段和分页对应用程序员透明。需要将分段和分页机制分开解释,或者说明它们如何共同工作。 最后,用户可能需要示例来理解转换过程,比如引用[2]中的具体数值例子,或者分页中的页号查找例子。同时,确保回答结构清晰,分点明确,使用LaTeX公式正确,如物理地址的计算公式,分页转换的步骤等。 总结回答的结构可能如下: 1. 定义对比:明确物理地址和逻辑地址的定义。 2. 核心区别:从特性上对比,如生成方式、可见性、转换需求等。 3. 转换关系:分模式讨论(实模式vs保护模式),并详细说明分页机制下的转换步骤。 4. 硬件支持:MMU的作用,页表等。 5. 实际示例:如引用中的Linux例子,分页转换的例子。 6. 常见问题:如为什么需要转换,透明性等。 需要确保引用标记正确,比如引用[1]、[2]等,并在回答末尾列出参考文献。同时,生成的相关问题要基于当前内容,引导用户深入理解。</think>### 物理地址与逻辑地址的区别及关系解析 --- #### 一、核心定义对比 1. **逻辑地址(Logical Address)** - 由程序直接生成的地址,表现为**内偏移量**(如C语言中的指针值)[^4]。 - 程序员可见,例如`0x8048000`(32位Linux程序的典型代码起始逻辑地址)[^2]。 - **本质**:CPU指令中使用的地址,需通过分段或分页机制转换为物理地址[^1][^4]。 2. **物理地址(Physical Address)** - 实际内存芯片(DRAM)上的硬件地址,如内存条中第`0x3`号存储单元[^2][^4]。 - 对应用程序透明,由操作系统和硬件(MMU)管理。 --- #### 二、核心区别 | 特性 | 逻辑地址 | 物理地址 | |--------------------|----------------------------------|----------------------------------| | **生成方式** | 程序编译时生成(+偏移) | 硬件实际寻址的最终结果[^1] | | **可见性** | 程序员直接操作(如指针) | 仅硬件和操作系统可见 | | **是否需要转换** | 必须通过分段/分页转换[^1][^4] | 直接用于内存访问 | | **唯一性** | 进程内唯一(不同进程可重复) | 全局唯一(全内存空间唯一)[^1] | --- #### 三、转换关系与机制 ##### 1. **转换必要性** - **保护模式需求**:现代操作系统通过地址转换隔离进程内存空间,防止越界访问[^4]。 - **虚拟内存支持**:逻辑地址可映射到物理内存或外存交换区(如Swap分区)[^1]。 ##### 2. **转换流程(分页机制为例)** 以逻辑地址$0xc0000003$转换为物理地址$0x3$为例(Linux x86 32位系统)[^2][^3]: 1. **分页机制拆分**: - **页号**:$\text{逻辑地址} \div \text{页面大小}$ - **页内偏移**:$\text{逻辑地址} \mod \text{页面大小}$ - 若页面大小为4KB,则页号为$0xc0000003 \div 4096$,页内偏移为$0xc0000003 \mod 4096$。 2. **页表查询**: - 通过页表(Page Table)将页号映射为物理内存的块号(Frame Number)[^3]。 - 例如,页号$0x300$对应块号$0x0$(具体映射由操作系统设定)。 3. **组合物理地址**: - $\text{物理地址} = \text{块号} \times \text{页面大小} + \text{页内偏移}$ - 若块号为$0x0$,则物理地址为$0x0 \times 4096 + 3 = 0x3$[^2]。 ##### 3. **模式差异** - **实模式**:逻辑地址直接等于物理地址(无地址转换)[^4]。 - **保护模式**:必须通过分段(Segment)和分页(Page)两级转换[^1][^4]。 --- #### 四、硬件支持与操作系统角色 1. **MMU(内存管理单元)** - 负责实时转换逻辑地址到物理地址[^1][^3]。 - 集成页表缓存(TLB)加速转换[^3]。 2. **操作系统职责** - 维护页表结构(如多级页表)[^1]。 - 处理缺页中断(Page Fault),加载外存数据到物理内存[^1]。 --- #### 五、实际应用示例 - **C语言指针操作**: ```c int *ptr = (int*)0x8048000; // 逻辑地址 printf("指针值:%p", ptr); // 输出逻辑地址 ``` 实际访问时,MMU将$0x8048000$转换为物理地址(如$0x1000$)[^4]。 - **Linux内核映射**: 32位系统中,内核空间逻辑地址$0xc0000000$以上固定映射到物理地址$0x0$起始区域。 --- ### 总结 逻辑地址是程序视角的虚拟地址,物理地址是硬件实际使用的内存位置。两者通过分段、分页机制动态映射,由操作系统和MMU协同管理。**核心关系**可概括为: $$ \text{物理地址} = f(\text{逻辑地址}) $$ 其中$f$表示由硬件和操作系统定义的转换规则(如减法偏移、页表查询等)[^2][^3]。 --- **相关问题**: 1. 分页机制中如何处理页表缺失(Page Fault)? 2. 为什么现代操作系统普遍使用分页而非分段机制? 3. TLB(快表)如何加速地址转换过程? : 内存管理中的分段与分页机制 [^2]: Linux x86 32位系统的逻辑地址映射规则 [^3]: 分页机制下的地址转换流程 [^4]: 逻辑地址与物理地址在编程中的实际表现
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值