内核执行时动态的vmlinux的反汇编解析方法及static_branch_likely机制

news/2024/12/23 6:41:01/

一、背景

在之前的博客里,我们讲到了tracepoint(内核tracepoint的注册回调及添加的方法_tracepoint 自定义回调-CSDN博客)和kprobe(获取任意一个进程的共享内存的fd对应的资源,增加引用,实现数据的接管——包含非export的内核函数的模块内使用-CSDN博客 里 第三章)和static_call(内核调度抢占模式——voluntary和full对比-CSDN博客 里 3.1.1 一节),它们都会在内核运行期间,动态修改内存里的代码段的内容,来实现高性能的分支跳转。这篇博客里,我们会以介绍static_branch_likely机制为手段,来去反汇编内核执行期间时的动态的vmlinux内容,来分析和确认内核的动态分支跳转是走的哪一个。

我们在介绍static_branch_likely的机制时,用的是export的symbol的local_clock的实现里的关键函数local_clock_noinstr来做为例子。关于local_clock及sched_clock等会在后面的章节来详细做对比和介绍。这篇博客只聚焦标题里描述的动态vmlinux反汇编及static_branch_likely机制这两个部分。

我们在第二章里会先介绍vmlinux的反汇编方法及执行期间的动态vmlinux的反汇编方法,然后再第三章里使用第二章里的工具来介绍和验证static_branch_likely机制

二、静态vmlinux的反汇编方法及执行期间的动态vmlinux的反汇编方法

静态vmlinux的反汇编方法在之前的博客里多次有提及,要objdump整个vmlinux会相当耗时,在 2.1 里会提及效率更高的指定区域的反汇编方法,并在 2.2 里介绍如何反汇编执行期间的动态vmlinux

2.1 静态vmlinux的反汇编方法及指定区域的反汇编方法

在内核编译时,一般在代码的目录下就会生成一个全符号的vmlinux(当然前期是你没有以strip方式去编译),而内核在实际运行是,用的使用vmlinux的压缩文件vmlinuz,两者大小相差有快30倍:

我们在objdump时,要用原始的vmlinux文件来进行反汇编:

2.1.1 静态vmlinux的反汇编方法及输出产物介绍

下面这句是反汇编整个vmlinux文件,这句指令会非常非常耗时,cpu性能较好的情况下也会运行数个小时,输出的可人眼阅读的反汇编后的嵌入源码与对应汇编及二进制.text的文件如命令里设的就是vmlinux.txt:

objdump -S vmlinux > vmlinux.txt

vmlinux.txt的内容形如:

我们以local_clock_noinstr这个函数为例,可以从下图中看到,反汇编出来的上图里左边框出的地址,并不是实际内核执行期的函数地址,而是编译器在编译时指定的一段连续虚拟地址空间里的地址,它是一个临时的一段虚拟的地址,当然由于代码段肯定是4k对齐的,或者说vmlinux的实际运行首地址肯定也是对齐的一个比较大的数值的,所以反汇编出来的最后n个bit是和实际运行期间的函数的虚拟地址的最后n个bit是一样的:

2.1.2 指定区域进行objdump的方法

由于objdump整个vmlinux非常耗时,我们有时候只关心某个区域里的反汇编情况,而同一个机器上同一套编译环境上相近代码的两次编译,其函数地址段往往是一样的,这个例子里,我们看到

local_clock_noinstr在整个vmlinux的反汇编产物里是位于ffffffff820c9910开始,到ffffffff820c99df结束:

可以用如下方式进行局部的反汇编:

objdump -S vmlinux --start-address=0xffffffff820c9910 --stop-address=0xffffffff820c99df > vmlinux_test.txt

下图里可以看到,局部反汇编出来的内容是和全部反汇编出来的内容在这一部分上是一致的:

甚至如上图左边的局部反汇编产物里红色小框里的内容,局部反汇编从局部代码的理解和阅读上可能会更加清晰细节更多。

