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()函数的物理地址。
最新文章
- 简单使用Git和Github来管理自己的代码和读书笔记
- 地图四叉树一般用在GIS中,在游戏寻路中2D游戏中一般用2维数组就够了
- 数据库阿里连接池 druid配置详解
- ASP.NET MVC4 执行流程
- 【转】数据库系统异常排查之DMV
- atitit.无线网卡 不能搜索到WiFi 无线路由信号的解决不能上网
- ASP.NET MVC5 网站开发实践
- C# 托管资源和非托管资源
- [AFN]AFNetworking错误总结
- crontab的应用
- Unity KGFMapSystem插件制作小地图
- CentOS 6.8yum源的配置
- CodeForce 356A Knight Tournament(set应用)
- mysql中的substr()函数
- 概念学习 - JNDI, JDBC, ODBC, DataSource
- Linux 下 安装jdk 1.7
- JavaScript类型相关常用操作
- JAVA进阶12
- P3810 -三维偏序(陌上花开)cdq-分治
- Tomcat通过Memcached实现session共享的完整部署记录
热门文章
- 『GoLang』字典Map
- fliebeat配置手册
- 鸿蒙内核源码分析(内存汇编篇) | 谁是虚拟内存实现的基础 | 百篇博客分析OpenHarmony源码 | v14.14
- String(char[] value, boolean share) {
- bash是什么?
- ES6箭头函数(箭头函数和普通函数的区别)
- scheduler源码分析——preempt抢占
- div标签的理解
- Spring Boot引入Swagger并对界面进行美化
- 1. SSTI(模板注入)漏洞(入门篇)