我是靠谱客的博主 无心泥猴桃,最近开发中收集的这篇文章主要介绍Linux内核模块CrC,Linux内核模块符号CRC检查机制,觉得挺不错的,现在分享给大家,希望可以做个参考。

概述

Linux内核不承诺模块编程接口兼容性,事实上这类编程接口在内核主线的演进过程中,不停地发生变化。那就引出一个问题:插入模块时,内核怎么判断该模块引用的内核接口已发生变化(二进制不兼容),防止模块不经重新编译就插入内核,造成系统Oops……。

由于内核只需要检查模块调用的接口与当前内核提供的接口,在语法和语义是否完全一致(即二进制兼容);而不需要做接口的兼容,甚至保持ABI接口不变。因此不需要使用类似glibc的版本机制,或者Window下的COM方案。

内核做法相对简单,只做两件事情:

1. 判断内核版本是否一致,以及几个重要的配置选项情况是否相同(CONFIG_PREEMPT, CONFIG_SMP)

2. 判断模块引用的导出模符号的CRC值,与当前内核该符号的CRC值是否相同

只有上述两个条件满足,才能说明模块不需要重新编译,本文重点分析CRC机制。

浅谈内核模块符号CRC机制

CRC是什么,很直观的理解就是签名或者哈希,只当数据保持不变时,CRC结果才保持不变;哪怕有一丁点的变化,它都会跳出来告诉你,有变化了,接口不匹配,请重新呼叫编译器进行工作。

那么问题来了,什么情况下导出符号(EXPORT_SYMBOL)不兼容,即二进制不兼容。

其实二进制接口兼容要求保持两个不变:

1. 语法保持不变

遵守这个条件,说明如果模块在新内核下重新编译,那应该没有任何语法问题。

即导出符号的类型名没有变化,如果是函数,则要求参数和返回值类型没有任何变化;如果这些类型是结构体的话,结构体的成员名也没有有任何变化。

2. 语义保持不变

这要求符号的类型不能有变化,如果类型本身是结构体(struct),则它成员的类型不能有变化,成员在结构体内的位置不能有变化,以及成员本身不能增删。

上述两点,背后朴素的道理就是:导出符号的签名不能有变化。

下面先讲述符号的CRC计算过程,然后再说明该CRC结果如何识别上述任何一个变化。

内核导出符号的CRC生成规则

如果你像我一样,对这个CRC生成规则感兴趣,一定要刨根问底了解它的规则才能睡着觉的话,那恭喜你,一定度过不眠之夜。因为生成这个CRC需要解释C源代码,像编译器一样,小心翼翼地根据C的语法识别各种类型定义,你得把lex和yacc的定义翻个朝天还是搞不懂。

好吧,我就以退为进吧,相信你也会赞成这个方法:增加调试输出,对感兴趣的内核符号输出CRC的整个计算过程,从而提取它的规则,然后在这里卖关子,哈哈:)

好!不扯了,下面谈一下具体的规则。

1.  CRC基本函数

Linux内核使用CRC32来做基本哈希运算, 它的定义如下:

static unsigned long partial_crc32_one(unsigned char c, unsigned long crc)

{

return crctab32[(crc ^ c) & 0xff] ^ (crc >> 8);

}

具体的实现细节,可以参考内核源码。相信大家不会对这个函数感兴趣,更多是关心最终符号的CRC结果与什么正相关。

ok,为了将关注的重点转移到CRC的计算结构,我们做下面的简单定义字符串的CRC计算结果:

H(, crc0)  := H(, crc0) = partial_crc32_one(未字符, H(, crc0))

这个递归定义太复杂了吧,直白地说,就是以crc0作为初值,对每个字符,都调用上述的partial_crc32_one,得到一个crc值,再将下个字符和该crc结果,调用partial_crc32_one,依次下去,直到字符串结束,得到的值就字符串的CRC值。

好了,有上述的约定,就可以计算每个符号的CRC值了。

2. 基础类型的crc规则

类型             CRC值

----------------------------------------------------------------

int                H("int", 0xffffffff) ^ 0xffffffff

char             H("char", 0xffffffff) ^ 0xffffffff

long             H("long", 0xffffffff) ^ 0xfffffff

....

那么再复杂一点的unsigned int, unsigned long该如何计算,很简单,使用复合+偏序的计算结构:

unsinged int的计算方法:

1) H("unsigned", 0xffffffff ) -> crc1

2) H(空白, crc1) -> crc2

