1. 程式人生 > >《coredump問題原理探究》Linux x86版3.2節棧佈局之函式楨

《coredump問題原理探究》Linux x86版3.2節棧佈局之函式楨

看一個例子:

void FuncA()
{
}

void FuncB()
{
    FuncA();
}

int main()
{
    FuncB();
    return 0;
}


用下面命令編譯出它釋出版本:

[[email protected] 1]$ g++  -o xuzhina_dump_c3_s1_relxuzhina_dump_c3_s1.cpp

在討論它們的棧之前,先分析一下main,FuncB,FuncA這三個函式的彙編:

(gdb) disassemble FuncA
Dump of assembler code for function_Z5FuncAv:
  0x08048470 <+0>:    push   %ebp
  0x08048471 <+1>:    mov    %esp,%ebp
  0x08048473 <+3>:    pop    %ebp
  0x08048474 <+4>:    ret   
End of assembler dump.

(gdb) disassemble FuncB
Dump of assembler code for function_Z5FuncBv:
  0x08048475 <+0>:    push   %ebp
  0x08048476 <+1>:    mov    %esp,%ebp
  0x08048478 <+3>:    call   0x8048470 <_Z5FuncAv>
  0x0804847d <+8>:    pop    %ebp
  0x0804847e <+9>:    ret   
End of assembler dump.

(gdb) disassemble main
Dump of assembler code for function main:
  0x0804847f <+0>:    push   %ebp
  0x08048480 <+1>:    mov    %esp,%ebp
  0x08048482 <+3>:    call   0x8048475 <_Z5FuncBv>
  0x08048487 <+8>:    mov    $0x0,%eax
  0x0804848c <+13>:   pop    %ebp
  0x0804848d <+14>:   ret   
End of assembler dump.


從它們的彙編,都可以看到在這三個函式的開頭,都有這樣的指令:

push  %ebp
mov   %esp,%ebp

而在它們的結尾則有這樣的指令:

pop   %ebp
ret   

在沒有使用gcc的-fomit-frame-pointer選項來編譯的函式一般都會有這樣的開頭和結尾。這幾行指令可以看作是函式的開頭和結尾的特徵。像FuncA這樣空葉子函式,一般就是由這兩個特徵拼起來的。

在x86裡,ebp存放著函式楨指標,而esp則指向當前棧頂位置,而eip則是要執行的下一條指令地址。

所以,函式開頭的兩條指令的含義如下

push  %ebp             // esp = esp – 4,把ebp的值放入到esp指向的地址
mov   %esp,%ebp        // 把esp的值放到ebp裡。即ebp = esp

函式結尾兩條指令的含義如下

pop   %ebp                      // 把esp指向地址的內容放到ebp, esp = esp+4
ret                             //把ebp指向地址下一個單元的內容放到eip,esp = esp + 4

下面驗證一下上面的內容。在main函式的開頭指令地址0x0804847f打斷點

(gdb) tbreak *0x0804847f
Temporary breakpoint 1 at 0x804847f

逐步地看一下是不是
(gdb) r
Starting program:/home/buckxu/work/3/1/xuzhina_dump_c3_s1_rel
 
Temporary breakpoint 1, 0x0804847f in main()
(gdb) i r ebp esp
ebp            0x0      0x0
esp            0xbffff4dc       0xbffff4dc
(gdb) x /4x $esp
0xbffff4dc:     0x4a8bf635      0x00000001      0xbffff574      0xbffff57c
(gdb) ni
0x08048480 in main ()
(gdb) i r ebp esp
ebp            0x0      0x0
esp            0xbffff4d8       0xbffff4d8
(gdb) x /4x $esp
0xbffff4d8:     0x00000000      0x4a8bf635      0x00000001      0xbffff574

果然,在運行了
push  %ebp

之後,esp的值由0xbffff4dc變為0xbffff4d8,而它所指向的單元剛好是ebp的值0。這一操作實質是把舊的函式楨指標儲存到棧裡。

再繼續執行下去

(gdb) ni
0x08048482 in main ()
(gdb) i r ebp esp
ebp            0xbffff4d8       0xbffff4d8
esp            0xbffff4d8       0xbffff4d8
(gdb) x /4x $esp
0xbffff4d8:     0x00000000      0x4a8bf635      0x00000001      0xbffff574

可見
mov   %esp,%ebp

確實是把esp的值賦給了ebp,這一操作實質是設定函式楨指標,新的函式楨指標所指向的地址剛好放著舊的函式楨指標。

