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

时间:2022-12-10 05:54:14

原文: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(我今后会将自己博文中提到的代码都放在这个仓库中)

 long mem_ioctl(struct file *fd, unsigned int cmd, unsigned long arg){
switch(cmd){
case MEM_RESTART:
printk("<0> memdev is restart");
break;
default:
return -EINVAL;
}
return ;

揭秘系统调用本质

由于我自己的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函数的汇编实现

  int main(void)
{
: e92d4800 push {fp, lr}
822c: e28db004 add fp, sp, # ; 0x4
: e24dd008 sub sp, sp, # ; 0x8
int fd = ;
: e3a03000 mov r3, # ; 0x0
: e50b3008 str r3, [fp, #-]
int test = ;
823c: e3a03000 mov r3, # ; 0x0
: e50b300c str r3, [fp, #-] fd = open("/dev/memdev0",O_RDWR);
: e59f004c ldr r0, [pc, #] ; <main+0x70>
: e3a01002 mov r1, # ; 0x2
824c: eb0028a3 bl 124e0 <__libc_open>
: e1a03000 mov r3, r0
: e50b3008 str r3, [fp, #-]
read(fd,&test,sizeof(int));
: e24b300c sub r3, fp, # ; 0xc
825c: e51b0008 ldr r0, [fp, #-]
: e1a01003 mov r1, r3
: e3a02004 mov r2, # ; 0x4
: eb0028e4 bl <__libc_read>//我们的read函数最终调用了__libc_read printf("the test is %d\n",test);
826c: e51b300c ldr r3, [fp, #-]
: e59f0024 ldr r0, [pc, #] ; 829c <main+0x74>
: e1a01003 mov r1, r3
: eb000364 bl <_IO_printf> close(fd);
827c: e51b0008 ldr r0, [fp, #-]
: eb0028ba bl <__libc_close>
return ;
: e3a03000 mov r3, # ; 0x0
}

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

  <__libc_read>:
: e51fc028 ldr ip, [pc, #-] ; 125e0 <__libc_close+0x70>
: e79fc00c ldr ip, [pc, ip]
: e33c0000 teq ip, # ; 0x0
1260c: 1a000006 bne 1262c <__libc_read+0x2c>
: e1a0c007 mov ip, r7
: e3a07003 mov r7, # ; 0x3
: ef000000 svc 0x00000000
1261c: e1a0700c mov r7, ip
: e3700a01 cmn r0, # ; 0x1000
: 312fff1e bxcc lr
: ea0008b4 b <__syscall_error>
1262c: e92d408f push {r0, r1, r2, r3, r7, lr}
: eb0003b9 bl 1351c <__libc_enable_asynccancel>
: e1a0c000 mov ip, r0
: e8bd000f pop {r0, r1, r2, r3}
1263c: e3a07003 mov r7, # ; 0x3//系统调用标号,一会解释大家先记主
: ef000000 svc 0x00000000
: e1a07000 mov r7, r0
: e1a0000c mov r0, ip
1264c: eb000396 bl 134ac <__libc_disable_asynccancel>
: e1a00007 mov r0, r7
: e8bd4080 pop {r7, lr}
: e3700a01 cmn r0, # ; 0x1000
1265c: 312fff1e bxcc lr
: ea0008a6 b <__syscall_error>
: e1a00000 nop (mov r0,r0)
: e1a00000 nop (mov r0,r0)
1266c: e1a00000 nop (mov r0,r0)

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

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

     .align
ENTRY(vector_swi)
sub sp, sp, #S_FRAME_SIZE
stmia sp, {r0 - r12} @ Calling r0 - r12
ARM( add r8, sp, #S_PC )
ARM( stmdb r8, {sp, lr}^ ) @ Calling sp, lr
THUMB( mov r8, sp )
THUMB( store_user_sp_lr r8, r10, S_SP ) @ calling sp, lr
mrs r8, spsr @ called from non-FIQ mode, so ok.
str lr, [sp, #S_PC] @ Save calling PC
str r8, [sp, #S_PSR] @ Save CPSR
str r0, [sp, #S_OLD_R0] @ Save OLD_R0
zero_fp /*
* Get the system call number.
*/ #if defined(CONFIG_OABI_COMPAT) /*
* If we have CONFIG_OABI_COMPAT then we need to look at the swi
* value to determine if it is an EABI or an old ABI call.
*/
#ifdef CONFIG_ARM_THUMB
tst r8, #PSR_T_BIT
movne r10, # @ no thumb OABI emulation
ldreq r10, [lr, #-] @ get SWI instruction
#else
ldr r10, [lr, #-] @ get SWI instruction
A710( and ip, r10, #0x0f000000 @ check for SWI )
A710( teq ip, #0x0f000000 )
A710( bne .Larm710bug )
#endif
#ifdef CONFIG_CPU_ENDIAN_BE8
rev r10, r10 @ little endian instruction
#endif #elif defined(CONFIG_AEABI) /*
* Pure EABI user space always put syscall number into scno (r7).
*/
A710( ldr ip, [lr, #-] @ get SWI instruction )
A710( and ip, ip, #0x0f000000 @ check for SWI )
A710( teq ip, #0x0f000000 )
A710( bne .Larm710bug ) #elif defined(CONFIG_ARM_THUMB) /* Legacy ABI only, possibly thumb mode. */
tst r8, #PSR_T_BIT @ this is SPSR from save_user_regs
addne scno, r7, #__NR_SYSCALL_BASE @ put OS number in
ldreq scno, [lr, #-] #else /* Legacy ABI only. */
ldr scno, [lr, #-] @ get SWI instruction
A710( and ip, scno, #0x0f000000 @ check for SWI )
A710( teq ip, #0x0f000000 )
A710( bne .Larm710bug ) #endif #ifdef CONFIG_ALIGNMENT_TRAP
ldr ip, __cr_alignment
ldr ip, [ip]
mcr p15, , ip, c1, c0 @ update control register
#endif
enable_irq get_thread_info tsk
adr tbl, sys_call_table @ load syscall table pointer

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

     .type    sys_call_table, #object
ENTRY(sys_call_table)
#include "calls.S"
#undef ABI
#undef OBSOLETE

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

 */
/* 0 */ CALL(sys_restart_syscall)
CALL(sys_exit)
CALL(sys_fork_wrapper)
CALL(sys_read)
CALL(sys_write)
/* 5 */ CALL(sys_open)
CALL(sys_close)
CALL(sys_ni_syscall) /* was sys_waitpid */
CALL(sys_creat)
CALL(sys_link)

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

 SYSCALL_DEFINE3(read, unsigned int, fd, char __user *, buf, size_t, count)
{
struct file *file;
ssize_t ret = -EBADF;
int fput_needed; file = fget_light(fd, &fput_needed);
if (file) {
loff_t pos = file_pos_read(file);
ret = vfs_read(file, buf, count, &pos);//调用虚拟文件系统的read
file_pos_write(file, pos);
fput_light(file, fput_needed);
} return ret;
}

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

 ssize_t vfs_read(struct file *file, char __user *buf, size_t count, loff_t *pos)
{
ssize_t ret; if (!(file->f_mode & FMODE_READ))
return -EBADF;
if (!file->f_op || (!file->f_op->read && !file->f_op->aio_read))
return -EINVAL;
if (unlikely(!access_ok(VERIFY_WRITE, buf, count)))
return -EFAULT; ret = rw_verify_area(READ, file, pos, count);
if (ret >= ) {
count = ret;
if (file->f_op->read)//我们的文件读函数指针不为空
ret = file->f_op->read(file, buf, count, pos);//执行我们驱动中的读函数
else
ret = do_sync_read(file, buf, count, pos);
if (ret > ) {
fsnotify_access(file);
add_rchar(current, ret);
}
inc_syscr(current);
} return ret;
}

2.       过程总结

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

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

2.        取出系统调用的标号

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

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

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

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

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