一篇文章搞定堆棧原理

前方高能預(yù)警三椿,本文較長,涉及到的原理性東西較多葫辐,建議收藏方便后期查看搜锰。


1.png

我們常常說堆棧堆棧,但是堆和棧其實是完全不同的兩個概念耿战。棧其實完全是為了函數(shù)調(diào)用而設(shè)計的蛋叼,那么函數(shù)調(diào)用如何通過棧實現(xiàn)的呢?不用函數(shù)調(diào)用方式剂陡,棧在行為上有什么區(qū)別呢狈涮?筆者曾經(jīng)去京東面試一個高級開發(fā)職位,面試官寫了一個從1累加到100的C程序鸭栖,讓筆者寫出對應(yīng)的匯編代碼歌馍,如果你熟悉棧的原理,其實這個題目就并不難晕鹊,相反松却,函數(shù)通過棧如何實現(xiàn)的,這確實是我們廣大開發(fā)者必須掌握的基礎(chǔ)知識之一溅话,因為也是面試中用于考察一個開發(fā)者基礎(chǔ)水平的一個常見題型晓锻。
好了买窟,那什么是棧呢囊骤?下面是正文:

系統(tǒng)棧的工作原理:

1辨嗽、內(nèi)存的不同用途:

如果您關(guān)注網(wǎng)絡(luò)安全問題羹蚣,那么一定聽過緩沖區(qū)溢出這個術(shù)語糠悼。簡單說來:緩沖區(qū)溢出就是在大緩沖區(qū)中的數(shù)據(jù)向小緩沖區(qū)復(fù)制的過程中,由于沒有注意小緩沖區(qū)的邊界痊末,“撐爆”了較小的緩沖區(qū)檀蹋,從而沖掉了和小緩沖區(qū)相鄰內(nèi)存區(qū)域的其他數(shù)據(jù)而引起的內(nèi)存問題。緩沖溢出是最常見的內(nèi)存錯誤之一灿里,也是攻擊者入侵系統(tǒng)時所用到的最強(qiáng)大关炼、最經(jīng)典的一類漏洞利用方式。
成功地利用緩沖區(qū)溢出漏洞可以修改內(nèi)存中變量的值匣吊,甚至可以劫持進(jìn)程儒拂,執(zhí)行惡意代碼,最終獲得主機(jī)的控制權(quán)色鸳。要透徹地理解這種攻擊方式社痛,我們需要回顧一些計算機(jī)體系架構(gòu)方面的基礎(chǔ)知識,搞淸楚CPU命雀、寄存器蒜哀、內(nèi)存是怎樣協(xié)同工作而讓程序流暢執(zhí)行的。
根據(jù)不同的操作系統(tǒng)吏砂,一個進(jìn)程可能被分配到不同的內(nèi)存區(qū)域去執(zhí)行撵儿。但是不管什么樣的操作系統(tǒng)、什么樣的計算機(jī)架構(gòu)狐血,進(jìn)程使用的內(nèi)存都可以按照功能大致分成以下4個部分淀歇。
(1)代碼區(qū):這個區(qū)域存儲著被裝入執(zhí)行的二進(jìn)制機(jī)器代碼,處理器會到這個區(qū)域取指令并執(zhí)行匈织。
(2)數(shù)據(jù)區(qū):用于存儲全局變量等浪默。
(3)堆區(qū):進(jìn)程可以在堆區(qū)動態(tài)地請求一定大小的內(nèi)存,并在用完之后歸還給堆區(qū)缀匕。動態(tài)分配內(nèi)存和回收內(nèi)存是堆區(qū)的特點纳决。
(4)棧區(qū):用于動態(tài)地存儲函數(shù)之間的調(diào)用關(guān)系,以保證被調(diào)用函數(shù)在返回時恢復(fù)到母函數(shù)中繼續(xù)執(zhí)行弦追。
題外話:這種簡單的內(nèi)存劃分方式是為了讓您能夠更容易地理解程序的運(yùn)行機(jī)制入理解計算機(jī)系統(tǒng)》一書中有更詳細(xì)的關(guān)于內(nèi)存使用的論述岳链,如有興趣可參考之。
在Windows平臺下劲件,高級語言寫出的程序經(jīng)過編譯鏈接掸哑,最終會變成所謂的PE文件。當(dāng)PE文件被裝載運(yùn)行后零远,就成了所謂的進(jìn)程苗分。
PE文件代碼段中包含的二進(jìn)制級別的機(jī)器代碼會被裝入內(nèi)存的代碼區(qū)(.text),處理器將到內(nèi)存的這個區(qū)域一條一條地取出指令和操作數(shù),并送入算術(shù)邏輯單元進(jìn)行運(yùn)算牵辣;如果代碼中請求開辟動態(tài)內(nèi)存摔癣,則會在內(nèi)存的堆區(qū)分配一塊大小合適的區(qū)域返回給代碼區(qū)的代碼使用;當(dāng)函數(shù)調(diào)用發(fā)生時,函數(shù)的調(diào)用關(guān)系等信息會動態(tài)地保存在內(nèi)存的棧區(qū)择浊,以供處理器在執(zhí)行完被調(diào)用函數(shù)的代碼時戴卜,返冋母函數(shù)。這個協(xié)作過程如圖2.1.1所示琢岩。