那為什麼要設定函式楨呢?只是考察了main函式,並不一定能夠找到規律,繼續執行FuncB,FuncA,看一下能不能找到規律。

(gdb) si
0x08048475 in FuncB() ()
(gdb) i r esp ebp
esp            0xbffff4d4       0xbffff4d4
ebp            0xbffff4d8       0xbffff4d8
(gdb) x /4x $esp
0xbffff4d4:     0x08048487      0x00000000      0x4a8bf635      0x00000001

只是進入了FuncB,沒有進行任何操作,esp的值卻比ebp要小4個位元組,變為0xbffff4d4,而0xbffff4d4地址卻放入一個值0x08048487,正好是
  0x08048482 <+3>:    call   0x8048475 <_Z5FuncBv>

的下一條指令的地址:

   0x08048487 <+8>:     mov   $0x0,%eax

這是由於main函式使用call指令呼叫FuncB時,call指令把0x08048487壓入棧的。這個地址實際上就是main函式的返回地址。

繼續執行

(gdb) ni
0x08048476 in FuncB() ()
(gdb) ni         
0x08048478 in FuncB() ()
(gdb) i r ebp esp
ebp            0xbffff4d0       0xbffff4d0
esp            0xbffff4d0       0xbffff4d0
(gdb) i r eip
eip            0x8048478        0x8048478 <FuncB()+3>
(gdb) x /4x $esp
0xbffff4d0:     0xbffff4d8      0x08048487      0x00000000      0x4a8bf635
(gdb) x /4x 0xbffff4d8
0xbffff4d8:     0x00000000      0x4a8bf635      0x00000001      0xbffff574

從上面可以看到,函式楨指標可能是一種單鏈表關係,它的表頭指標由ebp來放置。用C/C++語言方式表示,則是
struct  FramePointer
{
       struct  FramePointer* next;
};

它是不是一種連結串列關係呢?繼續執行到FuncA。

(gdb) tbreak FuncA
Temporary breakpoint 2 at 0x8048473
(gdb) c
Continuing.
 
Temporary breakpoint 2, 0x08048473 inFuncA() ()
(gdb) i r ebp esp
ebp            0xbffff4c8       0xbffff4c8
esp            0xbffff4c8       0xbffff4c8
(gdb) x /4x $ebp
0xbffff4c8:     0xbffff4d0      0x0804847d      0xbffff4d8      0x08048487
(gdb) x /4x 0xbffff4d0
0xbffff4d0:     0xbffff4d8      0x08048487      0x00000000      0x4a8bf635
(gdb) x /4x 0xbffff4d8
0xbffff4d8:     0x00000000      0x4a8bf635      0x00000001      0xbffff574

可見函式楨指標確實如上面structFramePointer,是由ebp為表頭。而且考察每個在棧裡的函式楨指標的下一個單元內容和eip的值,會發現這樣的情況:
(gdb) i r eip
eip            0x8048473        0x8048473 <FuncA()+3>
(gdb) info symbol 0x0804847d
FuncB() + 8 in section .text of/home/buckxu/work/3/1/xuzhina_dump_c3_s1_rel
(gdb) info symbol 0x08048487
main + 8 in section .text of/home/buckxu/work/3/1/xuzhina_dump_c3_s1_rel
正好和棧的內容一致:
(gdb) bt
#0  0x08048473 in FuncA() ()
#1  0x0804847d in FuncB() ()
#2  0x08048487 in main ()

所以,函式楨指標的結構擴充套件如下:

struct FramePointer
{
       struct FramePointer* next;
       void*  ret;               //返回地址
};

用圖形表示的話,如下:


gdb也是根據這個規律來解析棧,才能夠顯示正確的棧。那麼不正確的棧是怎樣的呢?

在瞭解這個問題之前,先考察一下函式結尾的特徵指令

pop   %ebp                      // 把esp指向地址的內容放到ebp, esp = esp+4
ret                             //把ebp指向地址下一個單元的內容放到eip, esp = esp + 4

由於現在程式已經執行到FuncA,看一下執行完FuncA,ebp,eip,esp的值是不是真的這樣變化:

(gdb) x /4x $ebp
0xbffff4c8:     0xbffff4d0     0x0804847d      0xbffff4d8      0x08048487
(gdb) i r eip esp ebp
eip            0x8048473        0x8048473 <FuncA()+3>
esp            0xbffff4c8       0xbffff4c8
ebp            0xbffff4c8       0xbffff4c8
(gdb) ni
0x08048474 in FuncA() ()
(gdb) ni
0x0804847d in FuncB() ()
(gdb) i r eip esp ebp
eip            0x804847d        0x804847d <FuncB()+8>
esp            0xbffff4d0       0xbffff4d0
ebp            0xbffff4d0       0xbffff4d0

