日期:2014-05-16  浏览次数:20669 次

linux kernel中的编码技巧:将运行期错误提前为链接报错

        //xk> 缘起

        《深入Linux内核架构》P144页。读书存疑,继而想通。


        //xk> 铺垫

        虚拟地址空间一般按3:1划分为进程地址空间和内核地址空间,32位机器4G的虚拟地址空间就有1G分为内核地址空间。

        (1) 内核地址空间前896M是直接映射的物理页帧,即物理内存上的896M能直接映射(通过线性偏移0xC000000)到内核地址空间的这个区域。

        (2) 然后是一小段内存间隙用于内核故障报错。

        (3) 然后是vmalloc区域,用于动态分配,内核自身会尽力避免非连续的物理地址,这段内存区主要用于动态加载模块时。

        (4) 然后是2个页的保护性间隔内存。

        (5) 然后是持久映射区,用于将highmemory中的非持久页映射到内核中。(存疑,暂不求甚解)

        (6) 最后是固定映射区,一直到虚拟地址4GB处。对于上面所述的直接映射区,虚拟地址平移__PAGE_OFFSET(IA-32上即为0xC000000)即得到物理地址,而固定映射不同,这个区域中的虚拟地址指向随机的物理地址,虚拟地址和物理地址的关联可以自由定义但定义后不能改变。固定映射的优点是:1. 编译时对此类地址的处理类似常数,内核一启动即为其分配了物理地址。 2. 此类地址的反引用比普通指针要快。 3. 内核确保在上下文切换时,对应于固定映射的页表项不会从TLB刷出,在访问固定映射的内存时,总是通过TLB高速缓存取得对应的物理地址。


        //xk> 正文

        对每个固定映射地址都会创建一个常数,加入到fixed_addresses枚举值列表中,__end_of_fixed_addresses是最后一个枚举成员,定义了最大的可能数字。 linux_kernel_2.6.27.62/include/asm-x86/fixmap_32.h :: fix_to_virt()函数用于根据固定映射的fixed_addresses常数计算虚拟地址

        如果idx比可能的最大值还大,那么出错,内核访问了无效的地址,调用__this_fixmap_does_not_exist()函数。注意:这个函数是没有定义的!在内核链接时会报错:由于存在未定义符号而无法生成映像文件。这么做的好处是:把运行期错误提前到了链接期,这种内核故障在编译链接时即可检测,而不会在运行时出现。

        问题来了,如果内核没出错呢?链接器并不知道if判断的执行流程,那它怎么知道不去链接__this_fixmap_does_not_exist()的定义呢?奥妙在编译器优化机制,因为fix_to_virt()是内联函数,而其参数idx是枚举值常数,在编译期就可以算出来(这就是要整个fixed_addresses枚举表而不是直接用固定映射区虚拟地址的原因)。在编译器优化阶段,if语句会被消除,如果idx值越界,__this_fixmap_does_not_exist()的调用会留在代码中,从而引发链接错误;如果idx值有效,__this_fixmap_does_not_exist()调用就被优化掉了,内核编译链接通过。

        OK, GCC优化依然不求甚解,相关知识估计也是一大块。不过下次遇到大牛使用这样的技巧,通过未定义的伪函数将运行期错误提前到链接报错,就不会一头雾水了^_^