2.1.1.png

如果把計算機(jī)看成一個有條不紊的1:廠投剥,我們可以得到如下類比。

  • CPU是完成工作的工人担孔。
  • 數(shù)據(jù)區(qū)江锨、堆區(qū)、棧區(qū)等則是用來存放原料糕篇、半成品啄育、成品等各種東西的場所。
  • 存在代碼區(qū)的指令則告訴CPU要做什么拌消,怎么做挑豌,到哪里去領(lǐng)原材料,用什么工具來做拼坎,做完以后把成品放到哪個貨艙去浮毯。
  • 值得一提的是,棧除了扮演存放原料泰鸡、半成品的倉庫之外,它還是車間調(diào)度主任的辦公室壳鹤。
    程序中所使用的緩沖區(qū)可以是堆區(qū)盛龄、棧區(qū)和存放靜態(tài)變景的數(shù)據(jù)區(qū)。緩沖區(qū)溢出的利用方法和緩沖區(qū)到底屬于上面哪個內(nèi)存區(qū)域密不可分芳誓。

2余舶、棧與系統(tǒng)棧:

從計算機(jī)科學(xué)的角度來看,棧指的是一種數(shù)據(jù)結(jié)構(gòu)锹淌,是一種先進(jìn)后出的數(shù)據(jù)表匿值。棧的最常見操作有兩種:壓棧(PUSH)、彈棧(POP):用于標(biāo)識棧的屬性也有兩個:棧頂(TOP)赂摆、棧底(BASE)挟憔。
可以把棧想象成一摞撲克牌。

  • PUSH:為棧增加一個元素的操作叫做PUSH烟号,相當(dāng)于在這摞撲克牌的最上面再放上—張绊谭。
  • POP:從棧中取出一個元素的操作叫做POP,相當(dāng)于從這摞撲克牌取出最上面的一張汪拥。
  • TOP:標(biāo)識棧頂位置达传,并且是動態(tài)變化的。每做一次PUSH操作,它都會自增1宪赶;相反宗弯,每做一次POP操作,它會自減1搂妻。棧頂元素相當(dāng)于撲克牌最上面一張罕伯,只有這張牌的花色是當(dāng)前可以看到的。
  • BASE:標(biāo)識棧底位置叽讳,它記錄著撲克牌最下面一張的位置追他。BASE用于防止棧空后繼續(xù)彈棧(牌發(fā)完時就不能再去揭牌了)岛蚤。很明顯邑狸,一般情況下,BASE是不會變動的涤妒。
    內(nèi)存的棧區(qū)實際上指的就是系統(tǒng)棧单雾。系統(tǒng)棧由系統(tǒng)自動維護(hù),它用于實現(xiàn)高級語言中函數(shù)的調(diào)用她紫。對于類似C語言這樣的高級語言硅堆,系統(tǒng)棧的PUSH、POP等堆棧平衡細(xì)節(jié)是透明的贿讹。
    —般說來渐逃,只有在使用匯編語言開發(fā)程序的時候,才需要和它直接打交道民褂。
    好茄菊,下面重點部分來了。

