從零開始之驅動發開、linux驅動(六十九、內核調試篇--內核卡死分析)

內核卡死有很多種可能,

  • 驅動程序因爲邏輯問題,出現死循環
  • 共享資源出現死鎖
  • 系統跑飛等

一般情況下,系統跑飛內核會打印Oops信息。

有了Oops信息,我們就可以通過上一節的方法來推斷出出錯位置。

 

而死鎖或者驅動程序死循環並不會打印Oops信息。

這個時候需要我們自己打印出來,出錯位置的寄存器( r0 ~ r15等)信息來反推出出錯位置。

 

因爲這個時候卡住的位置基本已經是死掉了,所以只能採用別的方式來打印出內核的寄存器信息或者oops信息。

那這個時候什麼東西是還在正常運行的呢?

中斷

因爲中斷是是突發的,不受到其它驅動和應用程序的影響。

而且,如果某個程序執行死循環時,中斷髮生後,也會把這個死循環的程序的寄存器進行存儲的。

 

如何有看過我之前,關於異常和中斷章節的文章應該知道。

https://blog.csdn.net/qq_16777851/article/details/82528933

https://blog.csdn.net/qq_16777851/article/details/82556519

https://blog.csdn.net/qq_16777851/article/details/82564302

 

 

無論是執行傳統的中斷函數

.L__vectors_start:
	W(b)	vector_rst
	W(b)	vector_und
	W(ldr)	pc, .L__vectors_start + 0x1000
	W(b)	vector_pabt
	W(b)	vector_dabt
	W(b)	vector_addrexcptn
	W(b)	vector_irq        @----->>>>>>>>
	W(b)	vector_fiq




	vector_stub	irq, IRQ_MODE, 4

	.long	__irq_usr			@  0  (USR_26 / USR_32)    ------->
	.long	__irq_invalid			@  1  (FIQ_26 / FIQ_32)
	.long	__irq_invalid			@  2  (IRQ_26 / IRQ_32)
	.long	__irq_svc			@  3  (SVC_26 / SVC_32)   -------->
	.long	__irq_invalid			@  4
	.long	__irq_invalid			@  5
	.long	__irq_invalid			@  6
	.long	__irq_invalid			@  7
	.long	__irq_invalid			@  8
	.long	__irq_invalid			@  9
	.long	__irq_invalid			@  a
	.long	__irq_invalid			@  b
	.long	__irq_invalid			@  c
	.long	__irq_invalid			@  d
	.long	__irq_invalid			@  e
	.long	__irq_invalid			@  f





	.align	5
__irq_svc:
	svc_entry
	irq_handler            @-------->>>>>>>>>

