在 Linux 里有很多工具可以用于追踪和记录内核运行期间发生的事件,例如 traceftrace 等工具提供了一种机制来追踪和记录在内核运行期间发生的事件,有助于理解系统的行为和性能瓶颈。内存流动(MEMORY FLUID)工具是专门为 BiscuitOS 系统开发,用于追踪和记录内存在内核运行期间发生的事件. 如果把其他追踪工具比喻为在一些特定点进行守株待兔,那么内存流动工具就是在兔子头上安装一个摄像头,可以看到内存在内核运行期间的所有事件. 那么接下来通过一个例子进行实例介绍:

图片无法显示,请右键点击新窗口打开图片

上图是一个经典采用惰性分配的匿名内存,main 函数里 21 行调用 mmap 函数分配内存时,系统只为其分配了虚拟内存,并没有分配具体的物理内存,这就是所谓的惰性分配,只有在进程真正使用这段虚拟内存时,系统才会为其分配物理内存。那么 main 函数 32 行首次对虚拟内存进行访问,此时硬件 MMU 发现虚拟内存并没有映射物理内存,那么直接触发缺页异常。在缺页异常处理函数里,内核为其分配物理内存并建立页表,将虚拟内存映射到物理内存上,那么待缺页异常返回之后,进程再次执行发送缺页异常的指令,此时进程可以正确访问到内存. 以上就是一个最基础的惰性分配内存的过程,那么对于开发者来说,如果想看匿名内存全部缺页的整个过程该怎么办呢? 如果能看到是不是对开发者学习匿名内存缺页有极大的帮助(对于初学者来说黑盒永远有无限种恐惧),接下来先展示使用内存流动工具查看匿名内存发生缺页的完整过程:

图片无法显示,请右键点击新窗口打开图片

