内核模块(上)

简介: 内核模块

基础知识

模块是一种向linux内核添加设备驱动程序,文件系统及其他组件的有效方法,不需要重新编译新内核或重启系统。

模块具有如下优点:

  • 通过使用模块,内核发布者能够预先编译大量驱动樨序,而不会致使内核映像的尺寸发膨胀。
  • 内核开发者可以试验性的代码打包到模块中,模块可以卸载,修改代码或重新打包后可以点新装载。

添加和删除

从用户的角度看,模块可以通过两个不同的系统程添加到运行的内核中。它们分别是modprobe和insmod。modprobe在识别出目标模块所依赖的模块之后,在内核中也会使用insmod,在用户空间对模块的处理也是基于insmod。

在处理该系统调用时,模块代码首先复制到内核内存中。接下来是重定位工作和解决模块中未定义的引用。因为模块使用了持久编译到内核中的函数,在模块本身编译时无法确定这些函数的地址,所以的要在这里处里未定义的引用。

处理未解决的引用,为与内核的剩余部分协作,模块必须使用内核提供的函数。这些可能是通用的辅助函数,比如几乎内核每一部分都会使的printk或kmalloc。

  1. insmod只加载一个单一的模块到内核中,而该模块可能只依赖内核中已存在的代码,并不关注所有依赖代码是通过模块动态加载,还是持久编译到内核中。
  2. modprobe在识别出目标模块所依赖的模块之后,在内核也会使用insmod。在用户空间对模块的处理基于insmod。
  3. 在加载模块时所需要的操作,与通过ld和ld.so借助于动态库链接应用程序的操作。从外部来看,模块只是普通的可重定位目标文件。file命令的输出表明模块文件是可重定位的,这是用户空间程序设计中一专业术语。可重位文件的函数都不会引用绝对地址,而只是指向代码中的相对地址,因此可以在内存的任意偏移地址加载,当然在映像加载到内存时,映像中的地址要由动态连接器ld.so进行适当的修改即可。重定位的工作由内核自身执行,而不是动态装载器。
  4. ramfs模块允许在内存中建立一个文件系统(称为RAM磁盘),因而须调用register_filesystem函数将自身添加到内核中可用文件系统的列表。此模块还使用内核代码中generic_file_read/generic_file_write标准函数,基本大多数内核文件系统都会使用这些普通函数。

很明显这些函定义在内核的基础代码中,因而已经加载到内存。但如何找到与相关函数名称匹配的地址,以便解决这些引用呢?为此,内核提供了一个所有导出函的列表。该列表给出了所有导出函数的内存地址和对应函数名,可以过pro文件系统访问,即文件/pro/kallsyms:

T表示符号位于代码段,而D表示符号位于数据段等。

在向内核添加模块时,需要考虑下列相关问题。

  • 内核提供的函数符号表,可以在模块加载时动态扩展其长度。
  • 如果模块之间有相互依赖,那么向内核添加模块的顺序很重要。

插入删除模块

用户空间工具和内核的模块实现之间的接口,包括两个系统调用。

init_module:将一个新的模块插入到内核中。用户空间工具只需提供二进制数据。所有其他工作(特别是重定位核解决引用)由内核自身完成。

delete_module:从内核移除一个模块。当然,前提是该模块的代码不再使用,并且其他模块也不再使用该模块导出的函数。

还有一个request_module函数(不是系统调用),用于从内核端加载模块。它不仅用于加载模块,还用于实现热插拔功能。

模块的表示

在详细讲解模块相关函数的实现之前,有必要解释如何在内核中表示模块(及具属性)。首先需要定义一组数据结构。真中,module是最重要的数据结构。内核中驻留的每一个模块,都分配了该结构的一个实例。

