日韩性视频-久久久蜜桃-www中文字幕-在线中文字幕av-亚洲欧美一区二区三区四区-撸久久-香蕉视频一区-久久无码精品丰满人妻-国产高潮av-激情福利社-日韩av网址大全-国产精品久久999-日本五十路在线-性欧美在线-久久99精品波多结衣一区-男女午夜免费视频-黑人极品ⅴideos精品欧美棵-人人妻人人澡人人爽精品欧美一区-日韩一区在线看-欧美a级在线免费观看

歡迎訪問 生活随笔!

生活随笔

當前位置: 首頁 > 运维知识 > linux >内容正文

linux

Linux内核出错的栈打印详解,linux内核中打印栈回溯信息 - dump_stack()函数分析

發布時間:2024/10/5 linux 48 豆豆
生活随笔 收集整理的這篇文章主要介紹了 Linux内核出错的栈打印详解,linux内核中打印栈回溯信息 - dump_stack()函数分析 小編覺得挺不錯的,現在分享給大家,幫大家做個參考.

簡介

當內核出現比較嚴重的錯誤時,例如發生Oops錯誤或者內核認為系統運行狀態異常,內核就會打印出當前進程的棧回溯信息,其中包含當前執行代碼的位置以及相鄰的指令、產生錯誤的原因、關鍵寄存器的值以及函數調用關系等信息,這些信息對于調試內核錯誤非常有用。

打印函數調用關系的函數就是dump_stack(),該函數不僅可以用在系統出問題的時候,我們在調試內核的時候,可以通過dump_stack()函數的打印信息更方便的了解內核代碼執行流程。

dump_stack()函數的實現和系統結構緊密相關,本文介紹ARM體系中dump_stack()函數的實現。該函數定義在arch/arm/kernel/traps.c文件中,調用dump_stack()函數不需要添加頭文件,基本上在內核代碼任何地方都可以直接使用該函數。

相關基本知識

讀者需要了解一些ARM匯編的基本知識。在講代碼之前,我先簡單說說內核中函數調用的一般過程。

關鍵寄存器介紹:

寄存器

含義

r0-r3

用作函數傳參,例如函數A調用函數B,如果A需要向B傳遞參數,則將參數放到寄存器r0-r3中,如果參數個數大于4,則需要借用函數的棧空間。

r4-r11

變量寄存器,在函數中可以用來保存臨時變量。

r9(SB)

靜態基址寄存器。

r10(SL)

棧界限寄存器。

r11(FP)

幀指針寄存器,通常用來訪問函數棧,幀指針指向函數棧中的某個位置。

r12(IP)

內部過程調用暫存寄存器。

r13(SP)

棧指針寄存器,用來指向函數棧的棧頂。

r14(LR)

鏈接寄存器,通常用來保存函數的返回地址。

r15(PC)

程序計數器,指向代碼段中下一條將要執行的指令,不過由于流水線的作用,PC會指向將要執行的指令的下一條指令。

內核中的函數棧

內核中,一個函數的代碼最開始的指令都是如下形式:

mov ip, sp

stmfd sp!, {r0 - r3} (可選的)

stmfd sp!, {..., fp, ip, lr, pc}

……

從其中兩條stmfd(壓棧)指令可以看出,一個函數的函數棧的棧底(高地址)的結構基本是固定的,如下圖:

首先我們約定被調用的函數稱為callee函數,而調用者函數稱為caller函數。

在進行函數調用的回溯時,內核中的dump_stack()函數需要做以下嘗試:

首先讀取系統中的FP寄存器的值,我們知道幀指針是指向函數棧的某個位置的,所以通過FP的值可以直接找到當前函數的函數棧的地址。

得到當前函數的代碼段地址,這個很容易,因為當前正在執行的代碼(可通過PC寄存器獲得)就處在函數的代碼段中。在函數棧中保存了一個PC寄存器的備份,通過這個PC寄存器的值可以定位到函數的第一條指令,即函數的入口地址。

得到當前函數的入口地址后,內核中保存了所有函數地址和函數名的對應關系,所以可以打印出函數名(詳見另一篇博客:內核符號表的查找過程)。

在當前函數的函數棧中還保存了caller函數的幀指針(FP寄存器的值),所以我們就可以找到caller函數的函數棧的位置。

繼續執行2-4步,直到某個函數的函數棧中保存的幀指針(FP寄存器的值)為0或非法。

