应用调试:自制系统调用,并编写进程查看器

简介

本文主要讲解在ARM Linux中系统调用的原理,并根据这些原理在系统中添加自制的系统调用函数,最后我们还将通过自制的系统调用函数来查看应用程序指定位置的信息,用此方法实现应用程序的调试。

系统调用

在计算机中,系统调用(英语: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会发现在sysdeps\unix\sysv\linux\arm\brk.c中找到关于swi的指令语句:

int __brk (void *addr)
{
  void *newbrk;
 
  asm ("mov a1, %1\n"	/* save the argment in r0 */
       "swi %2\n"	/* 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 。而我们在内核的arch\arm\kernel\calls.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

从上面看,系统调用表其实就是包含在arch\arm\kernel\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_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函数为例。我们看如果他想实现函数调用要做哪些事。

首先我们一定要在arch\arm\kernel\calls.S中加入write的CALL定义。来确保我们在上面汇编语句中能够找到有write这个选项。

接着我们就要真正的定义这个函数了,我们在fs\read_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;
}

函数定义完之后我们最后就是声明这个函数了,我们在include\linux\syscalls.h中声明这个函数

asmlinkage ssize_t sys_write(unsigned int fd, const char __user *buf,
				size_t count);

完成了这些我们就可以使用系统调用来调用这个函数了。

自制系统调用

我们根据上面的介绍来写自制的系统调用。这里我们自制一个hello的系统调用。我们按着上面介绍write的步骤写这个系统调用。

首先,我们在arch\arm\kernel\calls.S的末尾加上CALL(sys_hello),由于他为第352个CALL定义,所以他为352号。

然后我们去fs\read_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] = '\0';
 
		printk("sys_hello: %s \n",ker_buf);
	}
}
EXPORT_SYMBOL_GPL(sys_hello);

最后,我们去include\linux\syscalls.h中声明这个函数:

asmlinkage void sys_hello(char __user * buf, size_t count)

 做完上面的事,我们就完成了自制的系统调用。现在我们就可以测试这个系统调用了。下面是测试应用程序:

#include <errno.h>
#include <unistd.h>
 
#define __NR_SYSCALL_BASE 0x900000
 
void hello(char *buf,int count)
{
	/* swi */
	asm("mov r0 ,%0 \n"
		"mov r1 ,%1 \n"
		"swi %2 \n"
		:
		:"r"(buf),"r"(count),"i"(__NR_SYSCALL_BASE + 352)
		:"r0","r1"
	);
}
 
int main(int argc,char **argv)
{
	printf("in app,call hello \n");
	hello("jia", 12);
 
	return 0;
}

 

使用自制系统调用编写进程查看器:

这里我们就要介绍对上面这个自制系统调用的使用了,我们将使用这个系统调用在我们的应用程序中打断点,然后将应用程序中一些重要的信息输出。下面简要的说明一下这个过程:

1. 修改应用的可执行文件,替换其中某个位置的机器码为我们编写的系统调用的机器码。

2. 执行这个可执行文件,当运行到这个指定的位置时,进入系统调用函数

3. 在系统调用函数中打印信息,最后补上我们代替的机器码的指令

4. 从系统调用中返回,执行原来的指令

下面我列出一个简单的测试应用程序:

#include <stdio.h>
#include <unistd.h>
 
int cnt = 0;
 
void C(void)
{
	int i = 0;
	while(1){
		printf("hello,cnt = %d, i = %d\n",cnt,i);
		cnt++;
		i += 2;  //在这里打断点
		sleep(2);
	}
}
 
void B(void)
{
	C();
}
 
void A(void)
{
	B();
}
 
int main(int argc,char **argv)
{
	A();
	return 0;
}

我们在上面程序的C函数中i += 2;处打断点,也就是说我们要在这个测试程序的可执行文件中用swi指令的机器码代替这句话的机器码。而为什么我们要选择代替i += 2;而不是其他的语句是因为这个语句简单,我们要在后面补上这条语句,所以他要越简单越容易替补。同时如果是复杂的语句可能要多条机器码,而我们的swi只替代一条。

我们将上面的程序编译获得可执行文件,并使用命令:arm-linux-objdump -D test_sc > test_sc.dis 获得测试程序的反汇编文件。在反汇编文件中找到i += 2;所对应的汇编语句为:

00008490 <C>:
    8490:	e1a0c00d 	mov	ip, sp
    8494:	e92dd800 	stmdb	sp!, {fp, ip, lr, pc}
    8498:	e24cb004 	sub	fp, ip, #4	; 0x4
    849c:	e24dd004 	sub	sp, sp, #4	; 0x4
    84a0:	e3a03000 	mov	r3, #0	; 0x0
    84a4:	e50b3010 	str	r3, [fp, #-16]
    84a8:	e59f3030 	ldr	r3, [pc, #48]	; 84e0 <.text+0x144>
    84ac:	e59f0030 	ldr	r0, [pc, #48]	; 84e4 <.text+0x148>
    84b0:	e5931000 	ldr	r1, [r3]
    84b4:	e51b2010 	ldr	r2, [fp, #-16]
    84b8:	ebffffb4 	bl	8390 <.text-0xc>
    84bc:	e59f201c 	ldr	r2, [pc, #28]	; 84e0 <.text+0x144>
    84c0:	e59f3018 	ldr	r3, [pc, #24]	; 84e0 <.text+0x144>
    84c4:	e5933000 	ldr	r3, [r3]
    84c8:	e2833001 	add	r3, r3, #1	; 0x1
    84cc:	e5823000 	str	r3, [r2]
    84d0:	e51b3010 	ldr	r3, [fp, #-16]
    84d4:	e2833002 	add	r3, r3, #2	; 0x2 ,该条语句为i += 2;的汇编代码
    84d8:	e50b3010 	str	r3, [fp, #-16]
    84dc:	eafffff1 	b	84a8 <C+0x18>
    84e0:	00010788 	andeq	r0, r1, r8, lsl #15
    84e4:	00008650 	andeq	r8, r0, r0, asr r6
	e1a0c00d 	mov	ip, sp
    8494:	e92dd800 	stmdb	sp!, {fp, ip, lr, pc}
    8498:	e24cb004 	sub	fp, ip, #4	; 0x4
    849c:	e24dd004 	sub	sp, sp, #4	; 0x4
    84a0:	e3a03000 	mov	r3, #0	; 0x0
    84a4:	e50b3010 	str	r3, [fp, #-16]
    84a8:	e59f3030 	ldr	r3, [pc, #48]	; 84e0 <.text+0x144>
    84ac:	e59f0030 	ldr	r0, [pc, #48]	; 84e4 <.text+0x148>
    84b0:	e5931000 	ldr	r1, [r3]
    84b4:	e51b2010 	ldr	r2, [fp, #-16]
    84b8:	ebffffb4 	bl	8390 <.text-0xc>
    84bc:	e59f201c 	ldr	r2, [pc, #28]	; 84e0 <.text+0x144>
    84c0:	e59f3018 	ldr	r3, [pc, #24]	; 84e0 <.text+0x144>
    84c4:	e5933000 	ldr	r3, [r3]
    84c8:	e2833001 	add	r3, r3, #1	; 0x1
    84cc:	e5823000 	str	r3, [r2]
    84d0:	e51b3010 	ldr	r3, [fp, #-16]
    84d4:	e2833002 	add	r3, r3, #2	; 0x2 ,该条语句为i += 2;的汇编代码
    84d8:	e50b3010 	str	r3, [fp, #-16]
    84dc:	eafffff1 	b	84a8 <C+0x18>
    84e0:	00010788 	andeq	r0, r1, r8, lsl #15
    84e4:	00008650 	andeq	r8, r0, r0, asr r6

 从上面看:

84d4:	e2833002 	add	r3, r3, #2	; 0x2 ,该条语句为i += 2;的汇编代码

为i += 2;的汇编代码,而这条语句的机器码为e2833002,因此我们要到可执行文件中找: 02 30 83 e2 的机器码。然后用swi指令替换,那么我们swi的机器码为多少那?这个就要看一下我们前面一个测试程序的反汇编文件了:

00008490 <hello>:
    8490:	e1a0c00d 	mov	ip, sp
    8494:	e92dd800 	stmdb	sp!, {fp, ip, lr, pc}
    8498:	e24cb004 	sub	fp, ip, #4	; 0x4
    849c:	e24dd008 	sub	sp, sp, #8	; 0x8
    84a0:	e50b0010 	str	r0, [fp, #-16]
    84a4:	e50b1014 	str	r1, [fp, #-20]
    84a8:	e51b2010 	ldr	r2, [fp, #-16]
    84ac:	e51b3014 	ldr	r3, [fp, #-20]
    84b0:	e1a00002 	mov	r0, r2
    84b4:	e1a01003 	mov	r1, r3
    84b8:	ef900160 	swi	0x00900160       ;swi语句
    84bc:	e24bd00c 	sub	sp, fp, #12	; 0xc
    84c0:	e89da800 	ldmia	sp, {fp, sp, pc}

 从上面看他的swi指令的汇编代码为:

84b8:	ef900160 	swi	0x00900160       ;swi语句

所以他的机器码为:ef900160,其中的第24位到27位为1表示为swi机器码,而0x900000表示ARM的系统调用基址,而0x160就是352,即sys_hello的函数排序。因此我们要用机器码:60 01 90 ef 代码可执行文件中的 02 30 83 e2 。改完这个我们就可以去sys_hello函数中编写代码来获得我们想要的信息了。例如我们将sys_hello函数改为:

asmlinkage void sys_hello(char __user * buf, size_t count)
{
	int val;
	struct pt_regs *regs;
	
	/* 1.输出一些调试信息 */
	/* 这里我们输出应用程序中的cnt值,在反汇编文件test_sc.dis中搜cnt的cnt的地址为0x00010788 */
	copy_from_user(&val, (const void __user *)0x000107c4,4);
	printk("sys_hello : cnt = %d \n",val);
 
	/* 2. 执行被替代的指令 */
	regs = task_pt_regs(current);
	regs->ARM_r3 += 2;
        /* 获得应用程序中C函数局部变量i的值 */
	copy_from_user(&val,(const void __user *)(regs->ARM_fp - 16),4);
	printk("sys_hello : i = %d \n",val);
 
	/* 3. 返回 */
}

我们从上面的程序中可以获得应用程序中全局变量cnt的值和C函数中局部变量i的值。我们下面介绍获得这两个值的方法。

首先我们介绍获得cnt值的方法,我们从反汇编文件test_sc.dis中搜cnt,从而得到cnt的地址为0x00010788

然后我们使用copy_from_user从用户空间获得cnt的值。并将它存入val中。

下面我们介绍获得了C函数中局部变量i的值的方法。我们从反汇编文件test_sc.dis中知道i的值是在i += 2;的汇编代码前来确定他所在寄存器的位置。所以我们看汇编代码,i的值存放在[fp, #-16]的位置。

 而我们通过宏:task_pt_regs获得进程的寄存器信息。

#define task_pt_regs(p) \
	((struct pt_regs *)(THREAD_START_SP + task_stack_page(p)) - 1)

所以我们使用代码:task_pt_regs(current);获得当前进程的寄存器信息。而当前进程就是发生swi前应用程序的进程。所以i的值可以通过copy_from_user(&val,(const void __user *)(regs->ARM_fp - 16),4);获得并将i值赋给val。

而上面的regs->ARM_r3 += 2;语句就是我们替换的语句i += 2;的替补,因为在汇编中他的代码为:add r3, r3, #2 ; 即r3寄存器中的值自加2,所以对应的C语句为:regs->ARM_r3 += 2;

通过上面修改我们就可以在系统调用中获得应用程序的信息了。

参考文章:

系统调用
ARM Linux系统调用详细分析
浅析基于ARM的Linux下的系统调用的实现
ARM Linux上的系统调用代码分析
Linux系统调用(syscall)原理
Linux系统调用过程
从glibc源码看系统调用原理 原
ARM Linux系统调用的原理
Arm Linux系统调用流程详细解析
42.Linux应用调试-初步制作系统调用(用户态->内核态)

已标记关键词 清除标记
相关推荐
©️2020 CSDN 皮肤主题: 数字20 设计师:CSDN官方博客 返回首页