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(<子串1:未字符>, crc0) = partial_crc32_one(未字符, H(<子串1>, 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 *p;


计算结构为: type -> * p

简单的有如:int * p
它的计算结构:int -> * ->p

复杂的有如:
struct foo {
int a;
int b;
}

struct foo * p

那么p的计算结构:
struct -> foo -> { -> int -> a -> ; -> int -> b -> ; ->} -> * -> p


其它构造类型,在这里不一一枚举,有兴趣可以查阅相关代码;或者使有我后面提供的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;
long l;
};

函数定义:
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 的详细计算过程,有兴趣的小伴伙请联系我。

郑重声明:本站内容如果来自互联网及其他传播媒体,其版权均属原媒体及文章作者所有。转载目的在于传递更多信息及用于网络分享,并不代表本站赞同其观点和对其真实性负责,也不构成任何其他建议。