struct module {
  enum module_state state;//模块的状态
  /* Member of list of modules */
  struct list_head list;//用作模块链表的链表元素
  /* Unique handle for this module */
  char name[MODULE_NAME_LEN];//该模块唯一的句柄
  /* Sysfs stuff. */
  //导出符号
  struct module_kobject mkobj;
  struct module_attribute *modinfo_attrs;
  const char *version;
  const char *srcversion;
  struct kobject *holders_dir;
  /* Exported symbols */
  const struct kernel_symbol *syms;
  const unsigned long *crcs;
  unsigned int num_syms;
  /* Kernel parameters. */
  struct kernel_param *kp;
  unsigned int num_kp;
  /* GPL-only exported symbols. */
  //只适用于GPL的导出符号
  unsigned int num_gpl_syms;
  const struct kernel_symbol *gpl_syms;
  const unsigned long *gpl_crcs;
#ifdef CONFIG_UNUSED_SYMBOLS
  /* unused exported symbols. */
  const struct kernel_symbol *unused_syms;
  const unsigned long *unused_crcs;
  unsigned int num_unused_syms;
  /* GPL-only, unused exported symbols. */
  unsigned int num_unused_gpl_syms;
  const struct kernel_symbol *unused_gpl_syms;
  const unsigned long *unused_gpl_crcs;
#endif
#ifdef CONFIG_MODULE_SIG
  /* Signature was verified. */
  bool sig_ok;
#endif
  /* symbols that will be GPL-only in the near future. */
  const struct kernel_symbol *gpl_future_syms;
  const unsigned long *gpl_future_crcs;
  unsigned int num_gpl_future_syms;
  /* Exception table */
  //异常表
  unsigned int num_exentries;
  struct exception_table_entry *extable;
  /* Startup function. */
  //初始化函数
  int (*init)(void);//init是一个指针,指向一个在模块初始化时调用的函数
  /* If this is non-NULL, vfree after init() returns */
  void *module_init;
  /* Here is the actual code + data, vfree'd on unload. */
  void *module_core;
  /* Here are the sizes of the init and core sections */
  unsigned int init_size, core_size;
  /* The size of the executable code in each section.  */
  unsigned int init_text_size, core_text_size;
  /* Size of RO sections of the module (text+rodata) */
  unsigned int init_ro_size, core_ro_size;
  /* Arch-specific module values */
  struct mod_arch_specific arch;
  unsigned int taints;  /* same bits as kernel:tainted */
#ifdef CONFIG_GENERIC_BUG
  /* Support for BUG */
  unsigned num_bugs;
  struct list_head bug_list;
  struct bug_entry *bug_table;
#endif
#ifdef CONFIG_KALLSYMS
  /*
   * We keep the symbol and string tables for kallsyms.
   * The core_* fields below are temporary, loader-only (they
   * could really be discarded after module init).
   */
  Elf_Sym *symtab, *core_symtab;
  unsigned int num_symtab, core_num_syms;
  char *strtab, *core_strtab;
  /* Section attributes */
  struct module_sect_attrs *sect_attrs;
  /* Notes attributes */
  struct module_notes_attrs *notes_attrs;
#endif
  /* The command line arguments (may be mangled).  People like
     keeping pointers to this stuff */
  char *args;
#ifdef CONFIG_SMP
  /* Per-cpu data. */
  void __percpu *percpu;
  unsigned int percpu_size;
#endif
#ifdef CONFIG_TRACEPOINTS
  unsigned int num_tracepoints;
  struct tracepoint * const *tracepoints_ptrs;
#endif
#ifdef HAVE_JUMP_LABEL
  struct jump_entry *jump_entries;
  unsigned int num_jump_entries;
#endif
#ifdef CONFIG_TRACING
  unsigned int num_trace_bprintk_fmt;
  const char **trace_bprintk_fmt_start;
#endif
#ifdef CONFIG_EVENT_TRACING
  struct ftrace_event_call **trace_events;
  unsigned int num_trace_events;
  struct trace_enum_map **trace_enums;
  unsigned int num_trace_enums;
#endif
#ifdef CONFIG_FTRACE_MCOUNT_RECORD
  unsigned int num_ftrace_callsites;
  unsigned long *ftrace_callsites;
#endif
#ifdef CONFIG_LIVEPATCH
  bool klp_alive;
#endif
#ifdef CONFIG_MODULE_UNLOAD
  /* What modules depend on me? */
  struct list_head source_list;
  /* What modules do I depend on? */
  struct list_head target_list;
  /* Destruction function. */
  void (*exit)(void);
  atomic_t refcnt;
#endif
#ifdef CONFIG_CONSTRUCTORS
  /* Constructor functions. */
  ctor_fn_t *ctors;
  unsigned int num_ctors;
#endif
};

依赖关系和引用