最重要的,局部反汇编的运行时间如果地址范围不大的话,是非常短的,可以提高效率。

2.2 动态vmlinux(执行期间的vmlinux)的反汇编方法

这里说的动态,就是说在内核启动以后,会根据实际的运行情况,由于tracepoint/krobe/static_call/static_branch_likely等动态修改代码段实现高性能分支跳转的这些内核机制,会导致代码段内容发生变更。也就是说,2.1 里描述的静态反汇编vmlinux输出的文件,和系统当前正在运行时的vmlinux的情况会有不同。

为了获取到动态执行期间的vmlinux的实际的运行情况,我们可以用如下的反汇编方法,我们以具体函数local_clock_noinstr来举例。

2.2.1 先通过cat /proc/kallsyms | grep xx获取到内核函数xx的内核虚拟地址首地址

cat /proc/kallsyms | grep local_clock_noinstr

如上图,我们得到local_clock_noinstr函数对应的内核虚拟地址首地址是0xffffffffa9ac9910

2.2.2 编写一个ko来获取这个函数对应的地址段的内容

下面这个代码还是比较简单的,根据insmod时传入的address和size参数来决定dump哪段内存:

如果传入第三个参数filedir,则会把dump到的内存内容以二进制形式输出到文件里去:

为了方便使用,在insmod执行直接进行指定内存段的内容的获取,并输出到dmesg里,如果传入filedir则也同时输出到设置的文件里,insmod执行完后用EINVAL返回失败,这样不用rmmod直接可以再次insmod来读取下一个内存段(下面指令里的128字节,我是随手指定了一个size,不用太在意这个细节):

insmod testgetkmem.ko address=0xffffffffa9ac9910 size=128 filedir="output.txt"

可以通过读取dmesg里的内容获取内存段内容:

上面指令指定了filedir是output.txt,所以也可以看output.txt里的内容来获取内存段内容,但是由于是二进制,所以直接cat是得到的乱码:

可以用vscode里的hex editor工具,或者linux上直接用hexedit工具来查看:

完整代码:

#include <linux/module.h>
#include <linux/kernel.h>
#include <linux/init.h>
#include <linux/moduleparam.h>
#include <linux/mm.h>
#include <linux/uaccess.h>MODULE_LICENSE("GPL");
MODULE_AUTHOR("Xin Zhao");
MODULE_DESCRIPTION("A kernel module to read memory and print it as hex dump.");
MODULE_VERSION("1.0");// 定义模块参数
static unsigned long address = 0;  // 虚拟地址
static size_t size = 0;             // 打印的字节数
static char* filedir = NULL;        // 输出文件目录module_param(address, ulong, S_IRUGO);
MODULE_PARM_DESC(address, "Virtual address to read from");
module_param(size, ulong, S_IRUGO);
MODULE_PARM_DESC(size, "Number of bytes to read");
module_param(filedir, charp, S_IRUGO);
MODULE_PARM_DESC(filedir, "Directory to write output file");struct file* _file;static int __init getkmem_init(void) {if (size == 0) {printk(KERN_ERR "[getkmem] Size must be greater than 0.\n");return -EINVAL;}// 检查地址的有效性(这里可以添加更复杂的检查)if (!address) {printk(KERN_ERR "[getkmem] Invalid address provided.\n");return -EINVAL;}// 打印内存内容printk(KERN_INFO "[getkmem] Reading memory from address: %lx, size: %zu\n", address, size);print_hex_dump(KERN_INFO, "[getkmem] ", DUMP_PREFIX_ADDRESS, 16, 1,(void *)address, size, false);if (filedir) {loff_t pos = 0;_file = filp_open(filedir, O_WRONLY | O_CREAT | O_TRUNC, 0644);kernel_write(_file, address, size, &pos);filp_close(_file, NULL);}return -EINVAL;
}static void __exit getkmem_exit(void) {printk(KERN_INFO "[getkmem] Module exiting.\n");
}module_init(getkmem_init);
module_exit(getkmem_exit);