#ifdef CONFIG_PREEMPT
	ldr	r8, [tsk, #TI_PREEMPT]		@ get preempt count
	ldr	r0, [tsk, #TI_FLAGS]		@ get flags
	teq	r8, #0				@ if preempt count != 0
	movne	r0, #0				@ force flags to 0
	tst	r0, #_TIF_NEED_RESCHED
	blne	svc_preempt
#endif

	svc_exit r5, irq = 1			@ return from exception
 UNWIND(.fnend		)
ENDPROC(__irq_svc)






/*
 * Interrupt handling.
 */
	.macro	irq_handler
#ifdef CONFIG_GENERIC_IRQ_MULTI_HANDLER
	ldr	r1, =handle_arch_irq                @新的中斷方式,分發中斷
	mov	r0, sp
	badr	lr, 9997f
	ldr	pc, [r1]
#else
	arch_irq_handler_default                @老的傳統中斷方式
#endif
9997:
	.endm

 

先看老的傳統中斷方式

/*
 * Interrupt handling.  Preserves r7, r8, r9
 */
	.macro	arch_irq_handler_default
	get_irqnr_preamble r6, lr
1:	get_irqnr_and_base r0, r2, r6, lr
	movne	r1, sp
	@
	@ routine called with r0 = irq number, r1 = struct pt_regs *
	@
	badrne	lr, 1b
	bne	asm_do_IRQ



asmlinkage void asm_do_IRQ(struct pt_regs *regs)
{
	irq_hw_number_t hwirq = get_intr_src();
	handle_domain_irq(root_domain, hwirq, regs);
}

 

接下來看新的中斷處理

/*
 * 定義了函數指針
 */
#ifdef CONFIG_GENERIC_IRQ_MULTI_HANDLER
void (*handle_arch_irq)(struct pt_regs *) __ro_after_init;
#endif


/* 
 * 運行期間可以更改這個指針,來改變中斷處理函數
 */

#ifdef CONFIG_GENERIC_IRQ_MULTI_HANDLER
int __init set_handle_irq(void (*handle_irq)(struct pt_regs *))
{
	if (handle_arch_irq)
		return -EBUSY;

	handle_arch_irq = handle_irq;
	return 0;
}
#endif

下面是新的幾種中斷的處理當時,包括vic,gic等

/*
 * Keep iterating over all registered VIC's until there are no pending
 * interrupts.
 */
static void __exception_irq_entry vic_handle_irq(struct pt_regs *regs)
{
	int i, handled;

	do {
		for (i = 0, handled = 0; i < vic_id; ++i)
			handled |= handle_one_vic(&vic_devices[i], regs);
	} while (handled);
}

 


static asmlinkage void __exception_irq_entry gic_handle_irq(struct pt_regs *regs)
{
	u32 irqnr;

	do {
		irqnr = gic_read_iar();

		if (likely(irqnr > 15 && irqnr < 1020) || irqnr >= 8192) {
			int err;

			if (static_branch_likely(&supports_deactivate_key))
				gic_write_eoir(irqnr);
			else
				isb();

			err = handle_domain_irq(gic_data.domain, irqnr, regs);
			if (err) {
				WARN_ONCE(true, "Unexpected interrupt received!\n");
				if (static_branch_likely(&supports_deactivate_key)) {
					if (irqnr < 8192)
						gic_write_dir(irqnr);
				} else {
					gic_write_eoir(irqnr);
				}
			}
			continue;
		}
		if (irqnr < 16) {
			gic_write_eoir(irqnr);
			if (static_branch_likely(&supports_deactivate_key))
				gic_write_dir(irqnr);
#ifdef CONFIG_SMP
			/*
			 * Unlike GICv2, we don't need an smp_rmb() here.
			 * The control dependency from gic_read_iar to
			 * the ISB in gic_write_eoir is enough to ensure
			 * that any shared data read by handle_IPI will
			 * be read after the ACK.
			 */
			handle_IPI(irqnr, regs);
#else
			WARN_ONCE(true, "Unexpected SGI received!\n");
#endif
			continue;
		}
	} while (irqnr != ICC_IAR1_EL1_SPURIOUS);
}

 

 

 


static asmlinkage void __exception_irq_entry gic_handle_irq(struct pt_regs *regs)
{
	u32 irqnr;

	do {
		irqnr = gic_read_iar();

		if (likely(irqnr > 15 && irqnr < 1020) || irqnr >= 8192) {
			int err;

			if (static_branch_likely(&supports_deactivate_key))
				gic_write_eoir(irqnr);
			else
				isb();

			err = handle_domain_irq(gic_data.domain, irqnr, regs);
			if (err) {
				WARN_ONCE(true, "Unexpected interrupt received!\n");
				if (static_branch_likely(&supports_deactivate_key)) {
					if (irqnr < 8192)
						gic_write_dir(irqnr);
				} else {
					gic_write_eoir(irqnr);
				}
			}
			continue;
		}
		if (irqnr < 16) {
			gic_write_eoir(irqnr);
			if (static_branch_likely(&supports_deactivate_key))
				gic_write_dir(irqnr);
#ifdef CONFIG_SMP
			/*
			 * Unlike GICv2, we don't need an smp_rmb() here.
			 * The control dependency from gic_read_iar to
			 * the ISB in gic_write_eoir is enough to ensure
			 * that any shared data read by handle_IPI will
			 * be read after the ACK.
			 */
			handle_IPI(irqnr, regs);
#else
			WARN_ONCE(true, "Unexpected SGI received!\n");
#endif
			continue;
		}
	} while (irqnr != ICC_IAR1_EL1_SPURIOUS);
}

 

上面我們看到一個共同點,就是這幾個c函數的參數是一樣的。

這裏我們看一下他的定義,可以看到18個long,存放的也是中斷調轉前的那個程序的18個寄存器。

struct pt_regs {
	unsigned 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]

 

如果我們向要看pc的值,我們只需要在內核中C中斷處理函數的入口添加打印即可。

比如vic

/*
 * Keep iterating over all registered VIC's until there are no pending
 * interrupts.
 */
static void __exception_irq_entry vic_handle_irq(struct pt_regs *regs)
{
	int i, handled;

        printk(KERN_ERR"vic_handle_irq entry pt_regs->pc = %x\n ",regs->ARM_pc);

	do {
		for (i = 0, handled = 0; i < vic_id; ++i)
			handled |= handle_one_vic(&vic_devices[i], regs);
	} while (handled);
}

上面這個是任何時候觸發中斷都會打印。一般不是我們想要的。

而且像內核心跳定時器是以ms級別都在觸發的,我們不可能讓不同的打印。

 

這個時候我們就要根據卡死時的現象來決定處理打印,中斷執行前的pc。

 

比如假設,5s時間當前進程都沒有進行切換,那就證明卡死,需要打印pc值(或者其他值)。

/*
 * Keep iterating over all registered VIC's until there are no pending
 * interrupts.
 */
static void __exception_irq_entry vic_handle_irq(struct pt_regs *regs)
{
	int i, handled;

    static pid_t old_pid = 0;
    static unsigned int cnt = 0;


    /* 進程不一樣,表示沒卡死 */
    if(current->pid != old_pid )
    {
        old_pid  = current->pid;
        cnt  = 0;
    }
    else
    {
        /* 進程一樣,且連續5s進程都一樣,表示卡死,這是打印出我們需要的進中斷前的寄存器信息 */
        if(cnt  >= 5* HZ)
        {
             printk(KERN_ERR"vic_handle_irq entry pt_regs->pc = %x\n ",regs->ARM_pc);
             cnt = 0;  
        }  
    }
  

	do {
		for (i = 0, handled = 0; i < vic_id; ++i)
			handled |= handle_one_vic(&vic_devices[i], regs);
	} while (handled);
}

 

打印出來後,根據pc值,知道對應位置的代碼,進而找打卡死的原因。

 

 

 

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章