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 *
计算结构为: 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;
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 的详细计算过程,有兴趣的小伴伙请联系我。