2.2.3 找到动态获取到的代码段里的内容和静态的代码段内容不一致的内容

按照下面local_clock_noinstr反汇编得到的内容里的二进制代码段内容:

使用hexedit打开原始的vmlinux二进制文件,搜索上图中的二进制代码部分:

输入/后输入下图中红色框里的内容(红色框里的内容和上图中的红色框里内容一致):

可以搜到如下内容,且只能搜到一处,说明一定是这个地方:

关于搜索功能,hexedit没有vscode里的hex editor好用,vscode里的hex editor可以快速提示匹配的内容有多少个:

搜的代码段二进制是:554889e5415453eb26

如下图,我们发现local_clock_noinstr函数的静态反汇编得到的代码段二进制部分和实际运行期间时的代码对应地址段的二进制部分不一致,如下图,EB26变成了6690。关于为什么会有这样的变化,我们在第三章里会介绍说明。

2.2.4 复制一份vmlinux出来,修改动态执行期间变化了的部分

我们拷贝一份vmlinux出来,按照 2.2.3 里发现的不一样的地方修改掉:

用hexedit进行修改:

ctrl+x以后按y进行保存

2.2.5 使用 2.1.2 一节里提到的指定区域objdump的方法导出动态执行期间的某个函数的反汇编代码

使用如下命令进行局部反汇编,导出到vmlinux_test_1.txt输出文件里:

objdump -S vmlinux_test --start-address=0xffffffff820c9910 --stop-address=0xffffffff820c99df > vmlinux_test_1.txt

可以看到local_clock_noinstr函数的动态执行期间的反汇编内容如下:

可以从上图中看到修改部分的6690代码段对应的汇编代码是一条无实际作用的空转指令。在下面第三章里会介绍,这其实就是static_branch_likely机制所需要的一句可用于替换别的指令的“占坑”指令。

三、内核static_branch_likely机制

在上面的 2.2.5 里可以看到local_clock_noinstr函数在动态执行期间,把原来静态vmlinux里的原来的:

替换成了一条无实际作用的纯“占坑”指令:

3.1 编译生成的vmlinux会按照static_branch对应的变量默认的值来生成初始的指令代码

还是以local_clock_noinstr函数来分析,我们看编译生成的vmlinux里的local_clock_noinstr函数的跳转执行情况:

clock.c里的这个local_clock_noinstr函数如下实现:

__sched_clock_stable的默认值是false的(x86是默认打开CONFIG_HAVE_UNSTABLE_SCHED_CLOCK的):

所以,在__sched_clock_stable的默认值false的配置下,local_clock_noinstr的第一段分叉逻辑走不到。

再看第二段分叉逻辑的sched_clock_running变量,它默认值也是false:

但是它是!来判断,所以,按照默认值这段分支逻辑能走到:

这个分析和vmlinux的编译产出的反汇编汇编指令逻辑一致:

local_clock_noinstr先是如下图的jmp跳到了ffffffff820c993f:

如下图,再由ffffffff820c993f跳到了ffffffff820c99c5,而ffffffff820c99c5即执行return sched_clock_noinstr();和代码里的第二段分支逻辑匹配。

初始代码根据key的默认值生成对应汇编代码的细节:

3.2 static_branch_likely机制通过static_branch_enable来

而local_clock_noinstr函数在执行期间,__sched_clock_stable值发生了改变,从而导致local_clock_noinstr所执行的分支段发生了变化。我们通过编写了一个test_local_clock函数,export了symbol,在模块里执行来确认了这个分支运行情况:

这个逻辑从通过 2.2.5 里导出的local_clock_noinstr函数的动态反汇编内容里也可以得到是执行的第一个分支代码段:

事实上,它最终是通过static_branch_enable来标记的,关于这个__sched_clock_stable标记的调用链:

sched_clock_init_late->__set_sched_clock_stable->static_branch_enable(&__sched_clock_stable);

接下来,我们看一下static_branch_enable是如何最终改变分支的:

