linux内存模块遍历实现,解决Linux内核问题实用技巧之-dev/mem的新玩法

原创 dog250 Linux阅码场 2019-11-20

接着上一篇文章《解决Linux内核问题实用技巧之 - Crash工具结合/dev/mem任意修改内存》继续,本文中,我们来领略几种关于/dev/mem的玩法。

/dev/mem里有什么

简单来讲,/dev/mem是系统物理内存的映像文件,这里的 “物理内存” 需要进一步解释。

物理内存是指我们插在内存槽上的内存条吗?当然是,但物理内存不单单指内存条。

物理内存严格来讲应该是指 物理地址空间 ,内存条只是映射到这个地址空间的一部分,其余的还有各种PCI设备,IO端口等。我们可以从/proc/iomem中看到这个映射:

[root@localhost mem]# cat /proc/iomem

00000000-00000fff : reserved

00001000-0009fbff : System RAM

0009fc00-0009ffff : reserved

000c0000-000c7fff : Video ROM

000e2000-000ef3ff : Adapter ROM

000f0000-000fffff : reserved

000f0000-000fffff : System ROM

00100000-31ffffff : System RAM

01000000-01649aba : Kernel code

01649abb-01a74b7f : Kernel data

01c13000-01f30fff : Kernel bss

32000000-33ffffff : RAM buffer

3fff0000-3fffffff : ACPI Tables

e0000000-e0ffffff : 0000:00:02.0

e0000000-e0ffffff : vesafb

f0000000-f001ffff : 0000:00:03.0

f0000000-f001ffff : e1000

...

...

其中,只有RAM才是指内存条。关于物理地址空间的详细情况,请参考E820相关的资料。

明白了物理内存的构成之后,我们来看看/dev/mem里有什么。事实上,它就是一个活着的Linux系统实时映像,所有的进程taskstruct结构体,sock结构体,skbuff结构体,进程数据等等都在里面的某个位置:

a2083a97fcf8c01506365dc3a60a4dfe.png

如果能定位它们在/dev/mem里的位置,我们就能得到系统中这些数据结构的实时值,所谓的调试工具所做的也不过如此。其实我们在调试内核转储文件的时候,vmcore也是一个物理内存映像,和/dev/mem不同的是,它是一具尸体。

无论是活体,还是尸体,均五脏俱全,分析它们的手段是一致。和静态分析vmcore不同的是,/dev/mem是一个动态的内存映像,有时候借助它可以做一些正经的事情。

下面通过几个小例子,介绍和展示/dev/mem的一些玩法。

映射系统保留内存

Linux内核的内存管理子系统非常强大,同时也非常复杂。我们受其恩惠的同时,偶尔也会被其折磨得痛苦不堪。

动辄OOM杀掉关键进程,动辄刷脏页导致CPU飙高...

为了避免任意进程任意使用内存,我们引入资源隔离的机制,比如cgroup,但这样事情会变得更加复杂。

能不能保留一部分内存,不受内核的内存管理控制呢?就好像很多数据库不经文件系统直接访问裸盘一样,内核有没有什么机制让我们不经内存管理系统而直接使用内存呢?

当然有!加上mem启动参数即可实现。这里介绍一种关于保留内存的最简单配置,设置mem启动参数如下:

mem=800M

假设我们的系统总共有1G的内存(指内存条的总容量),那么上述启动参数将会保留 1G-800M 的内存不被系统内存管理系统所管理。因此我的保留内存就是200M:

[root@localhost mem]# cat /proc/cmdline

BOOT_IMAGE=/vmlinuz-3.10.0-327.x86_64 root=/dev/mapper/centos-root ro crashkernel=auto rd.lvm.lv=centos/root rd.lvm.lv=centos/swap rhgb quiet LANG=en_US.UTF-8 vga=793 mem=800M

[root@localhost mem]# cat /proc/iomem |grep RAM

00001000-0009fbff : System RAM

00100000-31ffffff : System RAM

32000000-33ffffff : RAM buffer

