• <ins id="pjuwb"></ins>
    <blockquote id="pjuwb"><pre id="pjuwb"></pre></blockquote>
    <noscript id="pjuwb"></noscript>
          <sup id="pjuwb"><pre id="pjuwb"></pre></sup>
            <dd id="pjuwb"></dd>
            <abbr id="pjuwb"></abbr>

            A Za, A Za, Fighting...

            堅信:勤能補拙

                 摘要: 程序分析是以某種語言書寫的程序為對象,對其內部的運作流程進行分析。程序分析的目的主要有三點:一是通過程序內部各個模塊之間的調用關系,整體上把握程序的運行流程,從而更好地理解程序,從中汲取有價值的內容。二是以系統優化為目的,通過對程序中關鍵函數的跟蹤或者運行時信息的統計,找到系統性能的瓶頸,從而采取進一步行動對程序進行優化。最后一點,程序分析也有可能用于系統測試和程序調試中。當系統跟蹤起來比較復雜,...  閱讀全文
            posted @ 2011-08-12 10:33 simplyzhao 閱讀(443) | 評論 (0)編輯 收藏
            轉載: http://www.ibm.com/developerworks/cn/linux/l-cn-valgrind/

            應用 Valgrind 發現 Linux 程序的內存問題


            Valgrind 概述

            體系結構

            Valgrind是一套Linux下,開放源代碼(GPL V2)的仿真調試工具的集合。Valgrind由內核(core)以及基于內核的其他調試工具組成。內核類似于一個框架(framework),它模擬了一個CPU環境,并提供服務給其他工具;而其他工具則類似于插件 (plug-in),利用內核提供的服務完成各種特定的內存調試任務。Valgrind的體系結構如下圖所示:


            圖 1 Valgrind 體系結構
            Valgrind 體系結構 

            Valgrind包括如下一些工具:

            1. Memcheck。這是valgrind應用最廣泛的工具,一個重量級的內存檢查器,能夠發現開發中絕大多數內存錯誤使用情況,比如:使用未初始化的內存,使用已經釋放了的內存,內存訪問越界等。這也是本文將重點介紹的部分。
            2. Callgrind。它主要用來檢查程序中函數調用過程中出現的問題。
            3. Cachegrind。它主要用來檢查程序中緩存使用出現的問題。
            4. Helgrind。它主要用來檢查多線程程序中出現的競爭問題。
            5. Massif。它主要用來檢查程序中堆棧使用中出現的問題。
            6. Extension。可以利用core提供的功能,自己編寫特定的內存調試工具。

            Linux 程序內存空間布局

            要發現Linux下的內存問題,首先一定要知道在Linux下,內存是如何被分配的?下圖展示了一個典型的Linux C程序內存空間布局:


            圖 2: 典型內存空間布局
            典型內存空間布局 

            一個典型的Linux C程序內存空間由如下幾部分組成:

            • 代碼段(.text)。這里存放的是CPU要執行的指令。代碼段是可共享的,相同的代碼在內存中只會有一個拷貝,同時這個段是只讀的,防止程序由于錯誤而修改自身的指令。
            • 初始化數據段(.data)。這里存放的是程序中需要明確賦初始值的變量,例如位于所有函數之外的全局變量:int val=100。需要強調的是,以上兩段都是位于程序的可執行文件中,內核在調用exec函數啟動該程序時從源程序文件中讀入。
            • 未初始化數據段(.bss)。位于這一段中的數據,內核在執行該程序前,將其初始化為0或者null。例如出現在任何函數之外的全局變量:int sum;
            • 堆(Heap)。這個段用于在程序中進行動態內存申請,例如經常用到的malloc,new系列函數就是從這個段中申請內存。
            • 棧(Stack)。函數中的局部變量以及在函數調用過程中產生的臨時變量都保存在此段中。

            內存檢查原理

            Memcheck檢測內存問題的原理如下圖所示:


            圖 3 內存檢查原理
            內存檢查原理 

            Memcheck 能夠檢測出內存問題,關鍵在于其建立了兩個全局表。

            1. Valid-Value 表:

            對于進程的整個地址空間中的每一個字節(byte),都有與之對應的 8 個 bits;對于 CPU 的每個寄存器,也有一個與之對應的 bit 向量。這些 bits 負責記錄該字節或者寄存器值是否具有有效的、已初始化的值。

            1. Valid-Address 

            對于進程整個地址空間中的每一個字節(byte),還有與之對應的 1 個 bit,負責記錄該地址是否能夠被讀寫。

            檢測原理:

            • 當要讀寫內存中某個字節時,首先檢查這個字節對應的 A bit。如果該A bit顯示該位置是無效位置,memcheck 則報告讀寫錯誤。
            • 內核(core)類似于一個虛擬的 CPU 環境,這樣當內存中的某個字節被加載到真實的 CPU 中時,該字節對應的 V bit 也被加載到虛擬的 CPU 環境中。一旦寄存器中的值,被用來產生內存地址,或者該值能夠影響程序輸出,則 memcheck 會檢查對應的V bits,如果該值尚未初始化,則會報告使用未初始化內存錯誤。

            回頁首

            Valgrind 使用

            第一步:準備好程序

            為了使valgrind發現的錯誤更精確,如能夠定位到源代碼行,建議在編譯時加上-g參數,編譯優化選項請選擇O0,雖然這會降低程序的執行效率。

            這里用到的示例程序文件名為:sample.c(如下所示),選用的編譯器為gcc。

            生成可執行程序 gcc –g –O0 sample.c –o sample


            清單 1
            清單 1 

            第二步:在valgrind下,運行可執行程序。

            利用valgrind調試內存問題,不需要重新編譯源程序,它的輸入就是二進制的可執行程序。調用Valgrind的通用格式是:valgrind [valgrind-options] your-prog [your-prog-options]

            Valgrind 的參數分為兩類,一類是 core 的參數,它對所有的工具都適用;另外一類就是具體某個工具如 memcheck 的參數。Valgrind 默認的工具就是 memcheck,也可以通過“--tool=tool name”指定其他的工具。Valgrind 提供了大量的參數滿足你特定的調試需求,具體可參考其用戶手冊。

            這個例子將使用 memcheck,于是可以輸入命令入下:valgrind <Path>/sample.

            第三步:分析 valgrind 的輸出信息。

            以下是運行上述命令后的輸出。


            清單 2
            清單 2 
            • 左邊顯示類似行號的數字(32372)表示的是 Process ID。
            • 最上面的紅色方框表示的是 valgrind 的版本信息。
            • 中間的紅色方框表示 valgrind 通過運行被測試程序,發現的內存問題。通過閱讀這些信息,可以發現:
              1. 這是一個對內存的非法寫操作,非法寫操作的內存是4 bytes。
              2. 發生錯誤時的函數堆棧,以及具體的源代碼行號。
              3. 非法寫操作的具體地址空間。
            • 最下面的紅色方框是對發現的內存問題和內存泄露問題的總結。內存泄露的大小(40 bytes)也能夠被檢測出來。

            示例程序顯然有兩個問題,一是fun函數中動態申請的堆內存沒有釋放;二是對堆內存的訪問越界。這兩個問題均被valgrind發現。

            回頁首

            利用Memcheck發現常見的內存問題

            在Linux平臺開發應用程序時,最常遇見的問題就是錯誤的使用內存,我們總結了常見了內存錯誤使用情況,并說明了如何用valgrind將其檢測出來。

            使用未初始化的內存

            問題分析:

            對于位于程序中不同段的變量,其初始值是不同的,全局變量和靜態變量初始值為0,而局部變量和動態申請的變量,其初始值為隨機值。如果程序使用了為隨機值的變量,那么程序的行為就變得不可預期。

            下面的程序就是一種常見的,使用了未初始化的變量的情況。數組a是局部變量,其初始值為隨機值,而在初始化時并沒有給其所有數組成員初始化,如此在接下來使用這個數組時就潛在有內存問題。


            清單 3
            清單 3 

            結果分析:

            假設這個文件名為:badloop.c,生成的可執行程序為badloop。用memcheck對其進行測試,輸出如下。


            清單 4
            清單 4 

            輸出結果顯示,在該程序第11行中,程序的跳轉依賴于一個未初始化的變量。準確的發現了上述程序中存在的問題。

            內存讀寫越界

            問題分析:

            這種情況是指:訪問了你不應該/沒有權限訪問的內存地址空間,比如訪問數組時越界;對動態內存訪問時超出了申請的內存大小范圍。下面的程序就是一個典型的數組越界問題。pt是一個局部數組變量,其大小為4,p初始指向pt數組的起始地址,但在對p循環疊加后,p超出了pt數組的范圍,如果此時再對p進行寫操作,那么后果將不可預期。


            清單 5
            清單 5 

            結果分析:

            假設這個文件名為badacc.cpp,生成的可執行程序為badacc,用memcheck對其進行測試,輸出如下。


            清單 6
            清單 6 

            輸出結果顯示,在該程序的第15行,進行了非法的寫操作;在第16行,進行了非法讀操作。準確地發現了上述問題。

            內存覆蓋

            問題分析:

            C 語言的強大和可怕之處在于其可以直接操作內存,C 標準庫中提供了大量這樣的函數,比如 strcpy, strncpy, memcpy, strcat 等,這些函數有一個共同的特點就是需要設置源地址 (src),和目標地址(dst),src 和 dst 指向的地址不能發生重疊,否則結果將不可預期。

            下面就是一個 src 和 dst 發生重疊的例子。在 15 與 17 行中,src 和 dst 所指向的地址相差 20,但指定的拷貝長度卻是 21,這樣就會把之前的拷貝值覆蓋。第 24 行程序類似,src(x+20) 與 dst(x) 所指向的地址相差 20,但 dst 的長度卻為 21,這樣也會發生內存覆蓋。


            清單 7
            清單 7 

            結果分析:

            假設這個文件名為 badlap.cpp,生成的可執行程序為 badlap,用 memcheck 對其進行測試,輸出如下。


            清單 8
            清單 8 

            輸出結果顯示上述程序中第15,17,24行,源地址和目標地址設置出現重疊。準確的發現了上述問題。

            動態內存管理錯誤

            問題分析:

            常見的內存分配方式分三種:靜態存儲,棧上分配,堆上分配。全局變量屬于靜態存儲,它們是在編譯時就被分配了存儲空間,函數內的局部變量屬于棧上分配,而最靈活的內存使用方式當屬堆上分配,也叫做內存動態分配了。常用的內存動態分配函數包括:malloc, alloc, realloc, new等,動態釋放函數包括free, delete。

            一旦成功申請了動態內存,我們就需要自己對其進行內存管理,而這又是最容易犯錯誤的。下面的一段程序,就包括了內存動態管理中常見的錯誤。


            清單 9
            清單 9 

            常見的內存動態管理錯誤包括:

              • 申請和釋放不一致

            由于 C++ 兼容 C,而 C 與 C++ 的內存申請和釋放函數是不同的,因此在 C++ 程序中,就有兩套動態內存管理函數。一條不變的規則就是采用 C 方式申請的內存就用 C 方式釋放;用 C++ 方式申請的內存,用 C++ 方式釋放。也就是用 malloc/alloc/realloc 方式申請的內存,用 free 釋放;用 new 方式申請的內存用 delete 釋放。在上述程序中,用 malloc 方式申請了內存卻用 delete 來釋放,雖然這在很多情況下不會有問題,但這絕對是潛在的問題。

              • 申請和釋放不匹配

            申請了多少內存,在使用完成后就要釋放多少。如果沒有釋放,或者少釋放了就是內存泄露;多釋放了也會產生問題。上述程序中,指針p和pt指向的是同一塊內存,卻被先后釋放兩次。

              • 釋放后仍然讀寫

            本質上說,系統會在堆上維護一個動態內存鏈表,如果被釋放,就意味著該塊內存可以繼續被分配給其他部分,如果內存被釋放后再訪問,就可能覆蓋其他部分的信息,這是一種嚴重的錯誤,上述程序第16行中就在釋放后仍然寫這塊內存。

            結果分析:

            假設這個文件名為badmac.cpp,生成的可執行程序為badmac,用memcheck對其進行測試,輸出如下。


            清單 10
            清單 10 

            輸出結果顯示,第14行分配和釋放函數不一致;第16行發生非法寫操作,也就是往釋放后的內存地址寫值;第17行釋放內存函數無效。準確地發現了上述三個問題。

            內存泄露

            問題描述:

            內存泄露(Memory leak)指的是,在程序中動態申請的內存,在使用完后既沒有釋放,又無法被程序的其他部分訪問。內存泄露是在開發大型程序中最令人頭疼的問題,以至于有人說,內存泄露是無法避免的。其實不然,防止內存泄露要從良好的編程習慣做起,另外重要的一點就是要加強單元測試(Unit Test),而memcheck就是這樣一款優秀的工具。

            下面是一個比較典型的內存泄露案例。main函數調用了mk函數生成樹結點,可是在調用完成之后,卻沒有相應的函數:nodefr釋放內存,這樣內存中的這個樹結構就無法被其他部分訪問,造成了內存泄露。

            在一個單獨的函數中,每個人的內存泄露意識都是比較強的。但很多情況下,我們都會對malloc/free 或new/delete做一些包裝,以符合我們特定的需要,無法做到在一個函數中既使用又釋放。這個例子也說明了內存泄露最容易發生的地方:即兩個部分的接口部分,一個函數申請內存,一個函數釋放內存。并且這些函數由不同的人開發、使用,這樣造成內存泄露的可能性就比較大了。這需要養成良好的單元測試習慣,將內存泄露消滅在初始階段。


            清單 11
            清單 1 

            清單 11.2
            清單 11.2 

            清單 11.3
            清單 11.3 

            結果分析:

            假設上述文件名位tree.h, tree.cpp, badleak.cpp,生成的可執行程序為badleak,用memcheck對其進行測試,輸出如下。


            清單 12
            清單 12 

            該示例程序是生成一棵樹的過程,每個樹節點的大小為12(考慮內存對齊),共8個節點。從上述輸出可以看出,所有的內存泄露都被發現。Memcheck將內存泄露分為兩種,一種是可能的內存泄露(Possibly lost),另外一種是確定的內存泄露(Definitely lost)。Possibly lost 是指仍然存在某個指針能夠訪問某塊內存,但該指針指向的已經不是該內存首地址。Definitely lost 是指已經不能夠訪問這塊內存。而Definitely lost又分為兩種:直接的(direct)和間接的(indirect)。直接和間接的區別就是,直接是沒有任何指針指向該內存,間接是指指向該內存的指針都位于內存泄露處。在上述的例子中,根節點是directly lost,而其他節點是indirectly lost。

            回頁首

            總結

            本文介紹了valgrind的體系結構,并重點介紹了其應用最廣泛的工具:memcheck。闡述了memcheck發現內存問題的基本原理,基本使用方法,以及利用memcheck如何發現目前開發中最廣泛的五大類內存問題。在項目中盡早的發現內存問題,能夠極大地提高開發效率,valgrind就是能夠幫助你實現這一目標的出色工具。

            posted @ 2011-08-11 17:22 simplyzhao 閱讀(316) | 評論 (0)編輯 收藏
            一、什么是對齊,以及為什么要對齊:

            1. 現代計算機中內存空間都是按照byte劃分的,從理論上講似乎對任何類型的變量的訪問可以從任何地址開始,但實際情況是在訪問特定變量的時候經常在特定的內存地址訪問,這就需要各類型數據按照一定的規則在空間上排列,而不是順序的一個接一個的排放,這就是對齊。

            2. 對齊的作用和原因:各個硬件平臺對存儲空間的處理上有很大的不同。一些平臺對某些特定類型的數據只能從某些特定地址開始存取。其他平臺可能沒有這種情況, 但是最常見的是如果不按照適合其平臺的要求對數據存放進行對齊,會在存取效率上帶來損失。比如有些平臺每次讀都是從偶地址開始,如果一個int型(假設為 32位)如果存放在偶地址開始的地方,那么一個讀周期就可以讀出,而如果存放在奇地址開始的地方,就可能會需要2個讀周期,并對兩次讀出的結果的高低 字節進行拼湊才能得到該int數據。顯然在讀取效率上下降很多。這也是空間和時間的博弈。

            二、對齊的實現

            通常,我們寫程序的時候,不需要考慮對齊問題。編譯器會替我們選擇適合目標平臺的對齊策略。當然,我們也可以通知給編譯器傳遞預編譯指令而改變對指定數據的對齊方法。
            但是,正因為我們一般不需要關心這個問題,所以因為編輯器對數據存放做了對齊,而我們不了解的話,常常會對一些問題感到迷惑。最常見的就是struct數據結構的sizeof結果,出乎意料。為此,我們需要對對齊算法所了解。
            對齊的算法:
            由于各個平臺和編譯器的不同,現以本人使用的gcc version 3.2.2編譯器(32位x86平臺)為例子,來討論編譯器對struct數據結構中的各成員如何進行對齊的。
            設結構體如下定義:
            struct A {
                int a;
                char b;
                short c;
            };
            結構體A中包含了4字節長度的int一個,1字節長度的char一個和2字節長度的short型數據一個。所以A用到的空間應該是7字節。但是因為編譯器要對數據成員在空間上進行對齊。
            所以使用sizeof(strcut A)值為8。
            現在把該結構體調整成員變量的順序。
            struct B {
                char b;
                int a;
                short c;
            };
            這時候同樣是總共7個字節的變量,但是sizeof(struct B)的值卻是12。
            下面我們使用預編譯指令#pragma pack (value)來告訴編譯器,使用我們指定的對齊值來取代缺省的。
            #progma pack (2) /*指定按2字節對齊*/
            struct C {
                char b;
                int a;
                short c;
            };
            #progma pack () /*取消指定對齊,恢復缺省對齊*/
            sizeof(struct C)值是8。

            修改對齊值為1:
            #progma pack (1) /*指定按1字節對齊*/
            struct D {
                char b;
                int a;
                short c;
            };
            #progma pack () /*取消指定對齊,恢復缺省對齊*/
            sizeof(struct D)值為7。

            對于char型數據,其自身對齊值為1,對于short型為2,對于int,float,double類型,其自身對齊值為4,單位字節。
            這里面有四個概念值:
            1)數據類型自身的對齊值:就是上面交代的基本數據類型的自身對齊值。

            2)指定對齊值:#pragma pack (value)時的指定對齊值value。

            3)結構體或者類的自身對齊值:其成員中自身對齊值最大的那個值。

            4)數據成員、結構體和類的有效對齊值:自身對齊值和指定對齊值中較小的那個值。

                   有了這些值,我們就可以很方便的來討論具體數據結構的成員和其自身的對齊方式。有效對齊值N是最終用來決定數據存放地址方式的值,最重要。有效對齊N,就是表示“對齊在N上”,也就是說該數據的"存放起始地址%N=0".而數據結構中的數據變量都是按定義的先后順序來排放的。第一個數據變量的起始地址就是 數據結構的起始地址。結構體的成員變量要對齊排放,結構體本身也要根據自身的有效對齊值圓整(就是結構體成員變量占用總長度需要是對結構體有效對齊值的整 數倍,結合下面例子理解)。這樣就不難理解上面的幾個例子的值了。
            例子分析:
            分析例子B;
            struct B {
                char b;
                int a;
                short c;
            };
            假設B從地址空間0x0000開始排放。該例子中沒有定義指定對齊值,在筆者環境下,該值默認為4。第一個成員變量b的自身對齊值是1,比指定或者默認指 定對齊值4小,所以其有效對齊值為1,所以其存放地址0x0000符合0x0000%1=0.第二個成員變量a,其自身對齊值為4,所以有效對齊值也為 4,所以只能存放在起始地址為0x0004到0x0007這四個連續的字節空間中,復核0x0004%4=0,且緊靠第一個變量。第三個變量c,自身對齊 值為2,所以有效對齊值也是2,可以存放在0x0008到0x0009這兩個字節空間中,符合0x0008%2=0。所以從0x0000到0x0009存 放的都是B內容。再看數據結構B的自身對齊值為其變量中最大對齊值(這里是b)所以就是4,所以結構體的有效對齊值也是4。根據結構體圓整的要求, 0x0009到0x0000=10字節,(10+2)%4=0。所以0x0000A到0x000B也為結構體B所占用。故B從0x0000到0x000B 共有12個字節,sizeof(struct B)=12;

            同理,分析上面例子C:
            #pragma pack (2) /*指定按2字節對齊*/
            struct C {
                char b;
                int a;
                short c;
            };
            #pragma pack () /*取消指定對齊,恢復缺省對齊*/
            第一個變量b的自身對齊值為1,指定對齊值為2,所以,其有效對齊值為1,假設C從0x0000開始,那么b存放在0x0000,符合0x0000%1= 0;第二個變量,自身對齊值為4,指定對齊值為2,所以有效對齊值為2,所以順序存放在0x0002、0x0003、0x0004、0x0005四個連續 字節中,符合0x0002%2=0。第三個變量c的自身對齊值為2,所以有效對齊值為2,順序存放
            在0x0006、0x0007中,符合0x0006%2=0。所以從0x0000到0x00007共八字節存放的是C的變量。又C的自身對齊值為4,所以 C的有效對齊值為2。又8%2=0,C只占用0x0000到0x0007的八個字節。所以sizeof(struct C)=8.

            有 了以上的解釋,相信你對C語言的字節對齊概念應該有了清楚的認識了吧。在網絡程序中,掌握這個概念可是很重要的喔,在不同平臺之間(比如在Windows 和Linux之間)傳遞2進制流(比如結構體),那么在這兩個平臺間必須要定義相同的對齊方式,不然莫名其妙的出了一些錯,可是很難排查的哦^_^。
            posted @ 2011-08-10 19:18 simplyzhao 閱讀(130) | 評論 (0)編輯 收藏

            原文標題:Anatomy of a Program in Memory

            原文地址:http://duartes.org/gustavo/blog/

             

            [注:本人水平有限,只好挑一些國外高手的精彩文章翻譯一下。一來自己復習,二來與大家分享。]

             

                內存管理模塊是操作系統的心臟;它對應用程序和系統管理非常重要。今后的幾篇文章中,我將著眼于實際的內存問題,但也不避諱其中的技術內幕。由于不少概念是通用的,所以文中大部分例子取自32x86平臺的LinuxWindows系統。本系列第一篇文章講述應用程序的內存布局。

             

                在多任務操作系統中的每一個進程都運行在一個屬于它自己的內存沙盤中。這個沙盤就是虛擬地址空間virtual address space),在32位模式下它總是一個4GB的內存地址塊。這些虛擬地址通過頁表page table)映射到物理內存,頁表由操作系統維護并被處理器引用。每一個進程擁有一套屬于它自己的頁表,但是還有一個隱情。只要虛擬地址被使能,那么它就會作用于這臺機器上運行的所有軟件,包括內核本身。因此一部分虛擬地址必須保留給內核使用:

             

             

             

             

             

                這并不意味著內核使用了那么多的物理內存,僅表示它可支配這么大的地址空間,可根據內核需要,將其映射到物理內存。內核空間在頁表中擁有較高的特權級ring 2或以下),因此只要用戶態的程序試圖訪問這些頁,就會導致一個頁錯誤(page fault)。在Linux中,內核空間是持續存在的,并且在所有進程中都映射到同樣的物理內存。內核代碼和數據總是可尋址的,隨時準備處理中斷和系統調用。與此相反,用戶模式地址空間的映射隨進程切換的發生而不斷變化:

             

             

             

             

                藍色區域表示映射到物理內存的虛擬地址,而白色區域表示未映射的部分。在上面的例子中,Firefox使用了相當多的虛擬地址空間,因為它是傳說中的吃內存大戶。地址空間中的各個條帶對應于不同的內存段(memory segment),如:堆、棧之類的。記住,這些段只是簡單的內存地址范圍,與Intel處理器的段沒有關系。不管怎樣,下面是一個Linux進程的標準的內存段布局:

             

             

             

                當計算機開心、安全、可愛、正常的運轉時,幾乎每一個進程的各個段的起始虛擬地址都與上圖完全一致,這也給遠程發掘程序安全漏洞打開了方便之門。一個發掘過程往往需要引用絕對內存地址:棧地址,庫函數地址等。遠程攻擊者必須依賴地址空間布局的一致性,摸索著選擇這些地址。如果讓他們猜個正著,有人就會被整了。因此,地址空間的隨機排布方式逐漸流行起來。Linux通過對、內存映射、的起始地址加上隨機的偏移量來打亂布局。不幸的是,32位地址空間相當緊湊,給隨機化所留下的空當不大,削弱了這種技巧的效果

             

                進程地址空間中最頂部的段是棧,大多數編程語言將之用于存儲局部變量和函數參數。調用一個方法或函數會將一個新的棧楨stack frame)壓入棧中。棧楨在函數返回時被清理。也許是因為數據嚴格的遵從LIFO的順序,這個簡單的設計意味著不必使用復雜的數據結構來追蹤棧的內容,只需要一個簡單的指針指向棧的頂端即可。因此壓棧(pushing)和退棧(popping)過程非常迅速、準確。另外,持續的重用棧空間有助于使活躍的棧內存保持在CPU緩存中,從而加速訪問。進程中的每一個線程都有屬于自己的棧。

             

                通過不斷向棧中壓入的數據,超出其容量就有會耗盡棧所對應的內存區域。這將觸發一個頁故障(page fault),并被Linuxexpand_stack()處理,它會調用acct_stack_growth()來檢查是否還有合適的地方用于棧的增長。如果棧的大小低于RLIMIT_STACK(通常是8MB),那么一般情況下棧會被加長,程序繼續愉快的運行,感覺不到發生了什么事情。這是一種將棧擴展至所需大小的常規機制。然而,如果達到了最大的??臻g大小,就會棧溢出(stack overflow),程序收到一個段錯誤(Segmentation Fault)。當映射了的棧區域擴展到所需的大小后,它就不會再收縮回去,即使棧不那么滿了。這就好比聯邦預算,它總是在增長的。

             

                動態棧增長是唯一一種訪問未映射內存區域(圖中白色區域)而被允許的情形。其它任何對未映射內存區域的訪問都會觸發頁故障,從而導致段錯誤。一些被映射的區域是只讀的,因此企圖寫這些區域也會導致段錯誤。

             

                在棧的下方,是我們的內存映射段。此處,內核將文件的內容直接映射到內存。任何應用程序都可以通過Linuxmmap()系統調用(實現)或WindowsCreateFileMapping() MapViewOfFile()請求這種映射。內存映射是一種方便高效的文件I/O方式,所以它被用于加載動態庫。創建一個不對應于任何文件的匿名內存映射也是可能的,此方法用于存放程序的數據。在Linux中,如果你通過malloc()請求一大塊內存,C運行庫將會創建這樣一個匿名映射而不是使用堆內存。‘大塊’意味著比MMAP_THRESHOLD還大,缺省是128KB,可以通過mallopt()調整。

             

                說到堆,它是接下來的一塊地址空間。與棧一樣,堆用于運行時內存分配;但不同點是,堆用于存儲那些生存期與函數調用無關的數據。大部分語言都提供了堆管理功能。因此,滿足內存請求就成了語言運行時庫及內核共同的任務。在C語言中,堆分配的接口是malloc()系列函數,而在具有垃圾收集功能的語言(如C#)中,此接口是new關鍵字。

             

                如果堆中有足夠的空間來滿足內存請求,它就可以被語言運行時庫處理而不需要內核參與。否則,堆會被擴大,通過brk()系統調用(實現)來分配請求所需的內存塊。堆管理是很復雜的,需要精細的算法,應付我們程序中雜亂的分配模式,優化速度和內存使用效率。處理一個堆請求所需的時間會大幅度的變動。實時系統通過特殊目的分配器來解決這個問題。堆也可能會變得零零碎碎,如下圖所示:

             

             

             

                最后,我們來看看最底部的內存段:BSS,數據段,代碼段。在C語言中,BSS和數據段保存的都是靜態(全局)變量的內容。區別在于BSS保存的是未被初始化的靜態變量內容,它們的值不是直接在程序的源代碼中設定的。BSS內存區域是匿名的:它不映射到任何文件。如果你寫static int cntActiveUsers,則cntActiveUsers的內容就會保存在BSS中。

             

                另一方面,數據段保存在源代碼中已經初始化了的靜態變量內容。這個內存區域不是匿名的。它映射了一部分的程序二進制鏡像,也就是源代碼中指定了初始值的靜態變量。所以,如果你寫static int cntWorkerBees = 10,則cntWorkerBees的內容就保存在數據段中了,而且初始值為10。盡管數據段映射了一個文件,但它是一個私有內存映射,這意味著更改此處的內存不會影響到被映射的文件。也必須如此,否則給全局變量賦值將會改動你硬盤上的二進制鏡像,這是不可想象的。

             

                下圖中數據段的例子更加復雜,因為它用了一個指針。在此情況下,指針gonzo4字節內存地址)本身的值保存在數據段中。而它所指向的實際字符串則不在這里。這個字符串保存在代碼段中,代碼段是只讀的,保存了你全部的代碼外加零零碎碎的東西,比如字符串字面值。代碼段將你的二進制文件也映射到了內存中,但對此區域的寫操作都會使你的程序收到段錯誤。這有助于防范指針錯誤,雖然不像在C語言編程時就注意防范來得那么有效。下圖展示了這些段以及我們例子中的變量:

             

             

             

                你可以通過閱讀文件/proc/pid_of_process/maps來檢驗一個Linux進程中的內存區域。記住一個段可能包含許多區域。比如,每個內存映射文件在mmap段中都有屬于自己的區域,動態庫擁有類似BSS和數據段的額外區域。下一篇文章講說明這些“區域”(area)的真正含義。有時人們提到“數據段”,指的就是全部的數據段 + BSS + 堆。

             

                你可以通過nmobjdump命令來察看二進制鏡像,打印其中的符號,它們的地址,段等信息。最后需要指出的是,前文描述的虛擬地址布局在Linux中是一種“靈活布局”(flexible layout),而且以此作為默認方式已經有些年頭了。它假設我們有值RLIMIT_STACK。當情況不是這樣時,Linux退回使用“經典布局”(classic layout),如下圖所示:

             

             

             

                對虛擬地址空間的布局就講這些吧。下一篇文章將討論內核是如何跟蹤這些內存區域的。我們會分析內存映射,看看文件的讀寫操作是如何與之關聯的,以及內存使用概況的含義

            posted @ 2011-08-10 17:11 simplyzhao 閱讀(461) | 評論 (0)編輯 收藏
            數組與指針,究竟有什么關聯?什么情況下等同,什么時候不等同? 老問題,新收獲
            前兩天看《C專家編程》,結果硬是卡在數組與指針這塊好久,原本以為自己已經掌握的東西,原來還是沒有掌握精髓

            拋出兩個問題,然后我們在解決這兩個問題的過程中review指針與數組:
            問題一:
            char arr[] = "abcdefg";
            char *ptr = "abcdefg";
            我們知道,arr[i]與ptr[i]都是正確的,但兩者又有什么根本的差異呢?如果是char (*pa)[10]與char **pp,那么pa[1][2]與pp[1][2]有什么區別呢?

            問題二:
            qsort在針對指針數組與二維數組排序時的區別?

            ----------------------------------------------------------------------------------------------------------

            問題一:
            首先,要明確一點,編譯器為每個標識符分配一個地址,這個地址在編譯階段可知,相反,存儲在標識符中的值只有在運行時才可知

            對于數組,有 &arr = arr = &(arr[0])
            對于指針,有 &ptr != ptr = &(ptr[0])
            對于編譯器,它所知道的是&arr與&ptr,這兩個標識符的地址
            基于上述三條(前兩條,寫個簡單的程序即可測試),即可得出結論: 雖然arr[i]與ptr[i]都能正確訪問某個字符(事實上,都會被編譯器改寫成*(arr+i)或者*(ptr+i)的形式),但是兩者生成的匯編代碼是不同的

            驗證:
            C語言代碼:
            char arr[] = "abcdefg";
            char *ptr = "abcdefg";

            void
            difference()
            {
                
            char a = arr[3];
                
            char b = ptr[3];
            }

            匯編代碼(gcc -S test.c)
            difference:
                pushl    
            %ebp
                movl    
            %esp, %ebp
                subl    $
            16%esp

                /* char a = arr[3] */
                movzbl    arr
            +3%eax /* 取地址(arr+3)處的值,放入寄存器 */
                movb    
            %al, -1(%ebp)

                /* char b = ptr[3] */
                movl    ptr, 
            %eax /* 取ptr的值(Mem[ptr]),放入寄存器 */
                addl    $
            3%eax
                movzbl    (
            %eax), %eax /* 將寄存器%eax的值作為地址,取該地址的值放入寄存器 */
                movb    
            %al, -2(%ebp)

                leave
                ret

            略微對上述匯編代碼進行了注釋,可以發現兩者的差異,指針跟數組相比,前者多了“一層間接引用”
            (Note: 匯編尋址,例如,$Imm是立即數,Imm是絕對尋址(=Mem[Imm]),Ea是寄存器尋址,(Ea)則是間接尋址(=Mem[Register[Ea]]);可參考《深入理解計算機系統》第3章)

            如果是char (*pa)[10]與char **pp,pa是指向數組的指針,而pp是指向指針的指針,這里有點類似于之前的討論
            需要明白的就是: pa是指向數組的指針,保存的就是數組的地址;數組的地址等于數組首元素的地址;pa[i]等同于一元數組名稱,就是指向數組首元素的指針,也保存的是數組首元素的地址,因此有: pa+i(指向數組的指針) = pa[i] = &(pa[i][0])
            表達式pa[1][2]與pp[1][2]所產生的匯編代碼也是不同的
            (Note: 指向數組的指針p與數組名a(指向數組首元素的指針),兩者的值相同,但含義不一樣,例如p+1與a+1就完全不同)
            驗證:
            C語言代碼:
            void
            array_print(
            char (*pa)[10])
            {
                
            char c = pa[1][2];
            }

            void
            pointer_print(
            char **pp)
            {
                
            char c = pp[1][2];
            }

            匯編代碼:
            array_print:
                pushl    
            %ebp
                movl    
            %esp, %ebp
                subl    $
            16%esp

                movl    
            8(%ebp), %eax /* 將pa的值(Mem[pa])放入寄存器 */
                addl    $
            10%eax 
                movzbl    
            2(%eax), %eax
                movb    
            %al, -1(%ebp)

                leave
                ret

            pointer_print:
                pushl    
            %ebp
                movl    
            %esp, %ebp
                subl    $
            16%esp

                movl    
            8(%ebp), %eax /* 將pp的值放入寄存器 */    addl    $4%eax
                movl    (
            %eax), %eax
                addl    $
            2%eax
                movzbl    (
            %eax), %eax
                movb    
            %al, -1(%ebp)

                leave
                ret

            問題二:
            有了前面的講解,這里直接上代碼
            #include "basic.h"

            char *strings[] = { "zhao"
                
            "pan",
                
            "anan",
                NULL };

            char arr[][5= { "zhao",
                
            "pan",
                
            "anan" };

            void
            test_a2_addr()
            {
                printf(
            "Addr Testing\n");
                printf(
            "addr(arr+1): %p\n", arr+1);
                printf(
            "addr(arr[1]): %p\n", arr[1]);
                printf(
            "addr(arr[1][0]): %p\n"&arr[1][0]);
            }

            int
            compare_pp(
            const void *arg1, const void *arg2)
            {
                
            return strcmp(*(char **)arg1, *(char **)arg2);
            }

            int
            compare_a2(
            const void *arg1, const void *arg2)
            {
                
            return strcmp(*(char (*)[5])arg1, *(char (*)[5])arg2);
                
            //return strcmp((char *)arg1, (char *)arg2); //ok,too
            }

            void
            test_pp()
            {    
                qsort(strings, 
            3sizeof(char *), compare_pp);

                printf(
            "\nResult of qsort for POINTER-ARRAY\n");
                
            char **= strings;
                
            while(*!= NULL) {
                    printf(
            "%s\n"*s);
                    
            ++s;
                }
            }

            void
            test_a2()
            {
                qsort(arr, 
            3sizeof(char)*5, compare_a2);
                
                printf(
            "\nResult of qsort for TWO-ARRAY\n");
                
            int i;
                
            for(i=0; i<3++i)
                    printf(
            "%s\n", arr[i]);
            }

            int
            main(
            int argc, char **argv)
            {
                test_a2_addr();
                test_pp();
                test_a2();
            }

            posted @ 2011-08-10 16:38 simplyzhao 閱讀(119) | 評論 (0)編輯 收藏
            比較排序算法的下界: O(nlogn)

            非比較排序: 位排序,計數排序,基數排序

            位排序適用于: 1. 輸入數據限制在相對較小的范圍內; 2. 數據沒有重復; 3. 對于每條記錄而言,除了單一整數外,沒有任何其他關聯數據

            計數排序
            前提: n個輸入數據的每一個都是介于0到k之間的整數
            時間復雜度: O(n),如果k=O(n)
            基本思想: 對于每個輸入元素x,統計出小于等于x的元素的個數,有了這個信息,就可以把x直接放到最終輸出數組的正確位置上
            特點: 計數排序是穩定的。計數排序的穩定性,對于基數排序是至關重要的
            代碼:
            /*
             * counting-sort: stable, O(n)
             * precondition: the value of n input integers must between 0 and k, k = O(n)
             
            */
            void
            counting_sort(
            int *array, int *ret, int n, int k)
            {
                
            int i, j, *count = (int *)malloc((k+1* sizeof(int));
                assert(count 
            != NULL);
                memset(count, 
            0sizeof(int)*(k+1));

                
            for(i=0; i<n; ++i) {
                    assert(array[i]
            >=0 && array[i]<=k);
                    
            ++count[array[i]];
                }

                
            for(i=1; i<=k; ++i)
                    count[i] 
            += count[i-1];

                
            for(j=n-1; j>=0--j) {
                    ret[count[array[j]]
            -1= array[j];
                    
            --count[array[j]];
                }

                free(count);
            }

            基數排序
            基本思想: 按照最低有效位到最高有效位的順序(這里的位,可以看作是關鍵值),采用一種穩定性排序算法對該位進行排序
            偽代碼:
                 RADIX-SORT(A, d)
                        for i = 1..d /* 每一位,按低到高的順序 */
                              do use a stable sort to sort array A on digit i
            應用舉例:
            假設我們想根據三個關鍵字年,月,日來對日期進行排序
            對于這個問題,可以用帶有比較函數的排序算法來做,而另一方面,我們可以采用另一種方法,即用一種穩定的排序方法對所給的信息進行三次排序:先以日,其次對月,再對年
            posted @ 2011-08-10 15:15 simplyzhao 閱讀(133) | 評論 (0)編輯 收藏
            堆排序: 時間復雜度 O(nlogn),非穩定排序

            比如:3 27 36 27
            如果堆頂3先輸出,則,第三層的27(最后一個27)跑到堆頂,然后堆穩定,繼續輸出堆頂,是剛才那個27,這樣說明后面的27先于第二個位置的27輸出,不穩定。

            #define LEFT(i) (((i)<<1)+1)
            #define RIGHT(i) (((i)+1)<<1)
            #define PARENT(i) (((i)-1)>>1)

            void
            min_heapify(
            int index, int *heap, int heap_size) //precondition: LEFT(index) and RIGHT(index) are both already min-heap
            {
                
            int min = index;
                
            if(LEFT(index)<heap_size && heap[LEFT(index)]<heap[min])
                    min 
            = LEFT(index);
                
            if(RIGHT(index)<heap_size && heap[RIGHT(index)]<heap[min])
                    min 
            = RIGHT(index);

                
            if(min != index) {
                    swap(heap
            +min, heap+index);
                    min_heapify(min, heap, heap_size);
                }
            }

            void
            build_heap(
            int *heap, int heap_size)
            {
                
            int i = (heap_size>>1);
                
            for( ; i>=0--i)
                    min_heapify(i, heap, heap_size);
            }

            void
            heap_sort(
            int *heap, int heap_size)
            {
                build_heap(heap, heap_size);

                
            while(heap_size > 1) {
                    swap(heap, heap
            +heap_size-1);
                    
            --heap_size;
                    min_heapify(
            0, heap, heap_size);
                }
            }
            posted @ 2011-07-29 20:25 simplyzhao 閱讀(99) | 評論 (0)編輯 收藏
            歸并排序: 平均時間復雜度與最壞時間復雜度都是O(nlogn),穩定排序

            歸并排序是把序列遞歸地分成短序列,遞歸出口是短序列只有1個元素(認為直接有序)或者2個序列(1次比較和交換),然后把各個有序的段序列合并成一個有序的長序列,不斷合并直到原序列全部排好序??梢园l現,在1個或2個元素時,1個元素不會交換,2個元素如果大小相等也沒有人故意交換,這不會破壞穩定性。那么,在短的有序序列合并的過程中,穩定是是否受到破壞?沒有,合并過程中我們可以保證如果兩個當前元素相等時,我們把處在前面的序列的元素保存在結果序列的前面,這樣就保證了穩定性。所以,歸并排序也是穩定的排序算法。


            void
            merge(
            int *array, int *aux, int begin, int mid, int end)
            {
                
            int len = end - begin + 1;
                memcpy(aux
            +begin, array+begin, sizeof(int)*len);

                
            int *first, *second, *ptr = array+begin;
                first 
            = aux+begin;
                second 
            = aux+mid+1;
                
            while(first<=aux+mid && second<=aux+end) {
                    
            if(*first <= *second)
                        
            *ptr++ = *first++;
                    
            else
                        
            *ptr++ = *second++;
                }
                
            if(first <= aux+mid)
                    
            while(first <= aux+mid)
                        
            *ptr++ = *first++;
                
            if(second <= aux+end)
                    
            while(second <= aux+end)
                        
            *ptr++ = *second++;
            }

            void
            merge_sort_dc(
            int *array, int *aux, int begin, int end)
            {
                
            if(begin >= end)
                    
            return;
                
            int mid = begin + ((end-begin)>>1);
                merge_sort_dc(array, aux, begin, mid);
                merge_sort_dc(array, aux, mid
            +1, end);
                merge(array, aux, begin, mid, end);
            }

            void 
            merge_sort(
            int *array, int len)
            {
                
            int *aux = (int *)malloc(sizeof(int* len);
                merge_sort_dc(array, aux, 
            0, len-1);
                free(aux);
            }

            struct Node {
                
            int val;
                
            struct Node *next;
            };

            struct Node *
            list_merge(
            struct Node *first, struct Node *second)
            {
                
            if(first == NULL)
                    
            return second;
                
            if(second == NULL)
                    
            return first;

                
            struct Node *node = NULL;
                
            if(first->val <= second->val) {
                    node 
            = first;
                    first 
            = first->next;
                } 
            else {
                    node 
            = second;
                    second 
            = second->next;
                }
                node
            ->next = list_merge(first, second);
                
            return node;
            }

            struct Node *
            list_merge_sort(
            struct Node *list)
            {
                
            if(list==NULL || list->next==NULL)
                    
            return list;
                
            struct Node *once = list;
                
            struct Node *twice = list;
                
            while(twice->next && twice->next->next) {
                    once 
            = once->next;
                    twice 
            = twice->next->next;
                }
                twice 
            = once->next;
                once
            ->next = NULL;
                once 
            = list;
                list_merge(list_merge_sort(once), list_merge_sort(twice));
            }
            posted @ 2011-07-29 19:39 simplyzhao 閱讀(329) | 評論 (0)編輯 收藏
            快速排序: 平均時間復雜度O(nlogn),最壞時間復雜度O(n^2),非穩定排序

            快速排序有兩個方向,左邊的i下標一直往右走,當a[i] <= a[center_index],其中center_index是中樞元素的數組下標,一般取為數組第0個元素。而右邊的j下標一直往左走,當a[j] > a[center_index]。如果i和j都走不動了,i <= j, 交換a[i]和a[j],重復上面的過程,直到i>j。 交換a[j]和a[center_index],完成一趟快速排序。在中樞元素和a[j]交換的時候,很有可能把前面的元素的穩定性打亂,比如序列為 5 3 3 4 3 8 9 10 11, 現在中樞元素5和3(第5個元素,下標從1開始計)交換就會把元素3的穩定性打亂,所以快速排序是一個不穩定的排序算法,不穩定發生在中樞元素和a[j]交換的時刻(這里所展示的是partition的第二種方案, partition2)。

            partition1與partition2的優劣具體參考《編程珠璣》 第11章

            int
            partition1(
            int *array, int begin, int end)
            {
                
            int pivot = array[begin];
                
            int i, j = begin;
                
            for(i=begin+1; i<=end; ++i) {
                    
            if(array[i] < pivot) {
                        
            ++j;
                        
            if(i != j)
                            swap(array
            +i, array+j);
                    }
                }
                
            if(begin != j)
                    swap(array
            +begin, array+j);
                
            return j;
            }

            int
            partition2(
            int *array, int begin, int end)
            {
                
            int i, j, pivot = array[begin];
                i 
            = begin+1;
                j 
            = end;
                
            while(1) {
                    
            while(i<=end && array[i]<pivot)
                        
            ++i;
                    
            while(j>begin && array[j]>pivot)
                        
            --j;
                    
            if(i >= j)
                        
            break;
                    swap(array
            +i, array+j);
                    
            ++i;
                    
            --j;
                }
                
            if(begin != j)
                    swap(array
            +begin, array+j);
                
            return j;
            }

            void
            quick_sort(
            int *array, int begin, int end)
            {
                
            if(begin >= end)
                    
            return;
                
            int mid = partition2(array, begin, end);
                quick_sort(array, begin, mid
            -1);
                quick_sort(array, mid
            +1, end);
            }
            posted @ 2011-07-29 12:14 simplyzhao 閱讀(136) | 評論 (0)編輯 收藏
            希爾排序: 針對插入排序的改進,縮小增量的思想,非穩定排序

            希爾排序(Shell sort)也稱“縮小增量排序”。它的做法不是每次一個元素挨一個元素的比較。而是先將整個待排記錄序列分割成為若干子序列分別進行直接插入排序,待整個序列中的記錄基本有序時,再對全體記錄進行一次直接插入排序。這樣大大減少了記錄移動次數,提高了排序效率。 
            算法思路:先取一個正整數d1(d1<n),把全部記錄分成d1個組,所有距離為dl的倍數的記錄看成是一組,然后在各組內進行插入排序;接著取d2(d2<d1),重復上述分組和排序操作;直到di=1 (i>=1),即所有記錄成為一個組為止。希爾排序對增量序列的選擇沒有嚴格規定,一般選d1約為n/2,d2為d1/2,d3為d2/2,…,di=1。

            在希爾排序開始時增量較大,分組較多,每組的記錄數目少,故各組內直接插入較快,后來增量di逐漸縮小,分組數逐漸減少,而各組的記錄數目逐漸增多,但由于已經按di-1作為距離排過序,使文件較接近于有序狀態,所以新的一趟排序過程也較快。因此,希爾排序在效率上較直接插人排序有較大的改進。

            希爾排序是按照不同步長對元素進行插入排序,當剛開始元素很無序的時候,步長最大,所以插入排序的元素個數很少,速度很快;當元素基本有序了,步長很小,插入排序對于有序的序列效率很高。所以,希爾排序的時間復雜度會比o(n^2)好一些。由于多次插入排序,我們知道一次插入排序是穩定的,不會改變相同元素的相對順序,但在不同的插入排序過程中,相同的元素可能在各自的插入排序中移動,最后其穩定性就會被打亂,所以shell排序是不穩定的。

            void
            shell_sort(
            int *array, int len)
            {
                
            int i, j, step, backup;
                
            for(step=len>>1; step>0; step>>=1) {
                    
            for(i=step; i<len; ++i) {
                        backup 
            = array[i];
                        
            for(j=i-step; j>=0 && array[j]>backup; j-=step) 
                            array[j
            +step] = array[j];
                        array[j
            +step] = backup;
                    }
                }
            }
            posted @ 2011-07-28 18:10 simplyzhao 閱讀(138) | 評論 (0)編輯 收藏
            僅列出標題
            共21頁: 1 2 3 4 5 6 7 8 9 Last 

            導航

            <2025年5月>
            27282930123
            45678910
            11121314151617
            18192021222324
            25262728293031
            1234567

            統計

            常用鏈接

            留言簿(1)

            隨筆分類

            隨筆檔案

            搜索

            最新評論

            閱讀排行榜

            評論排行榜

            国内精品久久久久伊人av| 久久久SS麻豆欧美国产日韩| 狠狠精品干练久久久无码中文字幕 | 狠狠精品干练久久久无码中文字幕| 伊人久久一区二区三区无码| 久久人人爽人人爽人人片AV麻豆 | 中文字幕亚洲综合久久菠萝蜜| 亚洲一区中文字幕久久| 久久Av无码精品人妻系列| 久久婷婷五月综合国产尤物app| 激情久久久久久久久久| 亚洲国产成人精品91久久久 | 久久人人爽人人爽AV片| 伊人久久国产免费观看视频| 国产69精品久久久久APP下载 | 久久综合狠狠综合久久激情 | 日本精品久久久久中文字幕| 久久久精品久久久久久 | 久久se这里只有精品| 久久久久久久免费视频| 国产成人综合久久综合| 久久久久久久久久免免费精品 | 理论片午午伦夜理片久久 | 人人狠狠综合久久亚洲88| 久久精品视频一| 国产综合免费精品久久久| 久久无码高潮喷水| 久久无码精品一区二区三区| 久久亚洲精品中文字幕三区| 亚洲精品乱码久久久久久蜜桃图片 | 无码8090精品久久一区| 成人a毛片久久免费播放| 久久久久久亚洲Av无码精品专口| 91亚洲国产成人久久精品| 婷婷伊人久久大香线蕉AV| 久久中文字幕人妻丝袜| 色婷婷久久综合中文久久一本| 久久99国产精品久久99| aaa级精品久久久国产片| 99久久精品毛片免费播放| 国产韩国精品一区二区三区久久|