果然ebp,esp,eip的變化在意料之中。

現在來仔細考察一下函式結尾的特徵指令,pop %ebp是把棧頂的內容放入ebp。如果棧頂的內容被修改了,指向一個非法的位置,結果會怎樣?

在這裡,重新跑一下程式,在FuncA退出前,修改一下棧的內容。

(gdb) tbreak FuncA
Temporary breakpoint 1 at 0x8048473
(gdb) r
Starting program:/home/buckxu/work/3/1/xuzhina_dump_c3_s1_rel
 
Temporary breakpoint 1, 0x08048473 inFuncA() ()
 (gdb) bt
#0  0x08048473 in FuncA() ()
#1  0x0804847d in FuncB() ()
#2  0x08048487 in main ()
(gdb) i r ebp eip esp
ebp            0xbffff4c8       0xbffff4c8
eip            0x8048473        0x8048473 <FuncA()+3>
esp            0xbffff4c8       0xbffff4c8
(gdb) x /4x $esp
0xbffff4c8:     0xbffff4d0      0x0804847d      0xbffff4d8      0x08048487
(gdb) set *0xbffff4c8 =0x10
(gdb) ni
0x08048474 in FuncA() ()
(gdb) i r ebp eip esp
ebp            0x10     0x10
eip            0x8048474        0x8048474 <FuncA()+4>
esp            0xbffff4cc       0xbffff4cc
(gdb) ni
Cannot access memory ataddress 0x14
(gdb) bt
#0  0x0804847d in FuncB() ()
Cannot access memory ataddress 0x14
(gdb) c
Continuing.
[Inferior 1 (process 1178) exited normally]

可以看到,gdb只能解析出FuncB函式楨,而無法解析出楨main函式楨,只因為FuncB的返回地址沒有被修改。

再看一下函式結尾的第二條指令,ret是把esp所指向的內容放到eip裡。假如esp所指向的內容是非法,棧又會變成怎樣?

在重新考察一下這個程式,但這次是修改esp+4那個單元的內容。

(gdb) tbreak FuncA
Temporary breakpoint 1 at 0x8048473
(gdb) r
Starting program: /home/buckxu/work/3/1/xuzhina_dump_c3_s1_rel
 
Temporary breakpoint 1, 0x08048473 inFuncA() ()
(gdb) bt
#0  0x08048473 in FuncA() ()
#1  0x0804847d in FuncB() ()
#2  0x08048487 in main ()
(gdb) i r eip esp ebp
eip            0x8048473        0x8048473 <FuncA()+3>
esp            0xbffff4c8       0xbffff4c8
ebp            0xbffff4c8       0xbffff4c8
(gdb) x /4x $esp
0xbffff4c8:     0xbffff4d0      0x0804847d      0xbffff4d8      0x08048487
(gdb) set *0xbffff4cc= 0x10
(gdb) ni
0x08048474 in FuncA() ()
(gdb) ni
0x00000010 in ?? ()
(gdb) bt
#0  0x00000010 in ?? ()
#1  0xbffff4d8 in ?? ()
#2  0x08048487 in main ()
(gdb) c
Continuing.
 
Program received signalSIGSEGV, Segmentation fault.
0x00000010 in ?? ()
(gdb) bt
#0  0x00000010 in ?? ()
#1  0xbffff4d8 in ?? ()
#2  0x08048487 in main ()
 

可以看到,出現了“??”的棧,這也是因為函式的返回地址被修改的原因。那麼,存放在棧上的函式楨指標和返回地址都被修改了,棧又會變成怎樣?
(gdb) tbreak FuncA
Temporary breakpoint 1 at 0x8048473
(gdb) r
Starting program:/home/buckxu/work/3/1/xuzhina_dump_c3_s1_rel
 
