嵌入式软件之裸板与驱动调试

目录

 

一、裸板调试

1.1 点灯法

1.2 串口调试

1.3 JTAG原理

1.4 JTAG调试

二、驱动调试

2.1 printk调试

2.1.1 printk原理

2.1.2 printk的使用

2.1.3 打印级别

2.2 打印至proc虚拟文件 

2.2.1 搭建框架

2.2.2 完善程序

2.2.3 完整程序

2.3 段错误分析

2.3.1 根据PC值找到导致错误的指令

2.3.2 根据栈信息找出函数调用关系

2.4 自制工具

2.5 修改内核定位系统僵死问题


一、裸板调试

1.1 点灯法

通过点亮LED来观察程序走到哪一步,汇编实现对LED点亮死循环

led_flicker:
			// 初始化LED
			ldr r0, =0x56000050  @ GPFCON
			ldr r1, =(1<<(4*2))
			str r1, [r0]
			
			// 循环点亮熄灭LED
			ldr r0, =0x56000054  @ GPFDAT
			ldr r1, =0
			ldr r2, =(1<<4)
loop:			
			str r1, [r0]   @ 点亮
			bl delay
			str r2, [r0]   @ 熄灭
			bl delay
			b loop

delay:
			ldr r3, =30000
1:
			sub r3, r3, #1
			cmp r3, #0
			bne 1b  @1b(backford)表示后面的1
			mov pc, lr

要调试的程序,此程序由链接文件实现了代码重定位,将代码定位到SDRAM中执行

@******************************************************************************
@ File:head.s
@ 功能:设置SDRAM,将程序复制到SDRAM,然后跳到SDRAM继续执行
@******************************************************************************       
  
.text
.global _start
_start:
                                            @函数disable_watch_dog, memsetup, init_nand, nand_read_ll在init.c中定义
            ldr     sp, =4096               @设置堆栈 
            bl      disable_watch_dog       @关WATCH DOG
            // 点灯
            bl      memsetup                @初始化SDRAM
            //b 		led_flicker
            bl      nand_init               @初始化NAND Flash
            //b 		led_flicker

                                            @将NAND Flash中地址4096开始的1024字节代码(main.c编译得到)复制到SDRAM中
                                            @nand_read_ll函数需要3个参数:
            ldr     r0,     =0x30000000     @1. 目标地址=0x30000000,这是SDRAM的起始地址
            mov     r1,     #0           	@2.  源地址   = 0
            mov     r2,     #4096           @3.  复制长度= 4096
            bl      nand_read               @调用C函数nand_read
            //b 		led_flicker

            ldr     sp, =0x34000000         @设置栈
            ldr     lr, =halt_loop          @设置返回地址
            ldr     pc, =main               @b指令和bl指令只能前后跳转32M的范围,所以这里使用向pc赋值的方法进行跳转
halt_loop:
            b       halt_loop

led_flicker:
			// 初始化LED
			ldr r0, =0x56000050  @ GPFCON
			ldr r1, =(1<<(4*2))
			str r1, [r0]
			
			// 循环点亮熄灭LED
			ldr r0, =0x56000054  @ GPFDAT
			ldr r1, =0
			ldr r2, =(1<<4)
loop:			
			str r1, [r0]   @ 点亮
			bl delay
			str r2, [r0]   @ 熄灭
			bl delay
			b loop

delay:
			ldr r3, =30000
1:
			sub r3, r3, #1
			cmp r3, #0
			bne 1b
			mov pc, lr

在关闭看门狗后执行点灯,能发现单板上的LED点亮,单板为S3C2440,在初始化SDRAM后执行点灯,发现LED没有闪烁,在memsetup中,根据反汇编来查看是否是位置无关码写的,位置无关码参考:S3C2440之代码重定位

/* WOTCH DOG register */
#define 	WTCON				(*(volatile unsigned long *)0x53000000)

/* SDRAM regisers */
#define 	MEM_CTL_BASE		0x48000000
 
void disable_watch_dog();
void memsetup();

/*上电后,WATCH DOG默认是开着的,要把它关掉 */
void disable_watch_dog()
{
	WTCON	= 0;
}

/* 设置控制SDRAM的13个寄存器 */
void memsetup()
{
	int 	i = 0;
	unsigned long *p = (unsigned long *)MEM_CTL_BASE;

    /* SDRAM 13个寄存器的值 */
    unsigned long  const    mem_cfg_val[]={ 0x22011110,     //BWSCON
                                            0x00000700,     //BANKCON0
                                            0x00000700,     //BANKCON1
                                            0x00000700,     //BANKCON2
                                            0x00000700,     //BANKCON3  
                                            0x00000700,     //BANKCON4
                                            0x00000700,     //BANKCON5
                                            0x00018005,     //BANKCON6
                                            0x00018005,     //BANKCON7
                                            0x008C07A3,     //REFRESH
                                            0x000000B1,     //BANKSIZE
                                            0x00000030,     //MRSRB6
                                            0x00000030,     //MRSRB7
                                    };

	for(; i < 13; i++)
		p[i] = mem_cfg_val[i];
	
}

其反汇编如下,mem_cfg_val局部变量存放在栈中,ip等于pc地址加8的地方取值,即300000f8的地方,值为3000060c,而mov    r4, ip  此指令就是问题所在,因为3000060c是SDRAM的地址,SDRAM还没有初始化,里面还什么都没有,因此有初始化的数组的时候是用的绝地地址,这是数组是位置无关,但是得到的数据来自SDRAM