發生函數調用時,函數棧和代碼段的關系如下圖所示:

dump_stack()函數

接下來我們就來看一下dump_stack()函數的實現。

dump_stack()主要是調用了下面的函數

c_backtrace(fp, mode);

兩個參數的含義為:

fp: current進程棧的fp寄存器。

mode: ptrace用到的PSR模式,在這里我們不關心。dump_stack傳入的值為0x10。

這兩個參數分別賦值給r0, r1寄存器傳給c_backtrace()函數。

c_backtrace函數定義如下(arch/arm/lib/backtrace.S):

@ 定義幾個局部變量

#define frame r4

#define sv_fp r5

#define sv_pc r6

#define mask r7

#define offset r8

@ 當前處于dump_backtrace函數的棧中

ENTRY(c_backtrace)

stmfd sp!, {r4 - r8, lr} @ 將r4-r8和lr壓入棧中,我們要使用r4-r8,所以備份一下原來的值。sp指向最后壓入的數據

movs frame, r0 @ frame=r0。r0為傳入的第一個參數,即fp寄存器的值

beq no_frame @ 如果frame為0,則退出

tst r1, #0x10 @ 26 or 32-bit mode? 判斷r1的bit4是否為0

moveq mask, #0xfc000003 @ mask for 26-bit 如果是,即r1=0x10,則mask=0xfc000003,即pc地址只有低26bit有效,且末兩位為0

movne mask, #0 @ mask for 32-bit 如果不是,即r1!=0x10,則mask=0

@ 下面是一段和該函數無關的代碼,用來計算pc預取指的偏移,一般pc是指向下兩條指令,所以offset一般等于8

1: stmfd sp!, {pc} @ 存儲pc的值到棧中,sp指向pc。

ldr r0, [sp], #4 @ r0=sp的值,即剛剛存的pc的值(將要執行的指令),sp=sp+4即還原sp

adr r1, 1b @ r1 = 標號1的地址,即指令 stmfd sp!, {pc} 的地址

sub offset, r0, r1 @ offset=r0-r1,即pc實際指向的指令和讀取pc的指令之間的偏移

/*

* Stack frame layout:

* optionally saved caller registers (r4 - r10)

* saved fp

* saved sp

* saved lr

* frame => saved pc @ frame即上面的fp,每個函數的fp都指向這個位置

* optionally saved arguments (r0 - r3)

* saved sp =>

*

* Functions start with the following code sequence:

* mov ip, sp

* stmfd sp!, {r0 - r3} (optional)

* corrected pc => stmfd sp!, {..., fp, ip, lr, pc} //將pc壓棧的指令

*/

@ 函數主流程:開始查找并打印調用者函數

for_each_frame: tst frame, mask @ Check for address exceptions

bne no_frame

@ 由sv_pc找到將pc壓棧的那條指令,因為這條指令在代碼段中的位置有特殊性,可用于定位函數入口。

