Theme Preview

无 kernel 源代码和 config 的情况下为 HG8120C 编译内核模块

由 李晓岚 在 2017年12月22日发表

有读者在评论区留言说,想为光猫编译 ext2 文件系统内核模块,但是才执行 insmod ext2.ko 插入编译好的模块就出现 kernel panic。这个问题其实很早之前在我为光猫编译 aufs 模块就已经遇到了,下面就来说说如何解决这一问题。

问题描述

设备光猫 HG8120C 使用的 SOC 是 Hisi sd511x,运行 linux v2.6.34.10 内核,生产商不提供内核 GPL 源代码(至少我没有找到可以方便获取源代码的方式),因此缺失对应设备驱动,所以不能运行自行编译的内核。原厂内核不支持某些我们想要的特性,比如 aufsext2 等文件系统,我们能不能使用 vanilla linux kernel 的源代码来编译这些模块,使得这些模块可以顺利运行在原厂内核中?

理论上,和设备硬件驱动无关的模块是可以通过这样的方式编译并运行的。但是呢,可能因为模块依赖的某些函数没有导出,或者是被编译优化掉了,又或者 CONFIG_* 设置的原因,某些内核结构体少了一些字段,又或者厂商在内核中加入了私货,改变了结构体的布局或大小,等等等等,都可能导致使用这种方式编译的模块运行失败。厂商没有在 /proc/config{,.gz} 留下配置,这进一步加大了问题的难度。基于之前 aufs 的经验,我判断 ext2 有 90% 的概率也是可以顺利运行起来的。

编译 ext2.ko

在之前编译 aufs 的环境是,原始 linux v2.6.34.10 源代码,没有任何厂商夹带的私货,随便找了一个 arm 架构支持 SMP 板子的默认配置文件作为基础,编译 aufs 模块,加载的目标系统测试运行,panic 后修正某些 CONFIG_* 配置(后面将用 loop.ko 做例子来示范如何定位错误根源),重复“编译->测试->panic后修正”这个过程,直到测试成功。

在这个环境中,编译 ext2.ko 后,在目标设备上运行 insmod ext2.ko 后,一切正常,看起来设备已经支持 ext2fs 了。有这样的结果其实并不意外,因为在 aufs 的环境中,我已经修正了一些与文件系统有关的配置和厂商对结构体布局的修改。

WAP(Dopra Linux) # modinfo ext2.ko
filename:       ext2.ko
description:    Second Extended Filesystem
author:         Remy Card and others
license:        GPL
vermagic:       2.6.34.10_sd5115v100_wr4.3 SMP mod_unload ARMv7
WAP(Dopra Linux) # insmod ext2.ko
WAP(Dopra Linux) # cat /proc/filesystems | grep ext2
        ext2

接下来就该测试挂载某个 ext2 文件系统的设备了,当然第一就想到了 loop 设备,但原厂内核不支持 loop 设备。没关系,我们继续编译一个 loop.ko 就行了。

WAP(Dopra Linux) # insmod loop.ko
# kernel panic and device rebooted.

评论区那名读者遇到的类似问题出现啦!

Debug loop.ko panic

该如何 debug 这样的问题呢?幸运的是,厂商很“贴心”地为我们保存好了 panic 的很多信息,存放在 /mnt/jffs2/panicinfo 路径的文件里。

