linux内核的物理内存访问误解

  • 为什么把内核虚拟地址放到从0xc000000开始最上面1g空间,原因是把从0开始的低地址留给用户态和设备层会更加方便些,而这个起点设置为0x c000000其实是任意的,主要考虑是那个版本内核内存占用不超过1g,假设你的系统内存占用大概最多不会超过256mb,那么起点设置为0xf000000也是可以的。
  • 内核汇编中寻址指令如何写,很简单,还是按照虚拟地址来写的,即使在汇编中也不是直接访问物理内存地址的还是传入虚拟地址(这句话不完全准确,详细可以查询x86实模式和32bit保护模式),然后硬件通过页表转换得到物理地址访问,而这个页表是在系统初始化时将高1g空间设置到某个物理地址范围(你的例子里一般是映射到物理内存从0开始的范围)然后载入到cr3(对x86架构)寄存器,之后每次汇编指令访问某个地址时,硬件会先从cr3寄存器找到页表再自动转换
  • 还有一个问题,为什么你看到一些资料会提到直接用 va-0xc000000,这个原因是在内核的某些特殊地方,例如页表还没使能的时候需要访问内存的情况下这样操作


     

    怎么访问内存跟MMU是否使能有关,内核本身决定不了怎么访问内存。在不使能MMU的情况下,cpu所执行的指令中涉及到的内存操作都是物理地址。在使能MMU的情况下,cpu所执行的指令中涉及到的内存操作都是虚拟地址。

    所以在MMU使能时,内核做的就是把虚拟地址映射到物理地址,也就是创建页表的过程。cpu执行指令的过程中对内存访问的虚拟地址都要经过MMU去翻译,然后MMU去访问物理内存。

    至于汇编层面怎么访问内存,本身和内核半毛钱关系都没有,在汇编代码里,访问内存对内核和用户层面来说都是一样的。下面我写了一个简单的内核函数test_mem,分别实现了对局部变量、静态局部变量、全局变量赋值操作的测试代码,还有对函数printk的调用:

    int global_var = 0;
    
    int __attribute__((optimize("O0"))) test_mem(int a, int b, int c, int d)
    {
        int i,j,k;
        static int l;  
    
        i = a, j = b, k = c;
        l = d;
        global_var = a;
    
        printk("%d %d %d %d %d\n", i, j, k, l, global_var);
    
        return 0;
    } 

    然后对上面的代码编译后进行反汇编,得到下面的汇编代码(基于x86_64 CPU):

    图1 test_mem函数的汇编代码,优化等级O0

    在上图可以看到,访问test_mem的局部变量是通过 offset(%rbp) 这种形式实现的,因为局部变量被存储在栈中,所以用到了rbp寄存器(保存了栈底基地址)。其中汇编第9~10行实现了“ i = a;”的操作,后面两个赋值语句(c语言的j = b, k = c;对应汇编第11~14行)以此类推。我们可以通过调用test_mem时栈的内存布局看的更直观一些:

    -------------------------------------------------  
      8(%rbp)     | previous rpb high |                high address
      4(%rpb)     | previous rpb low  |                     |
    0(%rpb)------------------------------------------       |
     -0x4(%rpb)   | local int k       |                     |
     -0x8(%rpb)   | local int j       |                     |
     -0xc(%rbp)   | local int i       |                     |
                  | alignment         |                     |
     -0x14(%rpb)  | argument int a    |                     |
     -0x18(%rbp)  | argument int b    |                     |
     -0x1c(%rbp)  | argument int c    |                     V
     -0x20(%rbp)  | argument int d    |                low address
    0(%rsp)------------------------------------------
    

    在汇编第17~18行是对全局变量global_var的赋值,它的访问形式是offset(%rip),rip的值是下一条指令的地址0xffffffff81001078(汇编第19行的第一个字节的地址), 0x16d4f98是global_var的符号地址减去rip的差值,所以我们可以计算一下global_var的地址为0xffffffff81001078 + 0x16d4f98 = 0xffffffff826d6010。此时我们打开System.map看一些global_var的虚拟地址:

    图2 全局变量global_var的虚拟地址

    是的,就是它。

    汇编第16行是关于静态局部变量的访问,它和全局变量的访问是一毛一样的,不再多说。

    在汇编第28行调用了printk函数,这里通过callq指令实现,直接传递printk的虚拟地址0xffffffff810c71fe完成调用。我们看一下System.map中printk函数的虚拟地址:

    图2 全局符号printk函数的虚拟地址

    上面就是所谓的汇编层面访问内存的实现,它跟0xc0000000(x86_64默认0xffff800000000000为用户空间和内核空间的分界线)并没什么关系,0xc0000000只是软件层面上给内核和用户空间做的划分,CPU和MMU并不关心这个值。当然这里只是通过一个简单的例子进行说明,还有其他的内存访问形式,有兴趣可以自己去研究。

    总结---

    要理解物理内存在MMU使能的情况下怎么被访问的,可以参考如下流程:

    1. 内核实现创建页表的过程(虚拟内存到物理内存的映射) -> 2. 编译链接的过程中由链接器分配虚拟地址给代码段、数据段和BSS段等 -> 3. cpu执行代码过程中,如果发生内存访问则将要访问的虚拟地址发送给MMU -> 4. MMU通过页表把虚拟地址转换成物理地址 -> 5. MMU访问物理地址 -> 6. CPU与内存完成数据交换。

  • 至于虚拟地址如何映射进物理内存,不是内核所关心的,是MMU要解决的问题,内核眼里只有虚拟存储的概念,MMU提供虚拟到现实的硬件实现,要把虚拟与MMU区分开来看

 

 

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值