一 使用strace命令跟踪系统调用

使用方法

  • 打补丁
patch -p1 < ../xxx.patch
  • 配置strace && 编译
./configure --host=arm-linux CC=arm-linux-gcc
make

编译完成之后得到一个strace应用程序,拷贝到开发板上就可以使用了
使用方法:

./strace -o log.txt ./firstdrvtext on

log.txt是调试信息输出的文件,./firstdrvtext on是应用程序使用方法

简单介绍strace的原理

./strace(父进程) -o log.txt ./firstdrvtext on(子进程)

父进程启动一个子进程,子进程先判断是否被跟踪,如果是,则会发送给父进程一些信号,然后进入休眠,父进程根据信号值决定记录哪些数据,父进程处理完之后(如记录),子进程继续运行。

子进程里面的open,read,write(c库)函数会调用一条swi指令 swi val
open —- swi #val1
read —- swi #val2
write —- swi #val3
这条指令会导致系统swi异常,进入内核态,调用sys_open,sys_read …

  • 打开内核文件arch/arm/kernel/entry-common.s:
...
tst	ip, #_TIF_SYSCALL_TRACE		@ are we tracing syscalls?
bne	__sys_trace
// 测试标记为,看是否被跟踪,如果被跟踪,则调用	__sys_trace
...
  • __sys_trace:
/*
 * This is the really slow path.  We're going to be doing
 * context switches, and waiting for our parent to respond.
 */
