调试记录 | Linux 内核静态库封装问题

系统 Linux
对于静态库的封装,大多数情况在应用层应用的封装的比较多,用起来比较熟悉。不过,在嵌入式开发中,有些时候,需要将一些私有修改隐藏起来,特别是,内核中的一些修改。

[[410536]]

本文转载自微信公众号「漫谈嵌入式」,作者Vinson 。转载本文请联系漫谈嵌入式公众号。

背景

对于静态库的封装,大多数情况在应用层应用的封装的比较多,用起来比较熟悉。不过,在嵌入式开发中,有些时候,需要将一些私有修改隐藏起来,特别是,内核中的一些修改。

此时需要在内核态制作静态库,然后链接到整个内核文件中。

对于一般(没有复杂的内核依赖关系)的内核静态库的封装,直接安装应用层封装即可。

对于内核中一些高级驱动的私有修改,在进行封装时,就需要格外注意了,包括正确编译,头文件交叉引用,如果正确被链接到内核中,而不是被编译器忽略掉了。

封装问题

我们以 usb_f_uvc.ko 这个uvc function driver为例,来分析,内核静态库的封装(假设,以下文件有修改或者定制)。最终,将usb_f_uvc.ko 打包成一个 静态库,链接到内核里面。

  1. # kernel/drivers/usb/gadget/function/Makefile 
  2. usb_f_uvc-y    := f_uvc.o uvc_queue.o uvc_v4l2.o uvc_video.o uvc_configfs.o 
  3. obj-$(CONFIG_USB_F_UVC)  += usb_f_uvc.o 

编译

我们将需要的文件,复杂到一个目录下,修改Makefile

  1. # Makefile 
  2.  
  3. # 可换成自己的工具链 
  4. CROSS_COMPILE ?= arm-linux-gnu-  
  5. CC := $(CROSS_COMPILE)gcc 
  6. LD := $(CROSS_COMPILE)ld 
  7. AR := $(CROSS_COMPILE)ar 
  8. CP := cp 
  9. RM := rm 
  10.  
  11. # 修改正确的kernel 路径 
  12. KERNEL_PATH := xxxx/kerenl 
  13.  
  14. # 获取gcc 版本 
  15. CC_PATH := ${shell which $(CC)} 
  16. CROSS_COMPILE_PATH := ${shell dirname $(CC_PATH)} 
  17.  
  18. CFLAGS := -nostdinc -isystem $(CROSS_COMPILE_PATH)/../lib/gcc/arm-linux-gnu/7.2.0/include 
  19.  
  20. # 头文件顺序很重要,换成自己平台的 
  21. INCLUDE = -I$(KERNEL_PATH)/arch/arm/include \ 
  22.         -I$(KERNEL_PATH)/arch/arm/include/generated/uapi \ 
  23.         -I$(KERNEL_PATH)/arch/arm/include/generated \ 
  24.         -I$(KERNEL_PATH)/include \ 
  25.         -I$(KERNEL_PATH)/arch/arm/include/uapi \ 
  26.         -I$(KERNEL_PATH)/include/uapi \ 
  27.         -I$(KERNEL_PATH)/include/generated/uapi/ \ 
  28.         -include $(KERNEL_PATH)/include/linux/kconfig.h 
  29.  
  30. INCLUDE += -I$(KERNEL_PATH)/arch/arm/xxxx/core/include \ 
  31.         -I$(KERNEL_PATH)/arch/arm/xxxx/soc-xxx/include \ 
  32.         -I$(KERNEL_PATH)/arch/arm/include/asm/mach-generic 
  33.          
  34. #CFLAGS += -fno-delete-null-pointer-checks -Wno-maybe-uninitialized -Wno-frame-address -Wno-format-truncation \ 
  35.         #-Wno-format-overflow -Wno-int-in-bool-context -Os --param=allow-store-data-races=0 -DCC_HAVE_ASM_GOTO \ 
  36.         #-Wframe-larger-than=1024 -fno-stack-protector -Wno-unused-but-set-variable -Wno-unused-const-variable \ 
  37.         #-fomit-frame-pointer -fno-var-tracking-assignments -Wdeclaration-after-statement \ 
  38.         #-Wno-pointer-sign -fno-strict-overflow -fconserve-stack -Werror=implicit-int \ 
  39.         #-Werror=strict-prototypes -Werror=date-time 
  40.   
  41. CFLAGS += -DEXPORT_SYMTAB 
  42.  
  43. # 这个一定要加 
  44. CFLAGS += -D__KERNEL__  
  45.  
  46. CFLAGS += $(INCLUDE) 
  47.  
  48. OBJS := uvc_queue.o uvc_v4l2.o uvc_video.o f_uvc.o uvc_configfs.o 
  49.  
  50. ARFLAG := -rcs 
  51.  
  52. LIB_TARGET := libxxx.a 
  53. TARGET := libxxx.hex 
  54.  
  55. all: $(TARGET) 
  56.  
  57. %.o:%.c 
  58.         $(CC) $(CFLAGS) -o $@ -c $^ 
  59.  
  60. $(TARGET): $(LIB_TARGET) 
  61.         $(CP) $(LIB_TARGET) $(TARGET) 
  62.         $(CP) -vf $(TARGET) $(KERNEL_PATH)/drivers/usb/gadget/function
  63.  
  64. $(LIB_TARGET): $(OBJS) 
  65.         $(AR) $(ARFLAG) $@ $^ 
  66.  
  67. clean: 
  68.         find . -name "*.o" | xargs rm -r 
  69.         $(RM) -vf $(LIB_TARGET) $(TARGET) 
  70.  
  71. install: 
  72.         $(CP) -vf $(TARGET) $(KERNEL_PATH)/drivers/usb/gadget/function

