嵌入式Linux——应用调试:自制系统调用,并编写进程查看器

2019-07-12 15:10发布

简介:     本文主要讲解在ARM Linux中系统调用的原理,并根据这些原理在系统中添加自制的系统调用函数,最后我们还将通过自制的系统调用函数来查看应用程序指定位置的信息,用此方法实现应用程序的调试。    Linux内核:linux-2.6.22.6  所用开发板:JZ2440 V3(S3C2440A)   C库           :glibc-2.3.6 声明:     本文主要是看完韦东山老师的视频后所写,同时文中会引用一些网友文章中的观点来完善这方面的知识。我会将参考的文章在该文章的末尾标出。希望我的文章会对你有所帮助。 系统调用:     在计算机中,系统调用(英语:system call)指运行在用户空间的程序向操作系统内核请求需要更高权限运行的服务。系统调用提供用户程序与操作系统之间的接口。大多数系统交互式操作需求在内核态运行。如设备IO操作或者进程间通信。     我们用下图进行说明:     从上图中可以看出,应用程序可以通过系统调用接口来访问内核空间,同时也可以调用C库中的函数来访问系统调用接口然后调用内核中的函数。而本文主要介绍后面一种方式。我们在应用程序中使用open,read,write函数,然后在C库中将open,read,write函数解析为swi指令加对应的立即数。而不同的立即数对应不同的函数。而swi为软件中断,swi指令会导致CPU异常,然后进入内核异常处理模式。在内核异常处理模式中会保护在用户模式的现场,然后进入内核态在内核中根据导致异常的swi指令,并从中取出立即数找到open,read,write函数对应的sys_open,sys_read,sys_write函数。然后在这些函数中完成我们想要完成的事情。从而实现系统调用。     我们在上面大致的描述了系统调用的过程,下面我们用代码描述这个过程。我们在glibc中搜索swi会发现在sysdepsunixsysvlinuxarmrk.c中找到关于swi的指令语句: int __brk (void *addr) { void *newbrk; asm ("mov a1, %1 " /* save the argment in r0 */ "swi %2 " /* do the system call */ "mov %0, a1;" /* keep the return value */ : "=r"(newbrk) : "r"(addr), "i" (SYS_ify (brk)) : "a1"); __curbrk = newbrk; if (newbrk < addr) { __set_errno (ENOMEM); return -1; } return 0; }     而上面代码在C语言中加有汇编语句,而其中就有swi指令。我们在分析这个汇编指令之前先要对这个汇编指令的格式有一定的了解。我们首先要介绍的是上面汇编代码中的三个‘:’,第一个‘:’后面表示的是要输出的参数,第二个‘:’后面表示的是输入参数,而第三个‘:’后面表示的是会变更的参数。而从第一个‘:’开始依次往下参数递增,并分别用%0,%1·····表示。而当面的字母‘r’表示的是寄存器‘i’表示立即数。下面我们就可以分析这个函数了,在这个汇编代码中有mov a1, %1其中的%1就是下面的输入参数addr,所以翻译过来就是mov a1,addr将addr的值放入a1寄存器中。而swi %2中的%2为(SYS_ify (brk)),这要看SYS_ify 的宏定义了: #undef SYS_ify #define SWI_BASE (0x900000) #define SYS_ify(syscall_name) (__NR_##syscall_name)     在上面的宏中##表示连词符,所以他的意思为将__NR_与后面SYS_ify中的字符串连接起来。而在本例中为__NR_brk。而我们在内核代码中找到: #define __NR_brk (__NR_SYSCALL_BASE+ 45)     而__NR_SYSCALL_BASE的定义为 #define __NR_OABI_SYSCALL_BASE 0x900000 #define __NR_SYSCALL_BASE __NR_OABI_SYSCALL_BASE     所以回到上面的汇编指令:swi %2其实就是swi 900045 。而我们在内核的archarmkernelcalls.S文件中看到第45个函数就是sys_brk。     而在内核中sys_brk的函数原型为: asmlinkage unsigned long sys_brk(unsigned long brk) {······ }     了解了这些我们再看内核中对于swi指令是如何反应的,即在内核中如何根据swi中的值确定调用那个函数。我们先看韦东山老师书中的图:     上图中列出了异常向量和他们对应的处理函数我们看代码中的异常向量为: .align 5 .LCvswi: .word vector_swi .globl __stubs_end __stubs_end: .equ stubs_offset, __vectors_start + 0x200 - __stubs_start .globl __vectors_start __vectors_start: swi SYS_ERROR0 b vector_und + stubs_offset ldr pc, .LCvswi + stubs_offset b vector_pabt + stubs_offset b vector_dabt + stubs_offset b vector_addrexcptn + stubs_offset b vector_irq + stubs_offset b vector_fiq + stubs_offset .globl __vectors_end __vectors_end:     上面列出了各种异常的异常向量。我们这里主要看swi的异常为:   ldr pc, .LCvswi + stubs_offset     而LCvswi的定义为: .LCvswi: .word vector_swi     所以我们找vector_swi所对应的函数为(我将不重要的部分删除): ENTRY(vector_swi) /* 首先我们进入异常前要保存现场 */ sub sp, sp, #S_FRAME_SIZE stmia sp, {r0 - r12} @ Calling r0 - r12 add r8, sp, #S_PC stmdb r8, {sp, lr}^ @ 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. */ ldr scno, [lr, #-4] @ get SWI instruction获得swi指令,其中scno就是(system call number的缩写) A710( and ip, scno, #0x0f000000 @ check for SWI ) @检测是否是swi指令 A710( teq ip, #0x0f000000 ) A710( bne .Larm710bug ) enable_irq /* 使能中断 */ adr tbl, sys_call_table @ 将系统调用表放入到tbl中 cmp scno, #NR_syscalls @ 检测系统调用是否超出最大范围 adr lr, ret_fast_syscall @ 设置系统调用后的返回地址 ldrcc pc, [tbl, scno, lsl #2] @ 进入系统调用函数     上面代码已经说明了系统调用的过程。我们这里总结一下为: 1. 首先我们进入异常前要保存现场
2. 获得swi指令
3. 将系统调用表放入到tbl中
4. 检测系统调用是否超出最大范围
5. 设置系统调用后的返回地址
6. 进入系统调用函数
    我们接下来对上面的一些知识点进行说明,首先是 and ip, scno, #0x0f000000     我们为什么通过上面的比较就能确定这是不是一个swi指令那?那我们就要去2440中看一下swi的命令格式了。     从上面看出,swi指令的第24位到第27位全为1,所以用0x0f000000来判断他是否为swi指令。     而系统调用表我们就要看后面: .type sys_call_table, #object ENTRY(sys_call_table) #include "calls.S" #undef ABI #undef OBSOLETE     从上面看,系统调用表其实就是包含在archarmkernelcalls.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_signalfd) /* 350 */ CALL(sys_timerfd) CALL(sys_eventfd)     在上面的文件中列出了各种系统调用的函数。我们上面的sys_call_table中存放的就是这些函数,而我们的scno就对应着这些调用函数。我们现在看CALL(x)的定义。 .equ NR_syscalls,0 #define CALL(x) .equ NR_syscalls,NR_syscalls+1 #include "calls.S" #undef CALL #define CALL(x) .long x     从上面可以看出CALL(x)其实就是.equ NR_syscalls,NR_syscalls+1的宏定义,而他的意思是NR_syscalls自加一。也就是说我们程序中有多少个CALL(x)就可以用NR_syscalls表示。所以NR_syscalls为系统中系统调用函数的总和。这也可以解释我们为什么要用NR_syscalls检测scno是否超出最大范围:   cmp scno, #NR_syscalls @ 检测系统调用是否超出最大范围     之后我们就要调用系统调用函数了 ,我们这里以write函数为例。我们看如果他想实现函数调用要做哪些事。     首先我们一定要在archarmkernelcalls.S中加入write的CALL定义。来确保我们在上面汇编语句中能够找到有write这个选项。     接着我们就要真正的定义这个函数了,我们在fs ead_write.c中定义这个函数为: asmlinkage ssize_t sys_write(unsigned int fd, const 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_write(file, buf, count, &pos); file_pos_write(file, pos); fput_light(file, fput_needed); } return ret; }     函数定义完之后我们最后就是声明这个函数了,我们在includelinuxsyscalls.h中声明这个函数asmlinkage ssize_t sys_write(unsigned int fd, const char __user *buf, size_t count);     完成了这些我们就可以使用系统调用来调用这个函数了。 自制系统调用:     我们根据上面的介绍来写自制的系统调用。这里我们自制一个hello的系统调用。我们按着上面介绍write的步骤写这个系统调用。     首先,我们在archarmkernelcalls.S的末尾加上CALL(sys_hello),由于他为第352个CALL定义,所以他为352号。     然后我们去fs ead_write.c中模仿sys_write函数写sys_hello函数: asmlinkage void sys_hello(char __user * buf, size_t count) { char ker_buf[100]; if(buf){ copy_from_user(ker_buf,buf,count<100 ? count:100); ker_buf[99] = '