设为首页 加入收藏

TOP

从module_init看内核模块(一)
2023-08-06 07:49:36 】 浏览:153
Tags:module_init

开篇

module_init是linux内核提供的一个宏, 可以用来在编写内核模块时注册一个初始化函数, 当模块被加载的时候, 内核负责执行这个初始化函数. 在编写设备驱动程序时, 使用这个宏看起来理所应当, 没什么特别的, 但毕竟我还是一个有点追求的程序员嘛:P, 这篇文章是我学习module_init相关源码的一个记录, 主要就回答了下面的3个问题, 篇幅略长, 做好准备.

Q1:

内核模块是什么?

Q2:

内核模块是怎么被加载的?

Q3:

内核怎么获取到module_init注册的初始化函数?


注: 以下回答是个人学习总结, 仅供参考.

A1:

编译好内核模块的代码, 会得到一个".ko"文件, 这个就是内核模块了. 实际上, ".ko"就是一个普通的ELF文件, 只不过可以使用insmod让内核去动态加载它. 查阅ELF格式标准可知, 主要有三种类型的ELF文件, 包括:

  • relocatable file
  • excutable file
  • shared object file

以上三种类型的ELF, 基本上可以简单对应编译得到的".o", "a.out", ".so". 这里讨论的".ko"模块文件, 属于relocatable file类型, 可以在系统里找一个内核模块文件验证一下.

junan@ZEN2:/lib/modules/5.19.0-50-generic/kernel/drivers/char$ ls
agp  applicom.ko  hangcheck-timer.ko  hw_random  ipmi  lp.ko  mwave  nvram.ko  pcmcia  ppdev.ko  tlclk.ko  tpm  uv_mmtimer.ko  xillybus
junan@ZEN2:/lib/modules/5.19.0-50-generic/kernel/drivers/char$ file lp.ko 
lp.ko: ELF 64-bit LSB relocatable, x86-64, version 1 (SYSV), BuildID[sha1]=98c89bd841e31b1140e61559c0bf312eb5128f5c, not stripped

使用gcc编译一个c文件, 可以得到对应的.o文件, 前面说过.o文件属于relocatable类型的ELF, 如果有多个.o文件, 可以使用链接器把它们"合并"成一个.o, 就像这样:

junan@ZEN2:~$ ls
a.c  a.o  b.c  b.o  Desktop  Documents  Downloads  Music  Pictures  Public  snap  Templates  Videos
junan@ZEN2:~$ file a.o b.o
a.o: ELF 64-bit LSB relocatable, x86-64, version 1 (SYSV), not stripped
b.o: ELF 64-bit LSB relocatable, x86-64, version 1 (SYSV), not stripped
junan@ZEN2:~$ ld -r a.o b.o -o c.o
junan@ZEN2:~$ file c.o
c.o: ELF 64-bit LSB relocatable, x86-64, version 1 (SYSV), not stripped

经过ld链接之后, 得到了"c.o", 这个"c.o"相当于合并了"a.o"和"b.o", 这可以从它们各自包含的符号中看出来:

junan@ZEN2:~$ nm a.o b.o c.o

a.o:
                 U add
0000000000000000 T call_add

b.o:
0000000000000000 T add

c.o:
000000000000001a T add
0000000000000000 T call_add

同样的道理, 一个设备驱动, 可能包含多个源文件, 但是编译之后最终可以合并成一个".ko"文件. 总结下来, ".ko"也是一种普通的ELF文件, 可以被内核动态加载和卸载.

A2:

一个内核模块, 除了自己实现一些功能外, 通常还要引用其他人提供的api, 包括:

  • 内核本身提供的api
  • 其他模块提供的api

内核仅仅把".ko"文件读到自己的地址空间中, 是远远不够的, 他要像链接器一样, 帮我们的内核模块正确地处理这些符号引用关系, 并且调用我们使用module_init注册的模块初始化函数. 下面分析一下这部分的内核源码. 我们的".ko"文件是使用insmod命令才加载进内核的, insmod命令实际上是一个符号链接. 和insmod一样, rmmod也指向/bin/kmod, 当kmod被执行的时候, 可以通过args[0]区分出是执行insmod还是rmmod, 或者其他的功能.

junan@ZEN2:~$ which insmod | xargs ls -l
lrwxrwxrwx 1 root root 9  7月 22 23:20 /usr/sbin/insmod -> /bin/kmod

在内核代码中, 专门为模块的加载和卸载提供了两个系统调用, 准确说是三个, 其中两个用于加载模块, 一个用于卸载模块. linux代码中使用SYSCALL_DEFINEx这个宏定义一个系统调用的入口, 其中x代表系统调用的参数个数, 看一下内核代码就可以找到和模块的加载以及卸载相关的syscall函数, 使用正则表达式或者其他工具能很快在内核代码中找到这三个系统调用的定义.

首先是init_module和finit_module:

SYSCALL_DEFINE3(init_module, void __user *, umod,
		unsigned long, len, const char __user *, uargs)
{
    // ...
	return load_module(&info, uargs, 0);
}

SYSCALL_DEFINE3(finit_module, int, fd, const char __user *, uargs, int, flags)
{
    // ...
	return load_module(&info, uargs, flags);
}

以上两个syscall负责模块的加载, 最终都调用了load_module去真正加载模块.

然后是delete_module:

SYSCALL_DEFINE2(delete_module, const char __user *, name_user,
		unsigned int, flags)
{
    // ...
}

这三个syscall定义在"kernel/module.c"文件中, 我使用的内核版本是5.4.250, 下面可以写一个什么都不做的内核模块, 通过gdb调试的方法, 看一下这几个syscall是怎么被调用的. 环境的准备包括:

  • qemu: 启动编译好的内核镜像, 以及gdb server, 等待gdb的连接
  • rootfs: 内核正常启动, 需要一个根文件系统, 使用busybox制作
  • kernel Imag
首页 上一页 1 2 3 4 5 下一页 尾页 1/5/5
】【打印繁体】【投稿】【收藏】 【推荐】【举报】【评论】 【关闭】 【返回顶部
上一篇【技术积累】Linux中的命令行【理.. 下一篇Linux学习环境搭建(VMware虚拟机..

最新文章

热门文章

Hot 文章

Python

C 语言

C++基础

大数据基础

linux编程基础

C/C++面试题目