首页
社区
课程
招聘
[原创]Linux驱动快速入门
发表于: 2023-2-7 08:38 9117

[原创]Linux驱动快速入门

2023-2-7 08:38
9117

本公众号分享的所有技术仅用于学习交流,请勿用于其他非法活动,如果错漏,欢迎留言指正

应用层:《LUNIX环境高级编程第二版》
《Linux程序设计(第四版)》

内核层:《Linux设备驱动程序第三版》
《Linux内核设计与实现第三版》
《Linux设备驱动开发详解》

源代码分析:《深入分析Linux内核源码》
《Linux_内核完全注释V11赵炯》

Linux驱动快速入门

Linux内核的诞生

Linux的发展

  • 1991年的10月5日,Linuscomp.os.minix新闻组上发布消息,正式向外宣布Linux内核系统的诞生(Free minix-like kernel sources for 386-AT)。这段消
    息可以称为Linux的诞生宣言,并且一直广为流传。因此10月5日对Linux社区来说是一全特殊的日子,许多后来Linux的新版本发布时都选择了这杀日子。所RedHat公司选择这个日子发布它的新系统也不是偶然的。
    • 0.00 (1991.2-4?)两个进程分别显示AAA BBB
    • 0.01 (1991.9?)第一个正式向外公布的Linux内核版本。
    • 0.02 (1991.10.5)该版本以及0.03版是内部版本,目前已经无法找到。
    • 0.03 (1991.10.5)
    • 0.10 (1991.10)由Ted Ts’o 发布的Linux内核版本。
    • 0.11 (1991.12.8)基本可以正常运行的内核版本(也是本书着重注释的版本)0.12 (1992.1.15)主要加入对数学协处理器的软件模拟程序。
    • 0.13 (1992.3.8)开始加入虚拟文件系统思想的内核版本。
    • 0.96 (1992.5.12)开始加入网络支持和虚拟文件系统VFS。
    • 0.97 (1992.8.1)
    • 0.98 (19p2.9.29)
    • 0.99 (1992.12.13)
    • 1.20 (1995.3.7)
    • 2.0 (1996.2.9)
    • 2.20 (1999.1.26)
    • 2.40 (2001.1.4)
  • 在kernel.org上可以看到最新版本,stable:5.17.5- 2022-04-27,
  • Linus(芬兰人)最初给他的操作系统取名为FREAX,怪物,异想天开

    Linux产生的5大基础

  • Linus参考的2本书和一份代码 (minix)
    • M.J.Bach的《UNIX操作系统设计》
    • A.S.Tanenbaum的书《操作系统:设计与实现》
  • LINUX产生的五大基础:
    • UNIX操作系统--UNIX于1969年诞生在Bell实验室。Linux就是UNIX的一种克隆系统。UNIX的重要性就不用多说了。
    • MINIX操作系统--Minix操作系统也是UNIX的一种克隆系统,它于1987年由著名计算机教授Andrew S. Tanenbaum开发完成。由于MINIX系统的出现并且提供源代码(只能免费用于大学内)在全世界的大学中刮起了学习UNIX系统旋风。Linux刚开始就是参照Minix系统于1991年才开始开发。
    • GNU计划--开发Linux操作系统,以及Linux上所用大多数软件基本上都出自GNU计划。Linux只是操作系统的一个内核,没有GNU软件环境(比如说bash shell),则Linux将寸步难行。
    • POSIX标准--该标准在推动Linux操作系统以后朝着正规路上发展起着重要的作用。是Linux.前进的灯塔。
    • INTERNET--如果没有Intenet网,没有遍布全世界的无数计算机骇客的无私奉献,那么Linux最多只能发展到0.13(0.95)版的水平。

      在Ubuntu 10.04+编译linux-2.6.32.65内核版本成功

  • 2.1下载新内核;
    • 最新内核地址: http://www.kernel.org/
    • 本次内核编译需要的版本号:linux:2.6.32.65
  • 2.2修改新内核添加一个系统调用:sys_mycall
    • 添加新的系统调用函数,用来判断输入数据的奇偶性。
  • 2.3进行新内核编译
    • 通过修改新版内核后,进行加载编译。最后通过编写测试程序进行测试
  • 3.1准备工作
    • 查看系统先前内核版本:(终端下)使用命令:uname -r查看Linux内核版本
  • 3.2下载内核
    • 这里使用的内核版本linux:2.6.32.65
  • 3.3解压新版内核
    • 将新版内核复制到/usr/src”目录下
    • 在终端下用命令:cd /usr/src进入到该文件目录
    • 解压内核:linux-2.6.32.65.tar.xz,在终端进入cd /ust/src 目录输入一下命令:
1
2
3
4
cp linux-2.6.32.65.tar.xz/usxlsxc/
cd /usxlsxc/
xz -d linux-2.6.32.65.tar.xz
tar xvf  linux-2.6.32.65.tar
  • 建立符号链接
    • ln -s linux-2.6.2.65 linux 创建了一个符号链接linux指向linux-2.6.32.65
  • 3.4安装必要工具

  • 3.5.0.asm、linux和 scsi等链接是指向要升级的内核源代码