如果模块B使用了模块A供的的数,那么模块A和模块B之间就存在关糸。可以两种不同的方式来这种关系。

  • 模块B依赖块A。除非模块A已经驻留在内孩内存,否则模块B无法卸载。
  • 模块B引用模块A。换句话说,除非模块B已经移除,否则模块A无法从内核移除。事实上,条件应该是所有引用模块A的模块都已经从内核移除。在内核中,这种关系你之为模块B使用模块A。为正确管理这些依赖关系,内核需要引入另一个数据结构:
/* modules using other modules: kdb wants to see this. */
struct module_use {//为正确管理依赖关系内核inrush此数据结构
  struct list_head source_list;
  struct list_head target_list;
  struct module *source, *target;
};

模块的二进制结构

模块使用ELF二进制恪式,模块中包含了几个额外的段,普通的程序或库中不会出现。除了少量由编译器产生、与我们的讨论不相关的段(主要是重定位段),模块由以下ELF段组成。

生成模块要执行下述3个步骤:

  1. 首先,模块源码中的所有C文件都编译成普通的.o目标文件。
  2. 在为所有模块产生目标文件后,内核可以分析它们。找到的附加信息(例如,模块依赖关系)保存在一个独立的文件中,也编译为一个二进制文件。
  3. 将前述两个步产生的二进制文件链接起来,生成最终的模块。


目录
相关文章
|
6月前
|
Linux
探索Linux操作系统的内核模块
本文将深入探讨Linux操作系统的核心组成部分——内核模块,揭示其背后的工作机制和实现方式。我们将从内核模块的定义开始,逐步解析其加载、卸载以及与操作系统其他部分的交互过程,最后探讨内核模块在系统性能优化中的关键作用。
|
存储 编译器 开发者
内核模块(下)
内核模块(下)
176 0
|
Linux KVM 虚拟化
Linux内核模块
在模块A编译好后会生成符号表文件Module.symvers, 里面有函数地址和函数名对应关系,把这个文件拷贝到需要调用的模块B的源代码下,替换模块B的该文件。 然后重新编译B模块.这样就能够让模块B调用模块A的函数,以后加载模块顺序也必须先A后B,卸载相反。
Linux内核模块
|
网络协议 物联网 Linux
内核模块4 | 学习笔记
快速学习内核模块4
内核模块4 | 学习笔记
|
Ubuntu 物联网 编译器
内核模块3 | 学习笔记
快速学习内核模块3
内核模块3 | 学习笔记
|
物联网 Linux 开发者
内核模块2 | 学习笔记
快速学习模块编写2
内核模块2 | 学习笔记
|
Linux
linux下自动加载设备驱动程序模块
<h1 class="postTitle" style="font-size: 14.7px; margin-bottom: 10px; color: rgb(75, 75, 75); font-family: Verdana, Geneva, Arial, Helvetica, sans-serif;"><br></h1> <div id="cnblogs_post_body" style=
2745 0
|
Linux
内核模块-实现一个简单的设备
上一篇文章讲了如何实现基于内核模块的“helloworld”,相信大家通过这个例子对于内核模块有了一个基本的了解。当然,内核模块绝不仅仅只能实现这点功能,其最大的应用就是实现硬件的驱动程序。其实,linux内核中很大一部代码都是硬件处理相关的,比如,设备-总线-驱动框架,USB框架、spi框架、i2c框架等等,对应于各种不同的硬件设备,相应的就会有设备驱动程序,从最简单的按键、LED驱动,到十分复杂的USB子系统驱动,可以好不夸张的说,Linux内核可以适配绝大多数的硬件设备。
175 3
|
缓存 IDE Linux
16.4 Linux内核(内核模块)的加载
GRUB 加载了内核之后,内核首先会再进行二次系统的自检,而不一定使用 BIOS 检测的硬件信息。这时内核终于开始替代 BIOS 接管 Linux 的启动过程了。
224 0
16.4 Linux内核(内核模块)的加载
|
Linux
16.16 Linux内核模块管理
Linux 的内核会在启动过程中自动检验和加载硬件与文件系统的驱动。一般这些驱动都是用模块的形式加载的,使用模块的形式保存驱动,可以不直接把驱动放入内核,有利于控制内核大小。
139 0
16.16 Linux内核模块管理