調(diào)試記錄 | Linux 內(nèi)核靜態(tài)庫(kù)封裝問(wèn)題
本文轉(zhuǎn)載自微信公眾號(hào)「漫談嵌入式」,作者Vinson 。轉(zhuǎn)載本文請(qǐng)聯(lián)系漫談嵌入式公眾號(hào)。
背景
對(duì)于靜態(tài)庫(kù)的封裝,大多數(shù)情況在應(yīng)用層應(yīng)用的封裝的比較多,用起來(lái)比較熟悉。不過(guò),在嵌入式開(kāi)發(fā)中,有些時(shí)候,需要將一些私有修改隱藏起來(lái),特別是,內(nèi)核中的一些修改。
此時(shí)需要在內(nèi)核態(tài)制作靜態(tài)庫(kù),然后鏈接到整個(gè)內(nèi)核文件中。
對(duì)于一般(沒(méi)有復(fù)雜的內(nèi)核依賴關(guān)系)的內(nèi)核靜態(tài)庫(kù)的封裝,直接安裝應(yīng)用層封裝即可。
對(duì)于內(nèi)核中一些高級(jí)驅(qū)動(dòng)的私有修改,在進(jìn)行封裝時(shí),就需要格外注意了,包括正確編譯,頭文件交叉引用,如果正確被鏈接到內(nèi)核中,而不是被編譯器忽略掉了。
封裝問(wèn)題
我們以 usb_f_uvc.ko 這個(gè)uvc function driver為例,來(lái)分析,內(nèi)核靜態(tài)庫(kù)的封裝(假設(shè),以下文件有修改或者定制)。最終,將usb_f_uvc.ko 打包成一個(gè) 靜態(tài)庫(kù),鏈接到內(nèi)核里面。
- # kernel/drivers/usb/gadget/function/Makefile
- usb_f_uvc-y := f_uvc.o uvc_queue.o uvc_v4l2.o uvc_video.o uvc_configfs.o
- obj-$(CONFIG_USB_F_UVC) += usb_f_uvc.o
編譯
我們將需要的文件,復(fù)雜到一個(gè)目錄下,修改Makefile
- # Makefile
- # 可換成自己的工具鏈
- CROSS_COMPILE ?= arm-linux-gnu-
- CC := $(CROSS_COMPILE)gcc
- LD := $(CROSS_COMPILE)ld
- AR := $(CROSS_COMPILE)ar
- CP := cp
- RM := rm
- # 修改正確的kernel 路徑
- KERNEL_PATH := xxxx/kerenl
- # 獲取gcc 版本
- CC_PATH := ${shell which $(CC)}
- CROSS_COMPILE_PATH := ${shell dirname $(CC_PATH)}
- CFLAGS := -nostdinc -isystem $(CROSS_COMPILE_PATH)/../lib/gcc/arm-linux-gnu/7.2.0/include
- # 頭文件順序很重要,換成自己平臺(tái)的
- INCLUDE = -I$(KERNEL_PATH)/arch/arm/include \
- -I$(KERNEL_PATH)/arch/arm/include/generated/uapi \
- -I$(KERNEL_PATH)/arch/arm/include/generated \
- -I$(KERNEL_PATH)/include \
- -I$(KERNEL_PATH)/arch/arm/include/uapi \
- -I$(KERNEL_PATH)/include/uapi \
- -I$(KERNEL_PATH)/include/generated/uapi/ \
- -include $(KERNEL_PATH)/include/linux/kconfig.h
- INCLUDE += -I$(KERNEL_PATH)/arch/arm/xxxx/core/include \
- -I$(KERNEL_PATH)/arch/arm/xxxx/soc-xxx/include \
- -I$(KERNEL_PATH)/arch/arm/include/asm/mach-generic
- #CFLAGS += -fno-delete-null-pointer-checks -Wno-maybe-uninitialized -Wno-frame-address -Wno-format-truncation \
- #-Wno-format-overflow -Wno-int-in-bool-context -Os --param=allow-store-data-races=0 -DCC_HAVE_ASM_GOTO \
- #-Wframe-larger-than=1024 -fno-stack-protector -Wno-unused-but-set-variable -Wno-unused-const-variable \
- #-fomit-frame-pointer -fno-var-tracking-assignments -Wdeclaration-after-statement \
- #-Wno-pointer-sign -fno-strict-overflow -fconserve-stack -Werror=implicit-int \
- #-Werror=strict-prototypes -Werror=date-time
- CFLAGS += -DEXPORT_SYMTAB
- # 這個(gè)一定要加
- CFLAGS += -D__KERNEL__
- CFLAGS += $(INCLUDE)
- OBJS := uvc_queue.o uvc_v4l2.o uvc_video.o f_uvc.o uvc_configfs.o
- ARFLAG := -rcs
- LIB_TARGET := libxxx.a
- TARGET := libxxx.hex
- all: $(TARGET)
- %.o:%.c
- $(CC) $(CFLAGS) -o $@ -c $^
- $(TARGET): $(LIB_TARGET)
- $(CP) $(LIB_TARGET) $(TARGET)
- $(CP) -vf $(TARGET) $(KERNEL_PATH)/drivers/usb/gadget/function/
- $(LIB_TARGET): $(OBJS)
- $(AR) $(ARFLAG) $@ $^
- clean:
- find . -name "*.o" | xargs rm -r
- $(RM) -vf $(LIB_TARGET) $(TARGET)
- install:
- $(CP) -vf $(TARGET) $(KERNEL_PATH)/drivers/usb/gadget/function/
Makefile 參數(shù)和頭文件如何來(lái)?
事實(shí)上,整個(gè)內(nèi)核打包的過(guò)程,筆者認(rèn)為,編譯是最難的一步,特別是第一次接觸的時(shí)候。
對(duì)于驅(qū)動(dòng)中的各符號(hào)和宏的定義,以及頭文件包含是層層套娃,根據(jù)錯(cuò)誤信息定位,簡(jiǎn)直要崩潰。
在這里,筆者建議,先參考【內(nèi)核編譯參數(shù)選項(xiàng)】,然后在逐一刪減無(wú)關(guān)選項(xiàng),這樣會(huì)方便很多。
具體操作如下:
- 正常編譯內(nèi)核:
- touch 修改 f_uvc.c:
- 重新編譯內(nèi)核:make uImage V=1 > build.txt
- vim build.txt 搜索f_uvc 即可看到編譯信息
使用 make V=1 參數(shù)將編譯的詳細(xì)信息輸出,包括頭文件包含順序,gcc 編譯參數(shù)選項(xiàng)等,然后將其添加到我們的Makefie上。最后在對(duì)我們的Makfile 做刪減。
添加到內(nèi)核
- #kernel/drivers/usb/gadget/function/Makefile
- usb_f_uvc-y := libxxx.a
- #obj-$(CONFIG_USB_F_UVC) += usb_f_uvc.o
- obj-y += usb_f_uvc.o
- # 防止Make distclean 把所有 .a都清掉了
- $(obj)/libxxx.a: $(obj)/libxxx.hex
- cp $(obj)/libxxx.hex $(obj)/libxxx.a
編譯內(nèi)核
重新編譯內(nèi)核,將.a 鏈接到內(nèi)核。然后燒到板子運(yùn)行。
運(yùn)行
實(shí)際運(yùn)行,發(fā)現(xiàn)根本沒(méi)有鏈到板子去。
原因分析
查看 EXPORT_SYMBOL
打開(kāi) Module.symvers 發(fā)現(xiàn),uvc 相關(guān)的接口并沒(méi)有導(dǎo)出來(lái),猜測(cè)有可能沒(méi)有成功鏈到內(nèi)核。
- vim Module.symvers
objdump 反匯編
使用objdump 將所有的符號(hào)表都輸出來(lái),然后在搜索查看,進(jìn)一步確認(rèn)鏈接是否正確。結(jié)果發(fā)現(xiàn)也找不到任何符號(hào)信息
- arm-linux-gnu-objdump -Dz vmlinux > kernel.dump
此時(shí)一個(gè)大膽的想法出現(xiàn)了,是否是被編譯器給優(yōu)化掉了?因?yàn)槭庆o態(tài)庫(kù),對(duì)于庫(kù)文件來(lái)說(shuō),其本身只是一些接口,自身不能執(zhí)行調(diào)用過(guò)程。如果接口沒(méi)有人調(diào)用,那么所有相關(guān)的符號(hào)是否自動(dòng)被忽略了?考慮一波對(duì)編譯鏈接的理解
分析源碼
- //f_uvc.c
- DECLARE_USB_FUNCTION_INIT(uvc, uvc_alloc_inst, uvc_alloc);
- MODULE_LICENSE("GPL");
- MODULE_AUTHOR("Laurent Pinchart");
這里的 DECLARE_USB_FUNCTION_INIT 很重要。我們,具體展開(kāi)。
- #define DECLARE_USB_FUNCTION_INIT(_name, _inst_alloc, _func_alloc) \
- DECLARE_USB_FUNCTION(_name, _inst_alloc, _func_alloc) \
- static int __init _name ## mod_init(void) \
- { \
- return usb_function_register(&_name ## usb_func); \
- } \
- static void __exit _name ## mod_exit(void) \
- { \
- usb_function_unregister(&_name ## usb_func); \
- } \
- module_init(_name ## mod_init); \
- module_exit(_name ## mod_exit)
這里看到 module_init 應(yīng)該很熟悉了,對(duì)于我們上面封裝的庫(kù)來(lái)說(shuō),本質(zhì)上也是一個(gè)驅(qū)動(dòng),是驅(qū)動(dòng)就有對(duì)應(yīng)的入口和出口。
對(duì)于內(nèi)核,所有的入口都被放在 .text.init 處,加載到內(nèi)核中后會(huì)按照相應(yīng)順序進(jìn)行初始化。
如果我們,把整個(gè)驅(qū)動(dòng)封裝成一個(gè)靜態(tài)庫(kù),DECLARE_USB_FUNCTION_INIT 屬于庫(kù)的接口,本身不會(huì)自己調(diào)用。所以內(nèi)核在鏈接的過(guò)程中,發(fā)現(xiàn)沒(méi)有調(diào)用關(guān)系,就自然而然會(huì)忽略掉libxxx.a的相關(guān)符號(hào)。
知道了原因,解決方法就很簡(jiǎn)單了。在內(nèi)核中一定要存在有調(diào)用DECLARE_USB_FUNCTION_INIT的地方。
- 方法1:手動(dòng)調(diào)用。不推薦
- 方法2:自動(dòng)調(diào)用。沿用內(nèi)核驅(qū)動(dòng)模型。將 DECLARE_USB_FUNCTION_INIT 從靜態(tài)庫(kù)中剝離出來(lái),其他文件打包成一個(gè)庫(kù)。
修改如下:
- // entry.c
- #include <linux/kernel.h>
- #include <linux/module.h>
- #include <linux/device.h>
- #include <linux/errno.h>
- #include <linux/list.h>
- #include <linux/mutex.h>
- #include <linux/string.h>
- #include <linux/usb/ch9.h>
- #include <linux/usb/gadget.h>
- #include <linux/usb/video.h>
- #include "u_uvc.h"
- #include "f_uvc.h"
- static struct usb_function_instance *uvc_alloc_inst(void)
- {
- return uvc_alloc_inst_callback();
- }
- static struct usb_function *uvc_alloc(struct usb_function_instance *fi)
- {
- return uvc_alloc_callback(fi);
- }
- DECLARE_USB_FUNCTION_INIT(uvc, uvc_alloc_inst, uvc_alloc);
- MODULE_LICENSE("GPL");
- MODULE_AUTHOR("Laurent Pinchart");
重新修改Makefile
- usb_f_uvc-y := entry.o libxxx.a
- obj-y += usb_f_uvc.o
- #obj-$(CONFIG_USB_F_UVC) += usb_f_uvc.o
- $(obj)/libxxx.a: $(obj)/libxxx.hex
- cp $(obj)/libxxx.hex $(obj)/libxxx.a
這樣重新,編譯內(nèi)核,就可以用了。以后只需要更新libxxx.a 即可。
總結(jié)
本文簡(jiǎn)單介紹內(nèi)核靜態(tài)庫(kù),打包遇到的一些坑。通過(guò)一個(gè)例子,介紹內(nèi)核靜態(tài)庫(kù)的封裝,以及遇到的問(wèn)題。
同時(shí)也加深了對(duì)編譯和鏈接的理解。有關(guān)應(yīng)用層靜態(tài)庫(kù)和內(nèi)核態(tài)的庫(kù)在使用上是一樣的,不過(guò)在制作時(shí)有些許麻煩。
- 頭文件的引用包含
- 編譯參數(shù)選項(xiàng)
- 是否成功鏈接
有關(guān)驅(qū)動(dòng)入口的部分,不能做到庫(kù)里面,避免踩雷。折騰其他,結(jié)果發(fā)現(xiàn)是鏈接時(shí)出了問(wèn)題。