# after boot up again.
WAP(Dopra Linux) # cat /mnt/jffs2/panicinfo
Kernel panic - not syncing: Fatal exception
CPU: 0    Tainted: P      D W   (2.6.34.10_sd5115v100_wr4.3 #1)
Process insmod (pid: 1793, stack limit = 0xc25a2270)
PC is at c0092944
LR is at c0092b3c
pc : [<c0092944>]    lr : [<c0092b3c>]    psr: 60000013
sp : c25a3edc  ip : 00000000  fp : 00000000
r10: 4000ef74  r9 : c25a2000  r8 : bf898484
r7 : bf898434  r6 : bf898484  r5 : 00000000  r4 : c387ba00
r3 : 00000007  r2 : c035eac8  r1 : 00000000  r0 : 000000a0
Flags: nZCv  IRQs on  FIQs on  Mode SVC_32  ISA ARM  Segment user
[<bf0c2a6c>] (hw_ssp_get_backtrace_info+0x0/0x78 [hw_ssp_depend]) from [<bf0c2bd0>] (hw_ssp_write_panic_info+0xec/0x11c [hw_ssp_depend])
[<bf0c2bd0>] (hw_ssp_write_panic_info+0xec/0x11c [hw_ssp_depend]) from [<c02e4368>] (panic+0xa0/0x124)
[<c02e4368>] (panic+0xa0/0x124) from [<c0030c94>] (die+0x1b0/0x1d4)
[<c0030c94>] (die+0x1b0/0x1d4) from [<c0033a2c>] (__do_kernel_fault+0x64/0x84)
[<c0033a2c>] (__do_kernel_fault+0x64/0x84) from [<c0033e0c>] (do_page_fault+0x140/0x1e4)
[<c0033e0c>] (do_page_fault+0x140/0x1e4) from [<c002c45c>] (do_DataAbort+0x34/0x98)
[<c002c45c>] (do_DataAbort+0x34/0x98) from [<c002cbcc>] (__dabt_svc+0x4c/0x60)
[<c002cbcc>] (__dabt_svc+0x4c/0x60) from [<c0092944>] (bdi_register+0x8/0x13c)
[<c0092944>] (bdi_register+0x8/0x13c) from [<bf898484>] (0xbf898484)

从上面的 panic 信息可以看出,导致 panic 的原因是 data abort(从 do_DataAbort 可以猜想到),也就是通常的野指针问题。具体发生的位置在 bdi_register 很靠前的位置(bdi_register+0x8),基本就是头一两行代码的样子。至于最后的 from [<bf898484>] 这个值和寄存器 LR 相去甚远,就可以选择不用相信了。

找到 bdi_register 函数对应的源代码 mm/backing-dev,很容易确定是因为 545 行的 bdi 导致野指针异常。

/* mm/backing-dev.c */
538 int bdi_register(struct backing_dev_info *bdi, struct device *parent,
539                 const char *fmt, ...)
540 {
541         va_list args;
542         int ret = 0;
543         struct device *dev;
544
545         if (bdi->dev)   /* The driver needs to use separate queues per device */
546                 goto exit;
547
548         va_start(args, fmt);
            ...
            ...
            ...
584         return ret;
585 }
586 EXPORT_SYMBOL(bdi_register);
587
588 int bdi_register_dev(struct backing_dev_info *bdi, dev_t dev)
589 {
590         return bdi_register(bdi, NULL, "%u:%u", MAJOR(dev), MINOR(dev));
591 }
592 EXPORT_SYMBOL(bdi_register_dev);

根据 LR 寄存器,追溯到 bdi_register 的调用者 bdi_register_dev,这之后的完整调用栈就只能靠猜了,剩下唯一能确定的就是调用栈最终应该回溯到 loop.ko 中的函数。

如果对内核相当熟悉,或者非常幸运(比如我),应该能很快梳理出完整的调用栈,如下:

bdi_register
bdi_register_dev
add_disk
loop_init_one
loop_probe

函数 bdi_register 中引起的异常的 bdi 参数值来源于 add_disk,549 行 bdi = &disk->queue->backing_dev_info;

/* block/genhd.c */
516 void add_disk(struct gendisk *disk)
517 {
518         struct backing_dev_info *bdi;
            ...
            ...
            ...
546         register_disk(disk);
547         blk_register_queue(disk);
548
549         bdi = &disk->queue->backing_dev_info;
550         bdi_register_dev(bdi, disk_devt(disk));
551         retval = sysfs_create_link(&disk_to_dev(disk)->kobj, &bdi->dev->kobj,
552                                    "bdi");
553         WARN_ON(retval);
554 }

函数 add_disk 是编译在内核中,可以通过查看其汇编代码来确定 queuebacking_dev_info 成员在结构体中的偏移量。

原始内核中结构体成员的偏移量

从上图高亮的汇编代码中可以看出,queue 偏移量是 0x11cbacking_dev_info 的偏移量是 0xa0。这是在原始厂商的内核中的偏移值。再来看看我们编译的 loop.ko 中的偏移值。由于是我们自行编译的,获取这些信息相对比较容易。只需要在编译时配置 CONFIG_DEBUG_INFO=y 后,使用 pahole 就能获取这些信息。

leexiaolan@mars:~/linux-2.6.34.10$ pahole -C gendisk drivers/block/loop.ko
struct gendisk {
        int                        major;                /*     0     4 */
        int                        first_minor;          /*     4     4 */
        int                        minors;               /*     8     4 */
        char                       disk_name[32];        /*    12    32 */
        char *                     (*devnode)(struct gendisk *, mode_t *); /*    44     4 */
        struct disk_part_tbl *     part_tbl;             /*    48     4 */

        /* XXX 4 bytes hole, try to pack */

        struct hd_struct           part0;                /*    56   336 */
        /* --- cacheline 6 boundary (384 bytes) was 8 bytes ago --- */
        const struct block_device_operations  * fops;    /*   392     4 */
        struct request_queue *     queue;                /*   396     4 */
        ...
        ...
}

queue 成员偏移量是 396,转换成 16 进制是 0x18c,和原始内核中的偏移量 0x11c 多了 0x70,所以位于 queue 前面的大小为 336part0 就很可疑了。继续深挖 struct hd_struct part0

leexiaolan@mars:~/linux-2.6.34.10$ pahole -C hd_struct drivers/block/loop.ko
struct hd_struct {
        sector_t                   start_sect;           /*     0     4 */
        sector_t                   nr_sects;             /*     4     4 */
        sector_t                   alignment_offset;     /*     8     4 */
        unsigned int               discard_alignment;    /*    12     4 */
        struct device              __dev;                /*    16   280 */
        /* --- cacheline 4 boundary (256 bytes) was 40 bytes ago --- */
        struct kobject *           holder_dir;           /*   296     4 */
        int                        policy;               /*   300     4 */
        int                        partno;               /*   304     4 */
        long unsigned int          stamp;                /*   308     4 */
        int                        in_flight[2];         /*   312     8 */
        /* --- cacheline 5 boundary (320 bytes) --- */
        struct disk_stats *        dkstats;              /*   320     4 */
        struct rcu_head            rcu_head;             /*   324     8 */

        /* size: 336, cachelines: 6, members: 12 */
        /* padding: 4 */
        /* last cacheline: 16 bytes */
};

这回可疑的是 struct device __dev

leexiaolan@mars:~/linux-2.6.34.10$ pahole -C device drivers/block/loop.ko
struct device {
        struct device *            parent;               /*     0     4 */
        struct device_private *    p;                    /*     4     4 */
        struct kobject             kobj;                 /*     8    36 */
        const char  *              init_name;            /*    44     4 */
        struct device_type *       type;                 /*    48     4 */
        struct semaphore           sem;                  /*    52    16 */
        /* --- cacheline 1 boundary (64 bytes) was 4 bytes ago --- */
        struct bus_type *          bus;                  /*    68     4 */
        struct device_driver *     driver;               /*    72     4 */
        void *                     platform_data;        /*    76     4 */
        struct dev_pm_info         power;                /*    80   120 */
        /* --- cacheline 3 boundary (192 bytes) was 8 bytes ago --- */
        ...
        ...
}

power 的大小很可疑,我们来看看 struct dev_pm_info 在头文件中的定义,一眼就可以发现其中有两个 CONFIG_* 控制的宏,检查 .config 文件,发现这两配置确实是打开的,估计这就是罪魁祸首。

451 struct dev_pm_info {
452         pm_message_t            power_state;
453         unsigned int            can_wakeup:1;
454         unsigned int            should_wakeup:1;
455         unsigned                async_suspend:1;
456         enum dpm_state          status;         /* Owned by the PM core */
457 #ifdef CONFIG_PM_SLEEP
458         struct list_head        entry;
459         struct completion       completion;
460 #endif
461 #ifdef CONFIG_PM_RUNTIME
462         struct timer_list       suspend_timer;
463         unsigned long           timer_expires;
464         struct work_struct      work;
465         wait_queue_head_t       wait_queue;
466         spinlock_t              lock;
467         atomic_t                usage_count;
468         atomic_t                child_count;
469         unsigned int            disable_depth:3;
470         unsigned int            ignore_children:1;
471         unsigned int            idle_notification:1;
472         unsigned int            request_pending:1;
473         unsigned int            deferred_resume:1;
474         unsigned int            run_wake:1;
475         unsigned int            runtime_auto:1;
476         enum rpm_request        request;
477         enum rpm_status         runtime_status;
478         int                     runtime_error;
479 #endif
480 };

关掉 CONFIG_PM_SLEEPCONFIG_PM_RUNTIME 这两个配置,重新编译 loop.ko,再来检查 queue 的偏移量:

leexiaolan@mars:~/linux-2.6.34.10$ pahole -C gendisk drivers/block/loop.ko|grep queue
        struct request_queue *     queue;                /*   284     4 */

284 == 0x11c,已经和原始内核中的偏移量保持一致,可以到设备上进行测试了。

测试 loop.ko 和 ext2.ko

WAP(Dopra Linux) # insmod loop.ko
WAP(Dopra Linux) # mkdir ext2-mount-test
WAP(Dopra Linux) # mount -t ext2 -o loop ext2.img ext2-mount-test
WAP(Dopra Linux) # cd ext2-mount-test
WAP(Dopra Linux) # > test.txt echo 'ext2 fs create/write test.'; ls
lost+found/ test.txt
WAP(Dopra Linux) # cat test.txt
ext2 fs create/write test.
WAP(Dopra Linux) # rm test.txt; ls
lost+found/

看起来是一切正常了。ext2.koloop.ko 能够正常运行的 config 文件和补丁,适用于 linux-2.6.34.10。顺带一提,xt_hashlimit.ko 也可以正常运行。

结论

上述因为 CONFIG_* 引起的结构体内存布局差异定位起来还是比较容易的,而刚好上面的例子中差异很大,更容易定位。而另一种因为厂商夹带私货引起的内存布局差异,就需要比对更多的成员偏移来精确定位和补丁。

模块和内核之间交互就是通过一组导出函数和各种内核对象进行的。导出函数缺失在加载模块时就能被发现,直接后果就是加载内核模块失败。而内核对象的内存布局,只有到运行时才能发现错误。故只要保证内核对象的内存布局一致,在没有源代码和对应 config 文件的情况下,使用 out-of-tree 来编译某些内核模块是完全可行的。

标签:ONTkernelREHG8120C

comments powered by Disqus