3赊堪、函數(shù)調(diào)用時發(fā)生了什么面殖?

我們下面就來探究一下高級語言中函數(shù)的調(diào)用和遞歸等性質(zhì)是怎樣通過系統(tǒng)棧巧妙實現(xiàn)的。請看如下代碼:

int func_B(int arg_B1, int arg_B2)
{
    int var_B1;
    int var_B2;
    var_B1 = arg_B1 + arg_B2;
    var_B2 = arg_B1 - arg_B2;
    return var_B1 * var_B2;
}
int func_A(int arg_A1, int arg_A2)
{
      int var_A;
      var_A = func_B(arg_A1, arg_A2) + arg_A1;
      return var_A;
}
int main(int argc, char** argv, char** envp)
{
      int var_main;
      var_main = func_A(3, 4);
      return 0;
}

這段代碼經(jīng)過編譯器編譯后哭廉,各個函數(shù)對應(yīng)的機(jī)器指令在代碼區(qū)中可能是這樣分布的脊僚,如圖2.1.2所示:
2.1.2.png

根據(jù)操作系統(tǒng)的不問、編譯器和編譯選項的不同遵绰,同一文件不同函數(shù)的代碼在內(nèi)存代碼區(qū)中的分布可能相鄰辽幌,也可能相離甚遠(yuǎn),可能先后有序街立,也可能無序舶衬;但它們都在同一個PE文件的代碼所映射的一個“節(jié)”里。我們可以簡單地把它們在內(nèi)存代碼區(qū)中的分布位置理解成是散亂無關(guān)的赎离。

當(dāng)CPU在執(zhí)行調(diào)用func_A函數(shù)的時候逛犹,會從代碼區(qū)中main函數(shù)對應(yīng)的機(jī)器指令的區(qū)域跳轉(zhuǎn)到func_A函數(shù)對應(yīng)的機(jī)器指令區(qū)域,在那里取指并執(zhí)行;當(dāng)函數(shù)執(zhí)行完閉虽画,需要返會的時候舞蔽,又會跳回到main函數(shù)對應(yīng)的指令區(qū)域,緊接著調(diào)用func_A后面的指令繼續(xù)執(zhí)行main函數(shù)的代碼码撰。在這個過程中渗柿,CPU的取指軌跡如圖2.1.3所示。
2.1.3.png
那么CPU是怎么知道要去func_A的代碼區(qū)取指脖岛,在執(zhí)行完func_A后又是怎么知道跳回到main函數(shù)(而不是func_B的代碼區(qū))的呢朵栖?這些跳轉(zhuǎn)地址我們在C語言中并沒有直接說明,CPU是從哪里獲得這些函數(shù)的調(diào)用及返回的信息的呢柴梆?
原來陨溅,這些代碼區(qū)中精確的跳轉(zhuǎn)都是在與系統(tǒng)棧巧妙地配臺過程中完成的。當(dāng)函數(shù)被調(diào)用時绍在,系統(tǒng)棧會為這個函數(shù)開辟一個新的棧幀门扇,并把它壓入棧中。這個棧幀中的內(nèi)存空間被它所屬的函數(shù)獨(dú)占偿渡,正常情況下是不會和別的函數(shù)共享的臼寄。當(dāng)函數(shù)返回時,系統(tǒng)棧會彈出該函數(shù)所對應(yīng)的棧幀溜宽。