Makefile 参数和头文件如何来?

事实上,整个内核打包的过程,笔者认为,编译是最难的一步,特别是第一次接触的时候。

对于驱动中的各符号和宏的定义,以及头文件包含是层层套娃,根据错误信息定位,简直要崩溃。

在这里,笔者建议,先参考【内核编译参数选项】,然后在逐一删减无关选项,这样会方便很多。

具体操作如下:

  • 正常编译内核:
  • touch 修改 f_uvc.c:
  • 重新编译内核:make uImage V=1 > build.txt
  • vim build.txt 搜索f_uvc 即可看到编译信息

使用 make V=1 参数将编译的详细信息输出,包括头文件包含顺序,gcc 编译参数选项等,然后将其添加到我们的Makefie上。最后在对我们的Makfile 做删减。

添加到内核

  1. #kernel/drivers/usb/gadget/function/Makefile 
  2. usb_f_uvc-y    := libxxx.a                                           
  3. #obj-$(CONFIG_USB_F_UVC)  += usb_f_uvc.o 
  4. obj-y += usb_f_uvc.o 
  5. # 防止Make distclean 把所有 .a都清掉了 
  6. $(obj)/libxxx.a: $(obj)/libxxx.hex 
  7.     cp $(obj)/libxxx.hex $(obj)/libxxx.a 

编译内核

重新编译内核,将.a 链接到内核。然后烧到板子运行。

运行

实际运行,发现根本没有链到板子去。

原因分析

查看 EXPORT_SYMBOL

打开 Module.symvers 发现,uvc 相关的接口并没有导出来,猜测有可能没有成功链到内核。

  1. vim Module.symvers 

objdump 反汇编

使用objdump 将所有的符号表都输出来,然后在搜索查看,进一步确认链接是否正确。结果发现也找不到任何符号信息

  1. arm-linux-gnu-objdump -Dz vmlinux > kernel.dump 

此时一个大胆的想法出现了,是否是被编译器给优化掉了?因为是静态库,对于库文件来说,其本身只是一些接口,自身不能执行调用过程。如果接口没有人调用,那么所有相关的符号是否自动被忽略了?考虑一波对编译链接的理解

分析源码

  1. //f_uvc.c 
  2. DECLARE_USB_FUNCTION_INIT(uvc, uvc_alloc_inst, uvc_alloc); 
  3. MODULE_LICENSE("GPL"); 
  4. MODULE_AUTHOR("Laurent Pinchart"); 