1
2
3
4
5
cd /usx/includel
rm -r asm linux sssi
ln -s /usr/src/linux/include/asm-generic asm
ln -s /usr/src/linux/include/linux linux 
ln -s /usc/src/linux/include/scsi scsi
  • 3.5.1 添加新的系统调用
    • 在文件:/usrlsrc/linux/arch/x86/kerne/syscall_table_32.S 最后增加一个系统表项:
    • .long sxs.mycall
  • 3.5.2 添加系统调用号
1
2
3
4
5
6
7
/// x86:/usr/src/linux/arch/x86/include/asm/unistd_32.h中添加:
#define __NR_mycall 337
#define __NR_syscalls 338 ///原来NR_syscalls是337,现在由于加了一个系统调用,所以应该加1
 
/// x64:/usr/src/linux/arch/x86/include/asm/unistd_64.h中添加:
#define __NR_mxcall 299
__SXSCALL(_NR_mxcall,sys_mycall)
  • 3.5.3 添加系统调用的处理函数
1
2
3
4
5
6
7
8
/// /usrlsrc/linux/kernel/sys.c 中添加以下处理函数:(判断奇偶数)
asmlinkage int sys_mycall(int n)
{
    if(n%2==0)
        return 1;
    else
        return 0;
}
  • 3.6 清除从前编译内核时残留的.o文件和不必要的关联(如果从前没有进行内核编译的话,则可以省略这一步),
    • 终端下切换至: cd /usx.src/linux
    • 输入以下命令: make mrproper
  • 3.7 配置内核,修改相关参数。
    • 输入以下命令: cd /usr/src/linux
    • 输入以下命令: make xconfig
    • 裁剪内核
    • 增加调试信息
  • 3.8 编译
    • 在“usx/sxclinux”目录下建立二进制的内核映像文件,命令如下:cd /usr/src/linux
    • sudo make(此步第一次耗对较长,约3个小时)
    • sudo make modules_install(注意别落下下划线)
    • sudo maeke install
    • sudo mkinitramfs -o /boot/initrd.img-2.6.32.65
    • sudo update-initramfs -c -k 2.6.32.65
    • sudo update-grub2 自动修改系统引导配置,根据/etc/default/grub,产生/boot/grub/grub.cfg启动文件。
    • sudo vim /etc/default/grube注释掉:#GRUB_HIDDEN_TIMEOUT=0,不然看不到启动菜单了
  • 3.9 重启系统
  • 4.1编写测试程序
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
#include <stdio.h>
#include<errno.h>
#include <unistd.h>
#inc1ude <sys/syscall.h>
#include <sys/txpes.h>
#define __NR_mycal1 299
/// 如果是x86,那么可能会需要定义#define __NR_myca11 337
 
