1. 缘起
该工具是起源于大神dog250的一篇帖子《Linux内核如何替换内核函数并调用原始函数》。通过替换原有函数,我们做很多事情。我常用的就是分析内核问题和制作内核热补丁。
虽然它很强大,帮助我解决了不少项目问题。但是每次使用都要重复定义一大堆函数指针,重复变量。繁杂而易错,让我不胜烦恼。
在使用中我一直在思考如何将通用的部分封装隐藏起来,提供简单易用的接口,使其更便捷、易用和高效。
2. 实现
突然有一天,灵感来了,很快就定义好相关的宏接口。细节部分推敲几次后也定了下来。就有了大家看到的funchook的初版代码(链接见文末)。
定义和实现部分是 funchook.c 和 funchook.h。module.c是其应用部分,上面有个简单的示例,和对应的注释,帮助大家快速上手,因为一共就HOOK_DEFINE、CALL_ORIG_FUNCION、HOOK_REGISTER、 HOOK_UNREGISTER 4个宏接口定义,非常简单,易于上手。
3. 示例
为了进一步帮助大家理解,我用一个实际例子,来帮助大家进一步掌握使用方法。
以 实现mt7621支持ethtool 设置rx-checksum 开关 为例。
ethtool工具在内核中是通过 dev_ioctl–>dev_ethtool–>通过ethcmd 来进行对应处理。
我想要确认 ethtool -K eth0 rx-checksum off 最终通过哪个ethcmd命令修改了网卡配置。
于是我们将要对 int dev_ethtool(struct net *net, struct ifreq *ifr) 函数进行hook替换,添加打印来获取到ethcmd的值。
3.1 使用HOOK_DEFINE定义 hook函数
HOOK_DEFINE的使用方法:
参数1:被替换的函数名,也就是dev_ethtool。
参数2:返回值类型,也就是int。(注意只写返回值类型,不需要携带static静态定义)
参数3~N:被替换函数的参数定义,也就是 struct net *net, struct ifreq *ifr。
简单来说就是将 函数名,返回值类型 插入到函数定义中参数第一,第二位置,再加上HOOK_DEFINE即可。
HOOK_DEFINE(dev_ethtool, int, struct net *net, struct ifreq *ifr)
{
//hook 函数的实现
}
3.2 hook函数实现
这是我们的核心环节,需要根据实际需求来实现hook函数。
对应需求不同有两种方式:
-
hook函数中调用原有函数(大多分析问题时用)
我们在分析内核问题时,经常用到这个,只需要在hook中增加相应debug信息,然后通过CALL_ORIG_FUNCION调用原有函数。不改变原有代码流程,相当于在原有函数前后增加了debug处理。 -
hook函数中直接实现原有函数功能(大多修复函数bug时用)
当我们发现原有函数存在bug,需要制作热补丁ko时,需要在hook中实现修复后的功能。然后直接hook替换原有函数工作,不需要在hook函数中调用原有函数。
分析内核问题时,需要分析函数中间部分,这时也需要在hook中直接实现原有函数功能,再在合适位置添加debug信息,帮助我们分析定位问题。
3.2.1 hook函数中调用原有函数前处理
添加自己想要的处理,比如打印相关参数值,打印函数调用栈等等。
本次我们增加一个打印,将函数的ethcmd打印出来即可。
struct net_device *dev = __dev_get_by_name(net, ifr->ifr_name);
void __user *useraddr = ifr->ifr_data;
u32 ethcmd;
int rc;
if (!dev || !netif_device_present(dev))
return -ENODEV;
if (copy_from_user(ðcmd, useraddr, sizeof(ethcmd)))
return -EFAULT;
pr_info("before dev_ethtool: ethcmd=%u\n", ethcmd);
3.2.2 hook函数中调用原有函数(hook直接替代原有函数则不需要这一步)
如果hook函数已经完全替代原有函数功能,则不需要调用原有函数。
CALL_ORIG_FUNCION使用方法:
参数1:被替换的函数名,也就是dev_ethtool。
参数2~N:被替换函数的变量列表,即 net, ifr。
简单来说就是将函数名插入到函数调用的第一位置,再加上 CALL_ORIG_FUNCION即可。
因为dev_ethtool是有返回值的,所以我们用rc得到处理的返回值。
rc = CALL_ORIG_FUNCION(dev_ethtool, net, ifr);
3.2.3 hook函数中调用原有函数后处理
添加自己想要的处理,比如打印返回值,打印相关参数值,打印函数调用栈等等。
本次我们不需要,留空即可。
因为dev_ethtool有返回值,我们通过return rc将返回值返回给上一层函数调用。
return rc;
3.2.4 hook函数完整代码
dev_ethtool的hook替代函数完整实现如下:
HOOK_DEFINE(dev_ethtool, int, struct net *net, struct ifreq *ifr)
{
struct net_device *dev = __dev_get_by_name(net, ifr->ifr_name);
void __user *useraddr = ifr->ifr_data;
u32 ethcmd;
int rc;
if (!dev || !netif_device_present(dev))
return -ENODEV;
if (copy_from_user(ðcmd, useraddr, sizeof(ethcmd)))
return -EFAULT;
pr_info("before dev_ethtool: ethcmd=%u\n", ethcmd);
rc = CALL_ORIG_FUNCION(dev_ethtool, net, ifr);
return rc;
}
3.3 hook函数的注册和注销
3.3.1 hook函数的注册
hook函数的注册,即使将hook函数替换原有函数开始在内核工作,后续内核中对原有函数的调用相当于调用hook函数。
我们将 hook的注册函数放到了 module init中,在模块加载时进行hook 注册。
HOOK_REGISTER只有一个参数,就是函数名,也就是 dev_ethtool。
HOOK_REGISTER(dev_ethtool);
3.3.1 hook函数的注销
hook函数的注销,即恢复原有函数,后续内核使用原有函数进行工作。
我们将 hook的注销函数放到了 module exit中,在模块卸载时进行hook 注销。
HOOK_UNREGISTER也只有一个参数,就是函数名,也就是 dev_ethtool。
HOOK_UNREGISTER(dev_ethtool);
3.4 module.c完整实现
module.c的完整实现如下,配合funchook.c和funckhook.h 即可编译出对应模块。
在实际应用中,你可以将hook定义和注册集成到你代码的任意地方。
#include <linux/module.h>
#include "funchook.h"
/* heades needed by patched function */
#include <net/sock.h>
/* module param defines */
/*
* 1.use HOOK_DEFINE macro to define the hook function.
* 1.1 if you need to call the original function in the hook function,
* you need to use CALL_ORIG_FUNCION macro.
* 1.2 if you need to call other kernel exported function, just include the
* related header(s).
* 1.3 if you need to call other kernel static function, you need to make a
* duplicate copy here.
*/
HOOK_DEFINE(dev_ethtool, int, struct net *net, struct ifreq *ifr)
{
struct net_device *dev = __dev_get_by_name(net, ifr->ifr_name);
void __user *useraddr = ifr->ifr_data;
u32 ethcmd;
int rc;
if (!dev || !netif_device_present(dev))
return -ENODEV;
if (copy_from_user(ðcmd, useraddr, sizeof(ethcmd)))
return -EFAULT;
pr_info("before dev_ethtool: ethcmd=%u\n", ethcmd);
rc = CALL_ORIG_FUNCION(dev_ethtool, net, ifr);
return rc;
}
static __init int funchook_init(void)
{
int ret = 0;
/*
* 2. use HOOK_REGISTER macro to replace original function with hook function
*/
ret = HOOK_REGISTER(dev_ethtool);
if (ret != 0)
{
pr_info("HOOK_REGISTER failed ret=%d\n", ret);
return ret;
}
return ret;
}
module_init(funchook_init);
static __exit void funchook_exit(void)
{
/*
* 3. use HOOK_UNREGISTER macro to restore original function.
*/
HOOK_UNREGISTER(dev_ethtool);
}
module_exit(funchook_exit);
MODULE_DESCRIPTION("hook sample");
MODULE_LICENSE("GPL");
MODULE_VERSION("1.1");
3.5 调试
这个 warning是宏定义的stub实现产生的。stub函数只提供跳转作用,并不会实际执行,所以可以忽略这个问题。
[root@localhost funchook]# make
make -C /lib/modules/3.10.0-514.26.2.el7.x86_64/build M=/root/git/github/funchook1 modules
make[1]: Entering directory `/usr/src/kernels/3.10.0-514.26.2.el7.x86_64'
CC [M] /root/git/github/funchook1/funchook.o
CC [M] /root/git/github/funchook1/module.o
In file included from /root/git/github/funchook1/module.c:2:0:
/root/git/github/funchook1/module.c: In function ‘stub_dev_ethtool’:
/root/git/github/funchook1/funchook.h:30:2: warning: ‘return’ with no value, in function returning non-void [-Wreturn-type]
return; \
^
/root/git/github/funchook1/module.c:19:1: note: in expansion of macro ‘HOOK_DEFINE’
HOOK_DEFINE(dev_ethtool, int, struct net *net, struct ifreq *ifr)
^
LD [M] /root/git/github/funchook1/sample.o
Building modules, stage 2.
MODPOST 1 modules
CC /root/git/github/funchook1/sample.mod.o
LD [M] /root/git/github/funchook1/sample.ko
make[1]: Leaving directory `/usr/src/kernels/3.10.0-514.26.2.el7.x86_64'
[root@localhost funchook1]# insmod sample.ko
执行 ethtool -K eth0 rx-checksum off命令,查看messages日志。
Dec 19 04:31:05 localhost kernel: [699670.756746] before dev_ethtool: ethcmd=55
Dec 19 04:31:05 localhost kernel: [699670.756781] before dev_ethtool: ethcmd=27
Dec 19 04:31:05 localhost kernel: [699670.756799] before dev_ethtool: ethcmd=20
Dec 19 04:31:05 localhost kernel: [699670.756801] before dev_ethtool: ethcmd=22
Dec 19 04:31:05 localhost kernel: [699670.756802] before dev_ethtool: ethcmd=24
Dec 19 04:31:05 localhost kernel: [699670.756803] before dev_ethtool: ethcmd=30
Dec 19 04:31:05 localhost kernel: [699670.756803] before dev_ethtool: ethcmd=33
Dec 19 04:31:05 localhost kernel: [699670.756804] before dev_ethtool: ethcmd=35
Dec 19 04:31:05 localhost kernel: [699670.756805] before dev_ethtool: ethcmd=43
Dec 19 04:31:05 localhost kernel: [699670.756805] before dev_ethtool: ethcmd=37
Dec 19 04:31:05 localhost kernel: [699670.756806] before dev_ethtool: ethcmd=58
Dec 19 04:31:05 localhost kernel: [699670.756807] before dev_ethtool: ethcmd=59 //ETHTOOL_SFEATURES
Dec 19 04:31:05 localhost kernel: [699670.756808] before dev_ethtool: ethcmd=20
Dec 19 04:31:05 localhost kernel: [699670.756809] before dev_ethtool: ethcmd=22
Dec 19 04:31:05 localhost kernel: [699670.756810] before dev_ethtool: ethcmd=24
Dec 19 04:31:05 localhost kernel: [699670.756810] before dev_ethtool: ethcmd=30
Dec 19 04:31:05 localhost kernel: [699670.756811] before dev_ethtool: ethcmd=33
Dec 19 04:31:05 localhost kernel: [699670.756812] before dev_ethtool: ethcmd=35
Dec 19 04:31:05 localhost kernel: [699670.756812] before dev_ethtool: ethcmd=43
Dec 19 04:31:05 localhost kernel: [699670.756813] before dev_ethtool: ethcmd=37
Dec 19 04:31:05 localhost kernel: [699670.756814] before dev_ethtool: ethcmd=58
通过查看ethcmd的相关定义,可知除了ethcmd=59,即ETHTOOL_SFEATURES是设置操作,其他都是get操作。从而得到 ethtool -K eth0 rx-checksum off实际执行的是ethcmd ETHTOOL_SFEATURES。
调试结束后,rmmod sample卸载调试模块即可。
4. github源码仓
github源码 funchook 欢迎下载使用,码字不易,大家多多点赞转发。