摘要: 利用objdump反汇编编译好的应用程序,分析了应用程序程序中调用read时时候,如何一层层的找到驱动中我们自己实现的对应的read方法。
一、反汇编编译好的应用程序
之前有一个读的应用程序read-mem.c,具体内容如下:
#include
#include
#include
#include
int main()
{
intfd = 0;
intdst = 0;
/*打开设备文件*/
fd= open("/dev/memdev0",O_RDWR);
/*写入数据*/
read(fd,&dst, sizeof(int));
printf("dstis %d\n",dst);
/*关闭设备*/
close(fd);
return0;
}
然后我们将其编译,在编译时候加上-g选项以及静态链接库。
#arm-linux-gcc –static –gread-mem.c –o read-mem
之后再对其进行反汇编,将结果输出到叫dump文件里看具体做了哪些事:
#arm-linux-objdump –D –S read-mem>dump
二、阅读dump文件
因为整个反汇编文件还是挺多的,我们只需要找到main函数里面的read就可以了,在第119行:
/*写入数据*/
read(fd, &dst, sizeof(int));
8258: e24b300c sub r3,fp, #12 ; 0xc
825c: e51b0008 ldr r0,[fp, #-8]
8260: e1a01003 mov r1,r3
8264: e3a02004 mov r2,#4 ; 0x4
8268: eb0028e4 bl 12600<__libc_read>
这里汇编一行一行的去理解比较费时费劲,自己尝试了下,首先就要了解fp寄存器已经程序的堆栈问题,这里前面几行代码就是把传进来的三个参数做一个保存,最后一句是终点,跳到__libc_read里面去了,我们切过去看下,__libc_read,在第10888行,真是长啊,还在文件的上半部分。。。看来静态链接确实会大大的增加文件的大小。
由于__lib_read也比较长,前面部分主要是对当前状态的一个“保护”,r1和r7寄存器的一个堆栈操作,防止调用svc之后,回不来了,但是主要关注第10904开始这两行:
1263c: e3a07003 mov r7,#3 ; 0x3
12640: ef000000 svc 0x00000000
第一行代码,自己在之前系统调用那个帖子里已经分析过了,其实是把系统调用号3传给了arm的r7寄存器,然后调用svc这个指令,svc是一个系统调用指令,执行之后我们的pc指针就会从用户态,切到内核态来执行,到内核态的这个入口是固定的。到了内核态之后,内核就回去取r7当中的这个系统调用号。这里开始就是上一次我们分析的系统调用了,具体步骤再说一遍。
三、内核如何获取系统调号找到对应的系统调用
主要是以下两个文件实现:
1.entry-common.S
当用户空间进行系统调用,产生“swi”中断,这时候entry-common.S当中,在第187行:
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_lrr8, 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
会对swi进行响应,因为响应之后,我们要切换到内核去执行系统调用,所以这里先对当前用户态执行的一些寄存器和状态进行堆栈操作,保存r0到r12的值,保存PC,CPSR,和OLD_R0。然后下面从第226行到第249行,会根据不同的情况进行具体的操作,我们这里是从r7当中获取系统调用号:
/*
* Pure EABI user space always put syscall number into scno(r7).
*/
A710( ldr ip, [lr, #-4] @get SWI instruction )
A710( and ip, ip, #0x0f000000 @ check for SWI )
A710( teq ip, #0x0f000000 )
A710( bne .Larm710bug )
#elifdefined(CONFIG_ARM_THUMB)
/* Legacy ABI only, possibly thumb mode. */
tst r8,#PSR_T_BIT @ this is SPSR fromsave_user_regs
addne scno, r7, #__NR_SYSCALL_BASE @ put OS number in
ldreq scno,[lr, #-4]
#else
/* Legacy ABI only. */
ldr scno,[lr, #-4] @ get SWI instruction
A710( and ip, scno, #0x0f000000 @ check for SWI )
A710( teq ip, #0x0f000000 )
A710( bne .Larm710bug )
#endif
然后在259行,载入了我们的系统调用表:
adr tbl, sys_call_table @ load syscall table pointer
这里sys_call_table在第470行:
.type sys_oabi_call_table, #object
ENTRY(sys_oabi_call_table)
#include "calls.S"
这时候指向了我们的calls.S文件。
2.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)
.........
/*355 */ CALL(sys_signalfd4)
CALL(sys_eventfd2)
CALL(sys_epoll_create1)
CALL(sys_dup3)
CALL(sys_pipe2)
/* 360*/ CALL(sys_inotify_init1)
CALL(sys_preadv)
CALL(sys_pwritev)
CALL(sys_rt_tgsigqueueinfo)
CALL(sys_perf_event_open)
这里面都是系统调用函数,我们有了调用号作为偏移地址,有了表格的基地址,那么就可以找到对应的系统调用。
3.unist.h
这里面也包含了各个系统调用的系统调用号,和calls.S是一致的。
#define__NR_restart_syscall (__NR_SYSCALL_BASE+ 0)
#define__NR_exit (__NR_SYSCALL_BASE+ 1)
#define__NR_fork (__NR_SYSCALL_BASE+ 2)
#define__NR_read (__NR_SYSCALL_BASE+ 3)
.. . . . .
#define__NR_preadv (__NR_SYSCALL_BASE+361)
#define__NR_pwritev (__NR_SYSCALL_BASE+362)
#define__NR_rt_tgsigqueueinfo (__NR_SYSCALL_BASE+363)
#define__NR_perf_event_open (__NR_SYSCALL_BASE+364)
其实说白了这个方法是通过用户空间传进来的read对应的系统调用号,内核根据这个号进行查表,找到sys_read。
四、sys_read
这个函数的原型在/fs/read_write.c里面,在第372行:
SYSCALL_DEFINE3(read, unsigned int, fd, char __user *, buf, size_t,count)
{
structfile *file;
ssize_tret = -EBADF;
intfput_needed;
file= fget_light(fd, &fput_needed);
if(file) {
loff_tpos = file_pos_read(file);
ret = vfs_read(file, buf, count, &pos);
file_pos_write(file,pos);
fput_light(file,fput_needed);
}
returnret;
}
这里名称变掉了,但是我们理解还是sys_read其实在老版本的linux内核里面,确实还是sys_read,但是由于在09年,随着大批量的64位处理器的出现,很多用户在调用的时候,无法填充64位的系统调用,就会被黑客利用,导致系统奔溃和权限升级,所以linux大牛们相处了一套通用的方法,开发出了这一套宏来避免这个bug,这个宏会对参数和系统调用名进行展开解析,从而变成我们需要的sys_read,具体如何做到的,网上有帖子,可以去参看。这里着重关心红色字体ret = vfs_read(file, buf, count, &pos);也就是又调用了一个vfs_read,切过去看。
五、vfs_read
还是在/fs/read-write.c里面,第277行:
ssize_t vfs_read(struct file *file, char __user *buf, size_tcount, loff_t *pos)
{
ssize_tret;
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->f_path.dentry);
add_rchar(current,ret);
}
inc_syscr(current);
}
return ret;
}
首先看函数参数,和我们的read是一致的,然后下面一行红字,判断file结构体里面对应的file_operations里面我们驱动当中的read是否存在,存在的的话就
ret = file->f_op->read(file, buf, count, pos);
调用我们驱动里面实现的read方法,然后一层层的返回过来,给ret,整个这个函数的返回值就用这个ret作为返回值。
六、总结
应用程序read->调用__libc_read->把调用号给r7,产生swi中断,执行svc指令切换到内核->内核读走调用号,与系统调用号表格查找对比找出是哪个系统调用(entry-common.S和calls.S)->调用SYSCALL_DEFINE3及sys_read->调用vfs_read->vfs_read调用与file绑定的file_operations里面的具体的我们实现的驱动的read->返回值给用户空间,结束!
这篇帖子就总结到这里,如有不正确的地方,还请指出,大家共同进步!