__sys_trace:
	mov	r2, scno
	add	r1, sp, #S_OFF
	mov	r0, #0				@ trace entry [IP = 0]
	bl	syscall_trace

	adr	lr, __sys_trace_return		@ return address
	mov	scno, r0			@ syscall number (possibly new)
	add	r1, sp, #S_R0 + S_OFF		@ pointer to regs
	cmp	scno, #NR_syscalls		@ check upper syscall limit
	ldmccia	r1, {r0 - r3}			@ have to reload r0 - r3
	ldrcc	pc, [tbl, scno, lsl #2]		@ call sys_* routine
	b	2b

strace的其他用法

二 使用GDB调试应用程序

编译与安装

  • 配置 && 编译 && 安装
./configure --target=arm-linux --disable-werror
  • 查看安装目录
vi makefile
prefix = /usr/local
  • 安装目录修改当前目录下的tmp
mkdir tmp
make install prefix=$PWD/tmp
  • 配置 && 编译gdbserver,并复制到网络文件系统
cd gdb/gdbserver
./configure --host=arm-linux
make
cp gdbserver /work/fs.. /bin/gdbserver

GDB远程调试步骤

编译要调试的应用程序,编译时要加上-g选项

  • 在ARM板上
gdbserver 192.168.1.17:2345 ./test_debug
//  ip是arm板的ip,端口随便写
  • 在pc机上
/bin/arm-linux-gdb  ./test_debug
target remote 192.168.1.17:2345
  • 常用命令:
l: 查看源码
break main  /  break test_debug:31: 打断点	 //main函数打断点 /  31行打断点
c:继续执行
step :单步调试
print *p:输出
quit:退出
...

GDB的另一种使用方法

让程序在开发板上直接运行,当它发送错误时,令它产生core dump文件
然后使用gdb根据core dump文件找到发生错误的地方。

  • 在开发板上输入
ulimit -c unlimited
  • 执行应用程序 ./test_debug 程序出错会在当前目录下生成名为core的文件

  • 拷贝core文件到pc上,pc机上执行:

cp core /work/xxx/core
/bin/arm-linux-gdb  ./test_debug  ./core

若程序出错,就可以看到很多调试信息。

三 配置内核,输出应用程序的段错误信息

配置内核,输出错误信息

  • 根据应用程序报错的信息在内核源码中搜索:
grep "Unable to handle kernel" * -nR
  • 找到并打开arch/arm/mm/fault.c,看到下面的函数:
/* 处理应用程序错误函数 */
static void
__do_user_fault(struct task_struct *tsk, unsigned long addr,
		unsigned int fsr, unsigned int sig, int code,
		struct pt_regs *regs)
{
	struct siginfo si;
	unsigned long ret;
	unsigned long val;
	int i =0;

#ifdef CONFIG_DEBUG_USER			// 1  配置内核
	if (user_debug & UDBG_SEGV) {   // 2 设置启动参数
		printk(KERN_DEBUG "%s: unhandled page fault (%d) at 0x%08lx, code 0x%03x\n",
		       tsk->comm, sig, addr, fsr);
		show_pte(tsk->mm, addr);
		show_regs(regs);
	/*  开始打印栈信息   */		
		printk("Stack: \n");
		while(i<1024)
			{	
				if(copy_from_user(&val,(const void __user *)(regs->ARM_sp+i*4),4)
					break;
				printk("%08x ",val);
				i++;
				if(i % 8 == 0)
					printk("\n");
			}
		printk("\nEnd of stack \n");
	/*  打印栈信息结束   */		
	}
#endif

	tsk->thread.address = addr;
	tsk->thread.error_code = fsr;
	tsk->thread.trap_no = 14;
	si.si_signo = sig;
	si.si_errno = 0;
	si.si_code = code;
	si.si_addr = (void __user *)addr;
	force_sig_info(sig, &si, tsk);
}
  • 配置内核 CONFIG_DEBUG_USER
make menuconfig 
/DEBUG_USER 	 //  搜索,配置成y
make uImage
  • uboot设置启动参数 user_debug
set bootargs user_debug=0xff ... ...
  • 执行./test_debug,可以看到调试信息和寄存器的值
  • 反汇编app
arm-linux-objdump -D test_debug > test_debug.dis

在反汇编中找到pc值处的代码,根据寄存器的值找到程序出错的原因

修改内核,打印栈信息

struct pt_regs 包含发生错误的瞬间所有寄存器的值:

struct pt_regs {
	long uregs[18];
};

#define ARM_cpsr	uregs[16]
#define ARM_pc		uregs[15]
#define ARM_lr		uregs[14]
#define ARM_sp		uregs[13]
#define ARM_ip		uregs[12]
#define ARM_fp		uregs[11]
#define ARM_r10		uregs[10]
#define ARM_r9		uregs[9]
#define ARM_r8		uregs[8]
#define ARM_r7		uregs[7]
#define ARM_r6		uregs[6]
#define ARM_r5		uregs[5]
#define ARM_r4		uregs[4]
#define ARM_r3		uregs[3]
#define ARM_r2		uregs[2]
#define ARM_r1		uregs[1]
#define ARM_r0		uregs[0]
#define ARM_ORIG_r0	uregs[17]

arch/arm/mm/__do_user_fault中加入下面的代码:

unsigned long ret;
unsigned long val;
int i =0;
printk("Stack: \n");
while(i<1024)
	{	
		/* 从堆栈指针sp处开始打印栈信息 */
		if(copy_from_user(&val,(const void __user *)(regs->ARM_sp+i*4),4)  
		break; 
		printk("%08x ",val);
		i++;
		if(i % 8 == 0)
			printk("\n");
	}
printk("\nEnd of stack \n");
  • 重新编译内核,复制到网络文件系统,并用新内核启动
make uImage 
cp arch/arm/uImage  /work/nfs_root/fs_mini_mdev/uImage
reboot
nfs 32000000 192.168.1.123:/work/.../uImage_new

main函数被动态库调用,怎么知道动态库的地址?

  • 方法1
ps						 //		查看进程ID
cat /proc/xxx(进程ID)
cat maps     			 //	    查看应用程序里面各段地址
  • 方法2,使用gdb调试,先启动gdb,使用命令info file查看程序里面的各个段

动态链接不容易看程序调用信息,把应用程序编译成静态链接的方式:

arm-linux-gcc -o test test.c -static

四 自制系统调用

ps:这种方法很少用,也比较繁琐,适合长时间打印调试信息

原理简介

app 调用open read write函数,导致:swi #val
程序跳转到vector_swi处去执行,得到“导致异常的指令”,取出里面的val,根据这个val调用对应的处理函数,如sys_open,sys_write,sys_read...
这些函数在一个数组里,通过查找数组调用对应的函数。

  • 仿照这种形式写一个自己的系统调用

自制系统调用

  • 写一个应用函数,仿glibc,用来触发系统调用

    仿照glibc里面的brk.c里面的_brk函数(c嵌入汇编的形式)

#include <errno.h>
#include <unistd.h>
#define __NR_OABI_SYSCALL_BASE	0x900000

//#if defined(__thumb__) || defined(__ARM_EABI__)
//#define __NR_SYSCALL_BASE	0
//#else
#define __NR_SYSCALL_BASE	__NR_OABI_SYSCALL_BASE
//#endif

void hello(char *buf ,int count )
{
	/* swi */
	 asm ("mov r0, %0\n"	/* save the argment in r0 */
          "mov r1, %1\n"	/* save the argment in r0 */
          "swi %2\n"		/* do the system call */
          : 
          : "r"(buf),"r"(count), "i" (__NR_SYSCALL_BASE + 352)
          : "r0","r1");		/* 过程中会改变的寄存器 */
}

void main(int argc , char ** argv)
{
	printf(" in app, call hello  \n");
	/* 执行hello函数触发一个swi指令异常 */
	hello("tjpuzh.top",10);
	
	return 0;
}
  • 修改内核,添加系统调用函数的声明和实现
  • 声明:
include\linux\syscalls.h中加入
asmlinkage ssize_t sys_hello(const char __user *buf,int count);
  • 实现:
在\fs\read_write.c中实现sys_hello
asmlinkage ssize_t sys_hello(const char __user * buf,int count)
{
	... ... 
	// 此处实现想要实现的功能,如open,read,write...
}
  • 添加
在 arch\arm\kernel\calls.S中,最后加入一项CALL(sys_hello)
  • 测试
make uImage
cp arch/arm/uImage /work/nfs_root/fs_mini_mdev/uImage
reboot
nfs 32000000 192.168.3.123:/work/nfs_root/fs_mini_mdev/uImage_hello
bootm 32000000

使用自制系统调用来打输出调试信息

整体流程

  • 修改应用程序的可执行文件,替换“某个位置”的代码为swi val
  • 执行程序
  • 执行到断点处,进入sys_hello
  • 在sys_hello,打印调试信息,执行原来的指令,返回

测试步骤

  • 写一个简单的应用程序 : debug_with_system_call.c
  • 替换
  1. 输出反汇编文件
arm-linux-objdump -D debug_with_system_call > debug_with_system_call.dis
  1. 根据适合打断点的指令,找到合适机器码

  2. 反汇编自制系统调用的测试程序,可以看到swi指令的机器码

arm-linux-objdump -D test_system_call > test_system_call.dis

  1. 修改应用程序的机器码为swi指令的机器码

保存为

debug_with_system_call_swi

加上执行权限:

sudo chmod +x debug_with_system_call_swi
  • 修改自制的sys_hello函数
#include <asm/processor.h>
asmlinkage ssize_t sys_hello(const char __user * buf,int count)
{
	int val;
	struct pt_regs *regs;
	/* 1 输出一些打印调试信息 */
	/* 应用程序的反汇编里:00010788 <cnt> */
	copy_from_user(&val,(const void __user *)0x00010788,4);
	printk("sys_hello:val = %d\n",val);
	/* 2 执行被替换的指令: add r3, r3, #2	; 0x2 */
	// 搜索pt_regs,在它的结果里再搜索current
	regs = task_pt_regs(current);
	regs->ARM_r3 += 2;
	/* 3 反回 */
	return;
}
  • 运行
./debug_with_system_call_swi

本博客所有文章除特别声明外,均采用 CC BY-SA 4.0 协议 ,转载请注明出处!

Linux文件操作 上一篇
call back机制 下一篇