static_branch_enable是调用的static_key_enable进行的key的设置:

在jump_label.h里进行的定义:

因为我们CONFIG_JUMP_LABEL是打开的:

所以static_key_enable函数的实现是在jump_label.c里实现的:

static_key_enable_cpuslocked的实现也在jump_label.c里,调用的jump_label_update进行的分支跳转相关指令代码的动态更新:

空转会根据指令的大小进行相应的替换,对于static_branch_likely机制的jmp指令,空转指令的大小是2,对应于如下的汇编实现就是:

所以就是6690作为汇编的空转指令,与 2.2.5 一节里导出的local_clock_noinstr的执行期间的变化部分的6690代码二进制一致。


http://www.ppmy.cn/news/1557394.html

相关文章

Numpy数组的三种主要索引方式讲解及源码示例

1、整数索引和切片 NumPy中可以通过整数索引和切片来访问和修改数组中的元素&#xff0c;数组的维度不同、整数索引和切片的用法也会有所不同。 对于一维数组来说&#xff0c;整数索引和切片的用法与Python列表中索引和切片的用法相同&#xff1b;对于二维数组来说&#xff0c…

k8s-1.28.1证书更新到100年-cenots7.9

一、查看信息 # cat /etc/redhat-release # kubeadm version k8s环境是通过kubeadm进行安装的。 二、安装依赖工具 # yum groupinstall "Development Tools" -y # yum install gcc make -y # yum install rsync jq -y 三、下载相应版本的k8s包 ## 下载地址&…

迅为RK3576开发板接口丰富4G/5G、wifi6、多网口、NPU等

典型应用方向iFPD、工业控制及网关、云终端、人脸识别设备、车载中控、商显主要特性 ARM 64位高性能八核通用处理器&#xff0c;丰富的PCIE/USB3.0/SATA/GMAC等各类高速及CAN FD/DSMC/UART/SPI/I2C/I3C等低速扩展接口&#xff0c;计算及扩展能力通用性强&#xff0c;一个平台可…

Houdini abc 导入 maya uv无法识别

参考&#xff1a;Houdini导出abc 至maya UV 无法识别_houdini导出abc没有uv-CSDN博客 从maya导入到houdini的uv默认是vertex层级的&#xff0c;而在maya中&#xff0c;uv是在point层级的&#xff1b;因此在houdini中导出abc时应将uv转为点层级&#xff0c;使用vertexsplit节点&…

开源轮子 - EasyExcel01(核心api)

EasyExcel01 - 核心api 本文整理自掘金大佬 - 竹子爱熊猫 https://juejin.cn/post/7405158045662576640 文章目录 EasyExcel01 - 核心api一&#xff1a;初相识EasyExcel1&#xff1a;写入excel入门2&#xff1a;读取Excel入门 二&#xff1a;数据模型注解1&#xff1a;读写通用…

Docker的容器编排

目录 1. 什么是容器编排&#xff08;Docker Compose&#xff09;2. 容器编排的功能3. 容器编排文件&#xff08;docker-compose.yml&#xff09;的介绍3.1 文件语法版本3.2 文件基本结构及常见指令 4. Docker Compose命令详解4.1 Docker Compose命令清单4.2 命令格式和常见选项…

VSCode中的Black Formatter没有生效的解决办法

说明 如果正常按照配置进行的话&#xff0c;理论上是可以生效的。 "[python]": {"editor.defaultFormatter": "ms-python.black-formatter","editor.formatOnSave": true }但我在一种情况下发现不能生效&#xff0c;应为其本身的bug…

探索Web3的核心原则:去中心化与用户控制

Web3作为未来互联网的愿景&#xff0c;正逐步改变我们对网络的认知。它的两大核心原则——去中心化和用户控制&#xff0c;不仅推动了技术的革新&#xff0c;也重新定义了互联网用户与平台之间的关系。这些原则的落地&#xff0c;能够让用户在数字世界中拥有更多的自主权、隐私…