上图就是通过内存流动工具追踪到匿名内存发生缺页的整个过程,图片不是内存流动工具制作的,而是根据内存流动工具追踪到的数据绘制出来的,如果开发者使用内存流动工具将一个匿名内存的缺页过程完整的追踪出来,并且制作成一个完整的调用图,那么这对匿名内存缺页原理的学习起到有效的帮助,让原本抽象难懂的技术变得直观易懂. 话不多说,接下来将介绍如何在 BiscuitOS 上使用内存流动工具. 参考如下命令(在部署之前请确保已经部署 BiscuitOS 开发环境,如果未部署请参考 《BiscuitOS 用户手册 - 1.2》:

# 以 Linux 6.10 X86 环境为例,切换到 BiscuitOS 项目的根目录
cd */BiscuitOS
# 选择 Linux 6.10 X86 项目
make linux-6.10-x86_64_defconfig
make docker
# 部署 "内存流动" 工具
make install_tools
# 部署测试用例(按 y/Y 进行选择)
make menuconfig

  [*] Package  --->
      [*] BiscuitOS Debug Stub Set  --->
          [*]  BiscuitOS MEMORY FLUID --->

# 保存退出,执行部署
make
# 源码目录
cd /BiscuitOS/output/linux-6.10-x86_64/package/BiscuitOS-MEMORY-FLUID-default/
# 部署源码
make download
# 在 BiscuitOS 中实践
make build

图片无法显示,请右键点击新窗口打开图片

部署完毕之后执行 ‘make download’ 命令下载测试代码,下载成功之后会在 BiscuitOS-MEMORY-FLUID-default 目录下多一个同名的 BiscuitOS-MEMORY-FLUID-default 目录,目录里包含了 main.c 文件,该文件就是测试主体,接下来打开该文件:

图片无法显示,请右键点击新窗口打开图片

测试用例是一个用户空间应用程序,其在 22 行调用 mmap 系统调用分配了一段匿名内存,内存只包含了虚拟内存部分,并没有映射物理内存,那么在 33 行首次访问虚拟内存时,MMU 发现物理内存不存在而直接触发缺页异常,缺页异常处理函数负责物理内存的分配和页表的建立,最后缺页异常返回进程可以正确访问到虚拟内存. 此时可以使用内存流动工具查看匿名内存的缺页过程,可以知道应用程序在 33 行处发生了缺页,那么首先在应用程序进行标记:

图片无法显示,请右键点击新窗口打开图片

应用程序要使用内存流动工具检测某个内存行为时,需要在该行为的前后加上 BiscuitOS_memory_fluid_enable()BiscuitOS_memory_fluid_disable() 将检测的区域标记出来,这样内存流动工具才知道检测的范围, 接下来是在内核里做标记.

图片无法显示,请右键点击新窗口打开图片

这里先假设已经知道匿名内存缺页流程会调用 do_anonymous_page 函数,那么在 do_anonymous_page 函数的 4040 行添加打印,因此缺页匿名发生缺页时流动到这个函数,那么接下来执行如下命令:

# 确保当前目录是 MEMORY FLUID 测试案例目录
cd /BiscuitOS/output/linux-6.10-x86_64/package/BiscuitOS-MEMORY-FLUID-default/
# 由于修改了内核代码,然后编译内核
make kernel
# 内核编译完毕之后,在 BiscuitOS 中实践测试用例
make build

图片无法显示,请右键点击新窗口打开图片

BiscuitOS 启动之后,直接运行 RunBiscuitOS.sh 脚本,脚本里包含了实践案例运行的所有命令,可以看到实践案例运行之后,内核把刚刚添加的 “bs_debug” 语句打印了,那么说明匿名内存在缺页异常处理里确实流动到 do_anonymous_page 这个函数了,并且此时打印字符串显示的缺页地址正好是应用程序访问的地址 “0x6000000000”,以上便是内存流动工具的基本用法, 让开发者看到了内存流动到了 do_anonymous_page 这个函数,采用同样的方法可以追踪匿名内存在整个内核里的流动. 到这里可能有的童鞋就会说内核里使用的 bs_debug 不就是一个 printk 这类的函数,为什么不直接用 printk,这里我先不做解释直接动手:

图片无法显示,请右键点击新窗口打开图片

根据需求,将 do_anonymous_page 函数里的 bs_debug 替换成 printk,然后重新编译内核是运行实践案例,同样使用如下命令:

# 确保当前目录是 MEMORY FLUID 测试案例目录
cd /BiscuitOS/output/linux-6.10-x86_64/package/BiscuitOS-MEMORY-FLUID-default/
# 由于修改了内核代码,然后编译内核
make kernel
# 内核编译完毕之后,在 BiscuitOS 中实践测试用例
make build

图片无法显示,请右键点击新窗口打开图片

当 BiscuitOS 启动之后调用用户空间的程序之后,内核疯狂输出在 do_anonymous_page 函数添加的 printk 字符串,接着运行 RunBiscuitOS.sh 脚本,还是疯狂打印字符串,都无法看清哪行是和实践案例匿名内存缺页有关. 通过这个例子的对比,就可以知道内存流动工具是为了让你“看到”指定的内存在内核里的流动.

内存流动工具提供了三套接口针对用户进程内部、内核内部和应用程序外部三个角度检测内存的流动,具体如下:

  • 用户态内存流动工具: 在应用程序内存,使用 “BiscuitOS_memory_fluid_enable()” 和 “BiscuitOS_memory_fluid_disable()” 接口将需要检测的区域进行隔离,”BiscuitOS_memory_fluid_enable()” 之后的区域进行检测,而 “BiscuitOS_memory_fluid_disable()” 之后的区域将停止检测. 因此开发者向查看进程内部某种内存在某个代码段里的流动,可以使用这套接口进行查看,具体使用方法请参考: 点击查看
  • 命令行内存流动工具: 在应用程序启动阶段的内存流动可以使用 “/proc/sys/BiscuitOS/BiscuitOS-MEMORY-FLUID” 接口观测,具体使用方法参考: 点击查看
  • 内核态内存流动工具: 在内核内部查看内存在内核里的流动,使用 “BiscuitOS_memory_fluid_enable()” 和 “BiscuitOS_memory_fluid_disable()” 接口将需要检测的区域进行隔离,”BiscuitOS_memory_fluid_enable()” 之后的区域进行检测,而 “BiscuitOS_memory_fluid_disable()” 之后的区域停止检测. 因此开发者想查看内核内部某些内存的流动,可以参考: 点击查看

当需要查看内存在应用程序内存如何流动时,可以使用 “BiscuitOS_memory_fluid_enable()” 和 “BiscuitOS_memory_fluid_disable()” 接口,并配合内核部分 “bs_debug” 接口的使用,就可以对某种内存行为进行追踪,那么接下来通过一个实践案例讲解这个功能的使用,实践案例在 BiscuitOS 上的部署逻辑如下(该功能适用于所有 BiscuitOS 用户态实践案例):

# 以 Linux 6.10 X86 环境为例,切换到 BiscuitOS 项目的根目录
cd */BiscuitOS
# 选择 Linux 6.10 X86 项目
make linux-6.10-x86_64_defconfig
make docker
# 部署测试用例(按 y/Y 进行选择)
make menuconfig

  [*] Package  --->
      [*] BiscuitOS Debug Stub Set  --->
          [*]  BiscuitOS MEMORY FLUID --->

# 保存退出,执行部署
make
# 源码目录
cd /BiscuitOS/output/linux-6.10-x86_64/package/BiscuitOS-MEMORY-FLUID-default/
# 部署源码
make download
# 在 BiscuitOS 中实践
make build

图片无法显示,请右键点击新窗口打开图片

部署完毕之后执行 ‘make download’ 命令下载测试代码,下载成功之后会在 BiscuitOS-MEMORY-FLUID-default 目录下多一个同名的 BiscuitOS-MEMORY-FLUID-default 目录,目录里包含了 main.c 文件,该文件就是测试主体,接下来打开该文件:

图片无法显示,请右键点击新窗口打开图片

测试用例是一个用户空间应用程序,其在 22 行调用 mmap 系统调用分配了一段匿名内存,内存只包含了虚拟内存部分,并没有映射物理内存,那么在 33 行首次访问虚拟内存时,MMU 发现物理内存不存在而直接触发缺页异常,缺页异常处理函数负责物理内存的分配和页表的建立,最后缺页异常返回进程可以正确访问到虚拟内存. 如果此时我想查看 mmap 是如何为进程分配虚拟内存的,那么首先在应用程序进行标记:

图片无法显示,请右键点击新窗口打开图片

首先在应用程序里,mmap 函数在 23-27 行进行调用,那么在 22 行使用 “BiscuitOS_memory_fluid_enable()” 函数表明接下来的代码区间将检测内存流动,然后在 28 行调用 “BiscuitOS_memory_fluid_disable()” 函数表示检测结束,那么 23-27 行代码将被内存流动工具检测内存的流动,这段代码逻辑是进程调用 mmap 分配一段虚拟内存,那么这里就可以检测 mmap 如何分配虚拟内存,接下来在内核里达标检测 mmap 系统调用的过程.

图片无法显示,请右键点击新窗口打开图片

接下来通过查阅 mmap 系统调用在内核里会调用 do_mmap 函数,但不知道哪些分支内存会流动到里面,那么可以在需要确认的分支里添加 bs_debug 函数打印一些信息,例如上图可以在 1419 行添加 bs_debug 函数确认内存已经流动这里,另外在 1422 行分支在添加打印,以此确认内存是否流动到里面,添加完标记之后,由于修改了内核需要更新一下内核,使用如下命令:

# 确保当前目录是 MEMORY FLUID 测试案例目录
cd /BiscuitOS/output/linux-6.10-x86_64/package/BiscuitOS-MEMORY-FLUID-default/
# 由于修改了内核代码,然后编译内核
make kernel
# 内核编译完毕之后,在 BiscuitOS 中实践测试用例
make build

图片无法显示,请右键点击新窗口打开图片

BiscuitOS 启动之后,直接运行 RunBiscuitOS.sh 脚本,脚本里包含了实践案例运行的所有命令,可以看到实践案例运行之后,内核打印了 “MEMORY FLUID ALLOC MEMORY 0x6000000000 on do_mmap” 语句,却没有打印 “MEMORY FLUID FIXED”, 那么说明内存没有流动到 do_mmap 函数 1422 行分支. 到这里内存流动工具已经发挥它该有的功能,那么是否还能再发挥一下该工具更多的用法呢? 就像刚刚这个场景,内存没有流动到 1422 的分支,那么怎么才能让内存流动到 1422 行分支,这是一个逆向思维的过程,需要有一定的基础,但基础都是从没有基础来到,对开发者可以利用内存流动工具来实现这个逆向过程,接下来我将演示这个过程:

图片无法显示,请右键点击新窗口打开图片

在 do_mmap 函数的 1421 行的分支条件是 flags 里需要包含 MAP_FIXED_NOREPLACE 标志,那么首先要知道 flags 来自哪里,这个时候假设我也不知道,我知道 mmap 系统调用会调用到该函数,那么首先确认 flags 来自那里,可以选缺页 mmap 系统调用的调用逻辑,此时可以使用 dump_stack 函数,但该函数不能直接用,因此 do_mmap 是公共路径,此时需要使用内存流动工具提供的能力配合使用,如下:

图片无法显示,请右键点击新窗口打开图片

内存流动工具提供了 “is_bs_enable()” 函数只对本次检测内存行为进行打印,其他公共路径的内存行为不进行打印,那么在 do_mmap 函数的 1421-1422 行添加该函数,并在确认内存流动检测开启之后才调用 dump_stack 函数,这样可以清晰的看到内存流动路径,接下来重新编译内核和运行实践案例:

图片无法显示,请右键点击新窗口打开图片

BiscuitOS 运行之后,直接运行 RunBiscuitOS.sh 脚本,此时可以看到实践案例运行之后看到 mmap 系统调用的大概流程,那么接下来要做的是确认 flags 这个参数是由用户空间的参数转换而来,此时直接查看路径上的源码进行确认.

图片无法显示,请右键点击新窗口打开图片

通过函数调用栈最终找到 “__x86_sys_mmap” 函数定义(该函数的定义需要一点点基础才能找到),其函数定义如上,可以看到 flags 是由 mmap 函数的第四个参数传递下来的,那么接下来可以知道直接在 mmap 函数里添加 MAP_FIXED_NOREPLACE 标志:

图片无法显示,请右键点击新窗口打开图片

在应用程序里,在 25 行直接添加 MAP_FIXED_NOREPLACE 标志,这里先不用考虑是否可以添加,先添加上去,然后直接编译应用程序(直接执行 make build),然后查看运行结果:

图片无法显示,请右键点击新窗口打开图片

BiscuitOS 运行之后,直接运行 RunBiscuitOS.sh 脚本,可以看到测试程序运行之后,内核不仅打印了函数调用栈,还打印了 “MEMORY FLUID FIXED” 字符串,说明应用程序添加 MAP_FIXED_NOREPLACE 是成功的,那么恭喜你,你构造了一个新的使用场景,接下来就是查阅 MAP_FIXED_NOREPLACE 的资料,了解这个技术的背景资料,那么无形中使用内存流动工具培养了你的逆向思维能力,知道某段代码背后的逻辑. 但有的情况可能没有这么一帆风顺,此时还是可以利用内存流动工具按着函数调用栈,bs_debug 将 flags 在每个函数和分支里进行打印,去发现 flags 的 MAP_FIXED_NOREPLACE 标志为什么消失了,然后把消失的条件逆向补齐,最终让内存向你想要的方向流动,整个过程就是一个反馈实践再反馈再实践的学习过程,它会让你越来越接近真相. 以上就是为什么我这么喜欢内存流动工具的原因, 希望你也能利用好这个工具.


上一节介绍了应用程序内部查看某些代码段的内存流动,那么对于一个应用程序启动是的内存流动如何查看呢? 例如一个应用程序的堆栈是如何构建的,内存如何在进程启动时流动,那么接下来通过实践来介绍如何使用内存流动工具来内存在进程启动时的流动,实践案例在 BiscuitOS 上的部署逻辑如下:

# 以 Linux 6.10 X86 环境为例,切换到 BiscuitOS 项目的根目录
cd */BiscuitOS
# 选择 Linux 6.10 X86 项目
make linux-6.10-x86_64_defconfig
make docker
# 部署测试用例(按 y/Y 进行选择)
make menuconfig

  [*] Package  --->
      [*] BiscuitOS Debug Stub Set  --->
          [*]  BiscuitOS MEMORY FLUID --->

# 保存退出,执行部署
make
# 源码目录
cd BiscuitOS/output/linux-6.10-x86_64/package/BiscuitOS-MEMORY-FLUID-default/
# 清除残留
make distclean
# 部署源码
make download
# 在 BiscuitOS 中实践
make build

图片无法显示,请右键点击新窗口打开图片

部署完毕之后执行 ‘make download’ 命令下载测试代码,下载成功之后会在 BiscuitOS-MEMORY-FLUID-default 目录下多一个同名的 BiscuitOS-MEMORY-FLUID-default 目录,目录里包含了 main.c 文件,该文件就是测试主体,接下来打开该文件:

图片无法显示,请右键点击新窗口打开图片

测试用例是一个用户空间应用程序,其在 22 行调用 mmap 系统调用分配了一段匿名内存,内存只包含了虚拟内存部分,并没有映射物理内存,那么在 33 行首次访问虚拟内存时,MMU 发现物理内存不存在而直接触发缺页异常,缺页异常处理函数负责物理内存的分配和页表的建立,最后缺页异常返回进程可以正确访问到虚拟内存. 此时完全可以完全不用关系应用程序的逻辑,因为我门比较关心这个应用程序启动时内存是如何流动的,那么接下里查看 RunBiscuitOS.sh 脚本:

图片无法显示,请右键点击新窗口打开图片

RunBiscuitOS.sh 脚本里调用 BiscuitOS-MEMORY-FLUID-default 来运行实践案例,在 BiscuitOS-MEMORY-FLUID-default 前向 BiscuitOS-MEMORY-FLUID 文件写入 1 表示开启内存流动检测,向 BiscuitOS-MEMORY-FLUID 写入 0 表示关闭内存流动检测. 添加完毕之后接下来在内核源码里添加检测点:

图片无法显示,请右键点击新窗口打开图片

进程在创建堆栈的时候会调用 setup_arg_pages 函数(不要问我为什么会知道,我也是使用内存流动工具逆向推出来的),那么在该函数的 759 行添加 bs_debug 函数,然后重新编译内核进行实践,因此确认进程启动时内存流动到这里,接下来使用如下命令:

# 确保当前目录是 MEMORY FLUID 测试案例目录
cd /BiscuitOS/output/linux-6.10-x86_64/package/BiscuitOS-MEMORY-FLUID-default/
# 由于修改了内核代码,然后编译内核
make kernel
# 内核编译完毕之后,在 BiscuitOS 中实践测试用例
make build

图片无法显示,请右键点击新窗口打开图片

BiscuitOS 启动之后,直接运行 RunBiscuitOS.sh 脚本,脚本里包含了实践案例运行的所有命令,可以看到实践案例运行之后,内核打印了 “MEMORY FLUID on setup_arg_pages” 语句,那么说明进程启动时,其内存流动到 setup_arg_pages 函数了. 以上便是内存流动工具检测应用程序启动时的内存流动,其实该工具还可以检测一些内存行为,例如检测 Drop CACHE 时内存的流动,使用如下命令:

echo 1 > /proc/sys/BiscuitOS/BiscuitOS-MEMORY-FLUID
echo 1 > /proc/sys/vm/drop_caches
echo 0 > /proc/sys/BiscuitOS/BiscuitOS-MEMORY-FLUID

然后在 DROP CACHE 的内核路径上使用 bs_debug 进行检测即可.


前面介绍了使用内存流动工具查看内存在内核里的流动,那么对于内核线程而言,其也需要使用内存,那么内存流动工具同理也可以查看这部分内存在内核里的流动,接下来通过一个实践案例进行介绍如何查看内核线程使用的内存在内核里的流动,实践案例在 BiscuitOS 的部署逻辑如下:

# 以 Linux 6.10 X86 环境为例,切换到 BiscuitOS 项目的根目录
cd */BiscuitOS
# 选择 Linux 6.10 X86 项目
make linux-6.10-x86_64_defconfig
make docker
# 部署测试用例(按 y/Y 进行选择)
make menuconfig

  [*] Package  --->
      [*] BiscuitOS Debug Stub Set  --->
          [*]  BiscuitOS MEMORY FLUID on Kernel --->

# 保存退出,执行部署
make
# 源码目录
cd /BiscuitOS/output/linux-6.10-x86_64/package/BiscuitOS-MEMORY-FLUID-KERNEL-default/
# 清除残留
make distclean
# 部署源码
make download
# 在 BiscuitOS 中实践
make build

图片无法显示,请右键点击新窗口打开图片

部署完毕之后执行 ‘make download’ 命令下载测试代码,下载成功之后会在 BiscuitOS-MEMORY-FLUID-KERNEL-default 目录下多一个同名的 BiscuitOS-MEMORY-FLUID-KERNEL-default 目录,目录里包含了 main.c 文件,该文件就是测试主体,接下来打开该文件:

图片无法显示,请右键点击新窗口打开图片

测试用例是一个内核模块,当加载模块时 BiscuitOS_init 函数会被自动调用,BiscuitOS_init 函数在 19 行调用 vmalloc 函数分配一段 VMALLOC 映射区的内存,然后在 25 行访问这段虚拟内存,最后在 29 行进行回收。以上是一个最简单的实践案例,那么接下来通过内存流动工具查看内存是如何在 VMALLOC 分配器里流动的:

图片无法显示,请右键点击新窗口打开图片

在内核里可以使用 BiscuitOS_memory_fluid_enable() 函数表示内存流动工具将检测内存在后面代码里的流动,然后 BiscuitOS_memory_fluid_disable() 函数则表示结束检测内存的流动,那么 “BiscuitOS_memory_fluid_enable()” 和 “BiscuitOS_memory_fluid_disable()” 之间形成了检测内存流动的区域. 接着就是在 vmalloc 函数路径上添加检测点:

图片无法显示,请右键点击新窗口打开图片

vmalloc 函数会调用 __vmalloc_area_node 函数映射物理内存,那么在 3007、3010、3014 行添加 bs_debug 打印,以此确认内存在该函数里是怎么流动的。由于改动内核了因此需要重新编译内容,使用如下命令:

# 确保当前目录是 MEMORY FLUID KERNEL 测试案例目录
cd /BiscuitOS/output/linux-6.10-x86_64/package/BiscuitOS-MEMORY-FLUID-KERNEL-default/
# 由于修改了内核代码,然后编译内核
make kernel
# 内核编译完毕之后,在 BiscuitOS 中实践测试用例
make build

图片无法显示,请右键点击新窗口打开图片

BiscuitOS 启动之后,直接运行 RunBiscuitOS.sh 脚本,脚本里包含了实践案例运行的所有命令,可以看到模块加载之后,内核打印的字符串可以知道内存是如何流动的,此时进入了 3014 分支. 以上便是内存流动工具在内核空间的使用方法.

图片无法显示,请右键点击新窗口打开图片