linux 内核源代码情景分析——地址映射的全过程
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 内核源代码情景分析——地址映射的全过程的更多相关文章
- Linux内核源代码情景分析系列
http://blog.sina.com.cn/s/blog_6b94d5680101vfqv.html Linux内核源代码情景分析---第五章 文件系统 5.1 概述 构成一个操作系统最重要的就 ...
- Linux内核源代码情景分析-fork()
父进程fork子进程: child = fork() fork经过系统调用.来到了sys_fork.具体过程请參考Linux内核源码情景分析-系统调用. asmlinkage int sys_fork ...
- linux 内核源代码情景分析——linux 内存管理的基本框架
386 CPU中的页式存管的基本思路是:通过页面目录和页面表分两个层次实现从线性地址到物理地址的映射.这种映射模式在大多数情况下可以节省页面表所占用的空间.因为大多数进程不会用到整个虚存空间,在虚存空 ...
- linux 内核源代码情景分析——linux 内核源代码中的C语言代码
linux 内核的主体是以GNU的C语言编写的,GNU为此提供了编译工具gcc.GNU对C语言本身作了不少扩充. 1) gcc 从 C++ 语言中吸收了"inline"和" ...
- linux 内核源代码情景分析——用户堆栈的扩展
上一节中,我们浏览了一次因越界访问而造成映射失败从而引起进程流产的过程,不过有时候,越界访问时正常的.现在我们就来看看当用户堆栈过小,但是因越界访问而"因祸得福"得以伸展的情景. ...
- Linux内核源代码情景分析-中断半
一.中断初始化 1.中断向量表IDT初始化 void __init init_IRQ(void) { int i; #ifndef CONFIG_X86_VISWS_APIC init_ISA_irq ...
- linux 内核源代码情景分析——越界访问
页式存储管理机制通过页面目录和页面表将每个线性地址转换成物理地址,当遇到下面几种情况就会使CPU产生一次缺页中断,从而执行预定的页面异常处理程序: ① 相应的页面目录或页表项为空,也就是该线性地址与物 ...
- linux 内核源代码情景分析——几个重要的数据结构和函数
页面目录PGD.中间目录PMD和页面表PT分别是由表项pgd_t.pmd_t和pte_t构成的数组,而这些表项都是数据结构 1 /* 2 * These are used to make use of ...
- linux 内核源代码情景分析——linux 内核源码中的汇编语言代码
1. 用汇编语言编写部分核心代码的原因: ① 操作系统内核中的底层程序直接与硬件打交道,需要用到一些专用的指令,而这些指令在C语言中并无对应的语言成分: ② CPU中的一些特殊指令也没有对应的C语言成 ...
随机推荐
- 查看Win10商店应用更新日期
查看Win10商店应用更新日期 需要用到一个工具--WP Snitch,网址 https://wpsnitch.appspot.com/ 打开网址后他会给出一个示例,比如给出的是 https://ww ...
- webpack线上和线下模式
区别: 1 线下模式代码没有压缩,source-map是全的,比较容易定位错误,调试方便 2 线上模式的代码是压缩的,文件小, 分开打包: 方式一:有点麻烦 在package.json文件 " ...
- composer出现问题: Could not open input file: composer.phar
可以执行下面命令 php -r "readfile('https://getcomposer.org/installer');" | php This will install c ...
- 深入xLua实现原理之C#如何调用Lua
本文主要是探讨xLua下C#调用Lua的实现原理,有关Lua如何调用C#的介绍可以查看深入xLua实现原理之Lua如何调用C# C#与Lua数据通信机制 无论是Lua调用C#,还是C#调用Lua,都需 ...
- Java基础系列(41)- 冒泡排序
冒泡排序 冒泡排序无疑是最为出名的排序算法之一,总共有八大排序 冒泡的代码还是相当简单的,两层循环,外层冒泡轮数,里层依次比较,江湖中人人尽皆知 我们看到的嵌套循环,应该立马就可以得出这个算法的时间复 ...
- javascript LinkedList js 双向循环链表 Circular Linked List
javascript LinkedList: function Node(elem, prev, next) { this.elem = elem; this.prev = prev ? prev : ...
- windows2012添加ssl证书
第一步: 先下载 rewrite_x64_zh-cn.msi ,并安装 (*这个是2.0版本,千万不要安装2.1版本,否则导致网站进程池全部关闭) https://www.microsoft. ...
- centos7.0 能ping通ip 无法ping通域名处理方法
第一步: 检查 vi /etc/sysconfig/network-scripts/ifcfg-eth0 查看网卡配置里的dns是否与 vi /etc/resolv.conf 的 nameser ...
- php页面 数组根据下标来排序
$a = [ ['id'=>1,'title'=>'星期二的早晨','author'=>'张三','date'=>'2021-6-1'], ['id'=>2,'title ...
- java8的新特性之lambda表达式和方法引用
1.1. Lambda表达式 通过具体的实例去体会lambda表达式对于我们代码的简化,其实我们不去深究他的底层原理和背景,仅仅从用法上去理解,关注两方面: lambda表达式是Java8的一个语法糖 ...