linux 内核采用页式存储管理。虚拟地址空间划分成固定大小的“页面”,由MMU在运行时将虚拟地址映射成某个物理内存页面中的地址。页式内存管理比段式内存管理有很多好处,但是由于Intel是先使用段式管理的,然后才发明了页式管理,为了兼容,i386 CPU 一律对程序中使用的地址先进行段式映射,然后才能进行页式映射,既然CPU的硬件结构是这样,linux内核也只好服从intel的选择。通过一个例子看看linux内核是怎样在i386 CPU 上进行地址映射的。

假设我们写了这么一个程序:

 1 #include <stdio.h>
2
3 greeting()
4 {
5 printf("hello, world!\n");
6 }
7
8 main()
9 {
10 greeting();
11 }

经过编译后,我们得到可执行代码hello。用命令:objdump -d hello来反汇编这一段二进制代码

从上面可以看出,ld给greeting()分配的地址为0x80483e4,在elf格式代码中,ld 总是从0x80000000开始安排程序的“代码段”,对每个程序都是这样,至于程序在执行时在物理内存中的实际位置则就要由内核在为其建立内存映射时临时作出安排。

假设该程序已经在运行,整个映射机制都已经建立好,并且CPU 正在执行main()中的“call 80483e4”这条指令,要转移到虚拟地址0x080483e4上去,接下来就一步一步的走过这个地址映射过程。

首先是段式映射,由于地址0x080483e4是一个程序的入口,更重要的是在执行的过程中是由CPU 中的“指令计数器”EIP所指向的,所以在代码段中,因此,i386 CPU使用代码段寄存器CS的当前值来作为段式映射的“选择码”,也就是用它作为在段描述表中的下标。

接下来看看CS的内容,内核建立一个进程时都要将其段寄存器设置好(?),代码在include/asm-i386/processor.h 中:

可以看到,除了CS被设置成USER_CS外,其他的所有段寄存器都被设置成USER_DS。就是说,虽然Intel 的意图是将一个进程的映像分成代码段、数据段和堆栈段,linux 内核中堆栈段和数据段式不分的。

再来看看USER_CS和USER_DS到底是什么

也就是说,linux 内核中只是用四种不同的段寄存器值,两种用于内核本身,两种用于所有的进程。现在我们将这四种数值用二进制展开并与段寄存器的格式对照:

Index               TI   RPL

————————————————————————————————————————————

__KERNEL_CS    0x10        0000  0000  0001  0  |  0  | 0  0

__KERNEL_DS    0x18        0000  0000  0001  1  |  0  | 0  0

__USER_CS      0x23        0000  0000  0010  0  |  0  | 1  1

__USER_DS      0x2B        0000  0000  0010  1  |  0  | 1  1

经过和段寄存器的格式对照:

__KERNEL_CS       index = 2              TI = 0              RPL = 0

__KERNEL_DS       index = 3              TI = 0              RPL = 0

__USER_CS         index = 4              TI = 0              RPL = 3

__USER_DS         index = 5              TI = 0              RPL = 3

TI 都是0说明用的都是GDT,RPL只用了0和3,内核为0级,用户为3级

我们上面写的程序显然不属于内核,所以在进程的用户空间中运行,内核在调度该进程进入运行时,把CS设置成__USER_CS,即0x23,所以,CPU 以4为下标在全局描述表GDT中招对应的段描述项。

GDT内容如下:

把4个段描述项的内容按二进制展开,和段描述项的定义对照,可以得出以下结论:

每个段都是从0地址开始的整个4GB空间,虚地址到线性地址的映射保持不变。所以,linux 内核设计的段式映射机制把地址0x080483e4 映射到了自身,现在作为线性地址出现了,下面进入页式映射过程:

每个进程都有其自身的页面目录PGD,指向这个目录的指针保持在每个进程的mm_struct数据结构中,每当调度一个进程进入运行的时候,内核都要为即将运行的进程设置好控制寄存器CR3,而MMU的硬件则总是从CR3中取得指向当前页面目录的指针。不过,CPU 在执行程序时使用的虚存地址,而MMU硬件在进行映射时所用的则是物理地址,这是在switch_mm()函数中完成的

1 static inline void switch_mm(struct mm_struct *prev, struct mm_struct *next, struct task_struct *tsk, unsigned cpu)
2 {
3 ……
4
5 asm volatile("movl %0, %%cr3" : : "r" (__pa(next->pgd)));
6 ……
7 }

当我们程序中要转移到地址0x80483e4去的时候,进程正在运行中,CR3早已设置好,指向我们这个进程的页面目录了,先将线性地址展开:

0000 1000 0000 0100 1000 0011 1110 0100

0000100000  0001001000   0011 1110 0100

32                    72                 996

已CR3中的内容指向的地址为基地址,以32为下标就找到了目录项,取得到的目录项中的高20位然后加上12个0就得到了该页面表的指针。同理再以刚刚找到的页面表指针为基地址,以72为下标,找到页表项,然后取高20位,再加上996即是greeting()函数的物理地址。