Temporary breakpoint 1, 0x08048473 inFuncA() ()
(gdb) bt
#0  0x08048473 in FuncA() ()
#1  0x0804847d in FuncB() ()
#2  0x08048487 in main ()
(gdb) i r eip esp ebp
eip            0x8048473        0x8048473 <FuncA()+3>
esp            0xbffff4c8       0xbffff4c8
ebp            0xbffff4c8       0xbffff4c8
(gdb) x /4x $esp
0xbffff4c8:     0xbffff4d0      0x0804847d      0xbffff4d8      0x08048487
(gdb) set *0xbffff4c8= 0x10
(gdb) set *0xbffff4cc= 0x20
(gdb) ni
0x08048474 in FuncA() ()
(gdb) ni
0x00000020 in ?? ()
(gdb) i r eip esp ebp           
eip            0x20     0x20
esp            0xbffff4d0       0xbffff4d0
ebp            0x10     0x10
(gdb) x /4x $esp           
0xbffff4d0:     0xbffff4d8      0x08048487      0x00000000      0x4a8bf635
(gdb) bt
#0  0x00000020 in ?? ()
#1  0xbffff4d8 in ?? ()
Backtrace stopped:previous frame inner to this frame (corrupt stack?)
(gdb) c
Continuing.
 
Program received signalSIGSEGV, Segmentation fault.
0x00000020 in ?? ()
(gdb) bt
#0  0x00000020 in ?? ()
#1  0xbffff4d8 in ?? ()
Backtrace stopped: previous frame inner tothis frame (corrupt stack?)
 

可以看到,這正好是前言看到那種的棧。現在可以知道,之後會出現“??“的棧,是因為存在棧上的函式楨指標和返回地址被修改了。在實際開發過程中,往往會由於拷貝記憶體導致這種情況。這種情況叫做棧溢位。

在這一章的最後一節“coredump例子“會顯示怎樣恢復部分正常的棧。而為什麼記憶體拷貝之類的操作會導致棧溢位,原因會放在第5章裡講述。





相關推薦

coredump問題原理探究Linux x863.2佈局函式

