• Linux内存地址映射-8086分段分页与缺页异常


    8086体系实模式地址映射

       8086cpu有16根数据线和20根地址线。

    寄存器:

    1. 段寄存器:描述内存分段时,保存段的信息。8086 内部有4 个段寄存器。其中,CS 是代码段寄存器,DS 是数据 段寄存器,ES 是附加段(Extra Segment)寄存器,IP 指令指针寄存器。
    2. 物理地址:对应内存条上的实际地址。
    3. 偏移地址: 段内存储单元相对于短地址的距离。
    4. 段地址: 段的起始地址(对应着一个物理地址)。
    5. 逻辑地址: 短地址+偏移地址。

        我们把一个程序分为数据段与代码段,在数据段寄存器(DS)中存储数据段的段地址,代码段中存储代码段的段地址。

           物理地址 = 段基址(DS/CS)+偏移地址/逻辑地址即段上的偏移量(IP寄存器)

          这种获取到的物理地址也称为实模式。实模式下的分段。

          CPU上电强制进入实模式,寻址空间是2^20 = 1M. 内核image 从0x100000(1M)开始加载,之前属于实模式的地址空间

            当处理器访问内存时,它把指令中指定的 内存地址看成是段内的偏移地址,而不是物理地址。这样,一旦处理器 遇到一条访问内存的指令,它将把DS 中的数据段起始地址和指令中提供的段内偏移相加,来得到访问内存所需要的物理地址。

           当一段代码开始执行时, CS 指向代码段的起始地址,IP 则指向段内偏移。这样,由CS 和IP 共同 形成逻辑地址,并由总线接口部件变换成物理地址来取得指令。然后, 处理器会自动根据当前指令的长度来改变IP 的值,使它指向下一条指令。

           8086的处理器地址引线:20根,那么逻辑地址就是20位。而我们的寄存器只有16位,为了解决这个问题:段地址实际上也是20位,将段寄存器中的值左移4位(每个分段必须加载到地址最低位为0的位置,相对于16进制表示而言)。偏移地址仍然是16位,也就意味着每个段的最大长度为65536个字节。

          8086 处理器的逻辑分段,起始地址都是16 的倍数,这称为是 按16 字节对齐的。

            同样在不允许段之间重叠的情况下,每个段的最大长度是64KB,因 为偏移地址也是16 位的,从0000H 到FFFFH。在这种情况下,1MB 的 内存,最多只能划分成16 个段,每段长64KB,段地址分别是0000H、1000H、2000H、3000H,…,一直到F000H。

    同一段内存,多种分段方案如下:

             10000H到100FFH组成一个段,起始地址( 基础地址,向左移4位) 为10000H,段地址为1000H, 偏移的大小为100H。当这段内存分成两个段后,起始地址( 基础地址 )为10000H和10080H,段地址为1000H和1008H, 大小均为80H。

            我们知道当段地址向左移4位,实际上就是乘以16,换句话说一个段的起始地址也一定是16的倍数。

            偏移地址为16位,16 位地址的寻址能力为64K,所以一个段的长度最大为64K。

    现在有一个物理地址为21F60H,段地址为2000H,把段地址向左移4位后就是20000H,再用物理地址减去段地址,那么就很容易推出偏移地址为1F60H

            在8086PC机中存储单元地址是怎样表示的呢?如果数据在21F60H内存单元中,段地址是2000H,地址表示如下:

            数据存在内存2000:1F60单元中
            数据存在内存的2000H段中的1F60H单元中
            另外,段地址和数据都是重要的,因此处理器至少需要提供两个段寄存器,分别是代码段寄存器(Code Segment,CS)和数据段寄存器(Data Segment,DS)。对CS内容的改变将导致处理器从新的代码段开始执行。同样的,在开始访问内存中的数据之前,也必须让DS寄存器指向数据段。

            通常情况下,段地址的选择取决于内存中哪些区域是空闲的,例如从物理地址00000H开始,到82215H之间的内存都被其他程序占用了,那么可以从82215H后面的空闲的内存区域开始加载程序。

    保护模式下的内存分段的地址映射

        所谓工作模式,是指CPU的寻址方式、寄存器大小、指令用法和内存布局等。

             实模式
         段基址:段内偏移地址”产生的逻辑地址就是物理地址,即程序员可见的地址完全是真实的内存地址。
             保护模式
         在保护模式中,内存的管理模式分为两种——段模式和页模式。其中页模式也是基于段模式的。也就是说,保护模式的内存管理模式事实上是:纯段模式和段页式。进一步说,段模式是必不可少的,而页模式则是可选的——如果使用页模式,则是段页式,否则这是纯段模式。
         保护模式下的段寄存器 由 16位的选择器 与 64位的段描述符寄存器 构成

    •  段描述符寄存器: 存储段描述符
    •  选择器:存储段描述符的索引

    在这里插入图片描述

            原先实模式下的各个段寄存器在保护模式下的作用是段选择器,仅仅是每个位表示的作用不同于实模式(实模式下是段基址)

            请求特权级(RPL)则代表选择子的特权级,共有4个特权级(0级、1级、2级、3级)。00 表示最高级别  11 表示最低级别

            高13位表示在段描述符表 中的索引号。 

            关于特权级的说明:任务中的每一个段都有一个特定的级别。每当一个程序试图访问某一个段时,就将该程序所拥有的特权级与要访问的特权级进行比较,以决定能否访问该段。系统约定,CPU只能访问同一特权级或级别较低特权级的段。


         全局描述符表GDT


            全局描述符表GDT(Global Descriptor Table)在整个系统中,全局描述符表GDT只有一张,GDT可以被放在内存的任何位置,但CPU必须知道GDT的入口,也就是基地址放在哪里,Intel的设计者提供了一个寄存器GDTR用来存放GDT的入口地址,程序员将GDT设定在内存中某个位置之后,可以通过LGDT指令将GDT的入口地址装入此寄存器,从此以后,CPU就根据此寄存器中的内容作为GDT的入口来访问GDT了。GDTR中存放的是GDT在内存中的基地址和其表长界限。

            全局描述符表在系统中只能有一个,且可以被每一个任务所共享.任何描述符都可以放在GDT中,但中断门和陷阱门放在GDT中是不会起作用的.能被多个任务共享的内存区就是通过GDT完成的,

            GDTR寄存器中的基地址指定GDT表在内存中的起始地址,表长度指明GDT表的字节长度值。

            指令LGDT和SGDT分别用于加载和保存GDTR寄存器的内容。在机器刚加电或处理器复位后,基地址被默认地设置为0,而长度值被设置成0xFFFF。在保护模式初始化过程中必须给GDTR加载一个新值。

    在这里插入图片描述

     

            在8086中,段寄存器中存储内存的段基址(上图中的内存的起始地址),在80386中保护模式下,内存的起始地址存放在GDT中,段寄存器 的作用是选择子。

            GDT共有2^13,8192个,因为占用的是段寄存器的高13位

    在linux内核源码中:arch/i386/kernel/head.S

    1. /*
    2. * The boot_gdt_table must mirror the equivalent in setup.S and is
    3. * used only by the trampoline for booting other CPUs
    4. */
    5. .align L1_CACHE_BYTES
    6. ENTRY(boot_gdt_table)
    7. .fill GDT_ENTRY_BOOT_CS,8,0
    8. .quad 0x00cf9a000000ffff /* kernel 4GB code at 0x00000000 */
    9. .quad 0x00cf92000000ffff /* kernel 4GB data at 0x00000000 */
    10. #endif
    11. .align L1_CACHE_BYTES
    12. ENTRY(cpu_gdt_table)
    13. .quad 0x0000000000000000 /* NULL descriptor */
    14. .quad 0x0000000000000000 /* 0x0b reserved */
    15. .quad 0x0000000000000000 /* 0x13 reserved */
    16. .quad 0x0000000000000000 /* 0x1b reserved */
    17. .quad 0x0000000000000000 /* 0x20 unused */
    18. .quad 0x0000000000000000 /* 0x28 unused */
    19. .quad 0x0000000000000000 /* 0x33 TLS entry 1 */
    20. .quad 0x0000000000000000 /* 0x3b TLS entry 2 */
    21. .quad 0x0000000000000000 /* 0x43 TLS entry 3 */
    22. .quad 0x0000000000000000 /* 0x4b reserved */
    23. .quad 0x0000000000000000 /* 0x53 reserved */
    24. .quad 0x0000000000000000 /* 0x5b reserved */
    25. .quad 0x00cf9a000000ffff /* 0x60 kernel 4GB code at 0x00000000 */
    26. .quad 0x00cf92000000ffff /* 0x68 kernel 4GB data at 0x00000000 */
    27. .quad 0x00cffa000000ffff /* 0x73 user 4GB code at 0x00000000 */
    28. .quad 0x00cff2000000ffff /* 0x7b user 4GB data at 0x00000000 */
    29. .quad 0x0000000000000000 /* 0x80 TSS descriptor */
    30. .quad 0x0000000000000000 /* 0x88 LDT descriptor */
    31. .......

    在其中定义了系统预先占用的全局描述符表项,剩下的可以使用。每一项段描述符:

    .quad 0x 00 cf 9a 00 00 00 ff ff   占8个字节

    内存的起始地址:B0-B15  ,B16-B23,B24-B31 一共32位

    内存的段大小:L0-L15,L16-L19, 一共20位   2^20=1M 长度

    G:表示长度的单位  0:字节  1:页(4K),因此段大小有可能是1*1M = 1M byte 或4K * 1M = 4G byte,32位linux内核给每个进程都会分配一个虚拟地址空间 4G ,3G用户空间,1G内核空间

    保护模式下的内存分段的地址映射

    如DS 段寄存器:

    首先左移三位得到index,在GDT中获取内存的起始地址,

    然后 IP寄存器相加得到线性地址;

    检查是否开启分页机制,如果没有则线性地址就是物理地址。

    如果开启分页机制,线性地址经过多级的页表映射得到物理地址 

    局部描述符表LDT


            局部描述符表LDT(Local Descriptor Table)局部描述符表可以有若干张,每个任务可以有一张。我们可以这样理解GDT和LDT:GDT为一级描述符表,LDT为二级描述符表。如图

    在这里插入图片描述

            局部描述符表在系统中可以有多个,通常情况下是与任务的数量保持对等,但任务可以没有局部描述符表.任务间不相干的部分也是通过LDT实现的.这里涉及到地址映射的问题.和GDT一样,中断门和陷阱门放在LDT中是不会起作用的.

             LDT和GDT从本质上说是相同的,只是LDT嵌套在GDT之中。

            LDTR记录局部描述符表的起始位置,与GDTR不同,LDTR的内容是一个段选择子。

            由于LDT本身同样是一段内存,也是一个段,所以它也有个描述符描述它,这个描述符就存储在GDT中,对应这个表述符也会有一个选择子,LDTR装载的就是这样一个选择子。LDTR可以在程序中随时改变,通过使用lldt指令。如上图,如果装载的是Selector 2则LDTR指向的是表LDT2。举个例子:如果我们想在表LDT2中选择第三个描述符所描述的段的地址12345678h。

    1. 首先需要装载LDTR使它指向LDT2 使用指令lldt将Select2装载到LDTR
    2. 通过逻辑地址(SEL:OFFSET)访问时SEL的index=3代表选择第三个描述符;TI=1代表选择子是在LDT选择,此时LDTR指向的是LDT2,所以是在LDT2中选择,此时的SEL值为1Ch(二进制为11 1 00b)。OFFSET=12345678h。逻辑地址为1C:12345678h
    3. 由SEL选择出描述符,由描述符中的基址(Base)加上OFFSET可得到线性地址,例如基址是11111111h,则线性地址=11111111h+12345678h=23456789h
    4. 此时若再想访问LDT1中的第三个描述符,只要使用lldt指令将选择子Selector 1装入再执行2、3两步就可以了(因为此时LDTR又指向了LDT1)

            由于每个进程都有自己的一套程序段、数据段、堆栈段,有了局部描述符表则可以将每个进程的程序段、数据段、堆栈段封装在一起,只要改变LDTR就可以实现对不同进程的段进行访问。

             当进行任务切换时,处理器会把新任务LDT的段选择符和段描述符自动地加载进LDTR中。在机器加电或处理器复位后,段选择符和基地址被默认地设置为0,而段长度被设置成0xFFFF。

            GDT表只有一个,是固定的;而LDT表每个任务就可以有一个,因此有多个,并且由于任务的个数在不断变化其数量也在不断变化。如果只有一个LDTR寄存器显然不能满足多个LDT的要求。因此INTEL的做法是把它放在放在GDT中。

    段选择子


            在保护模式下,段寄存器的内容已不是段值,而称其为选择子.该选择子指示描述符在上面这三个表中的位置,所以说选择子即是索引值。

            当我们把段选择子装入寄存器时不仅使该寄存器值,同时CPU将该选择子所对应的GDT或LDT中的描述符装入了不可见部分。这样只要我们不进行代码切换(不重新装入新的选择子)CPU就会不会对不可见部分存储的描述符进行更新,可以直接进行访问,加快了访问速度。一旦寄存器被重新赋值,不可见部分也将被重新赋值。

    在这里插入图片描述

            段选择子包括三部分:描述符索引(index)、TI、请求特权级(RPL)。

            index(描述符索引)部分表示所需要的段的描述符在描述符表的位置,由这个位置再根据在GDTR中存储的描述符表基址就可以找到相应的描述符。

            然后用描述符表中的段基址加上逻辑地址(SEL:OFFSET)的OFFSET就可以转换成线性地址,

            段选择子中的TI值只有一位0或1,0代表选择子是在GDT选择,1代表选择子是在LDT选择。

       

    例如给出逻辑地址:21h:12345678h转换为线性地址

    a. 选择子SEL=21h=0000000000100 0 01b 他代表的意思是:选择子的index=0000000000100 也就是4即选择GDT中的第4个描述符;TI=0代表选择子是在GDT选择;左后的01b代表特权级RPL=1

    b. OFFSET=12345678h若此时GDT第四个描述符中描述的段基址(Base)为11111111h,则线性地址=11111111h+12345678h=23456789h

    任务寄存器TR


            TR用于寻址一个特殊的任务状态段(Task State Segment,TSS)。TSS中包含着当前执行任务的重要信息。

            TR寄存器用于存放当前任务TSS段的16位段选择符、32位基地址、16位段长度和描述符属性值。它引用GDT表中的一个TSS类型的描述符。指令LTR和STR分别用于加载和保存TR寄存器的段选择符部分。当使用LTR指令把选择符加载进任务寄存器时,TSS描述符中的段基地址、段限长度以及描述符属性会被自动加载到任务寄存器中。当执行任务切换时,处理器会把新任务的TSS的段选择符和段描述符自动加载进任务寄存器TR中。

    实例


    1:访问GDT

    当TI=0时表示段描述符在GDT中,如上图所示:

    ①先从GDTR寄存器中获得GDT基址。

    ②然后再GDT中以段选择器高13位位置索引值得到段描述符。

    ③段描述符符包含段的基址、限长、优先级等各种属性,这就得到了段的起始地址(基址),再以基址加上偏移地址yyyyyyyy才得到最后的线性地址。

    在这里插入图片描述

    2:访问LDT

    在这里插入图片描述

    当TI=1时表示段描述符在LDT中,如上图所示:

    ①还是先从GDTR寄存器中获得GDT基址。

    ②从LDTR寄存器中获取LDT所在段的位置索引(LDTR高13位)。

    ③以这个位置索引在GDT中得到LDT段描述符从而得到LDT段基址。

    ④用段选择器高13位位置索引值从LDT段中得到段描述符。

    ⑤段描述符符包含段的基址、限长、优先级等各种属性,这就得到了段的起始地址(基址),再以基址加上偏移地址yyyyyyyy才得到最后的线性地址。
     

    分页模式:线性地址到物理地址的映射

    控制寄存器

      控制寄存器用于控制和确定CPU的操作模式。控制寄存器有Cr0Cr1Cr2Cr3Cr4Cr1被保留了,Cr3用于页目录表基址,其他的将继续详细讲解。

    Cr0

      Cr0是一个十分重要的寄存器,可以说它是总开关的集合体。如下图所示:

      PE位是启用保护模式(Protection Enable)标志。若PE = 1是开启保护模式,反之为实地址模式。这个标志仅开启段级保护,而并没有启用分页机制。若要启用分页机制,那么PEPG标志都要置位。
      PG位是启用分页机制。在开启这个标志之前必须已经或者同时开启PE标志。PG = 0PE = 0,处理器工作在实地址模式下。PG = 0PE = 1,处理器工作在没有开启分页机制的保护模式下。PG = 1PE = 0,在PE没有开启的情况下无法开启PGPG = 1PE = 1,处理器工作在开启了分页机制的保护模式下。
      WP位对于Intel 80486或以上的CPU,是写保护(Write Proctect)标志。当设置该标志时,处理器会禁止超级用户程序(例如特权级0的程序)向用户级只读页面执行写操作;当CPL < 3的时候,如果WP = 0可以读写任意用户级物理页,只要线性地址有效。如果WP = 1可以读取任意用户级物理页,但对于只读的物理页,则不能写。

    Cr2

      当CPU访问某个无效页面时,会产生缺页异常,此时,CPU会将引起异常的线性地址存放在CR2中,如下图所示:

    Cr4

      Cr4的结构如下图所示:

      VME用于虚拟8086模式。PAE用于确认是哪个分页,PAE = 1,是2-9-9-12分页,PAE = 010-10-12分页。PSE是大页是否开启的总开关,如果置0,就算PDE中设置了大页你也得是普通的页

        当Cr0中PG = 1PE = 1,处理器工作在开启了分页机制的保护模式下。

    从线性地址到物理地址的映射过程为:
    (1)从CR3寄存器中获取页面目录(Page Directory)的基地址;
    (2)以线性地址的dir位段为下标,在目录中取得相应页面表(Page Table)的基地址;
    (3)以线性地址中的page位段为下标,在所得到的页面表中获得相应的页面描述项;
    (4)将页面描述项中给出的页面基地址与线性地址中的offset位段相加得到物理地址。

              CR3寄存器的值从哪里来的?每个进程都会有自己的地址空间,页面目录也在内存不同的位置上,这样不同进程就有不同的CR3寄存器的值。CR3寄存器的值一般都保存在进程控制块中,如linux中的task_struct数据结构中。

              页目录项一项占四字节,一共是4*2^10=4K,同理页表大小也是4K,都是4K对齐,从地址0开始,每一个页面(大小4k)的起始地址都是4K的整数倍

             分页模式,4K对齐,也就是低12位为0,页目录中一项的大小为32位,低12位用于权限等标志,仅仅用高20位存储页表基址,同理有项表项

    在进程切换时,需要切换进程的目录的起始地址放入cr3寄存器中

             在linux内核启动时,会检测当前内存的大小,将内存分成一个个page(大小4k),用于内存分配

          如果页表项中:

    • 高20位是0,最低位也是0,表示物理页面还没有分配过,会触发缺页异常
    • 高20位不是0,最低位是0,表示物理页面在交换分区中,此时页表项中存储物理页在swap分区中的位置
    • 高20位不是0,最低位不是0,表示物理页面正常使用

          在页表项中,存储着物理内存页的起始地址,由于4k对齐,实际上只需要使用20位表示2^20=1M大小的范围,即 0 , 1, 2 ......2^20-1,因此也称为框号。

        线性地址转化为物理地址的计算过程,32位系统下两级页表的过程中在MMU中计算完成。

        虚拟内存的好处:   

     1 安全:每个进程的地址空间是相互隔离(0-4G),不受其他影响的

    2  虚拟地址连续,物理地址可以不连续,物理内存可以得到充分使用,使用LRU算法置换内存页面,将不经常使用的页面置换到磁盘。

    问题:

    1 能不能在用户空间定义一个指针指向内核空间的内存:不能,权限问题

    2 能不能在内核空间定义一个指针指向用户空间的内存:不能

       内核空间的地址映射和上面的地址映射(用户空间)不同;内核空间的地址映射

     _pa 把内核的虚拟地址转为物理地址,也就是x - 3G

    _va  把内核的物理地址转为虚拟地址,也就是x+ 3G,

    也就是内核的虚拟地址空间在3G-4G,内核启动放置镜像从0x100000  1M的位置(之前的位置放置实模式下的东西),映射到虚拟内存的0xc0100000

    bochs使用 查看分段分页

    资源:https://download.csdn.net/download/LIJIWEI0611/86539758

    安装后:

     拷贝资源中的mylinux到安装目录下:

    打开:

    依次点击:

     

     

    看到界面:

     调试命令类似gdb:

    c:继续

     进入mycode 目录下

     执行a.out 程序进入死循环

     因为data不可能为0,所以程序不可能停止运行。如何停止呢

    data的虚拟地址是0x3fffef4,如果能找到物理地址,修改物理地址中的内容为0,那么while就可以停止运行。

    data是局部变量,在栈上,在ss 栈内存中,输入命令sreg

     ss在0x0017 h,转换成二进制 0000 0000 0001 0111 B;

    根据上面段选择子内容的介绍:

     在这里插入图片描述

            段选择子包括三部分:描述符索引(index)、TI、请求特权级(RPL)。

            index(描述符索引)部分表示所需要的段的描述符在描述符表的位置,由这个位置再根据在GDTR中存储的描述符表基址就可以找到相应的描述符。

            然后用描述符表中的段基址加上逻辑地址(SEL:OFFSET)的OFFSET就可以转换成线性地址,

            段选择子中的TI值只有一位0或1,0代表选择子是在GDT选择,1代表选择子是在LDT选择。

    在 0000 0000 0001 0111 B;中

    第1,2位11 表示用户空间,

    第3位1 表示LDT

    剩下的表示index = 2,这块内存的信息存放在LDT[2]中。

            如何找到LDT?

    ldtr:0x0068h 即   0000 0000 0110 1(index=13)0(GDT)00(内核权限)

    LDT存储在GDT中index=13的位置中

    一个GDT表项占8字节,

    输入: xp/2w 0x0000000000005cb8+13*8   xp 查看字节偏移,w表示一个字节 2w表示两个字节,因为一个段描述符是两个字节8位, 0x0000000000005cb8是gdtr的基地址,偏移13(index)*8(段描述符8bit)

     小端模式:低地址存低字节

    因此是:

    0x000082fa

     0xd2d00068

    对照:

    0x000082fa : 0xe2d00068   对应的二进制为:

    0000

    82fa

    d2d0

    0068

    那么:

    内存的起始地址:B0-B15  ,B16-B23,B24-B31 一共32位 红色标出

    内存的段大小:L0-L15,L16-L19, 一共20位   2^20=1M 长度  绿色标出

    G:表示长度的单位  0:字节  1:页(4K),因此段大小有可能是1*1M = 1M byte 或4K * 1M = 4G byte,32位linux内核给每个进程都会分配一个虚拟地址空间 4G ,3G用户空间,1G内核空间

    内存起始地址:    0x 00fa d2d0 

    内存的段大小:  0x0068

    G:0 表示字节

    data存储在ldt[2]中,即 0x 00fa d2d0 +2*8 ,查看 xp/2w 0x00fad2d0+2*8

     查看这64位段描述符的意义:

      0x00003fff      0x10c0f300

    10c0

    f300

    0000

    3fff

    起始地址是1000 0000H

    偏移量即逻辑地址 0x3fffef4

    线性地址= 起始地址+偏移量=  1000 0000+  3ff fef4 = 0x13ff fef4

    分段已经完成,得到线性地址,现在查看是否分页:


      PG位是启用分页机制。在开启这个标志之前必须已经或者同时开启PE标志。PG = 0PE = 0,处理器工作在实地址模式下。PG = 0PE = 1,处理器工作在没有开启分页机制的保护模式下。PG = 1PE = 0,在PE没有开启的情况下无法开启PGPG = 1PE = 1,处理器工作在开启了分页机制的保护模式下。

         现在CR0=0x8000001b: PG=1,PE=1 开启了分页模式

    0x13ff fef4 对应的二进制

    二进制 0001 0011 1111 1111 111 1110 1111 0100 

    十进制 79 1023  3828

    页目录的基地址在cr3寄存器中:

     

    页目录基地址为0

    找到页目录的79项,一个项占四个字节 :

    页目录项内容的高20位表示页表项地址 :

    页表项地址 = 00fb1000 + 1023*4:

     最后的内容是a,因为data=10,也就是16进制的a; 物理地址是0x0000000000f95ef4

     setpmem 0x0000000000f95ef4       4 0   设置物理地址0x0000000000f95ef4 起始的四字节内容为0

     输出 c 继续执行程序,发现程序结束!

    mm_struct

     

            如上图所示,当进程需要访问一个虚拟地址,如0x00FFF213时:

            首先通过mm_struct 中的 mmp,mmp是一个红黑树,这样查找速度可以达到logn,加快虚拟地址的查找速度。mmp将进程的虚拟地址空间用一个个vm_area_struct组织起来,vm_area_struct中表示了它维持的虚拟地址空间区域的起始位置结束位置,读写权限等,属于哪个段;

           当查找mmp时找不到该地址所在的地址区间,则报错 segment fault (段错误)导致程序crash.

           当从mmp中找到后0x00FFF213所在的虚拟地址空间存在且合法后:

           访问pgd,页目录表起始地址,通过分段和分页最终找到物理地址。当访问pgd时找不到相应物理页面,则会触发缺页异常。

         关于分段分页可以参考:Linux内核网桥注释v0.11 :

    第六章 引导启动程序 6.4 head.s  在:

    在进入保护模式首先创建一个内存页目录表(4k)。0.11 内核是所有进程公用一个页目录表(唯一)。然后创建四个页表供内核代码使用;创建全局描述符表gdt.

             

     5.3 Linux内核对内存的管理与使用

    13章内存管理

    缺页异常:do_page_fault

     

     

     

     

             区别:如中断指令(系统调用)执行完中断处理函数,执行下一条指令;

                        异常:如缺页异常,执行完do_page_fault后执行执行当前指令。

    关于缺页异常:在linux0.11 内核完全注释中,13章内存管理 对写时拷贝和按需加载有详细的描述:

    linux内核2.6 中:

    do_page_fault 是页面异常的异常服务函数:

    在这个函数中,通过handle_mm_fault处理:

    1. dotraplinkage void __kprobes
    2. do_page_fault(struct pt_regs *regs, unsigned long error_code)
    3. {
    4. struct vm_area_struct *vma;
    5. struct task_struct *tsk;
    6. unsigned long address;
    7. struct mm_struct *mm;
    8. int fault;
    9. int write = error_code & PF_WRITE;
    10. unsigned int flags = FAULT_FLAG_ALLOW_RETRY | FAULT_FLAG_KILLABLE |
    11. (write ? FAULT_FLAG_WRITE : 0);
    12. tsk = current;//当前CPU正在执行的进程
    13. mm = tsk->mm;//当前进程的mm_struct
    14. /* Get the faulting address: */
    15. //获取出错地址,cr2 寄存器放置缺页异常时的地址
    16. address = read_cr2();
    17. ......
    18. //查找mm_struct中address所在的vma
    19. vma = find_vma(mm, address);
    20. if (unlikely(!vma)) {//没有找到 段错误 cause a SIGSEGV
    21. bad_area(regs, error_code, address);
    22. return;
    23. }//找到该区间(除了栈区域,其他段都是从低地址到高地址增长)
    24. if (likely(vma->vm_start <= address))
    25. goto good_area;
    26. //判断是不是栈区域
    27. if (unlikely(!(vma->vm_flags & VM_GROWSDOWN))) {
    28. bad_area(regs, error_code, address);
    29. return;
    30. }
    31. if (error_code & PF_USER) {
    32. /*
    33. * Accessing the stack below %sp is always a bug.
    34. * The large cushion allows instructions like enter
    35. * and pusha to work. ("enter $65535, $31" pushes
    36. * 32 pointers and then decrements %sp by 65535.)
    37. */
    38. if (unlikely(address + 65536 + 32 * sizeof(unsigned long) < regs->sp)) {
    39. bad_area(regs, error_code, address);
    40. return;
    41. }
    42. }
    43. //判断在不在栈的增加区域,不踩到其他区域
    44. if (unlikely(expand_stack(vma, address))) {
    45. bad_area(regs, error_code, address);
    46. return;
    47. }
    48. /*
    49. * Ok, we have a good vm_area for this memory access, so
    50. * we can handle it..
    51. */
    52. // address是合法的vma区域,解决该异常:
    53. good_area:
    54. //判断当前指令的操作和vma节点描述的内存区域的内存属性是否一致
    55. //如 在只读的vma属性中执行写操作
    56. if (unlikely(access_error(error_code, vma))) {
    57. bad_area_access_error(regs, error_code, address);
    58. return;
    59. }
    60. /*
    61. * If for any reason at all we couldn't handle the fault,
    62. * make sure we exit gracefully rather than endlessly redo
    63. * the fault:
    64. */
    65. //核心处理函数 pgd-> 页目录 ->页表 -> 物理地址
    66. fault = handle_mm_fault(mm, vma, address, flags);
    67. ......
    68. }

    __handle_mm_fault

    1. /*
    2. * By the time we get here, we already hold the mm semaphore
    3. */
    4. int __handle_mm_fault(struct mm_struct *mm, struct vm_area_struct *vma,
    5. unsigned long address, int write_access)
    6. {
    7. pgd_t *pgd;
    8. pud_t *pud;
    9. pmd_t *pmd;
    10. pte_t *pte;
    11. __set_current_state(TASK_RUNNING);
    12. count_vm_event(PGFAULT);
    13. if (unlikely(is_vm_hugetlb_page(vma)))
    14. return hugetlb_fault(mm, vma, address, write_access);
    15. //查找页目录项pgd :page dir
    16. pgd = pgd_offset(mm, address);
    17. pud = pud_alloc(mm, pgd, address);
    18. if (!pud)
    19. return VM_FAULT_OOM;
    20. pmd = pmd_alloc(mm, pud, address);
    21. if (!pmd)
    22. return VM_FAULT_OOM;
    23. //创建 pgd 指向的页表如果不存在,创建 pgd 指向的页表 :page table entry
    24. pte = pte_alloc_map(mm, pmd, address);
    25. if (!pte)
    26. return VM_FAULT_OOM;
    27. return handle_pte_fault(mm, vma, address, pte, pmd, write_access);
    28. }

    handle_pte_fault

    1. static inline int handle_pte_fault(struct mm_struct *mm,
    2. struct vm_area_struct *vma, unsigned long address,
    3. pte_t *pte, pmd_t *pmd, int write_access)
    4. {
    5. pte_t entry;
    6. spinlock_t *ptl;
    7. //entry 记录了页表项的内容 8字节描述符
    8. entry = *pte;
    9. //当前页表项还没有记录物理页面的有效信息:没有分配物理页面(按需加载)
    10. if (!pte_present(entry)) {
    11. //1 页表项是空的,没有记录物理页面信息->分配物理页面
    12. if (pte_none(entry)) {
    13. if (vma->vm_ops) {
    14. if (vma->vm_ops->nopage)
    15. //分配物理页面
    16. return do_no_page(mm, vma, address,
    17. pte, pmd,
    18. write_access);
    19. if (unlikely(vma->vm_ops->nopfn))
    20. return do_no_pfn(mm, vma, address, pte,
    21. pmd, write_access);
    22. }
    23. return do_anonymous_page(mm, vma, address,
    24. pte, pmd, write_access);
    25. }
    26. //2 页表项是空的,页表项指向一个文件的内容:
    27. // mmap file文件映射到内存:共享内存
    28. if (pte_file(entry))
    29. return do_file_page(mm, vma, address,
    30. pte, pmd, write_access, entry);
    31. //3 做页面的交换:页表项并不空,但是已经不在物理内存中
    32. //在swap交换分区中,重新加载到物理内存中,更新pte的内容
    33. return do_swap_page(mm, vma, address,
    34. pte, pmd, write_access, entry);
    35. }
    36. ptl = pte_lockptr(mm, pmd);
    37. spin_lock(ptl);
    38. if (unlikely(!pte_same(*pte, entry)))
    39. goto unlock;
    40. // 页表项是正常的,当前的指令需要写操作
    41. if (write_access) {
    42. //没有写权限
    43. if (!pte_write(entry))
    44. //wp(write on page) 写时拷贝:
    45. //父进程创建子进程时,复制页目录项,页表,此时共用
    46. //物理内存,共用的物理内存双方只读,不能写,如果需要写
    47. //则需要给子进程分配单独的物理页面,并且修改标志位可读
    48. return do_wp_page(mm, vma, address,
    49. pte, pmd, ptl, entry);
    50. entry = pte_mkdirty(entry);
    51. }
    52. //LRU 最近最久未使用算法,更新最新访问的页面
    53. entry = pte_mkyoung(entry);
    54. //更新物理page对应的pte
    55. if (ptep_set_access_flags(vma, address, pte, entry, write_access)) {
    56. update_mmu_cache(vma, address, entry);
    57. lazy_mmu_prot_update(entry);
    58. } else {
    59. /*
    60. * This is needed only for protection faults but the arch code
    61. * is not yet telling us if this is a protection fault or not.
    62. * This still avoids useless tlb flushes for .text page faults
    63. * with threads.
    64. */
    65. if (write_access)
    66. flush_tlb_page(vma, address);
    67. }
    68. unlock:
    69. pte_unmap_unlock(pte, ptl);
    70. return VM_FAULT_MINOR;
    71. }

  • 相关阅读:
    leetcode最大间距(桶排序+Python)
    SpringCloud Alibaba-Seata
    第十三届蓝桥杯总结
    linux安装MySql之错误
    mac电脑部署安装powershell
    html:lang属性设置为中文zh-CN
    【面试:并发篇20:多线程:多把锁问题】
    如何让 Llama2、通义千问开源大语言模型快速跑在函数计算上?
    Python批处理(一)提取txt中数据存入excel
    彻底理解Java并发:Java线程池
  • 原文地址:https://blog.csdn.net/LIJIWEI0611/article/details/126868246