这里的 DECLARE_USB_FUNCTION_INIT 很重要。我们,具体展开。

  1. #define DECLARE_USB_FUNCTION_INIT(_name, _inst_alloc, _func_alloc) \ 
  2.  DECLARE_USB_FUNCTION(_name, _inst_alloc, _func_alloc)  \ 
  3.  static int __init _name ## mod_init(void)   \ 
  4.  {        \ 
  5.   return usb_function_register(&_name ## usb_func); \ 
  6.  }        \ 
  7.  static void __exit _name ## mod_exit(void)   \ 
  8.  {        \ 
  9.   usb_function_unregister(&_name ## usb_func);  \ 
  10.  }        \ 
  11.  module_init(_name ## mod_init);     \ 
  12.  module_exit(_name ## mod_exit) 

这里看到 module_init 应该很熟悉了,对于我们上面封装的库来说,本质上也是一个驱动,是驱动就有对应的入口和出口。

对于内核,所有的入口都被放在 .text.init 处,加载到内核中后会按照相应顺序进行初始化。

如果我们,把整个驱动封装成一个静态库,DECLARE_USB_FUNCTION_INIT 属于库的接口,本身不会自己调用。所以内核在链接的过程中,发现没有调用关系,就自然而然会忽略掉libxxx.a的相关符号。

知道了原因,解决方法就很简单了。在内核中一定要存在有调用DECLARE_USB_FUNCTION_INIT的地方。

  • 方法1:手动调用。不推荐
  • 方法2:自动调用。沿用内核驱动模型。将 DECLARE_USB_FUNCTION_INIT 从静态库中剥离出来,其他文件打包成一个库。

修改如下:

  1. // entry.c 
  2. #include <linux/kernel.h> 
  3. #include <linux/module.h> 
  4. #include <linux/device.h> 
  5. #include <linux/errno.h> 
  6. #include <linux/list.h> 
  7. #include <linux/mutex.h> 
  8. #include <linux/string.h> 
  9. #include <linux/usb/ch9.h> 
  10. #include <linux/usb/gadget.h> 
  11. #include <linux/usb/video.h> 
  12.  
  13. #include "u_uvc.h" 
  14. #include "f_uvc.h" 
  15.  
  16. static struct usb_function_instance *uvc_alloc_inst(void) 
  17.     return uvc_alloc_inst_callback(); 
  18.  
  19. static struct usb_function *uvc_alloc(struct usb_function_instance *fi) 
  20.     return uvc_alloc_callback(fi); 
  21.  
  22. DECLARE_USB_FUNCTION_INIT(uvc, uvc_alloc_inst, uvc_alloc); 
  23. MODULE_LICENSE("GPL"); 
  24. MODULE_AUTHOR("Laurent Pinchart"); 

重新修改Makefile

  1. usb_f_uvc-y   := entry.o libxxx.a 
  2. obj-y  += usb_f_uvc.o 
  3.  
  4. #obj-$(CONFIG_USB_F_UVC) += usb_f_uvc.o 
  5.  
  6. $(obj)/libxxx.a: $(obj)/libxxx.hex 
  7.     cp $(obj)/libxxx.hex $(obj)/libxxx.a 

这样重新,编译内核,就可以用了。以后只需要更新libxxx.a 即可。

总结

本文简单介绍内核静态库,打包遇到的一些坑。通过一个例子,介绍内核静态库的封装,以及遇到的问题。

同时也加深了对编译和链接的理解。有关应用层静态库和内核态的库在使用上是一样的,不过在制作时有些许麻烦。

  • 头文件的引用包含
  • 编译参数选项
  • 是否成功链接

 

有关驱动入口的部分,不能做到库里面,避免踩雷。折腾其他,结果发现是链接时出了问题。

 

责任编辑:武晓燕 来源: 漫谈嵌入式
相关推荐

2010-01-22 11:01:04

linux内核模块

2014-08-28 15:08:35

Linux内核

2011-08-10 15:36:26

iPhone静态库控件

2021-11-14 07:29:55

Linux 内核静态追踪Linux 系统

2022-02-08 15:15:26

OpenHarmonlinux鸿蒙

2017-01-12 19:15:03

Linux内核调试自构proc

2010-01-07 17:36:38

Linux静态库

2016-08-23 09:17:08

LinuxD状态TASK_RUNNIN

2023-04-10 09:44:22

内核鼠标调试鸿蒙

2016-09-19 10:54:36

C语言静态连接语言

2010-03-04 10:17:57

Linux动态库

2016-10-28 09:18:47

Linux内核代码

2019-04-12 08:10:33

iOS静态分析Xcode

2011-06-29 17:00:26

QT 静态编译 Debug

2022-07-12 13:23:59

静态链接库可执行文件C 目标文件

2012-07-31 16:06:28

Linux内核编译

2021-02-20 06:08:07

LinuxWindows内核

2021-11-02 09:55:57

Linux内核内存

2010-03-02 09:17:32

Linux local

2009-12-21 16:36:08

ADO.Net数据库
点赞
收藏

51CTO技术栈公众号