1001: ldr sv_pc, [frame, #0] @ 獲取保存在callee棧里的sv_pc,它指向callee的代碼段的某個位置

1002: ldr sv_fp, [frame, #-12] @ get saved fp,這個fp就是caller的fp,指向caller的棧中某個位置

sub sv_pc, sv_pc, offset @ sv_pc減去offset,找到將pc壓棧的那條指令,即上面注釋提到的corrected pc。

bic sv_pc, sv_pc, mask @ mask PC/LR for the mode 清除sv_pc中mask為1的位,例如,mask=0x4,則清除sv_pc的bit2。

@ 定位函數的第一條指令,即函數入口地址

1003: ldr r2, [sv_pc, #-4] @ if stmfd sp!, {args} exists, 如果在函數最開始壓入了r0-r3

ldr r3, .Ldsi+4 @ adjust saved 'pc' back one. r3 = 0xe92d0000 >> 10

teq r3, r2, lsr #10 @ 比較stmfd指令機器碼是否相同(不關注是否保存r0-r9),目的是判斷是否為stmfd指令

subne r0, sv_pc, #4 @ allow for mov: 如果sv_pc前面只有mov ip, sp

subeq r0, sv_pc, #8 @ allow for mov + stmia: 如果sv_pc前面有兩條指令

@ 至此,r0為callee函數的第一條指令的地址,即callee函數的入口地址

@ 打印r0地址對應的符號名,傳給dump_backtrace_entry三個參數:

@ r0:函數入口地址,

@ r1:返回值即caller中的地址,

@ r2:callee的fp

ldr r1, [frame, #-4] @ get saved lr

mov r2, frame

bic r1, r1, mask @ mask PC/LR for the mode

bl dump_backtrace_entry

@ 打印保存在棧里的寄存器,這跟棧回溯沒關系,本文中不太關心

ldr r1, [sv_pc, #-4] @ if stmfd sp!, {args} exists, sv_pc前一條指令是否是stmfd指令

ldr r3, .Ldsi+4

teq r3, r1, lsr #10

ldreq r0, [frame, #-8] @ get sp。frame-8指向保存的IP寄存器,由于mov ip, sp,所以caller的sp=ip

@ 所以r0=caller的棧的低地址。

subeq r0, r0, #4 @ point at the last arg. r0+4就是callee的棧的高地址。

@ 由于參數的壓棧順序為r3,r2,r1,r0,所以這里棧頂實際上是最后一個參數。

bleq .Ldumpstm @ dump saved registers

@ 打印保存在棧里的寄存器,這跟棧回溯沒關系,本文中不太關心

1004: ldr r1, [sv_pc, #0] @ if stmfd sp!, {..., fp, ip, lr, pc}

ldr r3, .Ldsi @ instruction exists, 如果指令為frame指向的指令為stmfd sp!, {..., fp, ip, lr, pc}

teq r3, r1, lsr #10

subeq r0, frame, #16 @ 跳過fp, ip, lr, pc,即找到保存的r4-r10

bleq .Ldumpstm @ dump saved registers,打印出來r4-r10

@ 對保存在當前函數棧中的caller的fp做合法性檢查

teq sv_fp, #0 @ zero saved fp means 判斷獲取的caller的fp的值

beq no_frame @ no further frames 如果caller fp=0,則停止循環

@ 更新frame變量指向caller函數棧的位置,將上面注釋中的Stack frame layout

cmp sv_fp, frame @ sv_fp-frame

mov frame, sv_fp @ frame=sv_fp

bhi for_each_frame @ cmp的結果,如果frame

@ 這時frame指向caller棧的fp,由于函數中不會修改fp的值,所以這個fp肯定是指向caller保存的pc的位置的。

1006: adr r0, .Lbad @ 否則就打印bad frame提示

mov r1, frame

bl printk

no_frame: ldmfd sp!, {r4 - r8, pc}

ENDPROC(c_backtrace)

@ c_backtrace函數結束。

@ 將上面的代碼放到__ex_table異常表中。其中1001b ... 1006b是指上面的1001-1006標號。

.section __ex_table,"a"

.align 3

.long 1001b, 1006b

.long 1002b, 1006b

.long 1003b, 1006b

.long 1004b, 1006b

.previous

#define instr r4

#define reg r5

#define stack r6

@ 打印寄存器值

.Ldumpstm: stmfd sp!, {instr, reg, stack, r7, lr}

mov stack, r0

mov instr, r1

mov reg, #10

mov r7, #0

1: mov r3, #1

tst instr, r3, lsl reg

beq 2f

add r7, r7, #1

teq r7, #6

moveq r7, #1

moveq r1, #'\n'

movne r1, #' '

ldr r3, [stack], #-4

mov r2, reg

adr r0, .Lfp

bl printk

2: subs reg, reg, #1

bpl 1b

teq r7, #0

adrne r0, .Lcr

blne printk

ldmfd sp!, {instr, reg, stack, r7, pc}

.Lfp: .asciz "%cr%d:%08x"

.Lcr: .asciz "\n"

.Lbad: .asciz "Backtrace aborted due to bad frame pointer \n"

.align

.Ldsi:

@ 用來判斷是否是stmfd sp!指令,并且參數包含fp, ip, lr, pc,不包含r10

.word 0xe92dd800 >> 10 @ stmfd sp!, {... fp, ip, lr, pc}

@ 用來判斷是否是stmfd sp!指令,并且參數不包含r10, fp, ip, lr, pc

.word 0xe92d0000 >> 10 @ stmfd sp!, {}

與50位技術專家面對面20年技術見證,附贈技術全景圖

總結

以上是生活随笔為你收集整理的Linux内核出错的栈打印详解,linux内核中打印栈回溯信息 - dump_stack()函数分析的全部內容,希望文章能夠幫你解決所遇到的問題。

如果覺得生活随笔網站內容還不錯,歡迎將生活随笔推薦給好友。