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

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

背景


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


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


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

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


封装问题


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


# 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


编译


我们将需要的文件,复杂到一个目录下,修改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
# 头文件顺序很重要,换成自己平台的
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
# 这个一定要加
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 参数和头文件如何来?


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


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


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


具体操作如下:


  • 正常编译内核:


  • touch 修改 f_uvc.c:


  • 重新编译内核:make uImage V=1 > build.txt


  • vim build.txt 搜索f_uvc 即可看到编译信息


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


添加到内核


#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


编译内核


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


运行


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


原因分析


查看 EXPORT_SYMBOL


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


vim Module.symvers


objdump 反汇编


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


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


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


分析源码


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


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


#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 应该很熟悉了,对于我们上面封装的库来说,本质上也是一个驱动,是驱动就有对应的入口和出口。


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


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


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

DECLARE_USB_FUNCTION_INIT的地方。


  • 方法1:手动调用。不推荐


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


修改如下:


// 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


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


总结


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


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


  • 头文件的引用包含


  • 编译参数选项


  • 是否成功链接


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

相关文章
|
14天前
|
存储 编译器 Linux
动态链接的魔法:Linux下动态链接库机制探讨
本文将深入探讨Linux系统中的动态链接库机制,这其中包括但不限于全局符号介入、延迟绑定以及地址无关代码等内容。
189 19
|
15天前
|
运维 监控 Linux
BPF及Linux性能调试探索初探
BPF技术从最初的网络数据包过滤发展为强大的系统性能优化工具,无需修改内核代码即可实现实时监控、动态调整和精确分析。本文深入探讨BPF在Linux性能调试中的应用,介绍bpftune和BPF-tools等工具,并通过具体案例展示其优化效果。
40 14
|
20天前
|
算法 Linux
深入探索Linux内核的内存管理机制
本文旨在为读者提供对Linux操作系统内核中内存管理机制的深入理解。通过探讨Linux内核如何高效地分配、回收和优化内存资源,我们揭示了这一复杂系统背后的原理及其对系统性能的影响。不同于常规的摘要,本文将直接进入主题,不包含背景信息或研究目的等标准部分,而是专注于技术细节和实际操作。
|
20天前
|
存储 缓存 网络协议
Linux操作系统的内核优化与性能调优####
本文深入探讨了Linux操作系统内核的优化策略与性能调优方法,旨在为系统管理员和高级用户提供一套实用的指南。通过分析内核参数调整、文件系统选择、内存管理及网络配置等关键方面,本文揭示了如何有效提升Linux系统的稳定性和运行效率。不同于常规摘要仅概述内容的做法,本摘要直接指出文章的核心价值——提供具体可行的优化措施,助力读者实现系统性能的飞跃。 ####
|
21天前
|
监控 算法 Linux
Linux内核锁机制深度剖析与实践优化####
本文作为一篇技术性文章,深入探讨了Linux操作系统内核中锁机制的工作原理、类型及其在并发控制中的应用,旨在为开发者提供关于如何有效利用这些工具来提升系统性能和稳定性的见解。不同于常规摘要的概述性质,本文将直接通过具体案例分析,展示在不同场景下选择合适的锁策略对于解决竞争条件、死锁问题的重要性,以及如何根据实际需求调整锁的粒度以达到最佳效果,为读者呈现一份实用性强的实践指南。 ####
|
21天前
|
缓存 监控 网络协议
Linux操作系统的内核优化与实践####
本文旨在探讨Linux操作系统内核的优化策略与实际应用案例,深入分析内核参数调优、编译选项配置及实时性能监控的方法。通过具体实例讲解如何根据不同应用场景调整内核设置,以提升系统性能和稳定性,为系统管理员和技术爱好者提供实用的优化指南。 ####
|
23天前
|
负载均衡 算法 Linux
深入探索Linux内核调度机制:公平与效率的平衡####
本文旨在剖析Linux操作系统内核中的进程调度机制,特别是其如何通过CFS(完全公平调度器)算法实现多任务环境下资源分配的公平性与系统响应速度之间的微妙平衡。不同于传统摘要的概览性质,本文摘要将直接聚焦于CFS的核心原理、设计目标及面临的挑战,为读者揭开Linux高效调度的秘密。 ####
34 3
|
26天前
|
负载均衡 算法 Linux
深入探索Linux内核调度器:公平与效率的平衡####
本文通过剖析Linux内核调度器的工作机制,揭示了其在多任务处理环境中如何实现时间片轮转、优先级调整及完全公平调度算法(CFS),以达到既公平又高效地分配CPU资源的目标。通过对比FIFO和RR等传统调度策略,本文展示了Linux调度器如何在复杂的计算场景下优化性能,为系统设计师和开发者提供了宝贵的设计思路。 ####
40 6
|
26天前
|
消息中间件 安全 Linux
深入探索Linux操作系统的内核机制
本文旨在为读者提供一个关于Linux操作系统内核机制的全面解析。通过探讨Linux内核的设计哲学、核心组件、以及其如何高效地管理硬件资源和系统操作,本文揭示了Linux之所以成为众多开发者和组织首选操作系统的原因。不同于常规摘要,此处我们不涉及具体代码或技术细节,而是从宏观的角度审视Linux内核的架构和功能,为对Linux感兴趣的读者提供一个高层次的理解框架。
|
27天前
|
缓存 网络协议 Linux
深入探索Linux操作系统的内核优化策略####
本文旨在探讨Linux操作系统内核的优化方法,通过分析当前主流的几种内核优化技术,结合具体案例,阐述如何有效提升系统性能与稳定性。文章首先概述了Linux内核的基本结构,随后详细解析了内核优化的必要性及常用手段,包括编译优化、内核参数调整、内存管理优化等,最后通过实例展示了这些优化技巧在实际场景中的应用效果,为读者提供了一套实用的Linux内核优化指南。 ####
45 1