- 为什么把内核虚拟地址放到从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区分开来看