30000098 <memsetup>:
30000098:	e92d40f0 	stmdb	sp!, {r4, r5, r6, r7, lr} 
3000009c:	e59fc054 	ldr	ip, [pc, #84]	; 300000f8 <.text+0xf8>
300000a0:	e1a0400c 	mov	r4, ip = 3000060c
300000a4:	e8b4000f 	ldmia	r4!, {r0, r1, r2, r3}
300000a8:	e3a05000 	mov	r5, #0	; 0x0
300000ac:	e3a07312 	mov	r7, #1207959552	; 0x48000000
300000b0:	e24dd034 	sub	sp, sp, #52	; 0x34
300000b4:	e1a0e00d 	mov	lr, sp
300000b8:	e8ae000f 	stmia	lr!, {r0, r1, r2, r3}
300000bc:	e8b4000f 	ldmia	r4!, {r0, r1, r2, r3}
300000c0:	e28d6034 	add	r6, sp, #52	; 0x34
300000c4:	e8ae000f 	stmia	lr!, {r0, r1, r2, r3}
300000c8:	e8b4000f 	ldmia	r4!, {r0, r1, r2, r3}
300000cc:	e594c000 	ldr	ip, [r4]
300000d0:	e8ae000f 	stmia	lr!, {r0, r1, r2, r3}
300000d4:	e58ec000 	str	ip, [lr]
300000d8:	e5163034 	ldr	r3, [r6, #-52]
300000dc:	e7873105 	str	r3, [r7, r5, lsl #2]
300000e0:	e2855001 	add	r5, r5, #1	; 0x1
300000e4:	e355000c 	cmp	r5, #12	; 0xc
300000e8:	e2866004 	add	r6, r6, #4	; 0x4
300000ec:	dafffff9 	ble	300000d8 <memsetup+0x40>
300000f0:	e28dd034 	add	sp, sp, #52	; 0x34
300000f4:	e8bd80f0 	ldmia	sp!, {r4, r5, r6, r7, pc}
300000f8:	3000060c 	andcc	r0, r0, ip, lsl #12

修改代码如下

/* WOTCH DOG register */
#define 	WTCON				(*(volatile unsigned long *)0x53000000)

/* SDRAM regisers */
#define 	MEM_CTL_BASE		0x48000000
 
void disable_watch_dog();
void memsetup();

/*上电后,WATCH DOG默认是开着的,要把它关掉 */
void disable_watch_dog()
{
	WTCON	= 0;
}

/* 设置控制SDRAM的13个寄存器 */
void memsetup()
{
	int 	i = 0;
	unsigned long *p = (unsigned long *)MEM_CTL_BASE;
#if 0
    /* SDRAM 13个寄存器的值 */
    unsigned long  const    mem_cfg_val[]={ 0x22011110,     //BWSCON
                                            0x00000700,     //BANKCON0
                                            0x00000700,     //BANKCON1
                                            0x00000700,     //BANKCON2
                                            0x00000700,     //BANKCON3  
                                            0x00000700,     //BANKCON4
                                            0x00000700,     //BANKCON5
                                            0x00018005,     //BANKCON6
                                            0x00018005,     //BANKCON7
                                            0x008C07A3,     //REFRESH
                                            0x000000B1,     //BANKSIZE
                                            0x00000030,     //MRSRB6
                                            0x00000030,     //MRSRB7
                                    };

	for(; i < 13; i++)
		p[i] = mem_cfg_val[i];
#endif		

	p[0] = 0x22011110;     //BWSCON
	p[1] = 0x00000700;     //BANKCON0
	p[2] = 0x00000700;     //BANKCON1
	p[3] = 0x00000700;     //BANKCON2
	p[4] = 0x00000700;     //BANKCON3  
	p[5] = 0x00000700;     //BANKCON4
	p[6] = 0x00000700;     //BANKCON5
	p[7] = 0x00018005;     //BANKCON6
	p[8] = 0x00018005;     //BANKCON7
	p[9] = 0x008C07A3;     //REFRESH
	p[10] = 0x000000B1;     //BANKSIZE
	p[11] = 0x00000030;     //MRSRB6
	p[12] = 0x00000030;     //MRSRB7
}

在nand_init后面加上了点灯,可以闪烁,但是在nand_init中引用了全局变量,而该全局变量nand_chip结构体没有初始值,在函数中直接对全局变了进行赋值,此前已经对SDRAM进行了初始化,因此在这里LED可以正常闪烁,在nand_read进行点灯调试不能闪烁,是因为代码拷贝的过程中把nand_chip的值覆盖掉,因此破坏了原本需要初始化nand的值,LED就不能正常闪烁,需要将变量全部改变为局部变量

1.2 串口调试

点灯法有个缺点,单板上的LED并不多,调试信息比较多,需要调试一次就烧写一次(硬件电路)

初始化串口,并用串口进行打印出字符,字符串和十六进制数

#define TXD0READY   (1<<2)
#define RXD0READY   (1)

#define PCLK            12000000    //没有设置时钟,因此时钟为晶振12M
#define UART_CLK        PCLK        //  UART0的时钟源设为PCLK
#define UART_BAUD_RATE  57600      // 波特率
#define UART_BRD        ((UART_CLK  / (UART_BAUD_RATE * 16)) - 1)

/*
 * 初始化UART0
 * 57600,8N1,无流控
 */
/*
 * 初始化UART0
 * 57600,8N1,无流控
 */
void uart0_init(void)
{
    GPHCON  |= 0xa0;    // GPH2,GPH3用作TXD0,RXD0
    GPHUP   = 0x0c;     // GPH2,GPH3内部上拉

    ULCON0  = 0x03;     // 8N1(8个数据位,无较验,1个停止位)
    UCON0   = 0x05;     // 查询方式,UART时钟源为PCLK
    UFCON0  = 0x00;     // 不使用FIFO
    UMCON0  = 0x00;     // 不使用流控
    UBRDIV0 = UART_BRD; // 波特率为115200
}

/*
 * 发送一个字符
 */
void putc(unsigned char c)
{
    /* 等待,直到发送缓冲区中的数据已经全部发送出去 */
    while (!(UTRSTAT0 & TXD0READY));
    
    /* 向UTXH0寄存器中写入数据,UART即自动将它发送出去 */
    UTXH0 = c;
}

void puts(char *s)
{
	int i = 0;
	while (s[i])
	{
		putc(s[i]);
		i++;
	}
}

void puthex(unsigned long val)
{
	/* val = 0x1234ABCD */
	unsigned char c;
	int i = 0;
	
	putc('0');
	putc('x');

	for (i = 0; i < 8; i++)
	{
		c = (val >> ((7-i)*4)) & 0xf;
		if ((c >= 0) && (c <= 9))
		{
			c = '0' + c;
		}
		else if ((c >= 0xA) && (c <= 0xF))
		{
			c = 'A' + (c -  0xA);
		}
		putc(c);
	}
}

增加打印信息,对于汇编来说,利用r0-r3进行传参,在memsetup用字符串不能打印,是因为字符串存在SDRAM中,此时的SDRAM还没有初始化

@******************************************************************************
@ File:head.s
@ 功能:设置SDRAM,将程序复制到SDRAM,然后跳到SDRAM继续执行
@******************************************************************************       
  
.text
.global _start
_start:
                                            @函数disable_watch_dog, memsetup, init_nand, nand_read_ll在init.c中定义
            ldr     sp, =4096               @设置堆栈 
            bl      disable_watch_dog       @关WATCH DOG
            bl      uart0_init
            mov     r0, #0x6d               @ 输出"m"
            bl      putc
            bl      memsetup                @初始化SDRAM
            mov     r0, #0x6e               @ 输出"n"
            bl      putc

            bl      nand_init               @初始化NAND Flash

            mov     r0, #0x72               @ 输出"r"
            bl      putc

                                            @将NAND Flash中地址4096开始的1024字节代码(main.c编译得到)复制到SDRAM中
                                            @nand_read_ll函数需要3个参数:
            ldr     r0,     =0x30000000     @1. 目标地址=0x30000000,这是SDRAM的起始地址
            mov     r1,     #0   	        @2.  源地址   = 0
            mov     r2,     #4096           @3.  复制长度= 4096
            bl      nand_read               @调用C函数nand_read

            mov     r0, #0x6f               @ 输出"o"
            bl      putc

            ldr     sp, =0x34000000         @设置栈
            ldr     lr, =halt_loop          @设置返回地址
            ldr     pc, =main               @b指令和bl指令只能前后跳转32M的范围,所以这里使用向pc赋值的方法进行跳转
halt_loop:
            b       halt_loop

在memsetup中进行打印,分析其反汇编,其mem_cfg_val存放在栈中,最终去30000774的地方取值,但是SDRAM还没有初始化

    1.栈是临时生成的
    2.局部变量也是临时生成的
    3.局部变量的初始值怎么得来
        a.简单的话从add,mov得来
        b.复杂的话,初始值存在在程序里(mem_cfg_val是const,因此被放在只读数据段中)
        函数一开始的部分,读出这些值,用来初始化局部变量

300000e0 <memsetup>:
						// sp = 4096
						
300000e0:	e92d45f0 	stmdb	sp!, {r4, r5, r6, r7, r8, sl, lr}
									

300000e4:	e59fc0f4 	ldr	ip, [pc, #244]	; 300001e0 <.text+0x1e0>
300000e8:	e1a0e00c 	mov	lr, ip  // lr = ip = 30000774
300000ec:	e8be000f 	ldmia	lr!, {r0, r1, r2, r3}
                        // 去30000774取值,存入r0-r3,本意是把
                        // 22011110 => r0
                        // 0x00000700 => r1
                        // 0x00000700 => r2
                        // 0x00000700 => r3


300000f0:	e3a06000 	mov	r6, #0	; 0x0
300000f4:	e3a08312 	mov	r8, #1207959552	; 0x48000000  // p 
300000f8:	e3a0a203 	mov	sl, #805306368	; 0x30000000  // mem

300000fc:	e24dd034 	sub	sp, sp, #52	; 0x34
                        // sp=sp-52=4016=0xFB0

30000100:	e1a0c00d 	mov	ip, sp // ip=0xFB0
30000104:	e8ac000f 	stmia	ip!, {r0, r1, r2, r3}
						// 把r0-r4这4个值存入栈


30000108:	e8be000f 	ldmia	lr!, {r0, r1, r2, r3}
						// 再从SDRAM里取4个值放入r0-r3
						// 

3000010c:	e1a0700d 	mov	r7, sp
30000110:	e8ac000f 	stmia	ip!, {r0, r1, r2, r3}
						// 把r0-r4这4个值存入栈

1.3 JTAG原理

调试裸板最好的方式就是JTAG调试器,在个人单板上2440上有个JTAG单元
    1.CPU发出的地址信号,数据信号都通过JTAG单元
    2.JTAG可以控制CPU,比如当addr(地址信号)=xxx停止CPU(硬件断点),当data(数据信号)=xxx停止CPU(软件断点),也可以让CPU重新运行,读各个寄存器
    3.让JTAG直接访问外设

JTAG原理图如下,通过JTAG调试器使PC和JTAG接口相连,电脑软件如ADS、Keil、OpenOCD发出控制命令给JTAG调试器,JTAG调试器:并口wiggler、JLink、OpenJTAG等,并口现在已经淘汰掉了,JLink、OpenJTAG用的是USB口,JTAG调试器将命令转化为JTAG信号发给CPU,可以设置地址数据等

ARM9有2个断点/观察点,CPU从0地址执行,想让它运行到地址A的时候停下来,两种方法:

    1.设置JTAG的比较器,让addr等于A,当CPU发出A地址时停止,这种方法称为硬件断点,
    2.设置JTAG的比较器,让data等于某一个值B,并且修改A地址的值为B,当CPU读入的值为B时停止,重新运行时,恢复A地址的原来值,运行,这种方法称为软件断点

对于硬件断点来说,一个程序只能打2个硬件断点,因为ARM9的JTAG单元只有2个断点/观察点(比较器),对于软件断点,软件断点可以设置无数个,前提是修改的地址A1、A2、A3...可写,因此软件调试无法调试Nor、ROM上的程序,而硬件断点可以

1.4 JTAG调试

此软件为openocd用gui做的界面,实质是在cmd中运行openocd.exe

在cmd输入telnet 127.0.0.1 4444,连接openocd,效果与上图点击telnet一致,怎么操纵openocd,输入help可以看到很多输出信息

输入halt命令可以停止CPU,输入reg可以查看寄存器,在2440上程序烧在可读可写的nand上前4k会拷贝到片内内存

mdw(memory display word):显示1个字,mww(memory write word):写一个字

> mdw 0                                                                                                                 
0x00000000: ffffffff                                                                                                    
> mww 0 0x1234567                                                                                                       
> mdw 0                                                                                                                 
0x00000000: 01234567                                                                                                    
> mww 0 0x12345678                                                                                                      
> mdw 0                                                                                                                
0x00000000: 12345678 

烧写bin程序,烧写前需要先停止CPU,放在0地址中,因此可以读出0地址的数据,与bin文件的数据一致,执行resume 0(从0地址开始执行)后,可以看到led在闪烁

> load_image led.bin 0                                                                                                 
1516 bytes written at address 0x00000000                                                                                
downloaded 1516 bytes in 0.073842s (20.049 KiB/s)  
resume 0 

在每次点灯的时候都会执行delay函数,我们在delay这里打断点,查看其反汇编可知,在地址11c设置断点

 ...

108:    e5933000     ldr    r3, [r3]
 10c:    e1833002     orr    r3, r3, r2
 110:    e5813000     str    r3, [r1]
 114:    e3a00b61     mov    r0, #99328    ; 0x18400
 118:    e2800e2a     add    r0, r0, #672    ; 0x2a0
 11c:    ebffffc5     bl    38 <delay>
 120:    e51b3010     ldr    r3, [fp, #-16]
 124:    e2833001     add    r3, r3, #1    ; 0x1
 128:    e50b3010     str    r3, [fp, #-16] 

...

#include "s3c2440_soc.h"

void delay(volatile int d)
{
	while (d--);
}

int main(void)
{
	int val = 0;  /* val: 0b000, 0b111 */
	int tmp;

	/* 设置GPFCON让GPF4/5/6配置为输出引脚 */
	GPFCON &= ~((3<<8) | (3<<10) | (3<<12));
	GPFCON |=  ((1<<8) | (1<<10) | (1<<12));

	/* 循环点亮 */
	while (1)
	{
		tmp = ~val;
		tmp &= 7;
		GPFDAT &= ~(7<<4);
		GPFDAT |= (tmp<<4);
		delay(100000);
		val++;
		if (val == 8)
			val =0;
		
	}

	return 0;
}

设置断点前需要停止CPU,加上hw为硬件断点,每resume一次在开发板可以看到LED循环点亮

> help bp                                                                                                               
bp <address> [<asid>]<length> ['hw'|'hw_ctx']                                                                                 
list or set hardware or software breakpoint                                                                       
rbp address                                                                                                                   
remove breakpoint
> halt                                                                                                                  
target state: halted                                                                                                    
target halted in ARM state due to debug-request, current mode: Undefined instruction                                    
cpsr: 0x000000db pc: 0x0000004c                                                                                         
MMU: disabled, D-Cache: disabled, I-Cache: disabled                                                                     
> bp 0x11c 4 hw                                                                                                         
breakpoint set at 0x0000011c                                                                                            
> resume                                                                                                                
target state: halted                                                                                                    
target halted in ARM state due to breakpoint, current mode: Undefined instruction                                       
cpsr: 0x800000db pc: 0x0000011c                                                                                         
MMU: disabled, D-Cache: disabled, I-Cache: disabled   
> rbp 0x11c

设置软件断点,会先把值读出来后改为特殊值,删除软件断点后又把值恢复回去

> bp 0x11c 4                                                                                                            
breakpoint set at 0x0000011c                                                                                            
> rbp 0x11c                                                                                                             
> mdw 0x11c                                                                                                             
0x0000011c: ebffffc5                                                                                                    
> bp 0x11c 4                                                                                                            
breakpoint set at 0x0000011c                                                                                            
> mdw 0x11c                                                                                                             
0x0000011c: deeedeee                                                                                                    
> resume                                                                                                                
target state: halted                                                                                                    
target halted in ARM state due to breakpoint, current mode: Undefined instruction                                       
cpsr: 0x600000db pc: 0x0000011c                                                                                         
MMU: disabled, D-Cache: disabled, I-Cache: disabled 

也可以一步一步运行,如step 0,从0地址开始运行,在汇编中0地址对r0寄存器进行赋值,根据reg查看寄存器可以看到r0赋值信息,再次step执行一步,poll命令查看CPU当前状态

> step 0                                                                                                                
target state: halted                                                                                                    
target halted in ARM state due to single-step, current mode: Undefined instruction                                      
cpsr: 0x600000db pc: 0x00000004                                                                                         
MMU: disabled, D-Cache: disabled, I-Cache: disabled  
> reg
(0) r0 (/32): 0x53000000 (dirty)                                                                                        
(1) r1 (/32): 0x00000000 (dirty)                                                                                        
(2) r2 (/32): 0x56000050                                                                                                
(3) r3 (/32): 0x56000050                                                                                                
(4) r4 (/32): 0x00000620                                                                                                
(5) r5 (/32): 0x32410002  

> poll                                                                                                                  
background polling: on                                                                                                  
TAP: s3c2440.cpu (enabled)                                                                                              
target state: halted                                                                                                    
target halted in ARM state due to breakpoint, current mode: Supervisor                                                  
cpsr: 0x800000d3 pc: 0x30000000                                                                                         
MMU: disabled, D-Cache: disabled, I-Cache: enabled   
...

00000000 <_start>:
   0:    e3a00453     mov    r0, #1392508928    ; 0x53000000
   4:    e3a01000     mov    r1, #0    ; 0x0
   8:    e5801000     str    r1, [r0]
   c:    e3a01000     mov    r1, #0    ; 0x0
  10:    e5910000     ldr    r0, [r1]
  14:    e5811000     str    r1, [r1]

...

 当disable_watch_dog执行完后pc赋值为lr,lr就是memsetup的地址8,根据JTAG调试reg命令可以看到lr寄存器的值

@******************************************************************************
@ File:head.s
@ 功能:设置SDRAM,将程序复制到SDRAM,然后跳到SDRAM继续执行
@******************************************************************************       
  
.text
.global _start
_start:
                                            @函数disable_watch_dog, memsetup, init_nand, nand_read_ll在init.c中定义
            ldr     sp, =4096               @设置堆栈 
            bl      disable_watch_dog       @关WATCH DOG
            bl      memsetup                @初始化SDRAM
            bl      nand_init               @初始化NAND Flash

                                            @将NAND Flash中地址4096开始的1024字节代码(main.c编译得到)复制到SDRAM中
                                            @nand_read_ll函数需要3个参数:
            ldr     r0,     =0x30000000     @1. 目标地址=0x30000000,这是SDRAM的起始地址
            mov     r1,     #0           	@2.  源地址   = 0
            mov     r2,     #4096           @3.  复制长度= 2048(bytes),对于本实验的main.c,这是足够了
            bl      nand_read               @调用C函数nand_read

            ldr     sp, =0x34000000         @设置栈
            ldr     lr, =halt_loop          @设置返回地址
            ldr     pc, =main               @b指令和bl指令只能前后跳转32M的范围,所以这里使用向pc赋值的方法进行跳转
halt_loop:
            b       halt_loop



----------------------------------------------
nand_elf:     file format elf32-littlearm

Disassembly of section .text:

30000000 <_start>:
30000000:	e3a0da01 	mov	sp, #4096	; 0x1000
30000004:	eb00000b 	bl	30000038 <disable_watch_dog>
30000008:	eb00000e 	bl	30000048 <memsetup>
3000000c:	eb0000c5 	bl	30000328 <nand_init>
30000010:	e3a00203 	mov	r0, #805306368	; 0x30000000
30000014:	e3a01a01 	mov	r1, #4096	; 0x1000
30000018:	e3a02b02 	mov	r2, #2048	; 0x800
3000001c:	eb00010e 	bl	3000045c <nand_read>
30000020:	e3a0d30d 	mov	sp, #872415232	; 0x34000000
30000024:	e59fe004 	ldr	lr, [pc, #4]	; 30000030 <.text+0x30>
30000028:	e59ff004 	ldr	pc, [pc, #4]	; 30000034 <.text+0x34>

3000002c <halt_loop>:
3000002c:	eafffffe 	b	3000002c <halt_loop>
30000030:	3000002c 	andcc	r0, r0, ip, lsr #32
30000034:	30000580 	andcc	r0, r0, r0, lsl #11

30000038 <disable_watch_dog>:
30000038:	e3a02000 	mov	r2, #0	; 0x0
3000003c:	e3a03453 	mov	r3, #1392508928	; 0x53000000
30000040:	e5832000 	str	r2, [r3]
30000044:	e1a0f00e 	mov	pc, lr
...

上面为命令行的调试对汇编要求比较高,还有源码级别的调试,在C程序中设置断点,在linux中有arm-linux-gdb来操作openocd,在windows中有arm-elf-gdb来操作openocd,在arm-linux-gdb和arm-elf-gdb上面还有eclipe的GUI界面前台

源码级别的调试有前提:

  1. 源码已经重定位好,处于它的链接地址,比如在源文件中有wait函数左侧双击来调断点,实质是根据wait函数的链接地址去修改内存(软断点) 
  2. 链接脚本lds里,程序的text段、data段、bss段等分开存放地址连续
  3. 被调试的程序ELF格式里含有"调试信息"(即编译时有-g选项)

如果程序的链接地址是SDRAM,使用openodcd初始化SDRAM,使用arm-linux-gdb/arm-elf-gdb下载程序

链接脚本:

SECTIONS {
    . = 0x30000000;
    .text          :   { 
        head.o(.text) 
        init.o(.text) 
        nand.o
        *(.text) 
    }
    .rodata ALIGN(4) : {*(.rodata)} 
    .data ALIGN(4) : { *(.data) }
    .bss ALIGN(4)  : { *(.bss)  *(COMMON) }
}

对于2440来说,由于程序需要重定位,因此需要由openocd来初始化SDRAM,并把程序下载到0x30000000中,在Makefile中加上-g选项

objs := head.o init.o nand.o main.o

nand.bin : $(objs)
    arm-linux-ld -Tnand.lds    -o nand_elf $^
    arm-linux-objcopy -O binary -S nand_elf $@
    arm-linux-objdump -D -m arm  nand_elf > nand.dis

%.o:%.c
    arm-linux-gcc -Wall -c -g -O2 -o $@ $<

%.o:%.S
    arm-linux-gcc -Wall -c -g -O2 -o $@ $<

clean:
    rm -f  nand.dis nand.bin nand_elf *.o

需要安装windows的编译环境,并且将bin目录设置为windows的环境变量,就可以在cmd中执行

首先需要利用openocd将初始化SDRAM的程序在内存中执行

> load_image init/init.bin 0                                                                                            
288 bytes written at address 0x00000000                                                                                 
downloaded 288 bytes in 0.017957s (15.662 KiB/s)                                                                        
> resume 0                                                                                                              
> halt                                                                                                                  
target state: halted                                                                                                    
target halted in ARM state due to debug-request, current mode: Supervisor                                               
cpsr: 0x200000d3 pc: 0x000000bc                                                                                         
MMU: disabled, D-Cache: disabled, I-Cache: enabled 

在cmd中执行arm-elf-gdb nand_elf,利用target remote 127.0.0.1:3333与openocd连接,load命令将elf烧写在30000000的位置,si命令执行一条汇编指令,打断点break main.c:21,在main.c文件中21行打断点,执行c命令(continue),执行到21行就会停止,执行l命令(list)会列出源代码,执行quit命令退出

D:\桌面\debug_with_jtag_gdb_eclipse>arm-elf-gdb nand_elf
GNU gdb 6.8.50.20080308-cvs
Copyright (C) 2008 Free Software Foundation, Inc.
License GPLv3+: GNU GPL version 3 or later <http://gnu.org/licenses/gpl.html>
This is free software: you are free to change and redistribute it.
There is NO WARRANTY, to the extent permitted by law.  Type "show copying"
and "show warranty" for details.
This GDB was configured as "--host=i686-pc-mingw32 --target=arm-elf"...
(gdb) target remote 127.0.0.1:3333
Remote debugging using 127.0.0.1:3333
0x00479f8c in ?? ()
(gdb) load
Loading section .text, size 0x12a0 lma 0x30000000
Loading section .rodata, size 0x34 lma 0x300012a0
Start address 0x30000000, load size 4820
Transfer rate: 26 KB/sec, 2410 bytes/write.
(gdb) si
stepi ignored. GDB will now fetch the register state from the target.

Program received signal SIGINT, Interrupt.
_start () at head.S:10
warning: Source file is more recent than executable.
10                  ldr     sp, =4096               @设置堆栈
Current language:  auto; currently asm

(gdb) break main.c:21
Breakpoint 1 at 0x3000127c: file main.c, line 21.
(gdb) c
Continuing.

Breakpoint 1, main () at main.c:21
21                      wait(30000);
(gdb) c
Continuing.

Breakpoint 1, main () at main.c:21
21                      wait(30000);
(gdb) l
16              unsigned long i = 0;
17
18              GPFCON = GPF4_out|GPF5_out|GPF6_out;            // 将LED1-3对应的GPF4/5/6三个引脚设为输出
19
20              while(1){
21                      wait(30000);
22                      GPFDAT = (~(i<<4));             // 根据i的值,点亮LED1-3
23                      if(++i == 8)
24                              i = 0;
25              }
(gdb) l
26
27              return 0;
28      }(gdb) quit

还可以用elipse来调试,elipse是arm-elf-gdb的封装,新建工程,输入工程名称以及调试文件的路径,不能带中文名称

在路径中设置为elf文件,路径中不能带中文名称

设置命令,连接openocd,从0地址开始执行,之后根据界面调试,可一步一步执行,查看寄存器等

二、驱动调试

2.1 printk调试

2.1.1 printk原理

u-boot设置参数:console=ttySAC0(2440硬件串口),将内核输出信息打印到串口中去,也可以再加入一项console=tty1(2440硬件LCD),同时将信息输出在串口和lcd上

内核用printk打印,肯定要发送到具体硬件上去,会调用硬件处理函数

内核会根据命令行参数console=ttySAC0(这是2440的一个串口)来找到对应的硬件处理函数

1.在内核源码中里搜索"console="这一项,在printk.c中发现以下语句,当内核处理参数时,调用console_setup来处理,add_preferred_console根据名为"ttySAC0"的控制台,先记录下来

static int __init console_setup(char *str)
{
	char name[sizeof(console_cmdline[0].name)];
	char *s, *options;
	int idx;

	/*
	 * Decode str into name, index, options.
	 */
	if (str[0] >= '0' && str[0] <= '9') {
		strcpy(name, "ttyS");
		strncpy(name + 4, str, sizeof(name) - 5);
	} else {
		strncpy(name, str, sizeof(name) - 1);
	}
	name[sizeof(name) - 1] = 0;
	if ((options = strchr(str, ',')) != NULL)
		*(options++) = 0;
#ifdef __sparc__
	if (!strcmp(str, "ttya"))
		strcpy(name, "ttyS0");
	if (!strcmp(str, "ttyb"))
		strcpy(name, "ttyS1");
#endif
	for (s = name; *s; s++)
		if ((*s >= '0' && *s <= '9') || *s == ',')
			break;
	idx = simple_strtoul(s, NULL, 10);
	*s = 0;

	add_preferred_console(name, idx, options);
	return 1;
}

2.在硬件驱动的入口函数里:drivers/serial/s3c2410.c注册了console

register_console(&s3c24xx_serial_console); 

static struct console s3c24xx_serial_console =
{
	.name		= S3C24XX_SERIAL_NAME,
	.device		= uart_console_device,
	.flags		= CON_PRINTBUFFER,
	.index		= -1,
	.write		= s3c24xx_serial_console_write,
	.setup		= s3c24xx_serial_console_setup
};

 

3.在printk.c中printk->vprintk,在vprintk先把输出信息放入临时BUFFER,Copy the output into log_buf把临时BUFFER里的数据稍作处理,再写入log_buf,比如printk("abc")会得到"<4>abc", 再写入log_buf,默认的级别为4,在linux中可以用dmesg命令把log_buf里的数据打印出来重现内核的输出信息,在release_console_sem中最终会调用硬件的write函数输出,输出前会先判断打印级别,如果msg_log_level < console_loglevel就用write输出,这个write函数就是上述中注册的console结构体重的write函数输出

asmlinkage int printk(const char *fmt, ...)
{
	va_list args;
	int r;

	va_start(args, fmt);
	r = vprintk(fmt, args);
	va_end(args);

	return r;
}

asmlinkage int vprintk(const char *fmt, va_list args)
{
	unsigned long flags;
	int printed_len;
	char *p;
	static char printk_buf[1024];
	static int log_level_unknown = 1;

	preempt_disable();
	if (unlikely(oops_in_progress) && printk_cpu == smp_processor_id())
		/* If a crash is occurring during printk() on this CPU,
		 * make sure we can't deadlock */
		zap_locks();

	/* This stops the holder of console_sem just where we want him */
	raw_local_irq_save(flags);
	lockdep_off();
	spin_lock(&logbuf_lock);
	printk_cpu = smp_processor_id();

	/* Emit the output into the temporary buffer */
	printed_len = vscnprintf(printk_buf, sizeof(printk_buf), fmt, args);
	/*
	 * Copy the output into log_buf.  If the caller didn't provide
	 * appropriate log level tags, we insert them here
	 */
	for (p = printk_buf; *p; p++) {
		if (log_level_unknown) {
                        /* log_level_unknown signals the start of a new line */
			if (printk_time) {
				int loglev_char;
				char tbuf[50], *tp;
				unsigned tlen;
				unsigned long long t;
				unsigned long nanosec_rem;

				/*
				 * force the log level token to be
				 * before the time output.
				 */
				if (p[0] == '<' && p[1] >='0' &&
				   p[1] <= '7' && p[2] == '>') {
					loglev_char = p[1];
					p += 3;
					printed_len -= 3;
				} else {
					loglev_char = default_message_loglevel
						+ '0';
				}
				t = printk_clock();
				nanosec_rem = do_div(t, 1000000000);
				tlen = sprintf(tbuf,
						"<%c>[%5lu.%06lu] ",
						loglev_char,
						(unsigned long)t,
						nanosec_rem/1000);

				for (tp = tbuf; tp < tbuf + tlen; tp++)
					emit_log_char(*tp);
				printed_len += tlen;
			} else {
				if (p[0] != '<' || p[1] < '0' ||
				   p[1] > '7' || p[2] != '>') {
					emit_log_char('<');
					emit_log_char(default_message_loglevel
						+ '0');
					emit_log_char('>');
					printed_len += 3;
				}
			}
			log_level_unknown = 0;
			if (!*p)
				break;
		}
		emit_log_char(*p);
		if (*p == '\n')
			log_level_unknown = 1;
	}
	if (!down_trylock(&console_sem)) {
		/*
		 * We own the drivers.  We can drop the spinlock and
		 * let release_console_sem() print the text, maybe ...
		 */
		console_locked = 1;
		printk_cpu = UINT_MAX;
		spin_unlock(&logbuf_lock);

		/*
		 * Console drivers may assume that per-cpu resources have
		 * been allocated. So unless they're explicitly marked as
		 * being able to cope (CON_ANYTIME) don't call them until
		 * this CPU is officially up.
		 */
		if (cpu_online(smp_processor_id()) || have_callable_console()) {
			console_may_schedule = 0;
			release_console_sem();
...
}

-----------------------------------------------
void release_console_sem(void)
{
	unsigned long flags;
	unsigned long _con_start, _log_end;
	unsigned long wake_klogd = 0;

	if (console_suspended) {
		up(&secondary_console_sem);
		return;
	}

	console_may_schedule = 0;

	for ( ; ; ) {
		spin_lock_irqsave(&logbuf_lock, flags);
		wake_klogd |= log_start - log_end;
		if (con_start == log_end)
			break;			/* Nothing to print */
		_con_start = con_start;
		_log_end = log_end;
		con_start = log_end;		/* Flush */
		spin_unlock(&logbuf_lock);
		call_console_drivers(_con_start, _log_end);
		local_irq_restore(flags);
	}
	console_locked = 0;
	up(&console_sem);
	spin_unlock_irqrestore(&logbuf_lock, flags);
	if (wake_klogd)
		wake_up_klogd();
}
-----------------------------------------------
static void call_console_drivers(unsigned long start, unsigned long end)
{
	unsigned long cur_index, start_print;
	static int msg_level = -1;

	BUG_ON(((long)(start - end)) > 0);

	cur_index = start;
	start_print = start;
	while (cur_index != end) {
		if (msg_level < 0 && ((end - cur_index) > 2) &&
				LOG_BUF(cur_index + 0) == '<' &&
				LOG_BUF(cur_index + 1) >= '0' &&
				LOG_BUF(cur_index + 1) <= '7' &&
				LOG_BUF(cur_index + 2) == '>') {
			msg_level = LOG_BUF(cur_index + 1) - '0';
			cur_index += 3;
			start_print = cur_index;
		}
		while (cur_index != end) {
			char c = LOG_BUF(cur_index);

			cur_index++;
			if (c == '\n') {
				if (msg_level < 0) {
					/*
					 * printk() has already given us loglevel tags in
					 * the buffer.  This code is here in case the
					 * log buffer has wrapped right round and scribbled
					 * on those tags
					 */
					msg_level = default_message_loglevel;
				}
				_call_console_drivers(start_print, cur_index, msg_level);
				msg_level = -1;
				start_print = cur_index;
				break;
			}
		}
	}
	_call_console_drivers(start_print, end, msg_level);
}

-----------------------------------------------
static void _call_console_drivers(unsigned long start,
				unsigned long end, int msg_log_level)
{
	if ((msg_log_level < console_loglevel || ignore_loglevel) &&
			console_drivers && start != end) {
		if ((start & LOG_BUF_MASK) > (end & LOG_BUF_MASK)) {
			/* wrapped write */
			__call_console_drivers(start & LOG_BUF_MASK,
						log_buf_len);
			__call_console_drivers(0, end & LOG_BUF_MASK);
		} else {
			__call_console_drivers(start, end);
		}
	}
}

-----------------------------------------------
static void __call_console_drivers(unsigned long start, unsigned long end)
{
	struct console *con;

	for (con = console_drivers; con; con = con->next) {
		if ((con->flags & CON_ENABLED) && con->write &&
				(cpu_online(smp_processor_id()) ||
				(con->flags & CON_ANYTIME)))
			con->write(con, &LOG_BUF(start), end - start);
	}
}

2.1.2 printk的使用

例如简单的点灯字符设备驱动程序,把地址映射去掉直接使用物理地址,将printk定义为一个宏,把错误找出来后,可以把宏定义为别的去掉打印信息,在printk中__FILE__为绝地路径的宏,__FUNCTION__为调用函数,__LINE__为行数

#include <linux/module.h>
#include <linux/kernel.h>
#include <linux/fs.h>
#include <linux/init.h>
#include <linux/delay.h>
#include <asm/uaccess.h>
#include <asm/irq.h>
#include <asm/io.h>
#include <asm/arch/regs-gpio.h>
#include <asm/hardware.h>

static struct class *firstdrv_class;
static struct class_device	*firstdrv_class_dev;

volatile unsigned long *gpfcon = NULL;
volatile unsigned long *gpfdat = NULL;


//#define DBG_PRINTK printk
#define DBG_PRINTK(x...) 

static int first_drv_open(struct inode *inode, struct file *file)
{
	//printk("first_drv_open\n");
	/* 配置GPF4,5,6为输出 */
	DBG_PRINTK("%s %s %d\n", __FILE__, __FUNCTION__, __LINE__);
	*gpfcon &= ~((0x3<<(4*2)) | (0x3<<(5*2)) | (0x3<<(6*2)));
	DBG_PRINTK("%s %s %d\n", __FILE__, __FUNCTION__, __LINE__);
	*gpfcon |= ((0x1<<(4*2)) | (0x1<<(5*2)) | (0x1<<(6*2)));
	DBG_PRINTK("%s %s %d\n", __FILE__, __FUNCTION__, __LINE__);
	return 0;
}

static ssize_t first_drv_write(struct file *file, const char __user *buf, size_t count, loff_t * ppos)
{
	int val;

	//printk("first_drv_write\n");

	copy_from_user(&val, buf, count); //	copy_to_user();

	if (val == 1)
	{
		// 点灯
		*gpfdat &= ~((1<<4) | (1<<5) | (1<<6));
	}
	else
	{
		// 灭灯
		*gpfdat |= (1<<4) | (1<<5) | (1<<6);
	}
	
	return 0;
}

static struct file_operations first_drv_fops = {
    .owner  =   THIS_MODULE,    /* 这是一个宏,推向编译模块时自动创建的__this_module变量 */
    .open   =   first_drv_open,     
	.write	=	first_drv_write,	   
};


int major;
static int first_drv_init(void)
{
	DBG_PRINTK("%s %s %d\n", __FILE__, __FUNCTION__, __LINE__);
	major = register_chrdev(0, "first_drv", &first_drv_fops); // 注册, 告诉内核

	DBG_PRINTK("%s %s %d\n", __FILE__, __FUNCTION__, __LINE__);
	firstdrv_class = class_create(THIS_MODULE, "firstdrv");

	DBG_PRINTK("%s %s %d\n", __FILE__, __FUNCTION__, __LINE__);
	firstdrv_class_dev = class_device_create(firstdrv_class, NULL, MKDEV(major, 0), NULL, "xyz"); /* /dev/xyz */

	DBG_PRINTK("%s %s %d\n", __FILE__, __FUNCTION__, __LINE__);
	gpfcon = (volatile unsigned long *)ioremap(0x56000050, 16);
	gpfdat = gpfcon + 1;

	return 0;
}

static void first_drv_exit(void)
{
	unregister_chrdev(major, "first_drv"); // 卸载

	class_device_unregister(firstdrv_class_dev);
	class_destroy(firstdrv_class);
	iounmap(gpfcon);
}

module_init(first_drv_init);
module_exit(first_drv_exit);
MODULE_LICENSE("GPL");

2.1.3 打印级别

在kernel.h中定义了打印级别 

#define    KERN_EMERG    "<0>"    /* system is unusable            */
#define    KERN_ALERT    "<1>"    /* action must be taken immediately    */
#define    KERN_CRIT    "<2>"    /* critical conditions            */
#define    KERN_ERR    "<3>"    /* error conditions            */
#define    KERN_WARNING    "<4>"    /* warning conditions            */
#define    KERN_NOTICE    "<5>"    /* normal but significant condition    */
#define    KERN_INFO    "<6>"    /* informational            */
#define    KERN_DEBUG    "<7>"    /* debug-level messages            */ 

利用打印级别调试,加载该驱动程序,没有信息打印,是因为KERN_DEBUG级别比console_loglevel级别大

#include <linux/module.h>
#include <linux/kernel.h>
#include <linux/fs.h>
#include <linux/init.h>
#include <linux/delay.h>
#include <asm/uaccess.h>
#include <asm/irq.h>
#include <asm/io.h>
#include <asm/arch/regs-gpio.h>
#include <asm/hardware.h>

static struct class *firstdrv_class;
static struct class_device	*firstdrv_class_dev;

volatile unsigned long *gpfcon = NULL;
volatile unsigned long *gpfdat = NULL;


static int first_drv_open(struct inode *inode, struct file *file)
{
	//printk("first_drv_open\n");
	/* 配置GPF4,5,6为输出 */
	printk(KERN_DEBUG"%s %s %d\n", __FILE__, __FUNCTION__, __LINE__);
	*gpfcon &= ~((0x3<<(4*2)) | (0x3<<(5*2)) | (0x3<<(6*2)));
	printk(KERN_DEBUG"%s %s %d\n", __FILE__, __FUNCTION__, __LINE__);
	*gpfcon |= ((0x1<<(4*2)) | (0x1<<(5*2)) | (0x1<<(6*2)));
	printk(KERN_DEBUG"%s %s %d\n", __FILE__, __FUNCTION__, __LINE__);
	return 0;
}

static ssize_t first_drv_write(struct file *file, const char __user *buf, size_t count, loff_t * ppos)
{
	int val;

	//printk("first_drv_write\n");

	copy_from_user(&val, buf, count); //	copy_to_user();

	if (val == 1)
	{
		// 点灯
		*gpfdat &= ~((1<<4) | (1<<5) | (1<<6));
	}
	else
	{
		// 灭灯
		*gpfdat |= (1<<4) | (1<<5) | (1<<6);
	}
	
	return 0;
}

static struct file_operations first_drv_fops = {
    .owner  =   THIS_MODULE,    /* 这是一个宏,推向编译模块时自动创建的__this_module变量 */
    .open   =   first_drv_open,     
	.write	=	first_drv_write,	   
};


int major;
static int first_drv_init(void)
{
	printk(KERN_DEBUG"%s %s %d\n", __FILE__, __FUNCTION__, __LINE__);
	major = register_chrdev(0, "first_drv", &first_drv_fops); // 注册, 告诉内核

	printk(KERN_DEBUG"%s %s %d\n", __FILE__, __FUNCTION__, __LINE__);
	firstdrv_class = class_create(THIS_MODULE, "firstdrv");

	printk(KERN_DEBUG"%s %s %d\n", __FILE__, __FUNCTION__, __LINE__);
	firstdrv_class_dev = class_device_create(firstdrv_class, NULL, MKDEV(major, 0), NULL, "xyz"); /* /dev/xyz */

	printk(KERN_DEBUG"%s %s %d\n", __FILE__, __FUNCTION__, __LINE__);
	gpfcon = (volatile unsigned long *)ioremap(0x56000050, 16);
	gpfdat = gpfcon + 1;

	return 0;
}

static void first_drv_exit(void)
{
	unregister_chrdev(major, "first_drv"); // 卸载

	class_device_unregister(firstdrv_class_dev);
	class_destroy(firstdrv_class);
	iounmap(gpfcon);
}

module_init(first_drv_init);
module_exit(first_drv_exit);
MODULE_LICENSE("GPL");

我们可以手动来调用console_loglevel参数,在/proc/sys/kernel/printk中第一个参数就是console_loglevel为7,所以上述设置级别为7不会小于console_loglevel,因此不打印出信息,修改级别为8,重新加载驱动,就可以看到信息被打印出来

# cat /proc/sys/kernel/printk
7       4       1       7
# echo "8 4 1 7" > /proc/sys/kernel/printk
# insmod first_drv.ko
/home/book/xiaoma/dev_demo/23th_debug_with_printk/first_drv.c first_drv_init 63
/home/book/xiaoma/dev_demo/23th_debug_with_printk/first_drv.c first_drv_init 66
/home/book/xiaoma/dev_demo/23th_debug_with_printk/first_drv.c first_drv_init 69
/home/book/xiaoma/dev_demo/23th_debug_with_printk/first_drv.c first_drv_init 72

若想要在系统启动之后把打印信息去掉,在内核文档中Documentation/kernel-parameters.txt有说明设置启动参数loglevel来修改console_loglevel

    loglevel=    All Kernel Messages with a loglevel smaller than the
            console loglevel will be printed to the console. It can
            also be changed with klogd or other programs. The
            loglevels are defined as follows:

            0 (KERN_EMERG)        system is unusable
            1 (KERN_ALERT)        action must be taken immediately
            2 (KERN_CRIT)        critical conditions
            3 (KERN_ERR)        error conditions
            4 (KERN_WARNING)    warning conditions
            5 (KERN_NOTICE)        normal but significant condition
            6 (KERN_INFO)        informational
            7 (KERN_DEBUG)        debug-level messages

在uboot中设置启动参数loglevel,其他参数不变 

OpenJTAG> set bootargs loglevel=0 noinitrd root=/dev/nfs nfsroot=192.168.0.106:/work/nfs_root/first_fs ip=192.168.0.19:192.168.0.106:192.168.0.1:255.255.255.0::eth0:off init=/linuxrc console=ttySAC0
OpenJTAG> boot 

 所有的内核打印都没有了,还是可以进入系统,打印信息依然在暖冲数据中可以在dmesg命令中查看,还有其他参数debug(等级设置为10)、quiet(等级设置为4)

OpenJTAG> boot

NAND read: device 0 offset 0x60000, size 0x200000

Reading data from 0x25f800 -- 100% complete.
 2097152 bytes read: OK
## Booting image at 30007fc0 ...
   Image Name:   Linux-2.6.22.6
   Created:      2018-05-15  11:57:26 UTC
   Image Type:   ARM Linux Kernel Image (uncompressed)
   Data Size:    1848728 Bytes =  1.8 MB
   Load Address: 30008000
   Entry Point:  30008000
   Verifying Checksum ... OK
   XIP Kernel Image ... OK

Starting kernel ...

Uncompressing Linux...................................................................................................................... done, booting the kernel.
init started: BusyBox v1.7.0 (2020-12-27 14:56:30 CST)
starting pid 765, tty '': '/etc/init.d/rcS'
sh: missing ]
/etc/init.d/rcS: line 10: -e: not found

Please press Enter to activate this console.
starting pid 772, tty '/dev/s3c2410_serial0': '/bin/sh'
#
# dmesg
Linux version 2.6.22.6 (root@book-virtual-machine) (gcc version 3.4.5) #1 Tue May 15 19:57:22 CST 2018
CPU: ARM920T [41129200] revision 0 (ARMv4T), cr=c0007177
Machine: SMDK2440
Memory policy: ECC disabled, Data cache writeback

....

2.2 打印至proc虚拟文件 

dmesg的信息从哪里来,也是去打开某个文件,这个文件就是/proc/kmsg,proc是虚拟的文件系统,/etc/init.d/rcS中有条指令:mount -a把所有的文件系统都挂接上去,就是把/etc/fstab文件定义的文件系统都挂载上去,cat /proc/mounts查看信息

# cat /proc/mounts
rootfs / rootfs rw 0 0
/dev/root / nfs rw,vers=2,rsize=4096,wsize=4096,hard,nolock,proto=udp,timeo=11,retrans=2,sec=sys,addr=192.168.0.106 0 0
proc /proc proc rw 0 0
sysfs /sys sysfs rw 0 0
tmpfs /dev tmpfs rw 0 0
devpts /dev/pts devpts rw 0 0

不用dmesg命令,可以直接查看信息,每一个信息前面都含有打印级别

 # cat /proc/kmsg
<5>Linux version 2.6.22.6 (root@book-virtual-machine) (gcc version 3.4.5) #1 Tue May 15 19:57:22 CST 2018
<4>CPU: ARM920T [41129200] revision 0 (ARMv4T), cr=c0007177
<4>Machine: SMDK2440
<4>Memory policy: ECC disabled, Data cache writeback
<7>On node 0 totalpages: 16384
<7>  DMA zone: 128 pages used for memmap
<7>  DMA zone: 0 pages reserved
<7>  DMA zone: 16256 pages, LIFO batch:3
<7>  Normal zone: 0 pages used for memmap
<4>CPU S3C2440A (id 0x32440001)
<4>S3C244X: core 400.000 MHz, memory 100.000 MHz, peripheral 50.000 MHz
<6>S3C24XX Clocks, (c) 2004 Simtec Electronics
<4>CLOCK: Slow mode (1.500 MHz), fast, MPLL on, UPLL on
<4>CPU0: D VIVT write-back cache
<4>CPU0: I cache: 16384 bytes, associativity 64, 32 byte lines, 8 sets
<4>CPU0: D cache: 16384 bytes, associativity 64, 32 byte lines, 8 sets
<4>Built 1 zonelists.  Total pages: 16256
<5>Kernel command line: noinitrd root=/dev/nfs nfsroot=192.168.0.106:/work/nfs_root/first_fs ip=192.168.0.19:192.168.0.106:192.168.0.1:255.255.255.0::eth0:off init=/linuxrc console=ttySAC0
<4>irq: clearing subpending status 00000002
<4>PID hash table entries: 256 (order: 8, 1024 bytes)
<4>timer tcon=00500000, tcnt a2c1, tcfg 00000200,00000000, usec 00001eb8
<4>Console: colour dummy device 80x30
<7>selected clock c038f31c (pclk) quot 26, calc 11574

....

假设内核的打印信息很多,想把打印信息单纯的抽出来,自己也要构造一个类似/proc/kmsg的文件,驱动的全部信息打到哪一个buff去,对于printk的信息是兵分两路的,一路是到log_buf去,可以通过/proc/kmsg,另一路是打印出来,因此自己可以仿照一个mylog_vuf,把信息存放在my_buf中,在fs/proc目录中proc_misc.c中是构造了一个proc_fops结构体,并且设置它的读写函数,仿照其写法,在fs/proc目录做一个mymsg.c

2.2.1 搭建框架

加上字符设备驱动的头文件,同时还需要一个linux/proc_fs.h头文件,

#include <linux/module.h>
#include <linux/kernel.h>
#include <linux/fs.h>
#include <linux/init.h>
#include <linux/delay.h>
#include <asm/uaccess.h>
#include <asm/irq.h>
#include <asm/io.h>
#include <asm/arch/regs-gpio.h>
#include <asm/hardware.h>
#include <linux/proc_fs.h>

struct proc_dir_entry *myentry;

const struct file_operations proc_mymsg_operations = {
};

static int mymsg_init(void)
{
	myentry = create_proc_entry("mymsg", S_IRUSR, &proc_root);
	if (myentry)
		myentry->proc_fops = &proc_mymsg_operations;
	return 0;
}

static void mymsg_exit(void)
{
	remove_proc_entry("mymsg", &proc_root);
}

module_init(mymsg_init);
module_exit(mymsg_exit);
MODULE_LICENSE("GPL");

编译驱动并加载驱动,加载完成,但是读不出数据

# insmod mymsg.ko
# ls /proc/mymsg
/proc/mymsg
# cat /proc/mymsg
cat: read error: Invalid argument

2.2.2 完善程序

加上read函数

static ssize_t mymsg_read(struct file *file, char __user *buf,
			 size_t count, loff_t *ppos)
{
	printk("mymsg_read\n");
	return 0;
}

const struct file_operations proc_mymsg_operations = {
	.read = mymsg_read,
};

 重新编译测试,重新加载驱动,得到打印信息

# rmmod mymsg
# insmod mymsg.ko
# cat /proc/mymsg
mymsg_read

 参考,kmsg.c中的read,就是把mylog_buf的数据(环型缓冲区:输入子系统中有解释)通过cpoy_to_user函数传给应用层,然后返回return,自己制造一个buf,修改read函数,在入口函数中给buf初始值

static ssize_t mymsg_read(struct file *file, char __user *buf,
			 size_t count, loff_t *ppos)
{
	//printk("mymsg_read\n");
	/* 把mylog_buf的数据copy_to_user, return */
	copy_to_user(buf, mylog_buf, 10);
	
	return 10;
}

const struct file_operations proc_mymsg_operations = {
	.read = mymsg_read,
};

static int mymsg_init(void)
{
	sprintf(mylog_buf, "sajfsdjfausajfa");
...
}

编译并加载驱动程序,查看信息,cat命令过程有死循环去不断的read,因此一直能读取出数据

# rmmod mymsg
# insmod mymsg.ko
# cat /proc/mymsg
sajfsdjfausajfsdjfausajfsdjfausajfsdjfausajfsdjfausajfsdjfausajfsdjfausajfsdjfausajfsdjfausajfsdjfausajfsdjfausajfsdjfausajfsdjfausajfsdjfausajfsdjfausajfsdjfausajfsdjfausajfsdjfausajfsdjfausajfsdjfausajfsdjfausajfsdjfausajfsdjfausajfsdjfausajfsdjfausajfsdjfausajfsdjfausajfsdjfausajfsdjfausajfsdjfausajfsdjfausajfsdjfausajfsdjfausajfsdjf

 完善环型缓冲区,构造空与满函数,仿造vsprintf.c中的sprintf函数写出自己的myprintk,并参考kmsg.c中kmsg_read完善read函数,最后把myprintk上报给外部使用EXPORT_SYMBOL宏,当使用read函数后,数据为空休眠,直到mylog_putc得到数据唤醒

#include <linux/module.h>
#include <linux/kernel.h>
#include <linux/fs.h>
#include <linux/init.h>
#include <linux/delay.h>
#include <asm/uaccess.h>
#include <asm/irq.h>
#include <asm/io.h>
#include <asm/arch/regs-gpio.h>
#include <asm/hardware.h>
#include <linux/proc_fs.h>

#define MYLOG_BUF_LEN 1024

struct proc_dir_entry *myentry;

static char mylog_buf[MYLOG_BUF_LEN];
static char tmp_buf[MYLOG_BUF_LEN];
static int mylog_r = 0;
static int mylog_w = 0;

static DECLARE_WAIT_QUEUE_HEAD(mymsg_waitq);  //申请一个等待队列头

static int is_mylog_empty(void)//空
{
	return (mylog_r == mylog_w);
}

static int is_mylog_full(void)//满
{
	return ((mylog_w + 1)% MYLOG_BUF_LEN == mylog_r);
}

static void mylog_putc(char c)
{
	if (is_mylog_full())
	{
		/* 满就丢弃一个数据 */
		mylog_r = (mylog_r + 1) % MYLOG_BUF_LEN;
	}

	mylog_buf[mylog_w] = c;
	mylog_w = (mylog_w + 1) % MYLOG_BUF_LEN;

	/* 唤醒等待数据的进程 */	
    wake_up_interruptible(&mymsg_waitq);   /* 唤醒休眠的进程 */	
}

static int mylog_getc(char *p)
{
	if (is_mylog_empty())
	{
		return 0;
	}
	*p = mylog_buf[mylog_r];
	mylog_r = (mylog_r + 1) % MYLOG_BUF_LEN;
	return 1;
}

int myprintk(const char *fmt, ...)//参考了vsprintf.c中的sprintf函数
{
	va_list args;
	int i;
	int j;

	va_start(args, fmt);
	i = vsnprintf(tmp_buf, INT_MAX, fmt, args); //把信息临时放在tmp_buf中
	va_end(args);
	
	for (j = 0; j < i; j++)
		mylog_putc(tmp_buf[j]);  //把临时数据放在环型暖冲区中去
		
	return i;
}

static ssize_t mymsg_read(struct file *file, char __user *buf,  //参考kmsg_read
			 size_t count, loff_t *ppos)
{
	int error = 0;
	int i = 0;
	char c;

	/* 把mylog_buf的数据copy_to_user, return */
	if ((file->f_flags & O_NONBLOCK) && is_mylog_empty()) //非组撒打开或者环形缓冲区中没有数据立即返回错误
		return -EAGAIN;

	error = wait_event_interruptible(mymsg_waitq, !is_mylog_empty());//等待数据非空,若为空则休眠,写数据的时候唤醒

	/* copy_to_user */
	while (!error && (mylog_getc(&c)) && i < count) {  //参考kmsg_read
		error = __put_user(c, buf); //实质也是cpoy_to_user,作用一样
		buf++;
		i++;
	}
	
	if (!error)
		error = i;
	
	return error;
}

const struct file_operations proc_mymsg_operations = {
	.read = mymsg_read,
};

static int mymsg_init(void)
{	
	myentry = create_proc_entry("mymsg", S_IRUSR, &proc_root);
	if (myentry)
		myentry->proc_fops = &proc_mymsg_operations;
	return 0;
}

static void mymsg_exit(void)
{
	remove_proc_entry("mymsg", &proc_root);
}

module_init(mymsg_init);
module_exit(mymsg_exit);
EXPORT_SYMBOL(myprintk);//将myprintk给外部使用
MODULE_LICENSE("GPL");

在点灯驱动程序需要申明一下myprintk,在其open和write函数中利用myprintk打印

extern int myprintk(const char *fmt, ...);

static int cnt = 0;
myprintk("first_drv_open : %d\n", ++cnt);

测试,先加载mymsg,再加载first_drv,cat /proc/mymsg查看信息,但是只能查看看一次

# insmod mymsg.ko
# insmod first_drv.ko
# ./firstdrvtest on
# cat /proc/mymsg
first_drv_init
first_drv_open : 1
first_drv_write : 1

# cat /proc/mymsg

2.2.3 完整程序

让环型暖冲区中每一次r都能为原来的值,设置open函数每次打开,令r变成原来的值,定义一个r_for_read去暖冲,这样每次去cat就能得到信息,完整代码如下

#include <linux/module.h>
#include <linux/kernel.h>
#include <linux/fs.h>
#include <linux/init.h>
#include <linux/delay.h>
#include <asm/uaccess.h>
#include <asm/irq.h>
#include <asm/io.h>
#include <asm/arch/regs-gpio.h>
#include <asm/hardware.h>
#include <linux/proc_fs.h>

#define MYLOG_BUF_LEN 1024

struct proc_dir_entry *myentry;

static char mylog_buf[MYLOG_BUF_LEN];
static char tmp_buf[MYLOG_BUF_LEN];
static int mylog_r = 0;
static int mylog_r_for_read = 0;
static int mylog_w = 0;

static DECLARE_WAIT_QUEUE_HEAD(mymsg_waitq);

static int is_mylog_empty(void)
{
	return (mylog_r == mylog_w);
}

static int is_mylog_empty_for_read(void)
{
	return (mylog_r_for_read == mylog_w);
}

static int is_mylog_full(void)
{
	return ((mylog_w + 1)% MYLOG_BUF_LEN == mylog_r);
}

static void mylog_putc(char c)
{
	if (is_mylog_full())
	{
		/* 丢弃一个数据 */
		mylog_r = (mylog_r + 1) % MYLOG_BUF_LEN;

		if ((mylog_r_for_read + 1) % MYLOG_BUF_LEN == mylog_r)
		{
			mylog_r_for_read = mylog_r;
		}
	}

	mylog_buf[mylog_w] = c;
	mylog_w = (mylog_w + 1) % MYLOG_BUF_LEN;

	/* 唤醒等待数据的进程 */	
    wake_up_interruptible(&mymsg_waitq);   /* 唤醒休眠的进程 */	
}

static int mylog_getc(char *p)
{
	if (is_mylog_empty())
	{
		return 0;
	}
	*p = mylog_buf[mylog_r];
	mylog_r = (mylog_r + 1) % MYLOG_BUF_LEN;
	return 1;
}

static int mylog_getc_for_read(char *p)
{
	if (is_mylog_empty_for_read())
	{
		return 0;
	}
	*p = mylog_buf[mylog_r_for_read];
	mylog_r_for_read = (mylog_r_for_read + 1) % MYLOG_BUF_LEN;
	return 1;
}


int myprintk(const char *fmt, ...)
{
	va_list args;
	int i;
	int j;

	va_start(args, fmt);
	i = vsnprintf(tmp_buf, INT_MAX, fmt, args);
	va_end(args);
	
	for (j = 0; j < i; j++)
		mylog_putc(tmp_buf[j]);
		
	return i;
}

static ssize_t mymsg_read(struct file *file, char __user *buf,
			 size_t count, loff_t *ppos)
{
	int error = 0;
	int i = 0;
	char c;

	/* 把mylog_buf的数据copy_to_user, return */
	if ((file->f_flags & O_NONBLOCK) && is_mylog_empty_for_read())
		return -EAGAIN;

	error = wait_event_interruptible(mymsg_waitq, !is_mylog_empty_for_read());

	/* copy_to_user */
	while (!error && (mylog_getc_for_read(&c)) && i < count) {
		error = __put_user(c, buf);
		buf++;
		i++;
	}
	
	if (!error)
		error = i;
	
	return error;
}

static int mymsg_open(struct inode *inode, struct file *file)
{
	mylog_r_for_read = mylog_r;
	return 0;
}

const struct file_operations proc_mymsg_operations = {
	.open = mymsg_open,
	.read = mymsg_read,
};

static int mymsg_init(void)
{	
	myentry = create_proc_entry("mymsg", S_IRUSR, &proc_root);
	if (myentry)
		myentry->proc_fops = &proc_mymsg_operations;
	return 0;
}

static void mymsg_exit(void)
{
	remove_proc_entry("mymsg", &proc_root);
}

module_init(mymsg_init);
module_exit(mymsg_exit);
EXPORT_SYMBOL(myprintk);
MODULE_LICENSE("GPL");

2.3 段错误分析

2.3.1 根据PC值找到导致错误的指令

在一个点灯驱动入口函数中取消对地址的映射,引入段错误

static int first_drv_open(struct inode *inode, struct file *file)
{
	//printk("first_drv_open\n");
	/* 配置GPF4,5,6为输出 */
	*gpfcon &= ~((0x3<<(4*2)) | (0x3<<(5*2)) | (0x3<<(6*2)));
	*gpfcon |= ((0x1<<(4*2)) | (0x1<<(5*2)) | (0x1<<(6*2)));
	return 0;
}

...
static int first_drv_init(void)
{
	major = register_chrdev(0, "first_drv", &first_drv_fops); // 注册, 告诉内核

	firstdrv_class = class_create(THIS_MODULE, "firstdrv");

	firstdrv_class_dev = class_device_create(firstdrv_class, NULL, MKDEV(major, 0), NULL, "xyz"); /* /dev/xyz */

	gpfcon = (volatile unsigned long *)0x56000050; //(volatile unsigned long *)ioremap(0x56000050, 16);
	gpfdat = gpfcon + 1;

	return 0;
}

测试文件打开后根据内核打印出来的oops信息调试,分析其信息

Unable to handle kernel paging request at virtual address 56000050  //无法处理虚拟地址56000050,内核使用56000050发生了错误
pgd = c3ed4000
[56000050] *pgd=00000000
Internal error: Oops: 5 [#1] //内部错误oops

/* 有时候内核没有提供这些信息只有一个PC值 */
Modules linked in: first_drv 
CPU: 0    Not tainted  (2.6.22.6 #1)
PC is at first_drv_open+0x18/0x3c [first_drv]  //PC就是发出错误的指令的地址   0x18是该指令的偏移,0x3C是该函数的总大小
LR is at chrdev_open+0x14c/0x164 //LR寄存器的值


pc : [<bf000018>]    lr : [<c008d888>]    psr: a0000013 //PC = bf000018
sp : c3edbe88  ip : c3edbe98  fp : c3edbe94
r10: 00000000  r9 : c3eda000  r8 : c049aa80
r7 : 00000000  r6 : 00000000  r5 : c3e9e0c0  r4 : c07085a0
r3 : bf000000  r2 : 56000050  r1 : bf000964  r0 : 00000000  //执行这条导致错误的寄存器的值
Flags: NzCv  IRQs on  FIQs on  Mode SVC_32  Segment user
Control: c000717f  Table: 33ed4000  DAC: 00000015
Process firstdrvtest (pid: 777, stack limit = 0xc3eda258) //发出错误时当前进程的名称是firstdrvtest
Stack: (0xc06abe88 to 0xc06ac000)//栈
be80:                   c06abebc c06abe98 c008c888 bf000010 c06abf04 c3cd6dc0
bea0: c3e12b04 c008c73c c0454b20 c3e1124c c06abee4 c06abec0 c0088e48 c008c74c
bec0: c3cd6dc0 c06abf04 00000003 ffffff9c c002b044 c000c000 c06abefc c06abee8
bee0: c0088f64 c0088d58 00000000 00000002 c06abf68 c06abf00 c0088fb8 c0088f40
bf00: c06abf04 c3e1124c c0454b20 00000000 00000000 c3c39000 00000101 00000001
bf20: 00000000 c06aa000 c0464748 c0464740 ffffffe8 c000c000 c06abf68 c06abf48
bf40: c008916c c009ec70 00000003 00000000 c3cd6dc0 00000002 bef6bedc c06abf94
bf60: c06abf6c c00892f4 c0088f88 00008520 bef6bed4 0000860c 00008670 00000005
bf80: c002b044 4013365c c06abfa4 c06abf98 c00893a8 c00892b0 00000000 c06abfa8
bfa0: c002aea0 c0089394 bef6bed4 0000860c 00008720 00000002 bef6bedc 00000001
bfc0: bef6bed4 0000860c 00008670 00000001 00008520 00000000 4013365c bef6bea8
bfe0: 00000000 bef6be84 0000266c 400c98e0 60000010 00008720 564066e6 666b645e
Backtrace: //回溯,从下往上调用,根据内核的配置有无编译frame pointers,当发生错误的时候就会把回溯过程打印出来
[<bf000000>] (first_drv_open+0x0/0x3c [first_drv]) from [<c008c888>] (chrdev_open+0x14c/0x164)
[<c008c73c>] (chrdev_open+0x0/0x164) from [<c0088e48>] (__dentry_open+0x100/0x1e8)
 r8:c3e1124c r7:c0454b20 r6:c008c73c r5:c3e12b04 r4:c3cd6dc0
[<c0088d48>] (__dentry_open+0x0/0x1e8) from [<c0088f64>] (nameidata_to_filp+0x34/0x48)
[<c0088f30>] (nameidata_to_filp+0x0/0x48) from [<c0088fb8>] (do_filp_open+0x40/0x48)
 r4:00000002
[<c0088f78>] (do_filp_open+0x0/0x48) from [<c00892f4>] (do_sys_open+0x54/0xe4)
 r5:bef6bedc r4:00000002
[<c00892a0>] (do_sys_open+0x0/0xe4) from [<c00893a8>] (sys_open+0x24/0x28)
[<c0089384>] (sys_open+0x0/0x28) from [<c002aea0>] (ret_fast_syscall+0x0/0x2c)
Code: e24cb004 e59f1024 e3a00000 e5912000 (e5923000)
Segmentation fault

 大多时候,PC值只会给出一个地址,不到指示说是在哪个函数里,PC = bf000018,属于什么地址,是内核还是通过insmod加载的驱动程序,先判断是否属于内核的地址:看System.map确定内核的函数的地址范围c0004000~c03265a4,如果不属于System.map里的范围,则它属于insmod加载的驱动程序,由bf000018可知这是insmod加载的驱动程序

book@www.100ask.org:/work/system/linux-2.6.22.6$ vi System.map
    1 c0004000 A swapper_pg_dir
    2 c0008000 T __init_begin
    3 c0008000 T _sinittext
    4 c0008000 T stext
    5 c0008000 T _stext
    6 c0008030 t __enable_mmu
    7 c0008060 t __turn_mmu_on
...
22767 c03b4ea8 b registered_mechs_lock
22768 c03b4ea8 b rsi_table
22769 c03b4fa8 b rsc_table
22770 c03b5fa8 B krb5_seq_lock
22771 c03b5fac b i.0
22772 c03b5fb4 B _end

若加载了很多个驱动程序,怎么知道是哪个驱动程序的地址呢,先看加载的驱动程序的函数的地址范围,cat /proc/kallsyms  (内核函数、加载的函数的地址),总里面的信息找到一个相近的地址,在first_drv驱动程序中first_drv_open函数的地址为bf000000

bf000000 t first_drv_open    [first_drv]
bf000000 t $a    [first_drv]
bf000038 t $d    [first_drv]
bf00003c t $a    [first_drv]

找到了first_drv.ko,在PC上反汇编它: arm-linux-objdump -D first_drv.ko > frist_drv.dis,在dis文件里找到first_drv_open

    first_drv.dis文件里                    insmod后
00000000 <first_drv_open>:       bf000000 t first_drv_open    [first_drv]
00000018                                    pc = bf000018

00000000 <first_drv_open>:
   0:    e1a0c00d     mov    ip, sp
   4:    e92dd800     stmdb    sp!, {fp, ip, lr, pc}
   8:    e24cb004     sub    fp, ip, #4    ; 0x4 //这就是上面所讲的frame pointers,回溯的时候根据每一个fp找到对应的栈sp,从栈中找到lr值
   c:    e59f1024     ldr    r1, [pc, #36]    ; 38 <__mod_vermagic5>
  10:    e3a00000     mov    r0, #0    ; 0x0
  14:    e5912000     ldr    r2, [r1]
  18:    e5923000     ldr    r3, [r2]  // 在这里出错 r2=56000050

...

如果是内核的地址出现的错误,在drivers/char中把first_drv.c在Makefile中加上obj-y += first_drv.o,重新编译内核,执行测试程序,看System.map,c014e6c0属于内核

Modules linked in:
CPU: 0    Not tainted  (2.6.22.6 #2)
PC is at first_drv_open+0x18/0x3c
LR is at chrdev_open+0x14c/0x164
pc : [<c014e6c0>]    lr : [<c008638c>]    psr: a0000013
sp : c3a03e88  ip : c3a03e98  fp : c3a03e94
r10: 00000000  r9 : c3a02000  r8 : c03f3c60
r7 : 00000000  r6 : 00000000  r5 : c38a0c50  r4 : c3c1e780
r3 : c014e6a8  r2 : 56000050  r1 : c031a47c  r0 : 00000000
Flags: NzCv  IRQs on  FIQs on  Mode SVC_32  Segment user
Control: c000717f  Table: 339f0000  DAC: 00000015
Process firstdrvtest (pid: 750, stack limit = 0xc3a02258)
...

在内核源码下反汇编内核: arm-linux-objdump -D vmlinux > vmlinux.dis(这个文件比较庞大),在dis文件里搜c014e6c0,得到一样的结论

c014e6a8 <first_drv_open>:
c014e6a8:       e1a0c00d        mov     ip, sp
c014e6ac:       e92dd800        stmdb   sp!, {fp, ip, lr, pc}
c014e6b0:       e24cb004        sub     fp, ip, #4      ; 0x4
c014e6b4:       e59f1024        ldr     r1, [pc, #36]   ; c014e6e0 <.text+0x1276e0>
c014e6b8:       e3a00000        mov     r0, #0  ; 0x0
c014e6bc:       e5912000        ldr     r2, [r1]
c014e6c0:       e5923000        ldr     r3, [r2] // 在此出错 r2=56000050

2.3.2 根据栈信息找出函数调用关系

如果没有回溯信息(含有函数的调用关系)的话,我们就根据栈信息找出函数调用关系,有时候我们知道出错的函数在哪里,还需要它的调用的关系,有可能是它的调用过程中出的错误

首先根据PC值(insmod加载的驱动程序的情况)确定出错位置,bf000018 属于 insmod的模块,(查看/proc/kallsyms)在first_drv驱动程序的first_drv_open函数中,反汇编first_drv.c,它的栈占据4个字节

00000000 <first_drv_open>:
   0:    e1a0c00d     mov    ip, sp
   4:    e92dd800     stmdb    sp!, {fp, ip, lr, pc}
   8:    e24cb004     sub    fp, ip, #4    ; 0x4 
   c:    e59f1024     ldr    r1, [pc, #36]    ; 38 <__mod_vermagic5>
  10:    e3a00000     mov    r0, #0    ; 0x0
  14:    e5912000     ldr    r2, [r1]
  18:    e5923000     ldr    r3, [r2]  

在出错的栈信息中,是从调用函数的栈信息从地址低从高全部打印出来,c008d888就是first_drv_open函数lr的地址(对于stmdb指令可以参考ARM常用汇编指令与C程序机制),而c008d888是内核的地址

Stack: (0xc06abe88 to 0xc06ac000)//栈
be80:                   c06abebc c06abe98 c008c888 bf000010 c06abf04 c3cd6dc0
bea0: c3e12b04 c008c73c c0454b20 c3e1124c c06abee4 c06abec0 c0088e48 c008c74c
bec0: c3cd6dc0 c06abf04 00000003 ffffff9c c002b044 c000c000 c06abefc c06abee8
bee0: c0088f64 c0088d58 00000000 00000002 c06abf68 c06abf00 c0088fb8 c0088f40
bf00: c06abf04 c3e1124c c0454b20 00000000 00000000 c3c39000 00000101 00000001
bf20: 00000000 c06aa000 c0464748 c0464740 ffffffe8 c000c000 c06abf68 c06abf48
bf40: c008916c c009ec70 00000003 00000000 c3cd6dc0 00000002 bef6bedc c06abf94
bf60: c06abf6c c00892f4 c0088f88 00008520 bef6bed4 0000860c 00008670 00000005
bf80: c002b044 4013365c c06abfa4 c06abf98 c00893a8 c00892b0 00000000 c06abfa8
bfa0: c002aea0 c0089394 bef6bed4 0000860c 00008720 00000002 bef6bedc 00000001
bfc0: bef6bed4 0000860c 00008670 00000001 00008520 00000000 4013365c bef6bea8
bfe0: 00000000 bef6be84 0000266c 400c98e0 60000010 00008720 564066e6 666b645e

反汇编内核文件vmlinux.dis,在地址中c008c888属于chrdev_open函数,栈中有9个,加上sp自减4个字节因此总共10个,所以c0088e48就是first_drv_open函数lr的地址,而地址中c0088e48属于__dentry_open函数,依次类推

c008c73c <chrdev_open>:
c008c73c:    e1a0c00d     mov    ip, sp
c008c740:    e92dd9f0     stmdb    sp!, {r4, r5, r6, r7, r8, fp, ip, lr, pc}
c008c744:    e24cb004     sub    fp, ip, #4    ; 0x4
c008c748:    e24dd004     sub    sp, sp, #4    ; 0x4

总体调用关系如下,与一开始的回溯调用过程一致 

2.4 自制工具

加载驱动后想看寄存器是否正确,写出一个查看寄存器的驱动程序和应用程序

应用程序:

#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <stdio.h>
#include <poll.h>
#include <signal.h>
#include <sys/types.h>
#include <unistd.h>
#include <fcntl.h>
#include <stdlib.h>
#include <string.h>

#define KER_RW_R8      0
#define KER_RW_R16     1
#define KER_RW_R32     2

#define KER_RW_W8      3
#define KER_RW_W16     4
#define KER_RW_W32     5


/* Usage:
 * ./regeditor r8  addr [num]
 * ./regeditor r16 addr [num]
 * ./regeditor r32 addr [num]
 *
 * ./regeditor w8  addr val
 * ./regeditor w16 addr val 
 * ./regeditor w32 addr val
 */

void print_usage(char *file)
{
	printf("Usage:\n");
	printf("%s <r8 | r16 | r32> <phy addr> [num]\n", file);
	printf("%s <w8 | w16 | w32> <phy addr> <val>\n", file);
}

int main(int argc, char **argv)
{
	int fd;
	unsigned int buf[2];
	unsigned int i;
	unsigned int num;
	
	if ((argc != 3) && (argc != 4))
	{
		print_usage(argv[0]);
		return -1;
	}

	fd = open("/dev/ker_rw", O_RDWR);
	if (fd < 0)
	{
		printf("can't open /dev/ker_rw\n");
		return -2;
	}

	/* addr */
	buf[0] = strtoul(argv[2], NULL, 0);

	if (argc == 4)
	{
		buf[1] = strtoul(argv[3], NULL, 0);
		num    = buf[1];
	}
	else
	{
		num = 1;
	}

	if (strcmp(argv[1], "r8") == 0)
	{
		for ( i = 0; i < num; i++)
		{
			ioctl(fd, KER_RW_R8, buf);  /* val = buf[1] */
			printf("%02d. [%08x] = %02x\n", i, buf[0], (unsigned char)buf[1]);
			buf[0] += 1;
		}
	}
	else if (strcmp(argv[1], "r16") == 0)
	{
		for ( i = 0; i < num; i++)
		{
			ioctl(fd, KER_RW_R16, buf);  /* val = buf[1] */
			printf("%02d. [%08x] = %02x\n", i, buf[0], (unsigned short)buf[1]);
			buf[0] += 2;
		}
	}
	else if (strcmp(argv[1], "r32") == 0)
	{
		for ( i = 0; i < num; i++)
		{
			ioctl(fd, KER_RW_R32, buf);  /* val = buf[1] */
			printf("%02d. [%08x] = %02x\n", i, buf[0], (unsigned int)buf[1]);
			buf[0] += 4;
		}
	}
	else if (strcmp(argv[1], "w8") == 0)
	{
		ioctl(fd, KER_RW_W8, buf);  /* val = buf[1] */
	}
	else if (strcmp(argv[1], "w16") == 0)
	{
		ioctl(fd, KER_RW_W16, buf);  /* val = buf[1] */
	}
	else if (strcmp(argv[1], "w32") == 0)
	{
		ioctl(fd, KER_RW_W32, buf);  /* val = buf[1] */
	}
	else
	{
		printf(argv[0]);
		return -1;
	}

	return 0;
	
}

  驱动程序:

#include <linux/module.h>
#include <linux/kernel.h>
#include <linux/fs.h>
#include <linux/init.h>
#include <linux/delay.h>
#include <linux/irq.h>
#include <asm/uaccess.h>
#include <asm/irq.h>
#include <asm/io.h>
#include <asm/arch/regs-gpio.h>
#include <asm/hardware.h>
#include <linux/poll.h>
#include <linux/device.h>

#define KER_RW_R8      0
#define KER_RW_R16     1
#define KER_RW_R32     2

#define KER_RW_W8      3
#define KER_RW_W16     4
#define KER_RW_W32     5

static int major;
static struct class *class;
static struct class_device	*ker_dev;

static int ker_rw_ioctl(struct inode *inode, struct file *file, unsigned int cmd, unsigned long arg)
{
	volatile unsigned char  *p8;
	volatile unsigned short *p16;
	volatile unsigned int   *p32;
	unsigned int val;
	unsigned int addr;

	unsigned int buf[2];

	copy_from_user(buf, (const void __user *)arg, 8);
	addr = buf[0];
	val  = buf[1];
	
	p8  = (volatile unsigned char *)ioremap(addr, 4);
	p16 = p8;
	p32 = p8;

	switch (cmd)
	{
		case KER_RW_R8:
		{
			val = *p8;
			copy_to_user((void __user *)(arg+4), &val, 4);
			break;
		}

		case KER_RW_R16:
		{
			val = *p16;
			copy_to_user((void __user *)(arg+4), &val, 4);
			break;
		}

		case KER_RW_R32:
		{
			val = *p32;
			copy_to_user((void __user *)(arg+4), &val, 4);
			break;
		}

		case KER_RW_W8:
		{
			*p8 = val;
			break;
		}

		case KER_RW_W16:
		{
			*p16 = val;
			break;
		}

		case KER_RW_W32:
		{
			*p32 = val;
			break;
		}
	}

	iounmap(p8);
	return 0;
}

static struct file_operations ker_rw_ops = {
	.owner   = THIS_MODULE,
	.ioctl   = ker_rw_ioctl,
};

static int ker_rw_init(void)
{
	major = register_chrdev(0, "ker_rw", &ker_rw_ops);

	class = class_create(THIS_MODULE, "ker_rw");

	/* 为了让mdev根据这些信息来创建设备节点 */
	ker_dev = class_device_create(class, NULL, MKDEV(major, 0), NULL, "ker_rw"); /* /dev/ker_rw */
	
	return 0;
}

static void ker_rw_exit(void)
{
	class_device_unregister(ker_dev);
	class_destroy(class);
	unregister_chrdev(major, "ker_rw");
}

module_init(ker_rw_init);
module_exit(ker_rw_exit);
MODULE_LICENSE("GPL");

2.5 修改内核定位系统僵死问题

在点灯驱动程序写函数引入死循环的错误

static ssize_t first_drv_write(struct file *file, const char __user *buf, size_t count, loff_t * ppos)
{
	int val;

	//printk("first_drv_write\n");

	copy_from_user(&val, buf, count); //	copy_to_user();

	while (1);

	if (val == 1)
	{
		// 点灯
		*gpfdat &= ~((1<<4) | (1<<5) | (1<<6));
	}
	else
	{
		// 灭灯
		*gpfdat |= (1<<4) | (1<<5) | (1<<6);
	}
	
	return 0;
}

加载驱动执行测试程序,系统卡死 

# insmod first_drv.ko
# ./firstdrvtest on
 

内核的系统时钟中断时不断在执行的,我们可以加点打印语句,系统时钟的中断号是30,每时每刻都在变化,相当于linux 的心脏一样

# cat /proc/interrupts
           CPU0
 30:      33007         s3c  S3C2410 Timer Tick
 32:          0         s3c  s3c2410-lcd
 33:          0         s3c  s3c-mci
 34:          0         s3c  I2SSDI
 35:          0         s3c  I2SSDO
 37:         12         s3c  s3c-mci
 42:          0         s3c  ohci_hcd:usb1
 43:          0         s3c  s3c2440-i2c
 51:       2495     s3c-ext  eth0
 60:          0     s3c-ext  s3c-mci
 70:         56   s3c-uart0  s3c2440-uart
 71:         75   s3c-uart0  s3c2440-uart
 79:          0     s3c-adc  s3c2410_action
 80:          0     s3c-adc  s3c2410_action
 83:          0           -  s3c2410-wdt
Err:          0
# cat /proc/interrupts
           CPU0
 30:      33264         s3c  S3C2410 Timer Tick
 32:          0         s3c  s3c2410-lcd
 33:          0         s3c  s3c-mci
 34:          0         s3c  I2SSDI
 35:          0         s3c  I2SSDO
 37:         12         s3c  s3c-mci
 42:          0         s3c  ohci_hcd:usb1
 43:          0         s3c  s3c2440-i2c
 51:       2506     s3c-ext  eth0
 60:          0     s3c-ext  s3c-mci
 70:         58   s3c-uart0  s3c2440-uart
 71:        106   s3c-uart0  s3c2440-uart
 79:          0     s3c-adc  s3c2410_action
 80:          0     s3c-adc  s3c2410_action
 83:          0           -  s3c2410-wdt
Err:          0

在内核源码中查找Timer Tick(source insight中输入Timer Tick右键->Lookup References),arch\arm\plat-s3c24xx\time.c中出现其中断函数

/*
 * IRQ handler for the timer
 */
static irqreturn_t
s3c2410_timer_interrupt(int irq, void *dev_id)
{
	write_seqlock(&xtime_lock);
	timer_tick();
	write_sequnlock(&xtime_lock);
	return IRQ_HANDLED;
}

static struct irqaction s3c2410_timer_irq = {
	.name		= "S3C2410 Timer Tick",
	.flags		= IRQF_DISABLED | IRQF_TIMER | IRQF_IRQPOLL,
	.handler	= s3c2410_timer_interrupt,
};

在中断服务函数中根据当前进程pid进行判断,10s内如果同一个进程就打印

static irqreturn_t
s3c2410_timer_interrupt(int irq, void *dev_id) //每过5ms这个中断就会被调用一次
{
	/* 如果10s之内都是同一个进程在运行,就打印 */
	static pid_t pre_pid; //上一次进程pid号
	static int cnt = 0;

	if(pre_pid == current->pid){
		cnt++;
	}else{
		cnt = 0;
		pre_pid = current->pid;
	}
	if(cnt == 10*HZ){
		cnt = 0;
		printk("s3c2410_timer_interrupt : pid = %d, name = %s\n", current->pid, current->comm);//当前进程的pid和名字
	}

	write_seqlock(&xtime_lock);
	timer_tick();
	write_sequnlock(&xtime_lock);
	return IRQ_HANDLED;
}

 重新编译内核,加载驱动程序执行测试程序,每10s执行一次打印,可知道程序卡在firstdrvtest中

# insmod first_drv.ko
# ./firstdrvtest on
s3c2410_timer_interrupt : pid = 743, name = firstdrvtest
s3c2410_timer_interrupt : pid = 743, name = firstdrvtest
s3c2410_timer_interrupt : pid = 743, name = firstdrvtest 

 对于linux中断来说,先保存现场,后执行中断,再继续执行,循环执行,因此我们从保存现场中入手,存有各个寄存器的值,找到寄存器中pc值就可以知道程序具体的卡死在哪个地方,中断大致过程参考字符设备驱动详解,其中断总入口在arch\arm\kernel\asm_do_IRQ中,内核版本不同可能不是asm_do_IRQ这个函数,在asm_do_IRQ中pt_regs结构体中就保存着各种寄存器的值,irq_enter最终会进入系统时钟中断,把打印语句修改在asm_do_IRQ中

asmlinkage void __exception asm_do_IRQ(unsigned int irq, struct pt_regs *regs)
{
	struct pt_regs *old_regs = set_irq_regs(regs);
	struct irq_desc *desc = irq_desc + irq;
	static pid_t pre_pid; 
	static int cnt = 0;

	/*
	 * Some hardware gives randomly wrong interrupts.  Rather
	 * than crashing, do something sensible.
	 */
	if (irq >= NR_IRQS)
		desc = &bad_irq_desc;

	if(irq == 30){ //中断号30是系统时钟中断
		if(pre_pid == current->pid){
			cnt++;
		}else{
			cnt = 0;
			pre_pid = current->pid;
		}
		if(cnt == 10*HZ){
			cnt = 0;
			printk("asm_do_IRQ => s3c2410_timer_interrupt : pid = %d, task name = %s\n", current->pid, current->comm);//当前进程的pid和名字
			printk("pc = %08x\n", regs->ARM_pc);
		}		
	}

	irq_enter();

	desc_handle_irq(irq, desc);

	/* AT91 specific workaround */
	irq_finish(irq);

	irq_exit();
	set_irq_regs(old_regs);
}

 重新编译内核加载驱动执行测试程序,pc值为bf000084,根据内核地址可知这是insmod加载的驱动程序

# insmod first_drv.ko
# ./firstdrvtest on
asm_do_IRQ => s3c2410_timer_interrupt : pid = 742, task name = firstdrvtest
pc = bf000084

 一样查看cat /proc/kallsyms,找相近值

bf000000 t first_drv_open    [first_drv] 

反汇编first_drv.c,找到first_drv_open,本应该是在84的地方出错,对于中断, pc-4才是发生中断瞬间的地址,也就是我们故意加入的while的地方,对于实际情况有可能是卡在程序中某一段,因此我们可以根据PC值大致确定在哪个地方

00000000 <first_drv_open>:                    
0000003c <first_drv_write>:
  3c:    e1a0c00d     mov    ip, sp
  40:    e92dd800     stmdb    sp!, {fp, ip, lr, pc}
  44:    e24cb004     sub    fp, ip, #4    ; 0x4
  48:    e24dd004     sub    sp, sp, #4    ; 0x4
  4c:    e3cd3d7f     bic    r3, sp, #8128    ; 0x1fc0
  50:    e3c3303f     bic    r3, r3, #63    ; 0x3f
  54:    e5933008     ldr    r3, [r3, #8]
  58:    e0910002     adds    r0, r1, r2
  5c:    30d00003     sbcccs    r0, r0, r3
  60:    33a03000     movcc    r3, #0    ; 0x0
  64:    e3530000     cmp    r3, #0    ; 0x0
  68:    e24b0010     sub    r0, fp, #16    ; 0x10
  6c:    1a00001c     bne    e4 <init_module+0x5c>
  70:    ebfffffe     bl    70 <first_drv_write+0x34>
  74:    ea00001f     b    f8 <init_module+0x70>
  78:    e3520000     cmp    r2, #0    ; 0x0
  7c:    11a01002     movne    r1, r2
  80:    1bfffffe     blne    80 <first_drv_write+0x44>       // 卡死的地方
  84:    ea00001f     b    108 <init_module+0x80>

 

 

 

  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值