如圖2.1.4所示吉拳,在函數(shù)調(diào)用的過程中,伴隨的系統(tǒng)棧中的操作如下坑质。
2.1.4.png
  • 在main函數(shù)中調(diào)用func_A的時候合武,首先在自己的棧幀中壓入函數(shù)返回地址,然后為func_A創(chuàng)建新棧幀并壓入系統(tǒng)棧涡扼。
  • 在func__A調(diào)用func_B的時候,同樣先在自己的棧幀中壓入函數(shù)返回地址盟庞,然后為func_B創(chuàng)建新棧幀并壓入系統(tǒng)棧吃沪。
  • 在func_B返回時,func_B的棧幀被彈出系統(tǒng)棧什猖,func_A棧幀中的返回地址被“露”在棧頂票彪,此時處理器按照這個返回地址重新跳到func_A代碼區(qū)中執(zhí)行。
  • 在func_A返同時不狮,func_A的棧幀被彈出系統(tǒng)棧.macn函數(shù)棧幀中的返回地址被“露” 在棧頂降铸,此時處理器按照這個返回地址跳到main函數(shù)代碼區(qū)中執(zhí)行。

4摇零、寄存器與函數(shù)棧幀

每一個函數(shù)獨(dú)占自己的棧幀空間推掸。當(dāng)前正在運(yùn)行的函數(shù)的棧幀總是在棧頂。CPU系統(tǒng)提供兩個特殊的寄存器用于標(biāo)識位于系統(tǒng)棧頂端的棧幀。
(1) ESP:棧指針寄存器(extended stack pointer)谅畅,其內(nèi)存放著一個指針登渣,該指針永遠(yuǎn)指向系統(tǒng)棧地上面-個棧幀的棧頂。
(2) EBP:基址指針寄存器(extended base pointer)-其內(nèi)存放著一個指針毡泻,該指針永遠(yuǎn)指向系統(tǒng)棧展上面一個棧幀的底部胜茧。
注意:EBP指向當(dāng)前位于系統(tǒng)棧最上邊一個棧幀的底部,而不是系統(tǒng)棧的底部仇味。嚴(yán)格說來呻顽,“棧幀底部”和“棧底”是不同的概念,本文在敘述中將堅特使用“棧幀底部”這一提法以示區(qū)別丹墨;ESP所指的棧幀頂部和系統(tǒng)棧的頂部是同一個位置廊遍,所以后面敘述中并不嚴(yán)格區(qū)分“棧幀頂部”和“棧頂”的概念。請您注意這里的差異带到,不要產(chǎn)生概念混淆昧碉。

寄存器對棧幀的標(biāo)識作用如圖2.1.5所示
2.1.5.png
函數(shù)棧幀:ESP和EBP之間的內(nèi)存空間為當(dāng)前棧幀.EBP標(biāo)識了當(dāng)前棧幀的底部.ESP標(biāo)識了當(dāng)前棧幀的頂部。
在函數(shù)棧幀中揽惹,一般包含以下幾類重要信息被饿。
(1) 局部變量:為函數(shù)局部變量開辟的內(nèi)存空間。
(2)棧幀狀態(tài)值:保存前棧幀的頂部和底部(實際上只保存前棧幀的底部搪搏,前棧幀的頂部可以通過堆棧平衡計算得到)狭握,用于在本幀被彈出后恢復(fù)出上一個棧幀。

(3) 函數(shù)返回地址:保存當(dāng)前函數(shù)調(diào)用前的“斷點”信息疯溺,也就是函數(shù)調(diào)用前的指令位置论颅,以便在函數(shù)返回時能夠恢復(fù)到函數(shù)被調(diào)用前的代碼區(qū)中繼續(xù)執(zhí)行指令。
除了與棧相關(guān)的寄存器外囱嫩,您還需要記住另一個至關(guān)重要的寄存器恃疯。
EIP:指令寄存器(Extended Instruction Pointer),其內(nèi)存放著一個指針墨闲,該指針永遠(yuǎn)指向下一條等待執(zhí)行的指令地址今妄,可以說如果控制了EIP寄存器的內(nèi)容,就控制了進(jìn)程——我們讓EIP指向哪里鸳碧,CPU就會去執(zhí)行哪里的指令盾鳞。

5、函數(shù)調(diào)用約定與相關(guān)指令

