Linux内核模块补充知识

  • 在正式编写内核模块第一个程序之前,还需要了解一些关于Linux内核的知识。包括如下:
    • Linux内核模块与Linux用户程序的对比
    • Linux内核模块的可用的函数
    • Linux内核空间与用户空间
    • Linux内核模块的命名空间与Linux的代码空间(虚拟内存在内核和进程上的体现)
    • Linux内核模块最常见的一类 设备驱动程序相关的介绍

内核模块与用户程序对比

  • 用户程序:
    • 用户程序启动一般是从start->libc_start_main->libc_start_call_main->main这样的一个启动流程
    • 用户程序结束一般是执行完main函数之后返回到libc_start_call_main这边再调用exit函数从而结束一个进程的运行。
    • 用户程序的库函数:用户程序使用的printf()等库函数都是需要进行系统调用这一步。库函数完全由用户空间运行,为系统调用提供一个方便的接口。
  • 内核模块:
    • 内核模块的启动:一般就是从init_module或者module_init指定的函数作为入口。这个入口函数会告诉内核模块提供了什么功能并进行设置然后返回,之后模块处于空闲状态,当内核需要用到该模块的代码时就会执行该模块的代码。
    • 内核模块的结束:一般就是从cleanup_module或者module_exit指定的函数作为结束。这个退出函数是撤销入口函数所做的一切,注销已注册的功能。注意:每个模块都必须同时拥有入口函数和出口函数
    • 内核模块的函数:内核模块的符号在insmod时解析,符号的定义来自内核本身,要查看导出的符号,可以查看/proc/kallsyms这个文件目录。注意:可以通过编写替换掉内核系统调用模块,这样用户程序在进行某个进行系统调用的时候执行的并不是正常的系统调用,而是被替换掉的,这种技术在rootkitbootkit很常见;但是替换的目的也可以用于好的地方
  • 使用cat /proc/kallsyms命令查看内核符号表:

image-20260605230412027

内核模块可用函数

  • 对于监控用户程序的系统调用情况,可以使用strace命令对用户程序进行监控。
1
2
3
4
5
6
7
8
#include <stdio.h>
int main()
{
printf("hello");
return 0;
}

// gcc -Wall -o test test.c
  • 使用命令strace ./test,就会显示如下输出,strace是用于跟踪一个进程执行过程中发生的系统调用接收到的信号

image-20260604220143393

内核空间与用户空间

  • 内核管理对硬件资源的访问,如显卡、硬盘和内存。CPU可以在不同的模式下运行,每种模式提供不同程度的自由度。其中Intel 80386架构有4种这样的模式,称为环。Unix使用其中两种:环0即超级用户模式,允许一切操作;最低环即用户模式。

image-20260605234549921

  • 库函数在用户模式下运行,它们调用系统调用,系统调用作为内核的一部分在超级用户模式下执行。完成后,执行回到用户模式。
  • 所以根据上面的介绍,可以给用户态内核态下一个定义:
    • 用户态:CPU 运行在 ring3 + 用户进程运行环境上下文。
    • 内核态:CPU 运行在 ring0 + 内核代码运行环境上下文。

模块命名空间与代码空间

  • 在小型C程序中,变量名在局部范围内使用。但是对于大型C项目中的代码,全局变量可能会冲突。这个问题被称为命名空间污染

    • 内核代码(即使是最小的模块)需要链接到整个内核,因此命名空间污染需要非常重视。
    • 最佳做法有下面两种:
      • 将所有变量声明为static 并使用定义良好的小写前缀。
      • 可以声明一个符号表将其注册到内核。
  • 现代操作系统都有虚拟内存机制,并且对于国内操作系统课程来说,内存管理好像都是在讲操作系统管理硬件的,似乎都没有讲操作系统对进程的内存管理。对于操作系统对进程的内存管理,学PWN的确实比较容易理解

    • 对于进程来说:
      • 当进程被创建时,内核为其分配代码、变量、堆栈等所需的物理内存。这块内存从0x00000000开始,并根据需要扩展。
      • 但是因为虚拟内存机制,不同进程的内存空间不重叠,每个进程访问地址0xdeadbeff的时候,实际上是访问不同的物理内存位置。也就是说每个进程的内存空间是相互独立的。并且进程通常不能访问另一个进程的空间。
    • 对于内核来说:
      • 内核拥有自己的内存空间。由于模块是动态插入/移除的内核代码,它共享内核的代码空间,而不是拥有自己的空间。
      • 因此,如果某个模块发生段错误,内核也会段错误,这就导致内核崩溃在Windows上常见的是蓝屏。因此一个差错就会破坏内核数据或代码,这适用于任何具有单体内核的操作系统。
      • 微内核则为模块提供了各自的代码空间。
  • 下面贴一张图表示虚拟内存代码的具体布局,从用户态到内核态:

设备驱动程序

  • 在Linux内核模块中,最常见的一种模块类型就是设备驱动程序,为迎接(如电视卡或串口)提供功能。
  • Unix上,每个硬件都由/dev目录中的一个称为设备文件的文件夹表示。

image-20260606001558922

  • 驱动程序有主设备号以及次设备号,接下来以VM虚拟机中的/dev/cpu这个文件夹为例子,该虚拟机我定义了2个处理器,每个处理器4核,所以会出现如下:
    • 第一个数字203、202是主设备号,表示处理此硬件的驱动程序。
    • 第二给数字0、1、2是次设备号,表示驱动程序用于区分不同的硬件。
    • 注意:在这里应该算是两个驱动程序处理8个硬件,也就是cpuid、msr这两个驱动程序处理0~8个CPU核

image-20260606003242872

  • 设备一般分为两种类型:
    • 字符设备:字符设备则根据需要使用任意数量的字节,大多数设备是字符设备。
    • 块设备:块设备有请求缓冲区,可以优化请求顺序,这对存储设备非常重要,因为连续读取/写入附近扇区更快。块设备以固定大小的块接受输入/输出
    • 使用ls -l输出中的第一个字符b表示块设备,c表示字符设备,标识了设备类型。上面的cpu就显示为字符设备。
    • 已经分配的主设备号列表可以查看:/usr/src/linux/Documentation/devices.txt
  • 设备文件类型和普通文件类型是不一样的,所以不能使用mkdir创建设备文件,要使用mknod创建一个设备文件。
    • 例如:要创建一个名为coffee、主设备号为12、次设备号为2的新字符设备,就可以使用这样的一个命令:mknod /dev/coffee c 12 2
    • 注意:一般设备文件都是放在/dev这个目录下面的。
    • 注意:在测试的时候,通常是在工作目录下面创建设备文件。
  • 在给设备文件命名的时候一般会标识硬件类型或是某种标号,比如下面:
    • 下面这两种都是块设备,由同一个驱动程序处理,主设备号是2都代表同一个软盘驱动器
    • 其中一个软盘是标准的1.44MB格式,另一个是超级格式化1.68MB变体。
1
2
3
% ls -l /dev/fd0 /dev/fd0u1680
brwxrwxrwx 1 root floppy 2, 0 Jul 5 2000 /dev/fd0
brw-rw---- 1 root floppy 2, 44 Jul 5 2000 /dev/fd0u1680

字符设备文件与驱动