Linux内核分析(六)----字符设备控制方法实现|揭秘系统调用本质

原文:Linux内核分析(六)----字符设备控制方法实现|揭秘系统调用本质

Linux内核分析(六)

昨天我们对字符设备进行了初步的了解,并且实现了简单的字符设备驱动,今天我们继续对字符设备的某些方法进行完善。

今天我们会分析到以下内容:

1.      字符设备控制方法实现

2.      揭秘系统调用本质

在昨天我们实现的字符设备中有open、read、write等方法,由于这些方法我们在以前编写应用程序的时候,相信大家已经有所涉及所以就没单独列出来分析,今天我们主要来分析一下我们以前接触较少的控制方法。

字符设备控制方法实现

1.       设备控制简介

1.        何为设备控制:我们所接触的大部分设备,除了读、写、打开关闭等方法外,还应该具有控制方法,比如:控制电机转速、串口配置波特率等。这就是对设备的控制方法。

2.        用户如何进行设备控制:类似与我们在用户空间使用read、open等函数对设备进行操作,我们在用户空间对设备控制的函数是ioctl其原型为 int ioctl(int fd, int cmd, …)//fd为要控制的设备文件的描述符,cmd是控制命令,…依据第二个参数类似与我们的printf等多参函数。

3.        Ioctl调用驱动那个函数:在我们的用户层进行ioctl调用的时候驱动会根据内核版本不同调用不同的函数,有以下:

1)        2.6.36以前的内核版本会调用 long (*ioctl) (struct inode*,struct file *, unsigned int, unsigned long);

2)        2.6.36以后的内核会调用 long (*unlocked_ioctl) (struct file *, unsigned int, unsigned long);

2.       Ioctl实现

1.        控制命令解析:我们刚才说到ioctl进行控制的时候有个cmd参数其为int类型的也就是32位,我们的linux为了让这32位更加有意义,所表示的内容更多,所以将其分为了下面几个段

1)        Type(类型/幻数8bit):表明这是属于哪个设备的命令

2)        Number(序号8bit):用来区分统一设备的不同命令

3)        Direction(2bit):参数传递方向,可能的取值,_IOC_NODE(没有数据传输)、_IOC_READ(从设备读)、_IOC_WRITE(向设备写)

4)        Size(13/14bit()):参数长度

2.        定义命令:我们的控制命令如此复杂,为了方便我们的linux系统提供了固定的宏来解决命令的定义,具体如下:

1)         _IO(type,nr); :定义不带参数的命令

2)         _IOR(type,nr,datatype); :从设备读参数命令

3)         _IOW(type,nr,datatype); :向设备写入参数命令

下面定义一个向设备写入参数的命令例子

#define MEM_CLEAR _IOW(‘m’,0,int)//通常用一个字母来表示命令的类型

3.        Ioctl实现:下面我们去向我们上次实现的字符设备中添加ioctl方法,并实现设备重启命令(虚拟重启),对于不支持的命令我们返回-EINVAL代码如下,整体工程在https://github.com/wrjvszq/myblongs(我今后会将自己博文中提到的代码都放在这个仓库中)

 1 long mem_ioctl(struct file *fd, unsigned int cmd, unsigned long arg){
 2     switch(cmd){
 3     case MEM_RESTART:
 4         printk("<0> memdev is restart");
 5         break;
 6     default:
 7         return -EINVAL;
 8     }
 9     return 0;
10 } 

揭秘系统调用本质

由于我自己的PC的调用过程不太熟悉,下面以arm的调用过程分析一下我们用户层调用read之后发生了什么,是怎么调用到我们驱动写的read函数的呢,我们下面进行深入剖析。

1.       代码分析