int main()
{
    printf(" hello wor1dx n");
    if(syscall(__NR_mycall,5)
    {
        printf("even number!\n"); ///偶数
    }else
    {
        printf("odd number!\n"); ///奇数
    }
    return 0;

源代码分析

  • 要带着任务去读,以应用为目的去学
    • 不然读了不用忘得快,浪费时间
  • LINUX内核源线码目录树
    • init 启动
    • arch 架构
    • dirvers 驱动
    • fs 文件系统
    • net 网络
    • mm 内存管理
    • ipc 网络通信
    • kernel 运行时库
    • include
  • @todo

LINUX内核嵌入式汇编

AT&T汇编

1
2
3
4
5
6
7
8
9
///立即数intel
mov eax,8
mov ebx,0ffffh
int 80h
 
///at&t
movl $8,%eax
movl $0xffff,%ebx
int $0x80

LINUX内核嵌入式汇编

  • Linux的C语言不标准的c语言,是基于gcc的扩展版本
1
2
__asm__ __volatile__("<asm routine>" : output : input : modify); ///"<asm routine>"的运行结果放到输出参数:输入参数:IN_OUT
///__volatile__ 告诉编译器防止被优化的,避免"asm"指令被删除、移动或组合
  • 1.首先分析输出,输入部分,确定寄存器编号
  • 2.其次确定输人输出部分代码
    • 输出:"r"(__limit)///将—个寄存器如eax中的值放R入_limit中
    • 输入:"r"(segment)///将segment的值放入寄存器如ebx中
  • 3.最后分析asm routine部分
1
2
3
4
5
6
7
8
9
10
11
12
13
static inline unsigned long get_limit(unsigned long segment)
{
    unsigned long __limit;
    __asm__("lsll %1,%0"                ///3.最后分析asm routine部分
         :"=r"(__limit):"r"(segment))  ///1.首先分析输出,输入部分,确定寄存器编号。=表示输出,r表示任和通用寄存器,寄存器%0%1依次从output,input中用到的寄存器开始编码。
                                       ///2.其次确定输人输出部分代码。将%0寄存器(eax)中的值输出到__limit, segment输入%1寄存器(ebx)中
    return __limit + 1;
}
 
/// 嵌入式汇编等价于以下AT&T汇编
movl segment, %ebx;
lsll %ebx, %eax;
movl %eax,__limit;
  • m,v,o 表示内存单元
  • R 表示任何通用寄存器(eax, ebx, ecx,edx)
  • Q 表示寄存器eax, ebx, ecx,edx 之一
  • I, h 表示直接操作数
  • E, F 表示浮点数
  • G 表示“任意”
  • a, b,c,d 表示要求使用寄存器eax/ax/al, ebx/bx/bl, ecx/cx/cl或edx/dx/dl
  • S, D 表示要求使用寄存器esi或edi
  • I 表示常数(0~31)
  • =&a:
    • &表示寄存器不能重复
    • =号表示是输出
  • 双%号是因为有%1
  • \n换行,\t tab为了让gcc把嵌入式江编代码翻译成一般的汇编代码时能够保证换行和留有一定的空格
  • jne 1b:往后找到编号为1的代码
  • jmp 3f:往前找到编号为3的代码
    • 代码前进方向的下面定义为前面(front)。上面定义为后面(back)
  • @todo
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
static inline int strcmp(const char * cs,const char * ct)
{
    int d0,d1;
    register int_res;
    __asm__ __volatile__(
        "1:\t\lodsb\n\t" ///1: lodsb 装入串操作教,即从[esi]传送到al寄存器,然后esi指向串中下一个元素
        "scasb\n\t" ///scasb 扫描串操作数,即从al中减去es:[edi],不保留结果,只改变标志,lodsb和scasb 配合其实就是在比较cs和ct
        "jne 2f\n\t" ////如果两个字符不相等,则转到标号2
        "testb %%al,%%al \n\t" ///判断是否到达字符串末尾,test逻辑与运算结果为裂就把ZF(零标志)置1;
        "jne 1b\n\t"
        "xor %%eax,%%eax\n\t" ///清零退出
        "jmp 3f\n"
        "2:\tsbbl %%eax,%%eax\n\t" ///不相等-->2-->设置eax中的值(减去借位,正负);SBB:带借位减法格式SBB %SRC,%DST,执行的操作:(DST)=(DST)-(SRC)-CF
        "orb $1,%%al\n" ///OR 指令这里是用来清除CF标志位,同时会对al进行操作
        ///当SRC>DS时,SRC-DS>0,此时CF是=0,执行sddl %eax,%eax;后 eax=eax-eax-cf=0,接下来orb $1,%al后,al变成了1
        ///当SRC>DS时,SRC-DS>0,此时CF是=1,执行sddl %eax,%eax;后 eax=eax-eax-cf=-1,接下来orb $1,%al后,al变成了-1
        ///OR指令会清除进位和溢出标志位,并根据目标操作数的值来修改符号标志位、零标志位和奇偶标志位
        "3:"
        :"=a"(__res), "=&S"(d0), "=&D" (d1) ///1.mov %eax,_res;movl %esi,d0;mov %edi,d1;
        :"1"(cs),"2"(ct)); ///mov cs,%esi;mov ct,%edi;
    retun_res;
}

中断机制

  • windows和Linux的中断机制是一样的,毕竟都是x86平台

    中断、异常处理机制

  • 中断是硬件(电脑自身的硬件或者与电脑连接的外部设备)产生的一个电信号
  • 过程:比如外部设备有事情需要系统去处理的话,外部设备发出一个电信号,经过中断控制器得到中断向量传给cpu,cpu根据中断向量中断向量表找到对应的中断函数,来处理这个函数,处理分为上半部分和下半部分。
  • 上半部分,关中断,要求处理特别快,处理慢就会造成其他硬件等待时间变长,响应速度下降,性能下降,所以只把重要的部分(快速需要完成的任务)放在上半部分执行。
  • 下半部分,把不重要不紧急的放在下半部分去处理。
    • BH(2.4之前有):对bh函数执行严格串行化,一次只有1个CPU执行
    • softirq:同一个软中断可运行在不同CPU
    • tasklet:同一个tasklet不能运行在不同CPU,但不同的CPU可以。

      中断和异常的区别

  • 异常:CPU内部出现的中断
    • 特征是:IF维持不变,当cpu产生异常的时候,中断不会被屏蔽的,还可以响应其他中断请求。
    • 故障比如除0,缺页,越界,堆栈段错误(客观的)
    • 陷阱比如调试指令int 3了,溢出等(人为的有目的的)
  • 中断(外部中断): IF标志清零后,在处理完当前中断之前,不响应其他中断,直到IF标志置1
    • 非屏蔽中断:计算机内部硬件出错引起异常
    • 屏蔽中断

      中断向量

  • 从0~31的向量对应于异常非屏蔽中断
  • 从32~47的向量(即由I/0设备引起的中断)分配给屏蔽中断
    • 通过对8259A的配置,可将IRQ0~lRQ7对应到中断向量20h~27h,同样地IRQ8~IRQ15可对应到中断向量28h~2Fh。
  • 剩余的从48~255的向量用来标识软中断
    • Linux只用了其中的一个(即128或0x80向量)用来实现系统调用。当用户态下的进程执行一条int 0x80汇编指令时,CPU就切换到内核态,并开始执行system_call()内核函数。

      中断描述符表-IDT

  • 门:IDT表中每一项(8个字节)
  • XXX:3位门类型码
    • 中断门110 DPL-0关中断
    • 陷阱门111 DPL-0不关中断
    • 系统门010 DPL-3(向量号:3、4、5及0x80)

      进程上下文和中断上下文

  • Linux内核工作在进程上下文或者中断上下文提供系统调用服务的内核代码代表发起系统调用的应用程序运行在进程上下文,代表进程运行。
  • 另一方面,中断处理程序,异步运行在中断上下文,代表硬件运行,中断上文和特定进程无关。运行在中断上下文的代码就要受一些限制,不能做下面的事情:
    • 1.睡眠或者放弃CPU。(硬件与人睡眠例子)因为内核在进入中断之前会关闭进程调度,一旦睡眠或者放弃CPU,这时内核无法调度别的进程来执行,系统就会死掉。比如:分配内存的函数
    • 2.尝试获得信号量。如果获不到信号量,代码就会睡眠,效果同1。
    • 3.执行耗时的任务。中断处理应该尽可能快因为内核要响应大量服务和请求,中断上下文占用CPU时间太长严重影响系统功能。
    • 4.访问用户空间的虚拟地址。
  • 上下文context:上下文简单说来就是一个环境,相对于进程而言,就是进程执行时的环境。具体来说就是各个变量和数据,包括所有的寄存器变量、进程打开的文估、内存信息等。
    • 一个进程的上下文可以分为三个部分:用户级上下文、寄存器上下文以及系统级上下文。
    • 用户级上下文:正文、数据、用户堆栈以及共享存储区;
    • 寄存器上下文:通用寄存器、程序寄存器(IP)、处理器状态寄存器(EFLAGS)、栈指针(ESP);
    • 系统级上下文:进程控制块task_struct、内存管理信息(mm_struct、vm_area_struct、 pgd、pte)、内核栈。
  • 中断上下文:其实也可以看作就是硬件传递过来的这些参数和内核需要保存的一些其他环境(主要是当前被中断的进程环境)

内核启动过程

传统的BIOS+MBR

  • 电源开关或复粒按钮被按下时,通常所说的冷启动过程就开始了。
    • 中央处理器进入复位状态,它将内存中所有的数据清零、并对内存进行校验。
    • 如果没有错误,cs寄存器中将置入FFFF[0],IP寄存器中将置入0000[0]。其实,这个CS:IP组合指向的是BIOS的入口,它将作为处理器运行的第1条指令。系统就是通过这个方法进入BIOS启动过程的。
  • BIOS:首先是上电自检(POST Power-On SelfTest)
    • 然后是对系统内的硬件设备进行监测和连接,并把测试所得的数据存放到BIOS数据区(CMOS,系统的密码就是存放在这里),以便操作系统在启动时或启动后使用。
    • 最后:BiOS将从软盘或硬盘上读入Boot Loader
  • 到底是从软盘还是从硬盘启动要看BIOS的设置
    • 如果是从硬盘启动,BIOS将读入该盘的零柱面零磁道上的1扇区(MBR)至0x7c00处,这个扇区上就存放着Boot-Loader 。该扇区的最后一个字存放着系统标志。如果该标志的值为AA55,BiOS在完成硬件监测后会把控制权交给BootLoader。BiOS还提供一组中断以便对硬件设备的访问。

      UEFI+GPT

      @todo

      Linux文件系统

  • VFS 提供一个统一的接(实际上就是file_operation数据结构)
    • 一个具体文件系统要想被Linux支持,就必须按照这个接口编写自己的操作函数,而将自己的细节对内核其它子系统隐藏起来。
    • 因而,对内核其它子系统以及运行在操作系统之上的用户程序而言,所有的文件系统都是一样的

      微内核与宏内核

  • 微内核的系统有WindowNT,Minix,Mach等等
  • 宏内核(单一内核)的系统有Unix,Linux。
  • Minix:创建新进程的过程中,可以看到个很大特点,就是整个系统按功能分成几个部分,各模块之间利用消息机制通信,调用其他模块的函数必须通过目标模块的守护进程调用.系统启动后,kernel,mm,fs系统进程在各自的空间运行main()函数循环等待消息。
  • Linux:内部也是分模块的,但在运行的时候,他是一个独立的二进制大映像,其模块间的通讯是通过直接调用其他模块中的函数实现的
  • 宏内核与微内核的区别也就在这,微内核是一个信息中转站,自身完成很少功能,主要是传递一个模块对另文个模块的功能请求,而宏内核则是一个大主管,把内存管理,文件管理等等全部接管。

Linux内核模块的开发与编译

编写驱动代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
/// hello.c
#include <linux/init.h>
#include <linux/kernel.h>
#nclude <linux/module.h>
MODULE_LICENSE ("Dual BSD/GPL"); ///协议,GPL(表示开源,修改之后必须把代码公布出来)MIT,BSD,LGPL(注明原作者但可以商业,可以闭源,即打包成二进制文件发布,源文件不需要发布)
MODULE_AUTHOR("CISCO"); ///作者
int test_init(void) ///名字可以随便起,初始化函数,类似于DriverEntry
{
    printk(KERN_INFO "hello worldIn"); ///KERN_INFO表示日志的优先级KERN_DEBUG < KERN_INFO < KERN_WARRING < KERN_FUALT < KERN_ALERT @todo
    return 0;
}
void test_exit(void) ///名字可以随便起,退出函数,类似于DriverUnload
{
    printk(KERN_INFO "goodbye world\n");
}
 
module_init(test_init); ///注册初始化函数
module_exit(test_exit); ///注册退出函数
 
MODULE_DESCRIPTION ("MY_TEST"); ///模块描述
EXPORT_NO_SYMBOLS; ///不导出符号
EXPORT_SYMBOL(hello_data); ///导出符号,跟函数名或者变量名

Makefile生成hello.ko

  • Windows编译出来的驱动文件是.sys
  • Linux编译出来的驱动文件时.ko
1
2
3
4
5
6
7
8
9
10
11
# Makefile无后缀名,且M是大写
 
EXTRA_CFLAGS :=-g #指定c编译器的额外标记,-g(表示调试版本,带上调试符号),:=表示原来的符号基础上加上-g标记,如果=则表示把原来的标记覆盖为-g
obj-m = hello.o #如果只有一个C文件,那么hello.o要与hello.c同名
#hello-objs := file1,o file2.o #如果有多个源文件,加上这样一行
KVERSION= $(shell uname -r)
# make -C之前是一个tab键而不是4个空格
all:
    make -C /lib/modules/$(KVERSION)/build M=$(PWD) modules
clean:
    make -C /lib/modules/(KVERSIONy/build M=$(PWD) clean
  • 把Makefile文件和驱动代码(hello.c)放到同一个目录下,执行make就可以编译出hello.ko文件

    安装运行

  • sudo insmod hello.ko 安装驱动
  • sudo rmmod hello 卸载驱动
  • lsmod 查看系统已经加载的模块
  • modinfo hello 查看模块的信息
  • dmesg |tail 查看内核输出,tail只显示最后10行
  • 或者使用sudo cat /proc/kmsg 查看内核输出,滚动输出,proc是Linux的虚拟文件系统

    Ubuntu模块自启动

  • Ubuntu模块自启动:
      1. /lib/modules/2.6.32.65/kernel/lib/test/hello.ko 把hello.ko拷贝进去
      1. vim /etc/modules 在最后一行添加hello

        安装运行—驱动模块传参

  • 驱动模块中定义并指定变量
1
2
3
4
5
6
7
/// 在驱动模块中定义变量
static char *who= "world";
static int times = 1;
 
/// 指定为驱动模块的参数
module_param(times,int,S_IRUSR); ///参数名字,参数类型,参数权限:参数在sysfs文件系统(/sys/module)中的结点属性#define S_IRUSR 00400文件所有者可读
module_param(who,charp,S_IFRUSR); ///指定参数之后就可以用sudo cat lsyslmodulelhellolparameters/who去访问到这who这个参数,time参数同理
  • 加载驱动模块时传参
1
insmod hello.ko who="world" times=5 #在linux加载驱动的时候就可以为模块指定参数,而在windows中驱动模块需要传参的时候则是需要把参数放到注册表中
  • eg
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
#include <linux/init.h>
#include <linux/module.h>
#include <linux/moduleparam.h> 
 
MODULE_LICENSE("Dual BSD/GPL");    
MODULE_AUTHOR("CISCO");
/// 在驱动模块中定义变量
static char *who= "world";            
static int times = 1///默认是打印一次
/// 指定为驱动模块的参数
module_param(times,int,S_IRUSR);    
module_param(who,charp,S_IRUSR);  
 
static int hello_init(void)      
{
    int i;
    for(i=0;i<times;i++)
       printk(KERN_ALERT "(%d) hello, %s!\n",i,who);
     return 0;
}
 
static void hello_exit(void)
{
    printk(KERN_ALERT"Goodbye, %s!\n",who);
}
 
module_init(hello_init);
module_exit(hello_exit);

多个模块之间共享数据与接口

  • 编译顺序
    • 先编译导出符号的模块
    • 方法1:然后把编译出来的Module.symvers拷贝到使用符号的模块的目录下,这样使用符号的模块才能编译通过,比如,会出现Warning,提示说func1这个符号找不到,而编译得到的ko加载时也会出错。
    • 方法2:在使用符号的模块的Makefile文件中加上KBUILD EXTRA SYMBOLS = 导出符号的模块目录/Module.symvers让其指向导出符号的模块目录/Module.symvers
  • 加载顺序
    • 先加载导出符号的模块
    • 后加载使用符号的模块
  • 卸载顺序
    • 先卸载使用符号的模块
    • 这时候卸载导出符号的模块的应用计数变为0,才能成功卸载导出符号的模块
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
/// hello_e.c  导出符号的模块
#include <linux/init.h>
#include <linux/module.h>
#include <linux/kernel.h>
 
MODULE_LICENSE("GPL");
MODULE_AUTHOR("CISCO");
unsigned int hello_data=100;
EXPORT_SYMBOL(hello_data); ///导出符号
 
static int hello_h_init(void)
{
    hello_data+=5;
    printk(KERN_ERR "hello_data:%d\nhello kernel,this is hello_h module\n",hello_data);
 
    return 0;
}
 
static void hello_h_exit(void)
{
    hello_data-=5;
    printk(KERN_ERR "hello_data:%d\nleave hello_h module\n",hello_data);
}
 
module_init(hello_h_init);
module_exit(hello_h_exit);
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
/// hello.c 使用符号的模块
#include <linux/init.h>
#include <linux/module.h>
#include <linux/kernel.h>
 
MODULE_LICENSE("GPL");
MODULE_AUTHOR("CISCO");
extern int hello_data; ///使用外部的其他驱动模块的参数
 
static int hello_init(void)
{
    printk(KERN_ERR "hello,kernel!,this is hello module\n");
    printk(KERN_ERR "hello_data:%d\n",++hello_data); ///可以直接访问到外部驱动模块的参数
    return 0;
}
 
static void hello_exit(void)
{
    printk(KERN_ERR "hello_data:%d\n",--hello_data);
    printk(KERN_ERR "Leave hello module!\n");
}
module_init(hello_init);
module_exit(hello_exit);
 
MODULE_DESCRIPTION("This is hello module");
MODULE_ALIAS("A simple example");

字符设备驱动

  • sudo insmod timer-kernel.ko
  • cat /proc/devices 看看安装上的驱动生成的设备主功能号,如251。
  • 通过sudo mknod /dev/second 251 0命令创建/dev/second设备节点(可以放到脚本去做),类似windows驱动中的创建符号链接
  • 编译timer-client.c文件,并执行:./timer-client
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
/// kernel
#include <linux/module.h>
#include <linux/types.h>
#include <linux/fs.h>
#include <linux/errno.h>
#include <linux/mm.h>
#include <linux/sched.h>
#include <linux/init.h>
#include <linux/cdev.h>
#include <asm/io.h>
#include <asm/system.h>
#include <asm/uaccess.h>
#include <linux/timer.h>
#include <asm/atomic.h>
 
#define SECOND_MAJOR 0 ///设备对象默认为0
 
static int second_major = SECOND_MAJOR; // can be input by user,default value is 0
 
struct second_dev
{
    struct cdev cdev;
    atomic_t counter;
    struct timer_list s_timer;
};
 
struct second_dev *second_devp; // global data to store cdev,counter,timer
 
static void second_timer_handle(unsigned long arg) // timer handler
{
    mod_timer(&second_devp->s_timer, jiffies + HZ);
    atomic_inc(&second_devp->counter); /// inc the counter,原子操作
 
    printk(KERN_INFO "current jiffies is %ld\n", jiffies);
}
 
int second_open(struct inode *inode, struct file *filp)
{
    init_timer(&second_devp->s_timer);                    ///注册一个定时器
    second_devp->s_timer.function = &second_timer_handle; ///每隔1s就对counter进行+1
    second_devp->s_timer.expires = jiffies + HZ;          //中断每秒钟产生多少次,即Hz(赫兹)jiffies用来统计系统启动以来系统中产生的总节拍数
                                                          //节拍就是指系统中连续两次时钟中断的间隔时间,该值等于节拍率分之一,即1/Hz
                                                          //系统的运行时间(jiffies/Hz)
 
    add_timer(&second_devp->s_timer); // add a timer
    atomic_set(&second_devp->counter, 0);
    return 0;
}
 
int second_release(struct inode *inode, struct file *filp)
{
    del_timer(&second_devp->s_timer);
 
    return 0;
}
static ssize_t second_read(struct file *filp, char __user *buf, size_t count,
                           loff_t *ppos)
{
    int counter;
 
    counter = atomic_read(&second_devp->counter); ///原子读
    if (put_user(counter, (int *)buf))            // put value of  counter to buf
    {
        return -EFAULT;
    }
    else
    {
        return sizeof(unsigned int);
    }
}
 
static const struct file_operations second_fops =
    {
        .owner = THIS_MODULE,
        .open = second_open,
        .release = second_release,
        .read = second_read,
};
static void second_setup_cdev(struct second_dev *dev, int index)
{
    int err, devno = MKDEV(second_major, index); // get dev no.
    cdev_init(&dev->cdev, &second_fops);
    dev->cdev.owner = THIS_MODULE;
    dev->cdev.ops = &second_fops;         // open,read,write....functions for the dev
    err = cdev_add(&dev->cdev, devno, 1); // register the dev
    if (err)
    {
        printk(KERN_NOTICE "Error %d add second%d", err, index);
    }
}
int second_init(void)
{
    int ret;
    dev_t devno = MKDEV(second_major, 0); // make dev no. with major and minor(0)
 
    if (second_major) // device no. is configured which is not zero from user input
    {
        ret = register_chrdev_region(devno, 1, "second"); // second is the device name
    }
    else
    {
        ret = alloc_chrdev_region(&devno, 0, 1, "second"); // request a dev no inside.
        second_major = MAJOR(devno);
    }
    if (ret < 0)
    {
        return ret;
    }
    // second_devp is a global structure to save cdev,counter,timer
    second_devp = kmalloc(sizeof(struct second_dev), GFP_KERNEL);
    if (!second_devp)
    {
        ret = -ENOMEM;
        goto fail_malloc;
    }
 
    memset(second_devp, 0, sizeof(struct second_dev));
 
    second_setup_cdev(second_devp, 0); // register the dev
 
    return 0;
 
fail_malloc:
    unregister_chrdev_region(devno, 1);
    return ret;
}
 
void second_exit(void)
{
    cdev_del(&second_devp->cdev); // del the dev
    kfree(second_devp);
    unregister_chrdev_region(MKDEV(second_major, 0), 1); // unregister the dev
}
 
MODULE_AUTHOR("reversefish");
MODULE_LICENSE("Dual BSD/GPL");
 
module_param(second_major, int, S_IRUGO); // got a second_major no. from user input
 
module_init(second_init);
module_exit(second_exit);
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
/// client
 
#include <stdio.h>
#include <unistd.h>
#include <fcntl.h>
 
int main(void)
{
  int fd, i;
  int data;
  fd = open("/dev/second", O_RDONLY);
  if (fd < 0)
  {
    printf("open /dev/second error\n");
    return 0;
  }
  for (i = 0;; i++)
  {
    read(fd, &data, sizeof(data));
    printf("read /dev/second is %d\n", data);
    sleep(1); ///每个1s读取1
  }
  close(fd);
  return 0;
}

Linux内核调试

  • Windows奔溃叫PANIC(不是蓝屏,是黑屏)
  • Linux崩溃叫OOPS(拟声词,类似中文的哎呦,模块崩了系统不一定崩)

    oops

  • oops发生之后:
    • 情况1:驱动模块被killed(系统还活着(至少要满足两个条件:1.在进程上下文⒉没有设置panic_on_oops),会杀死当前进程。然后继续运行,好像什么事情都没有发生一样。不过,这祥的好事不经常发生,发生了也不会太持久。)
    • 情况2:导致系统panic
    • 可以设置,当发生oops后系统立即进入panic :
1
2
3
#打开panic,在/etc/sysctl.conf设置
kernel.panic_on_oops = 1
kernel.panic = 20  #panic error中自动重启,等待20秒

killed

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
#include <linux/kernel.h>
#include <linux/module.h>
 
static int __init hello_init(void)
{
    int *p = 0///null
 
    print("%d\n,*p); ///读引起的错误
    //*p = 1///写引起的错误,访问null地址,如果是在windows下就会panic,在Linux下则是发生oops
    return 0;
}
 
static void __exit hello_exit(void)
{
    return;
}
 
module_init(hello_init);
module_exit(hello_exit);
 
MODULE_LICENSE("GPL");
  • sudo insmod oops.ko执行之后发生killed(但是其实opps模块还是在内存中)
  • dmesg 或者 cat /proc/kmsg查看内核输出
    • RIP 发生错误的下一条指令,然后用objdump -S oops.o查看其反汇编代码
    • oops stack
1
2
3
4
5
6
7
8
die()首先打出一行:
oops:0000[#1]
其中0000代表错误码(读错误﹑发生在内核空间),#1代表oops发生次数。
 
error_code:
    bit 0 == 0 means no page found, 1 means protection fault
    bit 1 == 0 means read,1 means write
    bit 2 == 0 means kernel,1 means user-mode

panic

  • sudo vim /etc/sysctl.conf 打开panic
  • sudo sysctl -p 更新一下
  • 系统panic之后,内存的信息就丢失了,(无法用dmesg 或者 cat /proc/kmsg查看内核输出)
    方式1:这时需要切换到终端界面上操作的,内核输出会打印到终端上,这时候在倒计时的时间内拍个照保存一下信息
  • 切换到终端:
    • ALT+CTRL+F1
    • VM:先按ALT+CTRL+空格,然后再按ALT+CTRL+F1
  • 切换到UI:
    • ALT+CTRL+F7
    • VM:先按ALT+CTRL+空格,然后再按ALT+CTRL+F7
      方式2:用第三方工具KDUMP将oops保存为文件
  • sudo apt-get install kexec-tools
  • sudo apt-get install linux-crashdump
  • sudo vim letc/default/kexec
    • MLOAD KEXEC=true
  • reboot
  • sudo letc/init.d/kdump start
  • sudo echo “c" >/proc/sysrq-trigger 人为让系统panic
    • Is /var/crash/vmcore
  • http://ddebs.ubuntu.com/pool/main/I/linux/ 下载与uname-a命令输出匹配的内核符号
  • dpkg -i linux-image-2.6.32-65-generic-dbgsym-2.6.32-131_amd64.ddeb 安装符号
    • 将在/usr/lib/debug/lib/modules/p(uname -r)/下生成用于调试的vmlinux
  • sudo crash /usr/lib/debug/boot/vmlinux-2.6.32-65-generic /var/crash/vmcore
  • bt /ps/log/查看信息

printk

  • 程序运行不符合预期,调试
    • 在Windows下可以使用windbg可以加载符号,跟踪指令调试
    • 在Linux下没有这种好事,常用printk来打印输出来调试,printk使用的描述符格式和printf是一样的printk(KERN_INFO "hello world\n");
  • 日志级别一共有8个级别,printk的日志级别定义如下(在include/linux/kernel.h中):
    • #define KERN_EMERG 0 紧急事件消息,,系统崩溃之前提示,表示系统不可用
    • #define KERN_ALERT 1 报告消息表示必须立即采取措施
    • #define KERN_CRIT 2 临界条件,通常涉及严重的硬件或软件操作失败
    • #define KERN_ERR 3 错误条件、驱动程序常用KERN_ERR来握若硬件的错误
    • #define KERN_WARNING 4 警告条件,对可能出现问题的情况进行警告
    • #define KERN_NOTICE 5正常但又重要的条件,用于提醒
    • #define KERN_INFO 6 提示信息,如驱动程序启动时,打印硬件信息
    • #define KERN_DEBUG 7调试级别的消息
  • printk始终是能输出信息的,只不过不一定是到了终端上。 /var/log/messages这个文件里面去查看。
  • 如果klog没有运行,消息不会传递到用户空间只能查看/proc/kmsg
  • 通过读写/proc/syskernel/printk文件可读取和修改控制台的日志级别。
1
2
3
cat /proc/sys/kernel/printk
6 4 1 7
#对应控制台日志级别、默认的消息目志级别、最低的控制台日志级别和默认的控制台日志级别。
  • 设置当前日志级别:
    • echo 8>/proc/syskernel/printk
    • 这样所有级别<8,(0-7)的消息都可以显示在控制台上

      gdb

  • gdb主要用来调试应用层的程序
    • 如果用来调试内核层程序的话会受到很多限制的,比如只有读的功能(查看一些变量的值的信息),没有写的功能,也不能下断点
    • gdb is not able to modify kernel data ; it expects to be running a program tobe debugged under its own, control before playing with its memory image. lt is also not possible to set breakpoints or watchpoints, or to single-step through kernel functions.
      1. compile your kernel with the CONFIG_DEBUG_INFOoption set
      1. gdb /usr/src/linux/vmlinux /proc/kcore 启动gdb调试内核
      1. sudo cat /sys/module/hello/sections.text to get .text address(0xd0832000)
      1. (gdb)add-symbol-file /path/to/xxxx.ko 0xd0832000 -s .bss 0xd08371004 -s .data 0xd0836be0 添加符号
      1. p structname 打印变量的值
      1. print*(funcaddress) 打印file code line

        kdb

  • 获取kdb补丁
    • http://oss.sgi.com/www/projects/kdb/download
  • 补丁内核重新编译
    • ONFIG_KDB打开
    • CONFIG FRAME_POINTER
    • KDB off by default.用echo "1">/proc/sys/kernel/kdb打开kdb
  • starts kdb:按下pause/break/"oops occurs"
  • md(memory display)
  • mm(memory modify)
  • rd(register display)
  • rm(register modify)
  • bp 下断点
  • bt(stacktrace)等命令

KGDB

  • 双机联调
  • 看到的是汇编代码并不是源代码

http://blog.chinaunix.net/uid-23366077-id-4711134.html

strace

  • The strace command is a powerful tool that shows all the system calls issued by a user-space program. Not only does it show the calls, but it can also show the arguments to the calls and their return values in symbolic form. When a system call fails, both the symbolic value of the error (e.g.,ENOMEM) and the corresponding string(Out of memory) are displayed. strace has many command-lineoptions; the most useful of which are -t to display the time wheneach call is executed, -T to display the time spent in the call,-e to limit the types of calls traced, and -o to redirect the output to a file.By defaulit, strace prints tracing information on stderr .
  • sreace ls查看ls用到的系统调用情况 :调用的函数,函数参数,函数返回值
    • windows也有类似的工具(postman)可以观察API传参和返回值

      感恩比尔盖茨

  • Linux下不提供像Windows那样好用的调试工具的原因:Linus does not believe in interactive debuggers.
  • why the kernel does not have any more advanced debugging features built into it.The answer, quite simply, is that Linus does not believe in interactive debuggers. He fears that they lead to poor fixes, those which patch up symptoms rather than addressingthe real cause of problems. Thus, no built-indebuggers.
  • Linus认为,如果提供很好的调试工具,会导致程序员不注重代码架构(只针对现象,不解决本质问题)
    • 这样程序程序员写代码会认真思考,因为如果不小心写bug后续不好修。

[培训]内核驱动高级班,冲击BAT一流互联网大厂工作,每周日13:00-18:00直播授课

收藏
免费 4
支持
分享
最新回复 (2)
雪    币: 4437
活跃值: (6666)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
2
赞一个,修炼的功法
2023-2-7 10:03
1
雪    币: 14492
活跃值: (17493)
能力值: ( LV12,RANK:290 )
在线值:
发帖
回帖
粉丝
3
感谢分享
2023-2-7 11:09
1
游客
登录 | 注册 方可回帖
返回
//