蜗窝科技

总结Linux kernel开发中0的巧妙使用

snail · · 阅读 15483 · 2015年7月存档

kernel的源代码中,有很多令人拍案叫好的用法,此文简单总结了一下,我所知道的0的巧妙用法,权当抛砖引玉,希望能激发起读者相当更多的0的用法。

用法一:0长数组

0长数组在GNU C中是被允许的,0长数组作为一个结构体的最后一个元素是非常有用的,尤其
是当这个结构体表达的一个可变长度的对象时,如下:

struct line {

int length;

char contents[0];

};

struct line *thisline = (struct line *)malloc(sizeof (struct line) + this_length);

thisline->length = this_length;

特别要注意的一点是:结构体sizeof(struct line) == 4,也即数组contents[0]不占用任何空间,
在使用的时候动态分配。

详细描述可参考下面的链接:
https://gcc.gnu.org/onlinedocs/gcc-4.6.2/gcc/Zero-Length.html#Zero-Length
用法二: 获取结构体中成员变量的offset

#define offsetof(TYPE, MEMBER) ((size_t)&((TYPE *)0)->MEMBER)

上面是kernel源码中的一个宏定义,功能是获取结构体(TYPE)中成员变量(MEMBER)的offset。
我们都知道指针就是一个整型数值,这个数值代表的是内存的地址,而结构体所占用的内存是
连续分配的。假如成员MEMBER的地址为p1, TYPE的地址为p2,那么MEMBER的offset为p1-p2,这个
宏中假设p2为0,所以offset == p1.

用法三:debug kernel panic

当kernel panic时,终端会输出类似如下的log:

Call Trace:
[] show_trace_log_lvl+0x1a/0x2f
[] show_stack_log_lvl+0x9b/0xa3
[] show_registers+0xa3/0x1df
Code:
e3 a0 20 01 83 20 00 f5 7f f0 4e e3 a0 30 00 e5 c3 20 00

如果有此次kernel版本的vmlinux,那就很简单了,直接
#gdb vmlinux
(gdb) b*(0xc0406081 + 0x1a)

就可以知道panic发生在哪一行了。如果没有vmlinux怎么办呢?
从网上看到有人说,Linus通常使用一个小程序,类似这样:

const char array[] = “\xnn\xnn\xnn\xnn\xnn\xnn\xnn\xnn”;

int main(int argc, char * argv[])

{

printf("%p\n", array);

*(int *)0 = 0;

}

数组里就是发生panic时,最后的几个指令,上面用<>标记的红色的指令,就是发生panic的指令,

当实际解析这些指令的时候,需要注意大小端问题,如下:

#include <stdio.h>

#include <stdlib.h>

#include <string.h>

const char a[] = “\xe3\xa0\x20\x01\xe5\x83\x20\x00\xf5\x7f\xf0\x4e\xe3\xa0\x30\x00\xe5\xc3\x20\x00”;

int main(int argc, char *argv[])

{

printf("%p\n", a);

*(int *)0 = 0;

}

#gcc h.c

然后用gdb分析, 如下图,因为错误指令是a[4], 也即in $0x83, %eax

为了使反汇编的结果与c代码对应起来,kernel的Kbuild子系统提供了一个这样的功能,任何一个c文件,都可以

单独编译成汇编文件,如下:

make O=/kernel/ ARCH=arm CROSS_COMPILE=/prebuilts/gcc/linux-x86/arm/arm-eabi-4.8/bin/arm-eabi- KCFLAGS="-fno-pic -mno-unaligned-access" V=1 kernel/user.s

进而确定发生panic的源代码的位置。

说这个例子,只是解释一下0的使用技巧,这只是老的调试方法,因为kernel中有一个脚本:scripts/decodecode来做这个事了,用法:

把放生panic那行code,写入~/oops.txt,然后:

.scripts/decodecode < ~/oops.txt

可能大家会有一个疑问:为什么在宏的定义中访问0地址,不会放生程序错误,而在这个例子中会产生段错误呢?

这是因为在宏中,我们对0地址的访问方式是读,而在这个例子中的访问方式是向0地址写数据,kernel对0地址初始化为只读的,

可能就是这个原因吧,这是我的个人理解。

用法四:do {} while(0)

这个用法大家都已经很熟悉了,就是把宏定义用do {} while(0)语句括起来,形成一个整体。

例子就不枚举了,但是定义这样的宏也有一些限制,具体的可以参看kernel文档:

Documentation/zh_CN/CodingStyle

暂时先总结这些吧,不妥之处,敬请批评指正。

haobenqu@126.com

写于上海

标签:Kernelpanic空指针

继续讨论

新讨论请用 GitHub 账号在下方发言——评论会保存在本仓库的 Discussions 里,与站点同寿; 再下方的「历史评论」是原站的只读存档。

评论(6 条,只读存档)

以下为原站的历史评论(只读存档);继续讨论请用上方的评论区。

  • snail

    这篇文章的发布时间,系统搞错了!
  • linuxer

    文章写的真心不错,很有调理。
  • cuijier

    博主你好,我想问下,在调试android手机的过程中,是否也可以用 #gdb vmlinux (gdb) b*(0xc0406081 + 0x1a) 这个来定位 Trace 具体的位置?应该不行吧?
    • wowo

      @cuijier:如果只是调试kernel(vmlinux),是可以的。kernel是不区分Android还是其它系统的。
  • simonzhang

    其实对0地址,或0+一个小的offset,在运行时无论读写应该都会exception的,因为这个地址是不在当前的virtual memory space里面。 &((TYPE *)0)->MEMBER之所以不会有错误,其实是因为编译器会做出结构体内偏移的计算,这个值也就是一个常量。 do {} while(0)我的理解是为了保证宏展开时的安全性
    • wowo

      @simonzhang:"&((TYPE *)0)->MEMBER",这个应该是预编译时的计算出来的,不是运行时的事情,否则肯定会exception的。