Linux內核crash/Oops異常定位分析方法【轉】

轉自:https://www.cnblogs.com/muahao/p/7452737.html

在內核開發的過程中,經常會碰到內核崩潰,比如空指針異常,內存訪問越界。通常我們只能靠崩潰之後打印出的異常調用棧信息來定位crash的位置和原因。總結下分析的方法和步驟。

通常oops發生之後,會在串口控制檯或者dmesg日誌輸出看到如下的log,以某arm下linux內核的崩潰爲例,

<2>[515753.310000] kernel BUG at net/core/skbuff.c:1846!
<1>[515753.310000] Unable to handle kernel NULL pointer dereference at virtual address 00000000
<1>[515753.320000] pgd = c0004000
<1>[515753.320000] [00000000] *pgd=00000000
<0>[515753.330000] Internal error: Oops: 817 [#1] PREEMPT SMP
<0>[515753.330000] last sysfs file: /sys/class/net/eth0.2/speed
<4>[515753.330000] module:  http_timeout     bf098000    4142
...
<4>[515753.330000] CPU: 0    Tainted: P             (2.6.36 #2)
<4>[515753.330000] PC is at __bug+0x20/0x28
<4>[515753.330000] LR is at __bug+0x1c/0x28
<4>[515753.330000] pc : [<c01472d0>]    lr : [<c01472cc>]    psr: 60000113
<4>[515753.330000] sp : c0593e20  ip : c0593d70  fp : cf1b5ba0
<4>[515753.330000] r10: 00000014  r9 : 4adec78d  r8 : 00000006
<4>[515753.330000] r7 : 00000000  r6 : 0000003a  r5 : 0000003a  r4 : 00000060
<4>[515753.330000] r3 : 00000000  r2 : 00000204  r1 : 00000001  r0 : 0000003c
<4>[515753.330000] Flags: nZCv  IRQs on  FIQs on  Mode SVC_32  ISA ARM  Segment kernel
<4>[515753.330000] Control: 10c53c7d  Table: 4fb5004a  DAC: 00000017
<0>[515753.330000] Process swapper (pid: 0, stack limit = 0xc0592270)
<0>[515753.330000] Stack: (0xc0593e20 to 0xc0594000)
<0>[515753.330000] 3e20: ce2ce900 c0543cf4 00000000 ceb4c400 000010cc c8f9b5d8 00000000 00000000
<0>[515753.330000] 3e40: 00000001 cd469200 c8f9b5d8 00000000 ce2ce8bc 00000006 00000026 00000010
...
<4>[515753.330000] [<c01472d0>] (PC is at __bug+0x20/0x28)
<4>[515753.330000] [<c01472d0>] (__bug+0x20/0x28) from [<c0543cf4>] (skb_checksum+0x3f8/0x400)
<4>[515753.330000] [<c0543cf4>] (skb_checksum+0x3f8/0x400) from [<bf11a8f8>] (et_isr+0x2b4/0x3dc [et])
<4>[515753.330000] [<bf11a8f8>] (et_isr+0x2b4/0x3dc [et]) from [<bf11aa44>] (et_txq_work+0x24/0x54 [et])
<4>[515753.330000] [<bf11aa44>] (et_txq_work+0x24/0x54 [et]) from [<bf11aa88>] (et_tx_tasklet+0x14/0x298 [et])
<4>[515753.330000] [<bf11aa88>] (et_tx_tasklet+0x14/0x298 [et]) from [<c0171510>] (tasklet_action+0x12c/0x174)
<4>[515753.330000] [<c0171510>] (tasklet_action+0x12c/0x174) from [<c05502b4>] (__do_softirq+0xfc/0x1a4)
<4>[515753.330000] [<c05502b4>] (__do_softirq+0xfc/0x1a4) from [<c0171c98>] (irq_exit+0x60/0x64)
<4>[515753.330000] [<c0171c98>] (irq_exit+0x60/0x64) from [<c01431fc>] (do_local_timer+0x60/0x74)
<4>[515753.330000] [<c01431fc>] (do_local_timer+0x60/0x74) from [<c054f900>] (__irq_svc+0x60/0x10c)
<4>[515753.330000] Exception stack(0xc0593f68 to 0xc0593fb0)
在這裏,我們着重關注下面幾點:

Oops信息 kernel BUG at net/core/skbuff.c:1846! Unable to handle kernel NULL pointer dereference at virtual address 00000000 , 這裏能夠簡要的告訴是什麼問題觸發了oops,如果是由代碼直接調用BUG()/BUG_ON()一類的,還能給出源代碼中觸發的行號。

寄存器PC/LR的值 PC is at __bug+0x20/0x28 LR is at __bug+0x1c/0x28, 這裏PC是發送oops的指令, 可以通過LR找到函數的調用者

CPU編號和CPU寄存器的值 sp ip fp r0~r10,

oops時,應用層的Process Process swapper (pid: 0, stack limit = 0xc0592270) , 如果crash發生在內核調用上下文,這個可以用來定位對應的用戶態進程

最重要的是調用棧,可以通過調用棧來分析錯誤位置

這裏需要說明一點, skb_checksum+0x3f8/0x400,在反彙編後,可以通過找到skb_checksum函數入口地址偏移0x3f8來精確定位執行點

在需要精確定位出錯位置的時候,我們就需要用到反彙編工具objdump了。下面就是一個示例,

    objdump -D -S xxx.o > xxx.txt
舉個例子,比如我們需要尋找棧 (et_isr+0x2b4/0x3dc [et]) from [<bf11aa44>] (et_txq_work+0x24/0x54 [et]),這裏我們可以知道這個函數是在 [et] 這個obj文件中,那麼我們可以直接去找 et.o ,然後反彙編 objdump -D -S et.o > et.txt , 然後et.txt中就是反彙編後的指令。當然,單看彙編指令會非常讓人頭疼,我們需要反彙編指令和源碼的一一對應纔好分析問題。這就需要我們在編譯compile的時候加上 -g 參數,把編譯過程中的symbol和調試信息一併加入到最後obj文件中,這樣objdump反彙編之後的文件中就包含嵌入的源碼文件了。

對於內核編譯來講,就是需要在內核編譯的根目錄下,修改Makefile中 KBUILD_CFLAGS , 加上 -g 編譯選項。

    KBUILD_CFLAGS   := -g -Wall -Wundef -Wstrict-prototypes -Wno-trigraphs \                       
               -fno-strict-aliasing -fno-common \
               -Werror-implicit-function-declaration \
               -Wno-format-security \
               -fno-delete-null-pointer-checks -Wno-implicit-function-declaration \
               -Wno-unused-but-set-variable \
               -Wno-unused-local-typedefs
下面是一份反編譯完成後的文件的部分截取。我們可以看到,這裏0x1f0是<et_isr> 這個函數的入口entry,c的源代碼是在前面,後面跟的彙編代碼是對應的反彙編指令

f0 <et_isr>:
et_isr(int irq, void *dev_id)
#else
static irqreturn_t BCMFASTPATH
et_isr(int irq, void *dev_id, struct pt_regs *ptregs)
#endif
{
f0:   e92d40f8    push    {r3, r4, r5, r6, r7, lr}
f4:   e1a04001    mov r4, r1
    struct chops *chops;
    void *ch;
    uint events = 0;

    et = (et_info_t *)dev_id;
    chops = et->etc->chops;
f8:   e5913000    ldr r3, [r1]
    ch = et->etc->ch;

    /* guard against shared interrupts */
    if (!et->etc->up)
fc:   e5d32028    ldrb    r2, [r3, #40]   ; 0x28
    struct chops *chops;
    void *ch;
    uint events = 0;

    et = (et_info_t *)dev_id;
    chops = et->etc->chops;
:   e5936078    ldr r6, [r3, #120]  ; 0x78
    ch = et->etc->ch;
:   e593507c    ldr r5, [r3, #124]  ; 0x7c

    /* guard against shared interrupts */
    if (!et->etc->up)
:   e3520000    cmp r2, #0
c:   1a000001    bne 218 <et_isr+0x28>
:   e1a00002    mov r0, r2
:   e8bd80f8    pop {r3, r4, r5, r6, r7, pc}
        goto done;

    /* get interrupt condition bits */
    events = (*chops->getintrevents)(ch, TRUE);
:   e5963028    ldr r3, [r6, #40]   ; 0x28
c:   e1a00005    mov r0, r5
:   e3a01001    mov r1, #1
:   e12fff33    blx r3
:   e1a07000    mov r7, r0

    /* not for us */
    if (!(events & INTR_NEW))
c:   e2100010    ands    r0, r0, #16
:   08bd80f8    popeq   {r3, r4, r5, r6, r7, pc}

    ET_TRACE(("et%d: et_isr: events 0x%x\n", et->etc->unit, events));
    ET_LOG("et%d: et_isr: events 0x%x", et->etc->unit, events);

    /* disable interrupts */
    (*chops->intrsoff)(ch);
:   e5963038    ldr r3, [r6, #56]   ; 0x38
:   e1a00005    mov r0, r5
c:   e12fff33    blx r3
        (*chops->intrson)(ch);
    }
在objdump反彙編出指令之後,我們可以根據調用棧上的入口偏移來找到對應的精確調用點。例如, (et_isr+0x2b4/0x3dc [et]) from [<bf11aa44>] (et_txq_work+0x24/0x54 [et]) , 我們可以知道調用點在et_isr入口位置+0x2b4偏移 ,而剛纔我們看到 et_isr的入口位置是0x1f0 ,那就是說在 0x1f0+0x2b4=0x4a4偏移位置。我們來看看,如下指令 4a4: e585007c str r0, [r5, #124] ; 0x7c,其對應的源代碼就是上面那一段c代碼, skb->csum = skb_checksum(skb, thoff, skb->len - thoff, 0);。而我們也知道,下一個調用函數的確是 skb_checksum , 說明精確的調用指令是準確的。

        ASSERT((prot == IP_PROT_TCP) || (prot == IP_PROT_UDP));
        check = (uint16 *)(th + ((prot == IP_PROT_UDP) ?
c:   e3580011    cmp r8, #17
:   13a0a010    movne   sl, #16
:   03a0a006    moveq   sl, #6
            offsetof(struct udphdr, check) : offsetof(struct tcphdr, check)));
        *check = 0;
:   e18720ba    strh    r2, [r7, sl]
    thoff = (th - skb->data);
    if (eth_type == HTON16(ETHER_TYPE_IP)) {
        struct iphdr *ih = ip_hdr(skb);
        prot = ih->protocol;
        ASSERT((prot == IP_PROT_TCP) || (prot == IP_PROT_UDP));
        check = (uint16 *)(th + ((prot == IP_PROT_UDP) ?
c:   e087200a    add r2, r7, sl
:   e58d2014    str r2, [sp, #20]
            offsetof(struct udphdr, check) : offsetof(struct tcphdr, check)));
        *check = 0;
        ET_TRACE(("et%d: skb_checksum: \n", et->etc->unit));
        skb->csum = skb_checksum(skb, thoff, skb->len - thoff, 0);
:   e5952070    ldr r2, [r5, #112]  ; 0x70
:   e58dc008    str ip, [sp, #8]
c:   e0612002    rsb r2, r1, r2
a0:   ebfffffe    bl  0 <skb_checksum>
a4:   e585007c    str r0, [r5, #124]  ; 0x7c
        *check = csum_tcpudp_magic(ih->saddr, ih->daddr,
a8:   e5953070    ldr r3, [r5, #112]  ; 0x70

static inline __wsum
csum_tcpudp_nofold(__be32 saddr, __be32 daddr, unsigned short len,
           unsigned short proto, __wsum sum)
{     
    __asm__(
ac:   e59dc008    ldr ip, [sp, #8]
有幾點比較geek的地方需要注意:

函數調用棧的調用不一定準確(不知道why?可能因爲調用過程是通過LR來反推到的,LR在執行過程中有可能被修改?),但是有一點可以確認,調用的點是準確的,也就是說調用函數不一定準,但是調用函數+偏移是能夠找到準確的調入指令
inline的函數以及被優化的函數可能不會出現在調用棧上,在編譯的時候因爲優化的需要,會就地展開代碼,這樣就不會在這裏有調用棧幀(stack frame)存在了

REF
https://www.ibm.com/developerworks/cn/linux/l-cn-kdump4/index.html?ca=drs

[email protected]

 

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