3) H("int", crc2) - >crc3

4) crc3 ^ 0xfffffff -> 结果

为什么说是偏序呢?因此保持从左右到的计算结构,它的计算结构可以表示成:

0xffffffff -> "unsigned" -> 空白 -> "int" -> 0xffffffff

为了减少阅读的噪音,去掉空白、引号和0xffffffff,将这个表达结果简成下面这样:

unsigned -> int

简化之后规则,只使用计算结构进行表达,方便大家阅读。

3. 复合类型CRC规则

1. 结构体

如 struct foo {

int a;

int b;

};

它的crc计算方式很简单,它的计算结构为:

struct -> foo -> {  -> int -> a -> ; -> int -> b -> ; -> }

2. 数组 type arr[N];

计算结构为:

type -> arr -> [ -> N -> ]

注:这里的type本身可能是个复合类型,它的它的crc计算方法或者结构遵守上述的规则, 比如

type 为unsigned int,即:

unsigned int arr[N]

type -> arr -> [ -> N ->]  ==>unsigned int -> arr -> [ -> N -> ] ==>unsigned -> int -> arr -> [ -> N -> ]

3. 指针 type *

计算结构为: type -> *

简单的有如:int * p

它的计算结构:int -> *

复杂的有如:

struct foo {

int a;

int b;

}

struct foo *

那么p的计算结构:

struct -> foo -> { -> int -> a -> ; -> int -> b -> ; ->} -> *

其它构造类型,在这里不一一枚举,有兴趣可以查阅相关代码;或者使有我提供的patch,对你感兴趣的类型做测试。

4. 导出变量的CRC计算方法

上在谈的一直是类型,那变量呢,因为内核导出的符号最终是变量或者函数。

假设内核有下面的导出变量

type var;

EXPORT_SYMBOL(var);

计算结构为:type -> var

5. 函数的CRC计算方法

假设内核有下面的导出函数

type1 func(type2 a, type3 b)

计算结构为:type1 -> func -> ( -> type2 -> a -> , -> type3 -> b -> )

CRC计算过程如何保持符号的语法和语义

前面提到,符号CRC值不变,意味着符号的语法和语义保持不变,下面用例子说明:

struct foo1 {

int a;

int b;

};

struct foo {

int c;

struct foo1 b;

};

int func(int u, struct foo *foo);

EXPORT_SYMBOL(func);

那整个CRC计算结构如下:

int -> func -> (  ->

int -> u -> , ->

struct -> foo ->

{ ->

int -> c -> ; ->

struct -> foo1 ->

{ ->

int -> a -> ; ->

int -> b -> ; ->

} ->

b  ->

} ->

* -> foo ->

)

看到了吧:

1)语法属性:任保一个类型名,或者变量名发生变化,都会造成最终的CRC发生变化

2)语议属性:任何一个类型变化,或者结构成员出现位置调整,都会造成最终的CRC发生变化

不要恐慌

看了导出符号CRC的定义,对于通用的导出函数,只有它的类型树结构稍有点风吹草动,它的CRC就会发生变化,依赖该函数的内核模块就得重编了。

事实上没有这么大的恐慌,一般内核bugfix是不会造成核心数据结构的变化(当前是一般情况,但无绝对),因此无须太担心。 一般的bugfix只是增减代码,不会修改数据结构和函数签名。

一个具体的计算例子

上面一直使用计算结构来表过每个符号的计算过程,目的是使大家重点关注CRC值与哪些因素相关,而不是陷入万劫不复的计算细节中。 这里举个具体的例子来说明上述的计算结构是如何工作的。

数据结构的定义:

struct pair {

int a1;

int b1;

};

struct comp {

struct pair p;

longl;

};

函数定义:

void my_func_comp_p(struct comp *p) {}

EXPORT_SYMBOL(my_func_comp_p);

相信大家根据上面的规则很容易推算my_func_comp_p的计算结构。下面调试日志记录的计算过程,crc可以理解成上面的H定义。

上面描述计算结构时,我们一直将空格(空白字符)没有写出来,实际在计算过程,是需要使用空白字符来计算的。否则CRC就无法区分"unsigned int"类型和"unsignedint"变量了。

crc(void, 0xffffffff) = 0x2d842611

crc( , 0x2d842611) = 0x51f3841c

crc(my_func_comp_p, 0x51f3841c) = 0x3cde0c75

crc( , 0x3cde0c75) = 0x1b3d7b77