看一個例子: void FuncA() { } void FuncB() { FuncA(); } int main() { FuncB(); return 0; } 用下面命令編譯出它釋出版本: [[email protecte

coredump問題原理探究Linux x867.8vector相關的iterator對象

eip 有一個 匯編 cor get cto 一個 data iter 在前面看過了一個vectorcoredump的樣例,接觸了vector的iterator,能

rpm包依賴那些坑 ld-linux-x86-64.so.2:bad ELF interpreter

rpm缺少依賴 yum依賴沖突前言 在rpm/dpkg 安裝軟件包時會經常包缺少依賴類的錯誤,往往這時會選擇忽略依賴安裝,雖然有時這種方式解決了當下的問題,但卻為以後挖了坑。 往往是yum/apt 倉庫沒有相應軟件或相應版本時才使用rpm/dpkg 安裝軟件包,而出現這種情況大多數又是因為網絡限制或內部部署了

解決挖礦病毒占用cpu以及誤刪 ld-linux-x86-64.so.2 文件的問題

轉移 第一條 根目錄 man bios 原本 光盤 防止 隱藏權限 上次已經被抓去挖礦了當了一次曠工了,本以為解決了,沒想到竟然死灰復燃。 這次占用cpu的依然是一個ld-linux的進程,kill掉之後同樣就查了關於test用戶的進程,果然,test用戶的進程有100+個

relocation error: /usr/lib64/libc.so.6: symbol _dl_starting_up, version GLIBC_PRIVATE not defined in file ld-linux-x86-64.so.2 w

在建立一個錯誤的軟連線到ld-linux-x86-64.so.2時,悲劇就這麼發生了。此時大部分命令都不能使用,SSH當然也不能登入了。這個時候一定不要退出終端。 有人說那就把軟連線復原吧,可是ln也同樣無法使用。。。這時候我們就可以使用可愛的sln命令就可以了,哈哈。 lsn /usr/lib64/ld-

Linux下的ds18b20驅動(執行環境 Fedora9.0 交叉編譯 arm-linux-gcc-4.3.2 核心版本2.6.32)

今天在各位前輩已有成就的基礎上花了兩天時間終於把這個驅動給搞定了,從開始編譯成模組看效果,進行除錯,再到編譯進核心,最後又編譯了一個介面出來,雖說大多數的程式程式碼是用各位前輩的成果,但坐下來自己收穫也不小,現在寫下來,以供以後參考,也和各位愛好者交流一下,呵呵! 一.編譯成

arm-linux-gcc 4.3.2下載與安裝

  下載arm-linux-gcc-4.3.2.tgz(84MB) 安裝交叉編譯工具鏈: 1、首先以root使用者登入 2、複製arm-linux-gcc-4.3.2.tgz到根目錄下tmp資料夾裡 3、解壓命令tar xvzf arm-linux-gcc-4.3.2.tg

Arm-linux-gcc-4.3.2安裝步驟

  1.關於這個編譯好的工具的安裝: 下載arm-linux-gcc-4.3.2.tgz大約84m 首先以root使用者登入 複製arm-linux-gcc-4.3.2.tgz到根目錄下tmp資料夾裡 解壓命令tar xvzf arm-linux-gcc-4.3.2.tgz

C++Primer第五 6.3.2練習

練習6.30:編譯第200頁的str_subrange函式,看看你的編譯器是如何處理函式中的錯誤的。 答: E:\C++ Primer 第五版 練習和解答\第六章 函式\習題程式\練習6.30.cpp [Error] return-statement wi

ubuntu14.01 搭建交叉編譯環境arm-linux-gcc 4.3.2

安裝步驟 0. 安裝標準的C開發環境,由於Ubuntu 9.04 Linux安裝預設是不安裝的,所以需要先安裝一下(如果已經安裝好的話,就可以免去這一步了):$ sudo apt-get install gcc g++ libgcc1 libg++ make gdb如果

Linux安裝Redis-3.2.8流程和問題

以下是參考的一篇文章,基本上文章描述的問題都遇到了,也參考解決了。在這裡記錄下,以備後用。主要問題是依賴包和cpu64位不支援問題。需要改動的redis配置。 安裝依賴包 #yum install gcc gcc-c++ tcl -y PS:遇到的問題 a、如果不安

3.2共享空間

用一個數組還儲存兩個棧。陣列有兩個端點,兩個棧有兩個棧底,讓一個棧的棧底作為陣列的始端,即下標為0處,另一個棧為陣列的末端,即下標為陣列長度n-1處。這樣,兩個棧如果增加元素,就是兩端點向中間延伸。 關鍵思路: 它們是在陣列的兩端,向中間靠攏。top1和top2是棧1和棧2的棧頂指標,只要它們兩個不見面,

3.2判斷迴文字串

/* 演算法思想: 1.當字串的長度是偶數時,入棧的字元個數正好是整個字串的一半; 則在棧非空的情況下,依次將棧頂元素出棧,並和字串後半段的元素比較, 當棧頂元素和當前字串不相同,說明不是迴文串,返回false;反之, 將新的棧頂元素和下一個字元比較,直到棧為空時,說明是迴

cocos2dx[3.2](8)——新回撥函式std::bind

【嘮叨】 自從3.0引用了C++11標準後,回撥函式採用的新的函式介面卡:std::function、std::bind。 而曾經的回撥函式menu_selector、callfunc_selector、cccontrol_selector等都已經被無情的拋棄

Mybatis學習第2 -- 模糊查詢#和$的區別

url %s amp 參數 interface containe 模糊查詢 res ssi 先說結論, #是占位符,而$的行為是字符串拼接, 在參數是java的基本類型且只有一個參數的情況下,使用$時,只能用value作為參數傳遞 需求決定設計, 先在interface裏

深入理解Linux核心第3--筆記-2.pdf

Chapter 8. Memory Management 8.1. Page Frame Management 8.1.1. Page Descriptors State information of a page frame is kept in a page des

Linux系統程式設計【3.2】——ls命令優化和ls -l實現

## 前情提要 在筆者的上一篇部落格[Linux系統程式設計【3.1】——編寫ls命令](https://www.cnblogs.com/lularible/p/14386358.html)中,實現了初級版的ls命令,但是與原版ls命令相比,還存在著顯示格式和無顏色標記的不同。經過筆者近兩天的學習,基本解決了

ReSharper 2016.3.2 Ultimate 官方最新破解

per www ron blog idea article rpe key 最新 http://www.cheerfulstudy.com/article?newsid=1560# 分享幾個已經部署好的在線驗證服務器:(2017-03-21) http://idea.ite

Visual Studio 2017 Enterprise 發布 15.3.2 ,附離線安裝包下載。

安裝 net 離線 地址 2017年 out 全量 全部 lock Visual Studio 2017 Enterprise 更新至 15.3.2 ,本安裝包使用微軟原版安裝文件,配合layout指令全量下載後制作,內置中文語言包,包含 Visual Studio 201

3.2《深入理解計算機系統》筆記(二)內存和高速緩存的原理【插圖】

img sram 本質 text ddr rate too 是我 很大的 《深入計算機系統》筆記(一)主要是講解程序的構成、執行和控制。接下來就是運行了。我跳過了“處理器體系結構”和“優化程序性能”,這兩章的筆記繼續往後延遲! 《深入計算機系統》的一個很大的用處