我们首先使用得到arm上可执行的应用程序 arm-linux-gcc -g -static read_mem.c -o read_mem 然后使用 arm-linux-objdump -D -S read_mem >dump 得到汇编文件,我们找到main函数的汇编实现

 1  int main(void)
 2 {
 3     8228:    e92d4800     push    {fp, lr}
 4     822c:    e28db004     add    fp, sp, #4    ; 0x4
 5     8230:    e24dd008     sub    sp, sp, #8    ; 0x8
 6     int fd = 0;
 7     8234:    e3a03000     mov    r3, #0    ; 0x0
 8     8238:    e50b3008     str    r3, [fp, #-8]
 9     int test = 0;
10     823c:    e3a03000     mov    r3, #0    ; 0x0
11     8240:    e50b300c     str    r3, [fp, #-12]
12
13     fd = open("/dev/memdev0",O_RDWR);
14     8244:    e59f004c     ldr    r0, [pc, #76]    ; 8298 <main+0x70>
15     8248:    e3a01002     mov    r1, #2    ; 0x2
16     824c:    eb0028a3     bl    124e0 <__libc_open>
17     8250:    e1a03000     mov    r3, r0
18     8254:    e50b3008     str    r3, [fp, #-8]
19     read(fd,&test,sizeof(int));
20     8258:    e24b300c     sub    r3, fp, #12    ; 0xc
21     825c:    e51b0008     ldr    r0, [fp, #-8]
22     8260:    e1a01003     mov    r1, r3
23     8264:    e3a02004     mov    r2, #4    ; 0x4
24     8268:    eb0028e4     bl    12600 <__libc_read>//我们的read函数最终调用了__libc_read
25
26     printf("the test is %d\n",test);
27     826c:    e51b300c     ldr    r3, [fp, #-12]
28     8270:    e59f0024     ldr    r0, [pc, #36]    ; 829c <main+0x74>
29     8274:    e1a01003     mov    r1, r3
30     8278:    eb000364     bl    9010 <_IO_printf>
31
32     close(fd);
33     827c:    e51b0008     ldr    r0, [fp, #-8]
34     8280:    eb0028ba     bl    12570 <__libc_close>
35     return 0;
36     8284:    e3a03000     mov    r3, #0    ; 0x0
37 }

上面我们发现read最终调用了__libc_read函数我们继续在汇编代码中找到该函数

 1 00012600 <__libc_read>:
 2    12600:    e51fc028     ldr    ip, [pc, #-40]    ; 125e0 <__libc_close+0x70>
 3    12604:    e79fc00c     ldr    ip, [pc, ip]
 4    12608:    e33c0000     teq    ip, #0    ; 0x0
 5    1260c:    1a000006     bne    1262c <__libc_read+0x2c>
 6    12610:    e1a0c007     mov    ip, r7
 7    12614:    e3a07003     mov    r7, #3    ; 0x3
 8    12618:    ef000000     svc    0x00000000
 9    1261c:    e1a0700c     mov    r7, ip
10    12620:    e3700a01     cmn    r0, #4096    ; 0x1000
11    12624:    312fff1e     bxcc    lr
12    12628:    ea0008b4     b    14900 <__syscall_error>
13    1262c:    e92d408f     push    {r0, r1, r2, r3, r7, lr}
14    12630:    eb0003b9     bl    1351c <__libc_enable_asynccancel>
15    12634:    e1a0c000     mov    ip, r0
16    12638:    e8bd000f     pop    {r0, r1, r2, r3}
17    1263c:    e3a07003     mov    r7, #3    ; 0x3//系统调用标号,一会解释大家先记主
18    12640:    ef000000     svc    0x00000000
19    12644:    e1a07000     mov    r7, r0
20    12648:    e1a0000c     mov    r0, ip
21    1264c:    eb000396     bl    134ac <__libc_disable_asynccancel>
22    12650:    e1a00007     mov    r0, r7
23    12654:    e8bd4080     pop    {r7, lr}
24    12658:    e3700a01     cmn    r0, #4096    ; 0x1000
25    1265c:    312fff1e     bxcc    lr
26    12660:    ea0008a6     b    14900 <__syscall_error>
27    12664:    e1a00000     nop            (mov r0,r0)
28    12668:    e1a00000     nop            (mov r0,r0)
29    1266c:    e1a00000     nop            (mov r0,r0)

在上面代码中大部分汇编指令都知道用法,但是svc调用引起注意,通过查阅资料才发现,我们应用程序通过svc 0x00000000可以产生异常,进入内核空间。

然后呢,系统处理异常,这中间牵扯好多代码还有中断的一些知识,我们找时间在专门分析,总之经过一大堆的处理最后它会跳到entry-common.S中的下面代码

 1     .align    5
 2 ENTRY(vector_swi)
 3     sub    sp, sp, #S_FRAME_SIZE
 4     stmia    sp, {r0 - r12}            @ Calling r0 - r12
 5  ARM(    add    r8, sp, #S_PC        )
 6  ARM(    stmdb    r8, {sp, lr}^        )    @ Calling sp, lr
 7  THUMB(    mov    r8, sp            )
 8  THUMB(    store_user_sp_lr r8, r10, S_SP    )    @ calling sp, lr
 9     mrs    r8, spsr            @ called from non-FIQ mode, so ok.
10     str    lr, [sp, #S_PC]            @ Save calling PC
11     str    r8, [sp, #S_PSR]        @ Save CPSR
12     str    r0, [sp, #S_OLD_R0]        @ Save OLD_R0
13     zero_fp
14
15     /*
16      * Get the system call number.
17      */
18
19 #if defined(CONFIG_OABI_COMPAT)
20
21     /*
22      * If we have CONFIG_OABI_COMPAT then we need to look at the swi
23      * value to determine if it is an EABI or an old ABI call.
24      */
25 #ifdef CONFIG_ARM_THUMB
26     tst    r8, #PSR_T_BIT
27     movne    r10, #0                @ no thumb OABI emulation
28     ldreq    r10, [lr, #-4]            @ get SWI instruction
29 #else
30     ldr    r10, [lr, #-4]            @ get SWI instruction
31   A710(    and    ip, r10, #0x0f000000        @ check for SWI        )
32   A710(    teq    ip, #0x0f000000                        )
33   A710(    bne    .Larm710bug                        )
34 #endif
35 #ifdef CONFIG_CPU_ENDIAN_BE8
36     rev    r10, r10            @ little endian instruction
37 #endif
38
39 #elif defined(CONFIG_AEABI)
40
41     /*
42      * Pure EABI user space always put syscall number into scno (r7).
43      */
44   A710(    ldr    ip, [lr, #-4]            @ get SWI instruction    )
45   A710(    and    ip, ip, #0x0f000000        @ check for SWI        )
46   A710(    teq    ip, #0x0f000000                        )
47   A710(    bne    .Larm710bug                        )
48
49 #elif defined(CONFIG_ARM_THUMB)
50
51     /* Legacy ABI only, possibly thumb mode. */
52     tst    r8, #PSR_T_BIT            @ this is SPSR from save_user_regs
53     addne    scno, r7, #__NR_SYSCALL_BASE    @ put OS number in
54     ldreq    scno, [lr, #-4]
55
56 #else
57
58     /* Legacy ABI only. */
59     ldr    scno, [lr, #-4]            @ get SWI instruction
60   A710(    and    ip, scno, #0x0f000000        @ check for SWI        )
61   A710(    teq    ip, #0x0f000000                        )
62   A710(    bne    .Larm710bug                        )
63
64 #endif
65
66 #ifdef CONFIG_ALIGNMENT_TRAP
67     ldr    ip, __cr_alignment
68     ldr    ip, [ip]
69     mcr    p15, 0, ip, c1, c0        @ update control register
70 #endif
71     enable_irq
72
73     get_thread_info tsk
74     adr    tbl, sys_call_table        @ load syscall table pointer

该段代码中我们先会获取系统调用的标号刚才让大家记住的3,然后呢会去查找sys_call_table我们找到

1     .type    sys_call_table, #object
2 ENTRY(sys_call_table)
3 #include "calls.S"
4 #undef ABI
5 #undef OBSOLETE

在calls.S中我们找到了下面东西(列出部分)

 1 */
 2 /* 0 */        CALL(sys_restart_syscall)
 3         CALL(sys_exit)
 4         CALL(sys_fork_wrapper)
 5         CALL(sys_read)
 6         CALL(sys_write)
 7 /* 5 */        CALL(sys_open)
 8         CALL(sys_close)
 9         CALL(sys_ni_syscall)        /* was sys_waitpid */
10         CALL(sys_creat)
11         CALL(sys_link)

我们发现我们刚才记住的数字3刚好对应的是sys_read,在read_write.c中我们可以找到sys_read函数

 1 SYSCALL_DEFINE3(read, unsigned int, fd, char __user *, buf, size_t, count)
 2 {
 3     struct file *file;
 4     ssize_t ret = -EBADF;
 5     int fput_needed;
 6
 7     file = fget_light(fd, &fput_needed);
 8     if (file) {
 9         loff_t pos = file_pos_read(file);
10         ret = vfs_read(file, buf, count, &pos);//调用虚拟文件系统的read
11         file_pos_write(file, pos);
12         fput_light(file, fput_needed);
13     }
14
15     return ret;
16 }

关于SYSCALL_DEFINE3这个宏的解析大家可以去http://blog.csdn.net/p_panyuch/article/details/5648007 这篇文章查看,在此我就不分析了,我们继续找到vfs_read代码如下

 1 ssize_t vfs_read(struct file *file, char __user *buf, size_t count, loff_t *pos)
 2 {
 3     ssize_t ret;
 4
 5     if (!(file->f_mode & FMODE_READ))
 6         return -EBADF;
 7     if (!file->f_op || (!file->f_op->read && !file->f_op->aio_read))
 8         return -EINVAL;
 9     if (unlikely(!access_ok(VERIFY_WRITE, buf, count)))
10         return -EFAULT;
11
12     ret = rw_verify_area(READ, file, pos, count);
13     if (ret >= 0) {
14         count = ret;
15         if (file->f_op->read)//我们的文件读函数指针不为空
16             ret = file->f_op->read(file, buf, count, pos);//执行我们驱动中的读函数
17         else
18             ret = do_sync_read(file, buf, count, pos);
19         if (ret > 0) {
20             fsnotify_access(file);
21             add_rchar(current, ret);
22         }
23         inc_syscr(current);
24     }
25
26     return ret;
27 }

2.       过程总结

通过上面的分析我们已经了解的read函数的调用基本过程,下面我们将read函数的调用过程在进行总结:

1.        寻找svc异常总体入口,并进入内核空间

2.        取出系统调用的标号

3.        根据系统调用标号,在sys_call_table中找到对应的系统调用函数

4.        根据系统函数比如sys_read找到对应的虚拟文件系统的read

5.        虚拟文件系统在调用驱动的read。

至此我们的分析到此结束,当然整个过程中还有一部分异常处理没有说到,我们在分析中断的时候一块分析。

今天的分析到此结束,感谢大家的关注。

时间: 2024-10-07 05:31:26

Linux内核分析(六)----字符设备控制方法实现|揭秘系统调用本质的相关文章

《Linux内核分析》第四周学习总结

<Linux内核分析>第四周学习总结                         ——扒开系统调用的三层皮 姓名:王玮怡  学号:20135116 理论总结部分: 第一节 用户态.内核态和中断 一.用户态.内核态和中断处理过程 一般现代CPU都有几种不同的指令执行级别 1.用户态 在相应的低级执行状态下,代码的掌控范围会受到限制,此时为用户态. 2.内核态 在高执行级别下,代码可以执行特权指令,访问任意的物理地址,这种CPU执行级别就对应着内核态. 3.举例 Intel x86 CPU有四

Linux内核分析(五)----字符设备驱动实现

原文:Linux内核分析(五)----字符设备驱动实现 Linux内核分析(五) 昨天我们对linux内核的子系统进行简单的认识,今天我们正式进入驱动的开发,我们今后的学习为了避免大家没有硬件的缺陷,我们都会以虚拟的设备为例进行学习,所以大家不必害怕没有硬件的问题. 今天我们会分析到以下内容: 1.      字符设备驱动基础 2.      简单字符设备驱动实现 3.      驱动测试 l  字符设备基础 1.       字符设备描述结构 在linux2.6内核中,使用cdev结构体描述一

《Linux内核分析》第六周学习笔记

<Linux内核分析>第六周学习笔记 进程的描述和创建 郭垚 原创作品转载请注明出处 <Linux内核分析>MOOC课程http://mooc.study.163.com/course/USTC-1000029000 [学习视频时间:1小时 撰写博客时间:2小时] [学习内容:进程创建的过程.使用gdb跟踪分析内核处理函数sys_clone] 一.进程的描述 1.1 进程描述符task_struct数据结构(一) 1. 进程控制块PCB——task_struct 为了管理进程,内核

LINUX内核分析第六周学习总结——进程的描述和进程的创建

LINUX内核分析第六周学习总结——进程的描述和进程的创建 张忻(原创作品转载请注明出处) <Linux内核分析>MOOC课程http://mooc.study.163.com/course/USTC-1000029000 一.知识概要 进程的描述 进程描述符task_struct数据结构(一) 进程描述符task_struct数据结构(二) 进程的创建 进程的创建概览及fork一个进程的用户态代码 理解进程创建过程复杂代码的方法 浏览进程创建过程相关的关键代码 创建的新进程是从哪里开始执行的

《Linux内核分析》第六周学习小结

进程的描述和进程的创建 一.进程的描述 进程描述符task_struct数据结构: (1)操作系统的三大功能: 进程管理.内存管理.文件系统 (2)进程的作用: 将信号.进程间通信.内存管理和文件系统联系起来 (3)进程控制块PCB——task_struct数据结构 提供了内核需要了解的信息 (4)task_struct结构庞大,有400多行代码.包含了进程状态.内核堆栈等相关信息的定义. (5)Linux的进程和操作系统原理中描述的进程状态有所不同,实际内核中,就绪和运行状态都用TASK_RU

20135201李辰希《Linux内核分析》第六周 进程的描述与创建

李辰希 原创作品转载请注明出处 <Linux内核分析>MOOC课程http://mooc.study.163.com/course/USTC-1000029000 一.进程的描述 操作系统的三大管理功能: 进程管理(最重要的) 内存管理 文件系统 为了管理进程,内核必须对每个进程进行清晰的描述,进程描述符提供了内核所需了解的进程信息. 进程控制块PCB task_struct: 进程状态 进程打开的文件 进程优先级信息 task_struct总体数据结构的抽象: tty:控制台 fs:文件系统

《Linux内核分析》 第六节 分析Linux内核创建一个新进程的过程

范闻泽 + 原创作品转载请注明出处 + <Linux内核分析>MOOC课程http://mooc.study.163.com/course/USTC-1000029000 一.实验过程 1.删除原来的menu,并clone新的menu,用test_fork.c覆盖test.c 2. make rootfs之后新的内核启动,测试fork功能 3.使用-s -S冷冻内核,准备调试 4.设置断点 5.根据断点,进行跟踪,得到结果 1.以下是执行指令 cd LinuxKernel rm menu -r

Linux内核分析第六周作业

分析Linux内核创建一个新进程的过程 首先更新MenuOS的代码,加入调用fork的命令.吐槽一句,实验楼免费用户无法连网.还好只要去github复制一段代码即可 先观察一下fork命令的实现 1 int Fork(int argc, char *argv[]) 2 { 3 int pid; 4 /* fork another process */ 5 pid = fork(); 6 if (pid<0) 7 { 8 /* error occurred */ 9 fprintf(stderr,

Linux内核分析:实验六--Linux进程的创建过程分析

刘畅 原创作品转载请注明出处 <Linux内核分析>MOOC课程http://mooc.study.163.com/course/USTC-1000029000 概述 本次实验在MenuOS中加入fork系统调用,并通过GDB的调试跟踪,近距离的观察Linux中进程创建的过程.阅读Linux进程部分的源码,结合起来理解Linux内核创建新进程的过程. Linux中对进程的描述 Linux中task_struct结构体用于描述系统中的进程,对应x86机器的此结构体定义放在了/include/li