[root@localhost mem]#

我们关注一下保留内存的物理地址0x34000000(0x33ffffff+1) 。此时,如果用free命令或者/proc/meminfo,会看到物理内存少了200M,我们保留的200M内存不会记入内核的任何统计。

换句话说, 内核不再管这200M内存,你的程序可以任意涂抹,任意泄漏,任意溢出,任意覆盖它们,也不会对系统产生任何影响。

所谓的系统保留的含义就是 “内核不会为该段内存创建一一映射页表(x86_64位系统可以映射64T的物理内存)” 。

我们经常使用的crash工具读取内存使用的就是一一映射。

在x86_64平台,每一个非保留的物理内存页面可能会有多个映射,而保留物理内存页面不会有下面第一种映射:

1. 一一映射到0xffff880000000000开始虚拟地址。【保留页面缺失】

2. 映射到用户态使用它的进程地址空间。

3. 临时映射到内核态空间临时touch。

4. .....

我们试着用crash工具来读取一下保留内存:

crash> rd -p 0x34000000

rd: read error: physical address: 34000000 type: "64-bit PHYSADDR"

crash>

显然,内核并未对保留页面建立一一映射页表项,所以读取是失败的。

我们知道 /dev/mem 文件是整个物理内存映像,所以用户态进程可以使用mmap系统调用来重建用户态地址空间的页表。方法如下:

#include

#include

#include

#include

int main(int argc, char **argv)

{

int fd;

unsigned long *addr;

fd = open("/dev/mem", O_RDWR);

// 0x34000000 即/dev/mem的偏移,也就是保留内存在物理地址空间的偏移,我的例子就是0x34000000

addr = mmap(NULL, 4096, PROT_READ|PROT_WRITE, MAP_SHARED, fd, 0x34000000);

// ... 随意使用保留内存

close(fd);

munmap(addr, 4096);

return 1;

}

是不是很简单呢?

此时,在我们实施mmap的进程中便可以访问保留内存了:

crash> vtop 0x7f3751c3a000

VIRTUAL PHYSICAL

7f3751c3a000 34000000

PML: 6e477f0 => 2dbf7067

PUD: 2dbf76e8 => c524067

PMD: c524470 => 2c313067

PTE: 2c3131d0 => 8000000034000277

PAGE: 34000000

PTE PHYSICAL FLAGS

8000000034000277 34000000 (PRESENT|RW|USER|PCD|ACCESSED|DIRTY|NX)

VMA START END FLAGS FILE

ffff88000b7e7af8 7f3751c3a000 7f3751c3b000 50444fb /dev/mem

这个例子中,我们展示了/dev/mem如何用来访问保留内存。接下来我们继续用简单的小例子演示/dev/mem的其它玩法。

进程间交换页面

有这么一种需求:

我们不希望进程A和进程B共享任何页面,这意味着它们不能同时操作同一份数据。

偶尔我们希望进程A和进程B交换数据,却又不想用低效的传统进程间通信机制。

是不是觉得两难了呢?其实我们可以让两个进程的页面进行交换来达到目的。为了让页表项交换尽可能简单,我们依然使用保留内存,解除内核内存管理对操作的约束。

下面给出示例程序代码,先看进程A,master.c:

// gcc master.c -o master

#include

#include

#include

#include

#include

int main(int argc, char **argv)

{

int fd;

unsigned long *addr;

fd = open("/dev/mem", O_RDWR);

// 映射保留地址的一个页面P1

addr = mmap(NULL, 4096, PROT_READ|PROT_WRITE, MAP_SHARED, fd, 0x34000000);

// 写页面P1的内容

*addr = 0x1122334455667788;

printf("address at: %p content is: 0x%lx\n", addr, addr[0]);

// 等待交换

getchar();

printf("address at: %p content is: 0x%lx\n", addr, addr[0]);

close(fd);

munmap(addr, 4096);

return 1;

}

接下来看希望与之进程页面交换的进程B,slave.c:

// gcc slave.c -o slave

#

  • 0
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值