引:我们知道每一个字符设备在内核中都有一个cdev结构来描述之,而这个结构比较重要的一个成员就是
const struct file_operations *ops;
该结构的作用是将用户程序中的系统调用和驱动程序中的具体实现函数一一对应起来。当在用户程序中对一个字符设备文件调用某一系统调用时,就知道该对这个字符设备调用哪个具体的函数,但是问题来了,下面看两个函数原型:
//这是read系统调用的原型 ssize_t read(int fd, void *buf, size_t count);
//这是file_operations中对应read的函数原型 ssize_t (*read) (struct file *, char __user *, size_t, loff_t *);
可以看出,这两个函数的接口是不一样的,所以从系统调用到驱动中的具体实现,内核肯定是做了一些手脚的,下面我们就通过代码来分析从系统调用到驱动中具体实现函数的历程。
//这是一个简单的从文件中读取数据的程序,我们主要通过它来分析read系统调用的历程 #include <stdio.h> #include <sys/types.h> #include <sys/stat.h> #include <fcntl.h> #include <unistd.h> int main(int argc, char const *argv[]) { int fd; int temp; if((fd = open("/dev/memdev0", O_RDWR)) == -1) printf("open failed at line %d ", __LINE__); read(fd, &temp, sizeof(int)); printf("read failed at line %d ", __LINE__); printf("buffer is %d ", temp); close(fd); return 0; }
编译上面的代码,注意一定要采用静态编译,否则在之后的反汇编中会失去很重要的信息。
然后反汇编,导入一个dump文件中下面是read函数对应的汇编代码
read(fd, &temp, sizeof(int)); 8268: e24b300c sub r3, fp, #12 ; 0xc 826c: e51b0008 ldr r0, [fp, #-8] 8270: e1a01003 mov r1, r3 8274: e3a02004 mov r2, #4 ; 0x4 8278: eb0028e8 bl 12620 <__libc_read>
开始是传参数,最后一句跳转到了__libc_read标号处,继续跟踪
00012620 <__libc_read>: 12620: e51fc028 ldr ip, [pc, #-40] ; 12600 <__libc_close+0x70> 12624: e79fc00c ldr ip, [pc, ip] 12628: e33c0000 teq ip, #0 ; 0x0 1262c: 1a000006 bne 1264c <__libc_read+0x2c> 12630: e1a0c007 mov ip, r7 12634: e3a07003 mov r7, #3 ; 0x3 12638: ef000000 svc 0x00000000 1263c: e1a0700c mov r7, ip 12640: e3700a01 cmn r0, #4096 ; 0x1000 12644: 312fff1e bxcc lr 12648: ea0008b4 b 14920 <__syscall_error> 1264c: e92d408f push {r0, r1, r2, r3, r7, lr} 12650: eb0003b9 bl 1353c <__libc_enable_asynccancel> 12654: e1a0c000 mov ip, r0 12658: e8bd000f pop {r0, r1, r2, r3} 1265c: e3a07003 mov r7, #3 ; 0x3 12660: ef000000 svc 0x00000000 12664: e1a07000 mov r7, r0 12668: e1a0000c mov r0, ip 1266c: eb000396 bl 134cc <__libc_disable_asynccancel> 12670: e1a00007 mov r0, r7 12674: e8bd4080 pop {r7, lr} 12678: e3700a01 cmn r0, #4096 ; 0x1000 1267c: 312fff1e bxcc lr 12680: ea0008a6 b 14920 <__syscall_error> 12684: e1a00000 nop (mov r0,r0) 12688: e1a00000 nop (mov r0,r0) 1268c: e1a00000 nop (mov r0,r0)
代码很多,不过重要的只有两行,已经红色高亮显示出来了,具体做的事就是将3存入了r7寄存器中,然后通过svc指令将PC指针由用户空间从一个固定的入口跳入内核空间,之后内核根据r7中的编号,查表后执行对应的系统调用。在linux2.6.39版本的内核中,该表位于calls.S这个文件中
//这是该文件的部分代码 /* 0 */ CALL(sys_restart_syscall) CALL(sys_exit) CALL(sys_fork_wrapper) CALL(sys_read) CALL(sys_write)
可知用户空间中的read其实是调用了内核空间中的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); file_pos_write(file, pos); fput_light(file, fput_needed); } return ret; }
分析:我们知道没一个打开的文件在内核中都有一个file结构体来维护,这里就调用fget_light通过read函数提供的fd得到了该文件的file结构体,然后调用了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 >= 0) { 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 > 0) { fsnotify_access(file); add_rchar(current, ret); } inc_syscr(current); } return ret; }
同样,最重要的代码已经高亮显示出来了,这就是整个历程最重要的一句,它调用了file里的成员函数f_op->read。在驱动初始化的时候就将file_operations结构体赋值给了这个驱动对应的设备文件的成员f_op,所以这里就掉用到了最终的驱动程序中的具体实现函数:xxx_read。其实红色的话我目前是找不到内核代码来证明的,不过逻辑就是这样,而且作为新手在学习驱动期间不要过分的去看内核代码,得不偿失。不过哪位大神如果能够找到相关的代码,感激不尽!
如有疑问或错误,欢迎讨论!转载请注明出处!