linux 内核源代码情景分析——地址映射的全过程的更多相关文章

  1. Linux内核源代码情景分析系列

    http://blog.sina.com.cn/s/blog_6b94d5680101vfqv.html Linux内核源代码情景分析---第五章 文件系统  5.1 概述 构成一个操作系统最重要的就 ...

  2. Linux内核源代码情景分析-fork()

    父进程fork子进程: child = fork() fork经过系统调用.来到了sys_fork.具体过程请參考Linux内核源码情景分析-系统调用. asmlinkage int sys_fork ...

  3. linux 内核源代码情景分析——linux 内存管理的基本框架

    386 CPU中的页式存管的基本思路是:通过页面目录和页面表分两个层次实现从线性地址到物理地址的映射.这种映射模式在大多数情况下可以节省页面表所占用的空间.因为大多数进程不会用到整个虚存空间,在虚存空 ...

  4. linux 内核源代码情景分析——linux 内核源代码中的C语言代码

    linux 内核的主体是以GNU的C语言编写的,GNU为此提供了编译工具gcc.GNU对C语言本身作了不少扩充. 1) gcc 从 C++ 语言中吸收了"inline"和" ...

  5. linux 内核源代码情景分析——用户堆栈的扩展

    上一节中,我们浏览了一次因越界访问而造成映射失败从而引起进程流产的过程,不过有时候,越界访问时正常的.现在我们就来看看当用户堆栈过小,但是因越界访问而"因祸得福"得以伸展的情景. ...

  6. Linux内核源代码情景分析-中断半

    一.中断初始化 1.中断向量表IDT初始化 void __init init_IRQ(void) { int i; #ifndef CONFIG_X86_VISWS_APIC init_ISA_irq ...

  7. linux 内核源代码情景分析——越界访问

    页式存储管理机制通过页面目录和页面表将每个线性地址转换成物理地址,当遇到下面几种情况就会使CPU产生一次缺页中断,从而执行预定的页面异常处理程序: ① 相应的页面目录或页表项为空,也就是该线性地址与物 ...

  8. linux 内核源代码情景分析——几个重要的数据结构和函数

    页面目录PGD.中间目录PMD和页面表PT分别是由表项pgd_t.pmd_t和pte_t构成的数组,而这些表项都是数据结构 1 /* 2 * These are used to make use of ...

  9. linux 内核源代码情景分析——linux 内核源码中的汇编语言代码

    1. 用汇编语言编写部分核心代码的原因: ① 操作系统内核中的底层程序直接与硬件打交道,需要用到一些专用的指令,而这些指令在C语言中并无对应的语言成分: ② CPU中的一些特殊指令也没有对应的C语言成 ...

随机推荐

  1. 1.3redis小结--配置php reids拓展

    1.执行php文件 输出phpinfo();  <?php phpinfo(); 2.根据PHPinfo的信息确定需要下载的 php_redis.dll , php_igbinary.dll 版 ...

  2. jmeter加密解密(解密篇)

    上一篇已经讲解了公钥加密,这篇讲解公钥解密.解密比较简单,直接操作吧. 需求是:接口中的请求体的部分参数需要先加密再请求,返回的结果中部分字段需解密. 1.在请求下新建beanshell后置处理程序, ...

  3. vue 改变路由参数

    更改路由传递的参数: const query = JSON.parse(JSON.stringify(this.$route.query)) // 获取路由参数信息 query.id = Name / ...

  4. 我在学习Blazor当中踩的巨坑!Blazor WebAssembly调试

    最近嘛,看看Blazor已经蛮成熟的.顺便想在自家的框架里使用这个东西,毕竟我还是很念旧的,而且Blazor的技术栈也不麻烦.然后呢,在调试这一关我可是踩了大坑. 我的VS是2019,很早以前装的.然 ...

  5. Java-基础-JDK动态代理

    1. 简介 代理模式的定义:为其他对象提供一种代理以控制对这个对象的访问.在某些情况下,一个对象不适合或者不能直接引用另一个对象,而代理对象可以在客户端和目标对象之间起到中介的作用. 比如:我们在调用 ...

  6. Skywalking-12:Skywalking SPI机制

    SPI机制 基本概述 SPI 全称 Service Provider Interface ,是一种服务发现机制.通过提供接口.预定义的加载器( Loader )以及约定俗称的配置(一般在 META-I ...

  7. 巧用优先队列:重载运算符在STL优先队列中的应用

    前言 写优先队列优化dijkstra的时候,需要放进优先队列的常常有数值和编号两类,以下介绍让编号捆绑数值放入优先队列的几种方法. 由于过程比较简单,记住代码即可,下面不再讲解,只附上代码,请读者自行 ...

  8. cadvisor+prometheus+alertmanager+grafana完成容器化监控告警(一)

    一.概况 1.拓扑图 2.名词解释 Grafana 可视化监控容器运行情况 Prometheus: 开源系统监视和警报工具包 Alertmanager 一个独立的组件,负责接收并处理来自Prometh ...

  9. Serverless 架构到底要不要服务器?

    作者 | aoho 来源 | Serverless 公众号 Serverless 是什么? Serverless 架构是不是就不要服务器了?回答这个问题,我们需要了解下 Serverless 是什么. ...

  10. enum 试图表达64位数

    enum AttributeType: unsigned long long{ aa = 1, bb = 2, cc = 0x842AC1040000}; int main() { DWORD64 b ...