函數(shù)調(diào)用約定描述了函數(shù)傳遞參數(shù)方式和棧協(xié)同工作的技術(shù)細(xì)節(jié)瞻离。不同的操作系統(tǒng)腾仅、不同的語言、不同的編譯器在實現(xiàn)函數(shù)調(diào)用時的原理雖然基本相同套利,但具體的調(diào)用約定還是有差別的推励。這包括參數(shù)傳遞方式鹤耍,參數(shù)入棧順序是從右向左還是從左向古,函數(shù)返回時恢復(fù)堆棧平衡的操作在子函數(shù)中進(jìn)行還是在母函數(shù)中進(jìn)行吹艇。表2-1-1列出了幾種調(diào)用方式之間的差異惰蜜。
表2-1-1.png

具體的,對于Visual C++來說受神,可支持以下3中函數(shù)調(diào)用約定抛猖,如表2-1-2所示
表2-1-2.png
如果要明確使用某一種調(diào)用約定,只需要在函數(shù)前加上調(diào)用約定的聲明即可鼻听,否則默認(rèn)情況下會使用__cdecl的調(diào)用方式财著。
除了上邊的參數(shù)入棧方向和恢復(fù)棧平衡操作位置的不同之外,參數(shù)傳遞有時也會有所不同撑碴。例如撑教,每一個c++類成員函數(shù)都有一個this指針,在Wndows平臺中醉拓,這個指針一般是用ECX寄存器來傳遞的伟姐,但如果用GCC編譯器編譯,這個指針會作為最后一個參數(shù)壓入棧中亿卤。注意:同一段代碼用不同的編譯選項愤兵、不同的編譯器編譯鏈接后,得到的可執(zhí)行文件會有很多不同排吴。

函數(shù)調(diào)用大致包括以下幾個步驟.
(1) 參數(shù)入棧:將參數(shù)從右向左依次壓入系統(tǒng)棧中秆乳。
(2)返回地址入棧:將當(dāng)前代碼區(qū)調(diào)用指令的下一條指令地址壓入棧中,供函數(shù)返回時繼續(xù)執(zhí)行钻哩。
(3) 代碼區(qū)跳轉(zhuǎn):處理器從當(dāng)前代碼區(qū)跳轉(zhuǎn)到被調(diào)用函數(shù)的入口處屹堰。
(4)棧幀調(diào)整:具體包括。
保存當(dāng)前棧幀狀態(tài)值街氢,已備后面恢復(fù)本棧幀時使用(EBP入棧):
將當(dāng)前棧幀切換到新棧幀(將ESP值裝入EBP.更新棧幀底部):
給新棧幀分配空間(把ESP減去所需空間的大小扯键,抬高棧頂):

對于__stdcall調(diào)用約定,函數(shù)調(diào)用時用到的指令序列大致如下珊肃。
2.1.6.png
上面這段用于函數(shù)調(diào)用的指令在棧中引起的變化如圖2.1.7所示忧陪。
2.1.7.png

題外話.png
類似地,函數(shù)返回的步驟如下:
(1) 保存返回值:通常將函數(shù)的返回值保存在寄存器EAX中近范。
(2) 彈出當(dāng)前棧幀,恢復(fù)上一個棧幀延蟹。
具體包括:
  • 在堆棧平衡的基礎(chǔ)上评矩,給ESP加上棧幀的大小,降低棧頂阱飘,回收當(dāng)前棧幀的空間斥杜。
  • 將當(dāng)前棧幀底部保存的前棧幀EBP值彈入EBP寄存器虱颗,恢復(fù)出上一個棧幀。
  • 將函數(shù)返回地址彈給EIP寄存器蔗喂。
    (3) 跳轉(zhuǎn):按照函數(shù)返回地址跳同母函數(shù)中繼續(xù)執(zhí)行忘渔。
    還是以C語言和Win32平臺為例,函數(shù)返回時的相關(guān)的指令序列如下缰儿。
    add esp, xxx ;降低棧頂畦粮,回收當(dāng)前的棧幀
    pop ebp ;將上一個棧幀底部位置恢復(fù)至ebp.
    retn ;這條指令有兩個功能:
    a)彈出當(dāng)前棧頂元素,即彈出棧幀中的返回地址乖阵。棧幀恢復(fù)工作完成宣赔。
    b)讓處理器跳轉(zhuǎn)到彈出的返回地址,恢復(fù)調(diào)用前的代碼區(qū)瞪浸。
    按照這樣的函數(shù)調(diào)用約定組織起來的系統(tǒng)棧結(jié)構(gòu)如圖2.1.8所示:


    2.1.8.png
