Linux:从min宏开始入手LKM

引言

本学期开了陈老师的OS课程,作业比较丰富。对于我这种放飞自我的人来说,作业是“选做 = 不做,必做 = 乱做”的,但是点进去任务的页面,稍微看了一眼觉得挺感兴趣的,也有一点点自己的小问题,决定记录一下。

任务如下:

任务详情
搭建Linux环境,编写Linux内核模块,二选一或者都做
1.编写Linux内核模块,并求最大数,参考,https://mp.weixin.qq.com/s/0ZVNbTj7fGs8Gi03vj541A和 https://mp.weixin.qq.com/s/Kvle_fgr4rj5Jt4VDUmKHA
2.编写Linux内核模块,增加一个新的系统调用,参考https://www.cnblogs.com/wangzahngjun/p/4992045.html

我在这里选择的是任务一来做。
关于max宏的第一版本,Linux内核中max()宏的奥妙何在?(一) 已经写的比较到位了,我就不赘述了,大家可以先从师哥的这篇文章开始看起。

文章末尾放出一张邮件列表的截图,我将从邮件中一些没有提到的点说起。
在这里插入图片描述
首先可以看到有这么一句话:

Our old “min()” had the int ernal variables called “min1” and"nin2",which is crazy too.

为什么说很“crazy”?让我们接着往下看。

第一版的“疯狂”之处

第一版的宏确实用“ ernal variables”解决了多次自增的缺点,并且妙用了GCC扩展特性typeof和statement list(听说这两点也快被加到新的C标准中啦),但是很显然如果当变量名重复时,就会出现ub(Undefined Behaviour)。

让我们试试吧!

#include <stdio.h>

#define min(x, y)                  \
  ({                               \
    typeof(x) _min1 = (x);         \
    typeof(y) _min2 = (y);         \
    (void)(&_min1 == &_min2);      \
    _min1 < _min2 ? _min1 : _min2; \
  })

int main(int argc, char *argv[]) {
  int _min1 = 0, _min2 = 1;
  printf("min(_min1, _min2) is %d\n", min(_min1, _min2));
  return 0;
}

不过运行后我们就会发现结果居然每次都不一样!
任取三次感受一下:

min(_min1, _min2) is 22057
min(_min1, _min2) is -561487744
min(_min1, _min2) is -1579712384

预处理后看看咋回事:

➜  lkm git:(master) ✗ gcc -E max-v1.c | tail

# 2 "max-v1.c" 2
# 11 "max-v1.c"

# 11 "max-v1.c"
int main(int argc, char *argv[]) {
  int _min1 = 0, _min2 = 1;
  printf("min(_min1, _min2) is %d\n", ({ typeof(_min1) _min1 = (_min1); typeof(_min2) _min2 = (_min2); (void)(&_min1 == &_min2); _min1 < _min2 ? _min1 : _min2; }));
  return 0;
}

格式化后更好看一些:

  printf("min(_min1, _min2) is %d\n", ({
           typeof(_min1) _min1 = (_min1);
           typeof(_min2) _min2 = (_min2);
           (void)(&_min1 == &_min2);
           _min1 < _min2 ? _min1 : _min2;
         }));

问题就出在这里:

typeof(_min1) _min1 = (_min1);
typeof(_min2) _min2 = (_min2);

这就相当于int x= x,由于x是local variable(也就是邮件中强调的“ernal variables”),它的值是随机的,不过好在它并不会修改main函数中_min1和_min2的值,这是由作用域决定的。

__LINE__的弊端

