ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

OpenHarmony标准系统chip_ckm分区打包与内核ko编译适配指南

OpenHarmony标准系统chip_ckm分区打包与内核ko编译适配指南 做OpenHarmony标准系统的设备适配kernel ko编译和分区打包几乎是每个BSP工程师都绕不开的日常操作。标题这句话压缩了整套流程先绕过各种配置陷阱把目标驱动编成可加载的内核模块ko再把它塞进chip_ckm分区镜像最后烧到板子上验证insmod成功。这篇文章适合正在做OpenHarmony标准系统适配、ROM定制或者想搞清楚标准系统镜像分区机制的开发者参考尤其适合那些已经接触过Linux内核编译、但被分区命名和构建链路整懵的朋友。为什么一定要单独搞一个chip_ckm分区装ko而不是像普通Linux发行版那样直接挂在根文件系统里这跟标准系统的分区职责划分强相关。boot、system、vendor、chip_prod、chip_ckm各有各的定位把它们搞清楚后你就明白厂商定制的模块为什么要走这条稍微绕一点的编译打包路径。1. 项目拆解为什么要做一个独立的chip_ckm分区镜像1.1 标准系统的分区职责划分OpenHarmony标准系统也就是常说的Full系统面向带屏幕的富设备在镜像层面的规划跟传统Linux发行版差别挺大。拿到一套目标板的刷机包通常能看到system.img、vendor.img、chip_prod.img、chip_ckm.img、loader.img这些主要镜像。除了system和userdata这种常规分区最让从Linux BSP转过来的人困惑的就是chip_prod和chip_ckm这两个分区的边界到底在哪。从命名和实际职责来看chip_prod更偏向芯片厂商的生产数据区放厂商私有配置、专属可执行文件、产品级定制内容chip_ckm更像芯片通用内核模块区承载内核模块ko、固件firmware以及一些跟硬件强相关的二进制文件。实际适配某款SoC时GPU驱动、WiFi/BT固件、相机相关模块、编解码器这些厂商闭源或改动频繁的部分都会选择以ko形态放进来而不是编进zImage。这种隔离设计的第一层好处是职责单一系统升级时chip_ckm可以单独做分区升级不用动system也不用整个重新烧boot。第二层好处是内核镜像保持相对稳定核心内核和厂商驱动解耦后驱动模块有bug可以直接改ko替换省掉整包刷机的痛苦。第三层好处是分区内容可以按芯片系列复用同一款内核在不同板卡上跑只要替换对应chip_ckm镜像就行。1.2 内核模块独立分区的几个现实收益把ko打成独立分区映像不只是架构上的洁癖实际收益非常明显。我做过一次对比驱动以模块方式放chip_ckm后版本迭代时只需要重编ko、重新打包分区升级包体积从几百MB压到几十MB而且现场设备通过升级通道就能完成驱动热更不需要专业人员连接调试串口重刷boot。另一个容易被忽略的收益是调试效率。驱动还在开发期时经常要改参数、加打印、换firmware版本。如果驱动编在内核里每次改动都要重新编译完整内核镜像boot分区还得重新打包一次至少十分钟。把模块独立出来就不一样ko编译通常几十秒搞定重新打包chip_ckm镜像也就一两分钟烧录也快这种迭代速度对驱动的bring-up阶段非常重要。还有一点是安全问题。模块以独立文件存在可以单独做strip、签名、权限控制甚至可以在发布前用工具检查符号表泄漏。编进内核后所有符号都在/proc/kallsyms里露着想隐藏一些厂商私有实现逻辑非常难。所以从商用产品角度看ko独立分区也是保护知识产权的一种手段。2. 编译ko的前置准备与内核配置2.1 内核源码和交叉工具链怎么选最稳编译ko最关键的一条原则编译模块时使用的内核源码版本、配置和工具链必须与运行目标板上那个内核完全一致。这不是官方文档里随便写的建议而是血的教训。内核模块和内核之间不是简单的能编过就行符号版本CRC校验机制会严格比对模块编译时记录的符号CRC值和运行内核实际符号的CRC值任何一个不一致insmod直接报Unknown symbol。OpenHarmony标准系统的内核源码一般在kernel/linux/linux-5.10目录下具体版本号以仓库里的kernelrelease文件为准。拿到源码后先确认两件事一是厂商的BSP patch是否已经合入二是你手上这份源码是否就是最终烧录进设备的那份。很多人图省事从网上下一个同版本主线内核就开始编ko编译能过但加载必挂因为厂商patch里的新符号和配置改动都没了。工具链方面OpenHarmony标准系统构建链路默认偏向clang但不少厂商的BSP实际会切到gcc。我个人的经验是编ko时尽量复用编译内核时的同一套交叉编译工具链不要混搭。用gcc编的内核模块在clang编的内核上加载虽然大部分情况没问题但遇到内联函数、编译器内建符号相关的场景会出现诡异的符号找不到问题。在标准系统工程里可以先执行./build.sh --product-name 你的产品名 --ccache false跑一次完整构建从构建日志里找到内核实际使用的编译器路径然后固定下来。2.2 内核配置中必须打开的模块相关选项编译ko之前内核本身的模块支持必须确认开启。进入内核源码目录后先加载目标板对应的defconfig再执行make menuconfig检查几个关键选项。首先要确保CONFIG_MODULESy这个不开启后面编译模块阶段直接报module support disabled。其次建议打开CONFIG_MODULE_UNLOADy这样调试阶段反复insmod/rmmod会方便很多。CONFIG_MODVERSIONS这个选项需要重点说明开启后内核模块会记录每个引用符号的CRC值运行内核加载模块时做一致性检查。很多从传统Android BSP转过来的人习惯把这个选项关掉因为Android内核为了加载预编译模块经常关掉。但在OpenHarmony标准系统上我建议默认保持开启尤其是在多模块互相依赖的场景里。开启MODVERSIONS后模块A依赖模块B导出的符号如果模块B升级时改了某个函数签名或结构体布局加载模块A时会立刻报CRC不一致从源头拦截了潜在的崩溃风险。排查问题的时候错误信息也比no symbol version for xxx清晰得多。另外要关注CONFIG_MODULE_SIG这个选项。如果目标板的内核开启了模块签名校验而你手头的ko没有用对应私钥签名insmod时会报module verification failed: signature and/or required key missing。厂商的release内核经常打开这个但调试版可能关闭。选购或者搭建编译环境前先用目标板上的内核跑一次zcat /proc/config.gz | grep CONFIG_MODULE_SIG确认状态避免编了半天模块根本加载不进去。2.3 树内驱动和树外驱动两种编译方式ko来源主要分两种树内驱动和树外驱动。树内驱动是指驱动源码已经放进内核源码树的drivers目录下比如drivers/gpu/drm/xxx、drivers/net/wireless/xxx这类驱动在编译内核时如果对应Kconfig选项设置成M就会自动产出ko文件。编译完成后在drivers对应目录或net/xxx等目录下能找到.ko文件。树外驱动是指厂商单独维护一套驱动源码不在内核树内例如某些第三方GPU、DSP、安全引擎的驱动。这类模块需要在内核源码树下用make M方式单独编译命令大致是make ARCHarm64 CROSS_COMPILExxx M/path/to/driver_src modules。编译过程中会调用内核源码树里的modpost工具做符号校验所以源码树里必须已经完成过一次make modules_prepare。如果跳过这个步骤直接编外部模块经常报一堆找不到Generated符号的错。两种方式没有绝对好坏树内驱动跟随内核版本演进维护起来省心树外驱动跟内核源码树解耦厂商可以独立发版。从打包到chip_ckm的角度两者最终产物都是ko文件后续处理流程完全一样只是树外驱动需要你在编译脚本里显式指定模块路径容易在交叉工具链上翻车。3. 内核ko编译实操流程3.1 先编出一套可用的内核基础产物正式开始编译前先把运行环境理清楚。我习惯把交叉工具链路径、架构变量全部export到当前shell避免每条命令都带一堆前缀。架构统一设成arm64工具链按厂商提供的GCC路径填写。假设已经进入内核源码目录第一个动作是清理脏数据并加载目标板的defconfig。export ARCHarm64 export CROSS_COMPILE/opt/toolchains/aarch64-none-linux-gnu/bin/aarch64-none-linux-gnu- make distclean make product_defconfig不同芯片厂商的defconfig名字不一样有的叫ohos_xxx_defconfig有的直接沿用Linux主线风格。加载成功后用make menuconfig手动检查一下需要模块化的驱动是否已经配置成M。这一步是标准系统适配最常见的分叉点驱动若配置成Y会编进zImage而不是生成ko后续想打包到chip_ckm就无从谈起。配置确认结束后直接编译完整内核。这里不建议只编modules不编Image因为有些根设备依赖和固件生成规则只有执行完整编译才会触发。完整编译也方便后续用编译产物里的System.map、Module.symvers做模块依赖解析。make -j$(nproc)编完后确认Image/zImage和System.map存在。这时内核树内配置成M的驱动已经产出ko散落在各个drivers子目录里。下一步是把它们集中收集统一管理。3.2 用modules_install整理标准模块目录散落的ko文件直接拷走虽然也能用但insmod时若存在模块间依赖或者想让modprobe按名字加载就必须维持Linux标准的模块目录结构。Linux模块的规范路径是/lib/modules/$(uname -r)/下面还有kernel、extra等子目录。内核的Makefile提供了modules_install目标能自动把当前内核树内所有ko安装到指定rootfs路径下同时生成软链接和符号表信息。export INSTALL_MOD_PATH/tmp/chip_ckm_rootfs make modules_install执行后/tmp/chip_ckm_rootfs/lib/modules/5.10.xxx/目录下会出现一整套模块树。这里有个关键点INSTALL_MOD_PATH的目标目录不能是普通空目录最好先按照最终分区内容的目录结构规划好否则后面还得重新搬运。模块安装过程中会自动生成modules.order和modules.builtin这些辅助文件虽然加载时不是必需但建议保留。如果还需要打包厂商的树外驱动ko就把编译好的外部ko手动拷贝到kernel目录下合适的子目录。我一般习惯放在lib/modules/$(uname -r)/extra/下这是Linux为第三方模块预留的标准位置。拷贝完先跑depmod生成模块依赖关系。depmod -b /tmp/chip_ckm_rootfs 5.10.xxxdepmod的-b参数指定根文件系统所在路径后面跟的版本号必须和模块目录名一致。这个命令会生成modules.dep、modules.alias、modules.symbols等文件它们的作用是让modprobe知道某个模块放在哪个文件、依赖哪些其他模块、某个符号由哪个模块提供。很多新手打包模块时漏掉这一步结果设备上执行modprobe时总是提示找不到模块。3.3 用strip控制ko体积和符号暴露ko文件编译出来后默认包含大量调试符号和注释段体积偏大如果chip_ckm分区很小几兆的镜像可能直接放不下。在正式打包前用交叉工具链里的strip工具做一次裁剪是标准操作。aarch64-none-linux-gnu-strip --strip-unneeded xxx.ko--strip-unneeded只移除调试信息和不需要的符号保留模块加载和导出符号所需的部分不会破坏ko的可用性。做完后对多模块场景特别有用比如一个内核目录几百兆裁剪后可能降到几十兆。不过这里有个权衡裁剪掉调试符号后驱动crash时无法从ko里得到符号化调用栈只能靠内核日志里的PC指针加System.map手撸。调试阶段建议保留符号发布版本再裁剪。还有一个经常被忽略的细节strip工具和目标板内核架构必须一致用x86主机上的strip去处理arm64的ko会直接报错或者破坏文件格式这类问题排查起来非常浪费生命。4. 打包进chip_ckm分区的完整操作4.1 规划chip_ckm的目录结构chip_ckm分区虽然不是传统的根文件系统但内部目录结构依然遵循Linux常规布局。最简方案下分区根目录只需要lib/modules/$(uname -r)/这棵模块树。但实际产品通常会多放几样东西一是驱动固件文件比如WiFi的nvram、GPU的固件包这些必须放在/lib/firmware/或驱动源码指定的路径下二是厂商的脚本比如负责按顺序加载模块的insmod脚本三是芯片相关的配置文件比如电源管理参数、产品型号校准数据。目录结构规划时要想清楚一个前提chip_ckm分区在设备上会挂载到哪个路径。OpenHarmony标准系统里各分区的挂载点由init配置决定不同厂商的产品可能挂在/vendor/chip_ckm也可能挂在/chip_ckm。不是所有驱动都硬编码了从根路径找模块insmod脚本里尽量不要写死绝对路径。我之前踩过一次坑脚本写死insmod /vendor/chip_ckm/lib/modules/xxx.ko结果换了另一套产品配置挂载点变成/chip_ckm开机直接黑屏。一个稳妥做法在打包脚本里用一个变量统一定义挂载根路径未来挂载点调整只需改一处。模块目录本身用相对路径组织insmod脚本运行时先cd到模块目录再load这样对挂载路径的依赖降到最低。4.2 手动制作镜像并打fspad头目录内容准备好后进入分区镜像制作环节。OpenHarmony标准系统的分区镜像和普通Linux烧录镜像有点区别boot/system多数有特定镜像格式要求chip_ckm通常以ext4镜像加头部信息的方式打包。构建系统的完整流程比较复杂我这里给出一个可手动复现的思路便于调试阶段快速迭代。先把整理好的整个chip_ckm_rootfs目录制作成ext4镜像make_ext4fs -l 64M -s rootfs.img /tmp/chip_ckm_rootfs-l指定镜像大小需要和板级分区表中chip_ckm的size匹配-s表示精简镜像大小会按实际内容收缩。如果本机没有make_ext4fs可以用新版构建系统里自带的mkfs.ext4替代但注意部分工具生成的镜像不支持稀疏特性烧录后文件系统挂载会报错。ext4镜像做完后还需要加上标准系统要求的fspadfastboot sparse padding首部。具体执行哪个工具不同版本和厂商封装不一样常见的是mkimage.fspad。一条典型的打包命令如下mkimage.fspad -s 4096 -o chip_ckm.img rootfs.img这一步的4096是与扇区对齐相关的参数多数平台都是4K对齐。如果你的板级构建脚本里已经有chip_ckm的打包规则直接复用脚本里的工具和参数不要自己瞎猜。最稳的办法还是打开一次完整构建日志摘出构建系统实际使用的打包命令手动迭代时照着批量替换目录和镜像名就行。4.3 挂进分区表和解决容量不够的调整方案chip_ckm.img制作完成后烧录前还要确认分区表里chip_ckm的size和偏移没有冲突。如果镜像大小超过分区sizefastboot烧写会写越界轻则chip_ckm挂载失败重则破坏相邻分区。标准系统的分区表配置一般在板级工程的配置文件中常见的名字有partition.xml、partition_ns.json具体以你手上的工程为准。调整分区大小的操作套路是先在分区表配置里把chip_ckm的size字段改大比如从32M改成64M然后确认前后相邻分区的偏移值同步调整。这里特别提醒不要单独改chip_ckm一个分区的大小却忽略相邻分区的start_addr否则整个烧录链路会直接错乱。修改分区表后需要重新打包完整烧录镜像因为loader里嵌入了分区表信息只换chip_ckm.img是不够的。如果只是ko太大还有一种不需要改分区表的手法对ko排序裁剪削掉内置的debug段或者把不常用的模块拆出来放到vendor分区。但从职责划分看这属于临时妥协后续模块持续增大还是要调整分区表。建议在首次规划产品时就把chip_ckm的size预留到驱动包实际体积的1.5倍以上免得后面反复调表。4.4 开机自动加载ko的初始化手段分区烧进去只是第一步模块要真正生效还得让系统启动时自动加载。标准系统的init框架兼容类似Android的rc语法可以在产品init配置文件里增加一个oneshot服务启动时执行加载脚本。以最常见的insmod方案为例写一个shell脚本按依赖顺序逐个insmod然后在init rc文件里增加服务声明。顺序上建议放post-fs阶段之后因为模块依赖的某些设备节点或字符设备要等内核基础设备初始化完成后才存在。脚本内容大致如下#!/system/bin/sh MODDIR$(dirname $0)/lib/modules/$(uname -r) insmod $MODDIR/kernel/drivers/xxx/base.ko insmod $MODDIR/kernel/drivers/xxx/device.ko sleep 0.2 # 等待固件就绪 insmod $MODDIR/extra/vendor_driver.ko注意最后一行前面为什么要sleep 0.2。有些驱动probe时会向固件请求接口而固件加载是异步进行的立刻insmod有可能因为firmware还没就绪而上报EPROBE_DEFER错误。加一个短暂等待就能把这个窗口绕过去比在驱动里死等要省事得多。如果模块依赖关系比较复杂更推荐用modprobe配合depmod生成的modules.dep来加载。modprobe会自动解析依赖并按序加载只是对rootfs的完整性要求更高modules.dep和modules.alias不能丢。两种方式我都用过调试阶段用insmod加显式顺序更可控发布版本用modprobe更健壮。5. 烧录验证与常见问题排查5.1 烧录验证三板斧chip_ckm.img准备好后烧录验证流程并不复杂。板子进入fastboot模式后执行分区烧写fastboot flash chip_ckm chip_ckm.img fastboot reboot如果用的是升级包OTA通道按你产品的升级工具走就行但开发阶段还是fastboot最直接。重启后先确认分区挂载正常再检查模块是否被系统加载。三个关键排查命令mount | grep chip_ckm lsmod dmesg | grep -iE chip_ckm|firmware|xxx_driverlsmod看不到模块看dmesgdmesg没报错但模块没加载查init脚本有没有执行脚本执行了但modprobe找不到模块查depmod产物和uname -r的版本号是否一致。这套排查顺序基本能覆盖90%的问题。5.2 常见问题速查表我在chip_ckm分区适配里踩过的坑不少整理成一张速查表按出现概率排序现象大概率原因解决思路insmod报Exec format error架构不匹配或内核版本配置不一致确认ko用arm64交叉工具链编译编译内核源码与设备内核源码一致insmod报Unknown symbol依赖模块未先加载或MODVERSIONS CRC不一致先加载依赖模块确认模块源码和内核源码同步insmod报module verification failed内核开启了模块签名验证关闭CONFIG_MODULE_SIG或用私有钥签名komodprobe找不到模块depmod未执行或模块目录与uname -r不符重新跑depmod -b检查模块目录名和kernelrelease镜像烧录后分区挂载失败镜像大小超过分区表size或fspad头不对检查分区表重新用构建系统的打包工具生成镜像驱动probe失败但模块已加载firmware文件缺失或路径不对确认firmware放在/lib/firmware指定路径检查dmesg固件加载报错模块加载后设备节点不出现设备树里对应节点状态disabled检查设备树status和compatible配置strip后ko加载crashstrip破坏了必要符号用--strip-unneeded而不是--strip-all表里第三条值得多说两句厂商发布的内核镜像如果开了强制模块签名modprobe会直接拒绝加载未签名模块这是很多自制ko加载失败的头号原因。不要试图硬破解找厂商要签名工具链或者让开发版关闭签名校验才是正道。5.3 我的几个避坑心得整个流程走下来我最想提醒后来者的不是某个命令怎么写而是流程意识。编译ko、打包chip_ckm、烧录验证这三个环节每一环的错误都会在下一环被放大。编译时忽略版本一致性打包阶段看不出来烧录后insmod才爆雷打包时漏了depmod分区内容也看起来完整直到modprobe才失败。所以建议从一开始就建立一个自动化打包脚本把内核版本抓取、模块整理、depmod、strip、镜像制作整合成一条流水线每次改动驱动后一条命令完成从源码到镜像的构建。另外调试阶段不建议使用发行版那种裁剪过的模块保留调试符号至少能让你在crash日志里看到驱动内部函数名。发布前再统一做strip优化体积这样既保证开发效率也保证最终产物干净。最后分享一个小细节每次编译前用make clean之前记得备份Module.symvers文件。这个文件记录了所有已编译模块导出的符号CRC值树外模块编译时会读取它做符号校验。如果丢了它外部模块经常报出一堆missing symbol的错而且这类错误非常误导排查方向。备份一份也就几百KB却能让下一次编译流程顺畅不少。
返回列表