最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末儒将,一起剝皮案震驚了整個濱河市,隨后出現(xiàn)的幾起案子对蒲,更是在濱河造成了極大的恐慌钩蚊,老刑警劉巖,帶你破解...
    沈念sama閱讀 207,113評論 6 481
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件蹈矮,死亡現(xiàn)場離奇詭異砰逻,居然都是意外死亡,警方通過查閱死者的電腦和手機(jī)含滴,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 88,644評論 2 381
  • 文/潘曉璐 我一進(jìn)店門诱渤,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人谈况,你說我怎么就攤上這事勺美。” “怎么了碑韵?”我有些...
    開封第一講書人閱讀 153,340評論 0 344
  • 文/不壞的土叔 我叫張陵赡茸,是天一觀的道長。 經(jīng)常有香客問我祝闻,道長占卧,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 55,449評論 1 279
  • 正文 為了忘掉前任联喘,我火速辦了婚禮华蜒,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘豁遭。我一直安慰自己叭喜,他們只是感情好,可當(dāng)我...
    茶點故事閱讀 64,445評論 5 374
  • 文/花漫 我一把揭開白布蓖谢。 她就那樣靜靜地躺著捂蕴,像睡著了一般譬涡。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上啥辨,一...
    開封第一講書人閱讀 49,166評論 1 284
  • 那天涡匀,我揣著相機(jī)與錄音,去河邊找鬼溉知。 笑死陨瘩,一個胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的着倾。 我是一名探鬼主播拾酝,決...
    沈念sama閱讀 38,442評論 3 401
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼卡者!你這毒婦竟也來了蒿囤?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 37,105評論 0 261
  • 序言:老撾萬榮一對情侶失蹤崇决,失蹤者是張志新(化名)和其女友劉穎材诽,沒想到半個月后,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體恒傻,經(jīng)...
    沈念sama閱讀 43,601評論 1 300
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡脸侥,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 36,066評論 2 325
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發(fā)現(xiàn)自己被綠了盈厘。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片睁枕。...
    茶點故事閱讀 38,161評論 1 334
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖沸手,靈堂內(nèi)的尸體忽然破棺而出外遇,到底是詐尸還是另有隱情,我是刑警寧澤契吉,帶...
    沈念sama閱讀 33,792評論 4 323
  • 正文 年R本政府宣布跳仿,位于F島的核電站,受9級特大地震影響捐晶,放射性物質(zhì)發(fā)生泄漏菲语。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點故事閱讀 39,351評論 3 307
  • 文/蒙蒙 一惑灵、第九天 我趴在偏房一處隱蔽的房頂上張望山上。 院中可真熱鬧,春花似錦英支、人聲如沸胶哲。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,352評論 0 19
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽鸯屿。三九已至,卻和暖如春把敢,著一層夾襖步出監(jiān)牢的瞬間寄摆,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 31,584評論 1 261
  • 我被黑心中介騙來泰國打工修赞, 沒想到剛下飛機(jī)就差點兒被人妖公主榨干…… 1. 我叫王不留婶恼,地道東北人。 一個月前我還...
    沈念sama閱讀 45,618評論 2 355
  • 正文 我出身青樓柏副,卻偏偏與公主長得像勾邦,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子割择,可洞房花燭夜當(dāng)晚...
    茶點故事閱讀 42,916評論 2 344

推薦閱讀更多精彩內(nèi)容