crc((, 0x1b3d7b77) = 0xfbcf711e

crc( , 0xfbcf711e) = 0xc19ad2da

crc(struct, 0xc19ad2da) = 0x0950984a

crc( , 0x0950984a) = 0xad6ed8de

crc(comp, 0xad6ed8de) = 0xfc468378

crc( , 0xfc468378) = 0x654c9f45

crc({, 0x654c9f45) = 0xc1045134

crc( , 0xc1045134) = 0x1a1bd02c

crc(pair, 0xffffffff) = 0xf6a5e196

crc(struct, 0x1a1bd02c) = 0x64623aa1

crc( , 0x64623aa1) = 0x9adbd18c

crc(pair, 0x9adbd18c) = 0x71ccf17f

crc( , 0x71ccf17f) = 0xfba58094

crc({, 0xfba58094) = 0x304e5a69

crc( , 0x304e5a69) = 0x0f30b76e

crc(int, 0x0f30b76e) = 0x2404ed55

crc( , 0x2404ed55) = 0x204b815e

crc(a1, 0x204b815e) = 0x93bfb9fb

crc( , 0x93bfb9fb) = 0x1192b4e5

crc(;, 0x1192b4e5) = 0x617a6d67

crc( , 0x617a6d67) = 0xe8d9ae5e

crc(int, 0xe8d9ae5e) = 0x42b9fbe6

crc( , 0x42b9fbe6) = 0x7245de7e

crc(b1, 0x7245de7e) = 0xd6c2d0f1

crc( , 0xd6c2d0f1) = 0xf1022092

crc(;, 0xf1022092) = 0xaffb196c

crc( , 0xaffb196c) = 0x7fc5f6a2

crc(}, 0x7fc5f6a2) = 0x16130ab3

crc( , 0x16130ab3) = 0x6910d1f4

crc(p, 0x6910d1f4) = 0xeabc57e8

crc( , 0xeabc57e8) = 0x9555f6d5

crc(;, 0x9555f6d5) = 0x47279a89

crc( , 0x47279a89) = 0xaf4d3cd6

crc(long, 0xaf4d3cd6) = 0x372303f4

crc( , 0x372303f4) = 0x818935ce

crc(l, 0x818935ce) = 0x38594bf1

crc( , 0x38594bf1) = 0xf1ecbb09

crc(;, 0xf1ecbb09) = 0xc826bd3b

crc( , 0xc826bd3b) = 0x8aadef51

crc(}, 0x8aadef51) = 0x3252c10c

crc( , 0x3252c10c) = 0x32ea3e22

crc(*, 0x32ea3e22) = 0x0ee9620c

crc( , 0x0ee9620c) = 0x32d68581

crc(), 0x32d68581) = 0xd83ffd5f

crc( , 0xd83ffd5f) = 0xc0625350

0xc0625350 ^ 0xffffffff = 0x3f9dacaf

Result:  my_func_comp_p = 0x3f9dacaf

为什么会有这篇文章

相信大家都带着一个很大的疑问看到这里,为什么需要知道符号CRC的计算规则;将模块插入到内核时,只有符号CRC值有变化,内核会报告模块插入失败,重编模块就是了,对系统造成影响。

事实上,我是个苦逼的程序员,每次在内核合入小的修改,都尽可能地减少CRC的变化;如果有变化,还需要告诉为什么会有变化。这需求很贴心吧。

另外一点,这个CRC机制是否可以应用于 程序跟动态库的接口兼容 检查,开发一个新工具来检测修改前和修改后编译出来的动态库,对外接口上是否完全二进制兼容,这也是我们所期望的。

有需要请联系

csdn博客不能上传代码,可谓一大遗憾。我通过修改生成crc模块的代码,增加调试信息,可以在编译过程中,通过命令行参数来控制,生成哪些导出符号CRC 的详细计算过程,有兴趣的小伴伙请联系我。

0818b9ca8b590ca3270a3433284dd417.png

最后

以上就是无心泥猴桃为你收集整理的Linux内核模块CrC,Linux内核模块符号CRC检查机制的全部内容,希望文章能够帮你解决Linux内核模块CrC,Linux内核模块符号CRC检查机制所遇到的程序开发问题。

如果觉得靠谱客网站的内容还不错,欢迎将靠谱客网站推荐给程序员好友。

本图文内容来源于网友提供,作为学习参考使用,或来自网络收集整理,版权属于原作者所有。
点赞(43)

评论列表共有 0 条评论

立即
投稿
返回
顶部