And our __UNIQUE_D( macro is garbage anyway,since it falls back cnthe line rumber,which doesn’t really work for macros aryway.But wehave proper macros for both clang and gcc,so maybe we should ignorethe broken fallback.

garbage这词都用上了,不愧是你linus。原因出在“it falls back cnthe line rumber”。我在这里中找到了类似的实现。

# define __UNIQUE_ID(prefix) __PASTE(__PASTE(__UNIQUE_ID_, prefix), __LINE__)

这个宏想必也是耳熟能详的,在不少自定义的debug函数中我们可以看到它的身影,预处理时,编译器会自动把它替换成当前行号。但是完全依赖行号,当多个源文件对应行行号一致,在链接时(编译不会)会报错。

最新版本

我在Github上找到了目前Linux内核中某些文件正在使用的版本,链接在下方给出。根据这些源文件,我们提取出相关内容并简单写了一个小例子进行讨论。

#include <stdio.h>

#define __min(t1, t2, min1, min2, x, y) \
  ({                                    \
    t1 min1 = (x);                      \
    t2 min2 = (y);                      \
    (void)(&min1 == &min2);             \
    min1 < min2 ? min1 : min2;          \
  })

#define ___PASTE(a, b) a##b
#define __PASTE(a, b) ___PASTE(a, b)

#define __UNIQUE_ID(prefix) __PASTE(__PASTE(__UNIQUE_ID_, prefix), __COUNTER__)

#define min(x, y) \
  __min(typeof(x), typeof(y), __UNIQUE_ID(min1_), __UNIQUE_ID(min2_), x, y)

#define min_t(type, x, y) \
  __min(type, type, __UNIQUE_ID(min1_), __UNIQUE_ID(min2_), x, y)

int main(int argc, char *argv[]) {
  int smaller = 0, bigger = 1;
  printf("min(bigger, smaller) is %s\n",
         min(bigger, smaller) == smaller ? "smaller" : "bigger");
  printf("min_t(int, bigger, smaller) is %s\n",
         min_t(int, bigger, smaller) == smaller ? "smaller" : "bigger");
  return 0;
}

输出是符合预期的:

min(bigger, smaller) is smaller
min_t(int, bigger, smaller) is smaller

这版跟上面的变化不大,主要是__LINE__换成了__COUNTER__来避免上面那种情况。
我们看到这里实际上有两个宏,区别在于min_t多了一个type参数,可能是用于在编码时明确指定比较类型,使代码更清晰吧。它和min实际上都调用了__min。

__PASTE和___PASTE

初看的时候我觉得还挺奇怪的,为什么一个连接字符串的宏还要分成两层来做,macro __min实际上类似于一个xxx_helper、或者do_xxx函数,是有用的,那么这里的macro PASTE到底有什么用呢?

首先我们试一下没有___PASTE会发生什么?
一个简单的例子:

#include <stdio.h>

#define __PASTE(a, b) a##b

int main(int argc, char *argv[]) {
  // result: __PASTE(a, b) c;
  __PASTE(__PASTE(a, b), c);
  return 0;
}

查看预处理后的结果:

➜  lkm git:(master) ✗ gcc -E __PASTE.c | tail
__PASTE.c:7:23: error: pasting ")" and "c" does not give a valid preprocessing token
    7 |   __PASTE(__PASTE(a, b), c);
      |                       ^
__PASTE.c:3:23: note: in definition of macro ‘__PASTE’
    3 | #define __PASTE(a, b) a##b
      |                       ^




# 5 "__PASTE.c"
int main(int argc, char *argv[]) {

  __PASTE(a, b) c;
  return 0;
}

可以看到,__PASTE(__PASTE(a, b), c)并不会展开为abc,而是__PASTE(a, b) c,为什么呢?

我们需要先了解一下C语言的宏展开规则:

Macro arguments are completely macro-expanded before they are substituted into a macro body, unless they are stringified or pasted with other tokens. After substitution, the entire macro body, including the substituted arguments, is scanned again for macros to be expanded. The result is that the arguments are scanned twice to expand macro calls in them.

有看到一句更清晰的解释:

An occurrence of a parameter in a function-like macro, unless it is the operand of # or ##, is expanded before substituting it and rescanning the whole for further expansion.

其中,expanded before substituting我们可以称之为pre-expanded。

__PASTE(__PASTE(a, b), c) -> __PASTE(a, b) c。

而当我们这么写:

#define __UNIQUE_ID(prefix) __PASTE(__PASTE(__UNIQUE_ID_, prefix), __COUNTER__)

展开顺序应该是:

__UNIQUE_ID(prefix) ->
__PASTE(__PASTE(__UNIQUE_ID_, prefix), __COUNTER__) ->
__PASTE(___PASTE(__UNIQUE_ID_, prefix), __COUNTER__) ->
__PASTE(__UNIQUE_ID_##prefix, __COUNTER__) ->
__PASTE(__UNIQUE_ID_prefix, __COUNTER__) ->
___PASTE(__UNIQUE_ID_prefix, __COUNTER__) ->
__UNIQUE_ID_prefix##__COUNTER__ ->
__UNIQUE_ID_prefix__COUNTER__

防止不恰当的pre-expanded,这就是为什么要有两重PASTE宏的原因。顺带一提,我个人觉得这个宏形参的名称应该取名suffix更为恰当,毕竟是“后缀”嘛。

自定义LKM

这步基本上其他博客都有,我们就照葫芦画瓢(把别人文章拿来改改),不过我没有成功。

make -C /usr/src/linux-`uname -r` SUBDIRS=$PWD modules

问题锦集

/usr/src/下为空

用包管理器下载对应的linux-header即可。

无法进入到对应目录,原因比较简单:

➜  min-lkm git:(master)ls /usr/src/
linux-headers-5.4.0-72/  linux-source-5.4.0/  linux-source-5.4.0.tar.bz2@
➜  min-lkm git:(master)uname -r
5.4.72-microsoft-standard-WSL2

所以说,WSL还是会碰到一些小坑的。

/bin/sh: 1: flex: not found

安装flex即可,bison也同理。

sudo apt-get install flex
sudo apt-get install bison

Configuration file “.config” not found!

生成配置文件即可,注意后面一串东西是要加上的,这样才能找到Makefile。

sudo make menuconfig -C /usr/src/linux-headers-5.4.0-72/ SUBDIRS=$PWD modules

The present kernel configuration has modules disabled.

在这里插入图片描述这是因为忘了开启LKM支持,进入menuconfig选中这项再保存即可。

No rule to make target ‘arch/x86/entry/syscalls/syscall_32.tbl’, needed by ‘arch/x86/include/generated/asm/syscalls_32.h’. Stop.

目前卡在这步,原因不详。
在这里插入图片描述

Reference

  • 1
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值