常見C++面試題(三)


strcpy和memcpy有什么區別?strcpy是如何設計的,memcpy呢?
 
strcpy提供了字符串的復制。即strcpy只用於字符串復制,並且它不僅復制字符串內容之外,還會復制字符串的結束符。(保證dest可以容納src。)
memcpy提供了一般內存的復制。即memcpy對於需要復制的內容沒有限制,因此用途更廣。
 
strcpy的原型是:char* strcpy(char* dest, const char* src);
char * strcpy(char * dest, const char * src) // 實現src到dest的復制
{
  if ((src == NULL) || (dest == NULL)) //判斷參數src和dest的有效性
  {
 
      return NULL;
  }
  char *strdest = dest;        //保存目標字符串的首地址
  while ((*strDest++ = *strSrc++)!='\0'); //把src字符串的內容復制到dest下
  return strdest;
}

memcpy的原型是:void *memcpy( void *dest, const void *src, size_t count );

void *memcpy(void *memTo, const void *memFrom, size_t size)
{
  if((memTo == NULL) || (memFrom == NULL)) //memTo和memFrom必須有效
         return NULL;
  char *tempFrom = (char *)memFrom;             //保存memFrom首地址
  char *tempTo = (char *)memTo;                  //保存memTo首地址      
  while(size -- > 0)                //循環size次,復制memFrom的值到memTo中
         *tempTo++ = *tempFrom++ ;  
  return memTo;
}

strcpy和memcpy主要有以下3方面的區別。
1、復制的內容不同。strcpy只能復制字符串,而memcpy可以復制任意內容,例如字符數組、整型、結構體、類等。
2、復制的方法不同。strcpy不需要指定長度,它遇到被復制字符的串結束符"\0"才結束,所以容易溢出。memcpy則是根據其第3個參數決定復制的長度。
3、用途不同。通常在復制字符串時用strcpy,而需要復制其他類型數據時則一般用memcpy

常見的str函數:
  • strtok
  • extern char *strtok( char *s, const char *delim );

    功能:分解字符串為一組標記串。s為要分解的字符串,delim為分隔符字符串。

    說明:strtok()用來將字符串分割成一個個片段。當strtok()在參數s的字符串中發現到參數delim的分割字符時則會將該字符改為 \0 字符。在第一次調用時,strtok()必需給予參數s字符串,往后的調用則將參數s設置成NULL。每次調用成功則返回被分割出片段的指針。當沒有被分割的串時則返回NULL。所有delim中包含的字符都會被濾掉,並將被濾掉的地方設為一處分割的節點。

  • strstr
  • char * strstr( const char * str1, const char * str2 );

    功能:從字符串 str1 中尋找 str2 第一次出現的位置(不比較結束符NULL),如果沒找到則返回NULL。

  • strchr
  • char * strchr ( const char *str, int ch );

    功能:查找字符串 str 中首次出現字符 ch 的位置
    說明:返回首次出現 ch 的位置的指針,如果 str 中不存在 ch 則返回NULL。

  • strcpy/strncpy
  • char * strcpy( char * dest, const char * src );

    功能:把 src 所指由NULL結束的字符串復制到 dest 所指的數組中。
    說明:src 和 dest 所指內存區域不可以重疊且 dest 必須有足夠的空間來容納 src 的字符串。返回指向 dest 結尾處字符(NULL)的指針。

    類似的:

    strncpy

    char * strncpy( char * dest, const char * src, size_t num );
  • strcat/strncat
  • char * strcat ( char * dest, const char * src );

    功能:把 src 所指字符串添加到 dest 結尾處(覆蓋dest結尾處的'\0')並添加'\0'。
    說明:src 和 dest 所指內存區域不可以重疊且 dest 必須有足夠的空間來容納 src 的字符串。
    返回指向 dest 的指針。

    類似的 strncat

    char * strncat ( char * dest, const char * src, size_t num );
  • strcmp/strncmp
  • int strcmp ( const char * str1, const char * str2 );

    功能:比較字符串 str1 和 str2。
    說明:
    當s1<s2時,返回值<0
    當s1=s2時,返回值=0 
    當s1>s2時,返回值>0

    類似的:

    strncmp

    int strncmp ( const char * str1, const char * str2, size_t num );
  • strlen
size_t strlen ( const char * str );

 

功能:計算字符串 str 的長度
說明:返回 str 的長度,不包括結束符NULL。(注意與 sizeof 的區別)

類似的 strnlen:它從內存的某個位置(可以是字符串開頭,中間某個位置,甚至是某個不確定的內存區域)開始掃描,直到碰到第一個字符串結束符'\0'或計數器到達以下的maxlen為止,然后返回計數器值。

size_t strnlen(const char *str, size_t maxlen);

二、mem 系列

1.memset

void * memset ( void * ptr, int value, size_t num );

功能:把 ptr 所指內存區域的前 num 個字節設置成字符 value。
說明:返回指向 ptr 的指針。可用於變量初始化等操作

舉例:

復制代碼
#include <stdio.h>
#include <string.h>

int main ()
{
    char str[] = "almost erery programer should know memset!";
    memset(str, '-', sizeof(str)); 
    printf("the str is: %s now\n", str);
    return 0;
}
復制代碼

2.memmove

void * memmove ( void * dest, const void * src, size_t num );

功能:由 src 所指內存區域復制 num 個字節到 dest 所指內存區域。
說明:src 和 dest 所指內存區域可以重疊,但復制后 src 內容會被更改。函數返回指向dest的指針。

舉例:

復制代碼
#include <stdio.h>
#include <string.h>

int main ()
{
    char str[] = "memmove can be very useful......";
    memmove(str + 20, str + 15, 11); 
    printf("the str is: %s\n", str);
    return 0;
}
復制代碼
the str is: memmove can be very very useful.

3.memcpy

void * memcpy ( void * destination, const void * source, size_t num );

類似 strncpy。區別:拷貝指定大小的內存數據,而不管內容(不限於字符串)。

memcpy和memmove作用是一樣的,唯一的區別是,當內存發生局部重疊的時候,memmove保證拷貝的結果是正確的,memcpy不保證拷貝的結果的正確。(memcpy更快)

但當源內存和目標內存存在重疊時,memcpy會出現錯誤,而memmove能正確地實施拷貝,但這也增加了一點點開銷。

memmove的處理措施:

(1)當源內存的首地址等於目標內存的首地址時,不進行任何拷貝

(2)當源內存的首地址大於目標內存的首地址時,實行正向拷貝

(3)當源內存的首地址小於目標內存的首地址時,實行反向拷貝

4.memcmp

int memcmp ( const void * ptr1, const void * ptr2, size_t num );

類似 strncmp

5.memchr

void * memchr ( const void *buf, int ch, size_t count); 

功能:從 buf 所指內存區域的前 count 個字節查找字符 ch。
說明:當第一次遇到字符 ch 時停止查找。如果成功,返回指向字符 ch 的指針;否則返回NULL。


類的析構函數為什么設計成虛函數?

析構函數的作用與構造函數正好相反,是在對象的生命期結束時,釋放系統為對象所分配的空間,即要撤消一個對象。

用對象指針來調用一個函數,有以下兩種情況:

  1. 如果是虛函數,會調用派生類中的版本。(在有派生類的情況下)

  2. 如果是非虛函數,會調用指針所指類型的實現版本。

析構函數也會遵循以上兩種情況,因為析構函數也是函數嘛,不要把它看得太特殊。 當對象出了作用域或是我們刪除對象指針,析構函數就會被調用。

當派生類對象出了作用域,派生類的析構函數會先調用,然后再調用它父類的析構函數, 這樣能保證分配給對象的內存得到正確釋放。

但是,如果我們刪除一個指向派生類對象的基類指針,而基類析構函數又是非虛的話, 那么就會先調用基類的析構函數(上面第2種情況),派生類的析構函數得不到調用。

補充構造函數為什么不能是虛函數:

1. 從存儲空間角度,虛函數對應一個指向vtable虛函數表的指針,這大家都知道,可是這個指向vtable的指針其實是存儲在對象的內存空間的。問題出來了,如果構造函數是虛的,就需要通過 vtable來調用,可是對象還沒有實例化,也就是內存空間還沒有,怎么找vtable呢?所以構造函數不能是虛函數。
2. 從使用角度,虛函數主要用於在信息不全的情況下,能使重載的函數得到對應的調用。構造函數本身就是要初始化實例,那使用虛函數也沒有實際意義呀。所以構造函數沒有必要是虛函數。虛函數的作用在於通過父類的指針或者引用來調用它的時候能夠變成調用子類的那個成員函數。而構造函數是在創建對象時自動調用的,不可能通過父類的指針或者引用去調用,因此也就規定構造函數不能是虛函數。
3. 構造函數不需要是虛函數,也不允許是虛函數,因為創建一個對象時我們總是要明確指定對象的類型,盡管我們可能通過實驗室的基類的指針或引用去訪問它但析構卻不一定,我們往往通過基類的指針來銷毀對象。這時候如果析構函數不是虛函數,就不能正確識別對象類型從而不能正確調用析構函數。
4. 從實現上看,vbtl在構造函數調用后才建立,因而構造函數不可能成為虛函數從實際含義上看,在調用構造函數時還不能確定對象的真實類型(因為子類會調父類的構造函數);而且構造函數的作用是提供初始化,在對象生命期只執行一次,不是對象的動態行為,也沒有必要成為虛函數。
5. 當一個構造函數被調用時,它做的首要的事情之一是初始化它的VPTR。因此,它只能知道它是“當前”類的,而完全忽視這個對象后面是否還有繼承者。當編譯器為這個構造函數產生代碼時,它是為這個類的構造函數產生代碼——既不是為基類,也不是為它的派生類(因為類不知道誰繼承它)。所以它使用的VPTR必須是對於這個類的VTABLE。而且,只要它是最后的構造函數調用,那么在這個對象的生命期內,VPTR將保持被初始化為指向這個VTABLE, 但如果接着還有一個更晚派生的構造函數被調用,這個構造函數又將設置VPTR指向它的 VTABLE,等.直到最后的構造函數結束。VPTR的狀態是由被最后調用的構造函數確定的。這就是為什么構造函數調用是從基類到更加派生類順序的另一個理由。但是,當這一系列構造函數調用正發生時,每個構造函數都已經設置VPTR指向它自己的VTABLE。如果函數調用使用虛機制,它將只產生通過它自己的VTABLE的調用,而不是最后的VTABLE(所有構造函數被調用后才會有最后的VTABLE)。

 


說兩種進程間通信的方式。

1. 管道pipe:管道是一種半雙工的通信方式,數據只能單向流動,而且只能在具有親緣關系的進程間使用。進程的親緣關系通常是指父子進程關系。
2. 命名管道FIFO:有名管道也是半雙工的通信方式,但是它允許無親緣關系進程間的通信。
3. 內存映射MemoryMapping
4. 消息隊列MessageQueue:消息隊列是由消息的鏈表,存放在內核中並由消息隊列標識符標識。消息隊列克服了信號傳遞信息少、管道只能承載無格式字節流以及緩沖區大小受限等缺點。
5. 共享存儲SharedMemory:共享內存就是映射一段能被其他進程所訪問的內存,這段共享內存由一個進程創建,但多個進程都可以訪問。共享內存是最快的 IPC 方式,它是針對其他進程間通信方式運行效率低而專門設計的。它往往與其他通信機制,如信號兩,配合使用,來實現進程間的同步和通信。
6. 信號量Semaphore:信號量是一個計數器,可以用來控制多個進程對共享資源的訪問。它常作為一種鎖機制,防止某進程正在訪問共享資源時,其他進程也訪問該資源。因此,主要作為進程間以及同一進程內不同線程之間的同步手段。
7. 套接字Socket:套解口也是一種進程間通信機制,與其他通信機制不同的是,它可用於不同及其間的進程通信。
8. 信號 ( sinal ) : 信號是一種比較復雜的通信方式,用於通知接收進程某個事件已經發生。

 詳情查看博客中關於進程通信方式的總結線程同步的總結


 問了一些調試的問題,VS為什么能進行斷點單步調式,原理是啥?(騰訊實習生面試也問過這個問題,軟中斷)

 調試斷點依賴於父進程和子進程之間的通信,打斷點實際上就是在被調試的程序中,改變斷點附近程序的代碼,這個斷點使得被調試的程序暫時停止,然后發送信號給父進程(調試器進程),然后父進程能夠得到子進程的變量和狀態,達到調試的目的。修改斷點附近程序的指令為int3,含義是,使得當前用戶態程序發生中斷,告訴內核當前程序有斷點,那么內核中會向當前進程發送sigtrap信號,使得當前進程暫停。父進程調用wait函數,等待子進程的運行狀態發生改變。 調試的大體原理:通過設置被調試的進程ptrace字段,標志這個進程被trace,斷點附近的程序代碼被替換成了int 3,中斷程序,引發了do_int3函數,導致了被trace進程的暫停,這樣父進程就能通過ptrace系統調用獲得子進程的運行情況了。
 
軟中斷:
什么是硬件中斷?
CPU有一個單獨的執行序列,會一條指令一條指令的順序執行。要處理類似I/O或者硬件時鍾這樣的異步事件,CPU就要用到中斷。硬件中斷通常是一個專門的電信號,連接到一個特殊的“響應電路”上。這個電路會感知中斷的到來。然后讓CPU停止當前的執行流,保存當前的狀態,然后跳轉到一個預定義的地址處去執行,這個地址上有一個中斷處理例程。當中斷處理例程完成它的工作后,CPU就從之前停止的地方恢復執行。
軟中斷的原理類似:CPU支持特殊指令來模擬一個中斷。當執行到這個指令之后,CPU將其當做一個中斷,停止當前的正常的執行流,保存狀態然后跳轉到一個處理例程中執行。這種“陷阱”讓許多現代操作系統得以有效的完成很多復雜的任務——任務調度,虛擬內存,內存保護,調試等。
 
int3指令:
上文所提到的特殊指令就是通過int3完成的,陷阱指令。——對預定義的中斷處理例程的調用。x86支持int指令帶有一個8位的操作數,用來指定所發生的中斷號。因此,理論上可以支持256種“陷阱”。前32個由CPU自己保留,這里第3號就是我們感興趣的——稱為“trap to debugger”。
 
一個例子:
通過 int 3 指令在調試器中設定斷點

要在被調試進程中的某個目標地址上設定一個斷點,調試器需要做下面兩件事情:

1.  保存目標地址上的數據

2.  將目標地址上的第一個字節替換為int 3指令

然后,當調試器向操作系統請求開始運行進程時(通過前一篇文章中提到的PTRACE_CONT),進程最終一定會碰到int 3指令。此時進程停止,操作系統將發送一個信號。這時就是調試器再次出馬的時候了,接收到一個其子進程(或被跟蹤進程)停止的信號,然后調試器要做下面幾 件事:

1.  在目標地址上用原來的指令替換掉int 3

2.  將被跟蹤進程中的指令指針向后遞減1。這么做是必須的,因為現在指令指針指向的是已經執行過的int 3之后的下一條指令。

3.  由於進程此時仍然是停止的,用戶可以同被調試進程進行某種形式的交互。這里調試器可以讓你查看變量的值,檢查調用棧等等。

4.  當用戶希望進程繼續運行時,調試器負責將斷點再次加到目標地址上(由於在第一步中斷點已經被移除了),除非用戶希望取消斷點。


內存泄漏怎么處理的?
 
內存泄露:
 
什么是內存泄露?
這個問題,在博客中好像有一個文章,雖然是轉載的但是對內存泄露做了一些說明,就是當我們用new或者malloc申請了內存,但是沒有用delete或者ree及時的釋放了內存,結果導致一直占據該內存。內存泄漏形象的比喻是“操作系統可提供給所有進程的存儲空間被某個進程榨干”,最終結果是程序運行時間越長,占用存儲空間越來越多,最終用盡全部存儲空間,整個系統崩潰。
程序退出以后,能不能回收內存?
程序結束后,會釋放 其申請的所有內存,這樣是可以解決問題。但是你的程序還是有問題的,就如你寫了一個函數,申請了一塊內存,但是沒有釋放,每調用一次你的函數就會白白浪費一些內存。如果你的程序不停地在運行,就會有很多的內存被浪費,最后可能你的程序會因為用掉內存太多而被操作系統殺死。
 
智能指針:Effective C++ 建議我們將對象放到智能指針里,可以有效的避免內存泄露。
 
什么是智能指針?
 
一種類似指針的數據類型,將對象存儲在智能指針中,可以不需要處理內存泄露的問題,它會幫你調用對象的析構函數自動撤銷對象(主要是智能指針自己的析構函數用了delete ptr,delete會自動調用指針對象的析構函數,前提該內存是在堆上的,如果是在棧上就會出錯),釋放內存。因此,你要做的就是在析構函數中釋放掉數據成員的資源。
template <class T> class auto_ptr
{
    T* ptr;
public:
    explicit auto_ptr(T* p = 0) : ptr(p) {}
    ~auto_ptr()                 {delete ptr;}
    T& operator*()              {return *ptr;}
    T* operator->()             {return ptr;}
    // ...
};

 從上面auto_ptr可以看出來,智能指針將基本類型指針封裝為類對象指針(這個類肯定是個模板,以適應不同基本類型的需求),並在析構函數里編寫delete語句刪除指針指向的內存空間。

  • 所有的智能指針類都有一個explicit構造函數,以指針作為參數。比如auto_ptr的類模板原型為:
    templet<class T>
    class auto_ptr { explicit auto_ptr(X* p = 0) ; ... };

    因此不能自動將指針轉換為智能指針對象,必須顯式調用:

    shared_ptr<double> pd; double *p_reg = new double; pd = p_reg;                               // not allowed (implicit conversion)
    pd = shared_ptr<double>(p_reg);           // allowed (explicit conversion)
    shared_ptr<double> pshared = p_reg;       // not allowed (implicit conversion)
    shared_ptr<double> pshared(p_reg);        // allowed (explicit conversion)

     

  • 對全部三種智能指針都應避免的一點:
    string vacation("I wandered lonely as a cloud."); shared_ptr<string> pvac(&vacation);   // No

    pvac過期時,程序將把delete運算符用於非堆內存,這是錯誤的。

四種智能指針:

STL一共給我們提供了四種智能指針:auto_ptr、unique_ptr、shared_ptr和weak_ptr(本文章暫不討論)。

模板auto_ptr是C++98提供的解決方案,C+11已將將其摒棄,並提供了另外兩種解決方案。然而,雖然auto_ptr被摒棄,但它已使用了好多年:同時,如果您的編譯器不支持其他兩種解決力案,auto_ptr將是唯一的選擇。

為什么摒棄auto_ptr?

先來看下面的賦值語句:

auto_ptr< string> ps (new string ("I reigned lonely as a cloud.”);
auto_ptr<string> vocation; vocaticn = ps;

上述賦值語句將完成什么工作呢?如果ps和vocation是常規指針,則兩個指針將指向同一個string對象。這是不能接受的,因為程序將試圖刪除同一個對象兩次——一次是ps過期時,另一次是vocation過期時。要避免這種問題,方法有多種:

  • 定義陚值運算符,使之執行深復制。這樣兩個指針將指向不同的對象,其中的一個對象是另一個對象的副本,缺點是浪費空間,所以智能指針都未采用此方案。
  • 建立所有權(ownership)概念。對於特定的對象,只能有一個智能指針可擁有,這樣只有擁有對象的智能指針的構造函數會刪除該對象。然后讓賦值操作轉讓所有權。這就是用於auto_ptr和unique_ptr 的策略,但unique_ptr的策略更嚴格。
  • 創建智能更高的指針,跟蹤引用特定對象的智能指針數。這稱為引用計數。例如,賦值時,計數將加1,而指針過期時,計數將減1,。當減為0時才調用delete。這是shared_ptr采用的策略。

四種智能指針:

 

列1 列2
auto_ptr 內部使用一個成員變量,指向一塊內存資源(構造函數),
並在析構函數中釋放內存資源。(未實現深復制,因此拷貝一個auto_ptr將會有刪除兩次一個內存的潛在問題)
unique_ptr 獨享所有權的智能指針:
1、擁有它指向的對象

2、無法進行復制構造,無法進行復制賦值操作。即無法使兩個unique_ptr指向同一個對象。但是可以進行移動構造和移動賦值操作(所有權轉讓)

3、保存指向某個對象的指針,當它本身被刪除釋放的時候,會使用給定的刪除器釋放它指向的對象
shared_ptr 使用計數機制來表明資源被幾個指針共享。可以通過成員函數use_count()來查看資源的所有者個數。
拷貝構造時候,計數器會加一。當我們調用release()時,當前指針會釋放資源所有權,計數減一。當計數等於0時,資源會被釋放
會有死鎖問題,引入weak_ptr:,如果說兩個shared_ptr相互引用,那么這兩個指針的引用計數永遠不可能下降為0,資源永遠不會釋放。
weak_ptr 構造和析構不會引起引用記數的增加或減少。協助shared_ptr,沒有重載*和->但可以使用lock獲得一個可用的shared_ptr對象
 
VLD(Visual Leak Detector):一個檢測內存泄露的工具

Visual C++內置內存泄露檢測工具,但是功能十分有限。VLD就相當強大,可以定位文件、行號,可以非常准確地找到內存泄漏的位置,而且還免費、開源

在使用的時候只要將VLD的頭文件和lib文件放在工程文件中即可。

也可以一次設置,新工程就不用重新設置了。只介紹在Visual Studio 2003/2005中的設置方法,VC++ 6.0類似:

    1. 打開Tools -> Options -> Projects and Solutions -> VC++ Directories;
    2. 然后點擊include files下拉列表,在末尾把VLD安裝目錄中的include文件夾添加進來;
    3. 同樣點擊lib下拉列表,把VLD的lib也添加進來;
    4. 在需要檢測內存泄漏的源文件中添加
#include “vld.h”

順序無所謂,但是一定不能在一些預編譯的文件前(如stdafx.h)。我是加在stdafx.h文件最后。

  1. 把安裝目錄下dll文件夾中的所有dll文件拷貝到工程Debug目錄,也就是Debug版.exe生成的位置。點擊Debug –> Start Debugging 調試程序,在OUTPUT窗口中就會顯示程序運行過程中的內存泄漏的文件、行號還有內容了。
---------- Block 2715024 at 0x04D8A368: 512 bytes ----------
  Call Stack:
    d:\kangzj\documents\visual studio 2005\projects\rsip.root\readtiff\readtiff\segmentflag.cpp (56): CSegmentFlag::GetFlagFromArray
    d:\kangzj\documents\visual studio 2005\projects\rsip.root\readtiff\readtiff\wholeclassdlg.cpp (495): segmentThreadProc
    f:\dd\vctools\vc7libs\ship\atlmfc\src\mfc\thrdcore.cpp (109): _AfxThreadEntry
    f:\dd\vctools\crt_bld\self_x86\crt\src\threadex.c (348): _callthreadstartex
    f:\dd\vctools\crt_bld\self_x86\crt\src\threadex.c (331): _threadstartex
    0x7C80B729 (File and line number not available): GetModuleFileNameA
 
 CRT庫也有內存檢測工具,博客里有總結,不贅述了。
 
STL組件里,有sort函數,它用了哪些排序算法?
 STL提供的各式算法里,sort算法是最復雜,最龐大的一個。這個算法接受兩個randomaccessiterator(隨機存取迭代器),然后將元素從小到大排序。
縱觀STL的container,關系型container例如map和set利用RB樹自動排序,不需要用到sort。stack和queue和priority-queue都有特定的出入口,不允許用戶進行排序。
剩下vector,list和deque,list的迭代器屬於bidirectional-iterator,剩下vector和deque適合用sort算法。如果要對list和slist排序,應該使用member function sort。
 
STL的 sort 算法,數據量大的時候采用Quick Sort,分段遞歸排序。一旦分段后的數據量小於某個門檻,為避免quick sort的遞歸調用帶來過大的額外負擔,就改用插入排序,還會改用堆排序。
 

std::sort的實現

template <class RandomAccessIterator>
inline void sort(RandomAccessIterator first, RandomAccessIterator last) {
    if (first != last) {
        __introsort_loop(first, last, value_type(first), __lg(last - first) * 2);
        __final_insertion_sort(first, last);
    }
}

 

它是一個模板函數,只接受隨機訪問迭代器。if語句先判斷區間有效性,接着調用__introsort_loop,它就是STL的Introspective Sort實現。在該函數結束之后,最后調用插入排序。我們來揭開該算法的面紗:

template <class RandomAccessIterator, class T, class Size>
void __introsort_loop(RandomAccessIterator first,
                      RandomAccessIterator last, T*,
                      Size depth_limit) {
    while (last - first > __stl_threshold) {
        if (depth_limit == 0) {
            partial_sort(first, last, last);
            return;
        }
        --depth_limit;
        RandomAccessIterator cut = __unguarded_partition
          (first, last, T(__median(*first, *(first + (last - first)/2),
                                   *(last - 1))));
        __introsort_loop(cut, last, value_type(first), depth_limit);
        last = cut;
    }
}

 

 

 

我們來比較一下兩者的區別,試想,如果一個序列只需要遞歸兩次便可結束,即它可以分成四個子序列。原始的方式需要兩個遞歸函數調用,接着兩者各自調 用一次,也就是說進行了7次函數調用,如下圖左邊所示。但是STL這種寫法每次划分子序列之后僅對右子序列進行函數調用,左邊子序列進行正常的循環調用, 如下圖右邊所示。

兩種遞歸調用對比

兩者區別就在於STL節省了接近一半的函數調用,由於每次的函數調用有一定的開銷,因此對於數據量非常龐大時,這一半的函數調用可能能夠省下相當可觀的時 間。真是為了效率無所不用其極,令人驚嘆!更關鍵是這並沒有帶來太多的可讀性的降低,稍稍一經分析便能夠讀懂。這種稍稍以犧牲可讀性來換取效率的做法在 STL的實現中比比皆是,本文后面還會有例子。(more)

 

調試器工作原理(2):實現斷點

本文是關於調試器工作原理探究系列的第二篇。在開始閱讀本文前,請先確保你已經讀過本系列的第一篇(基礎篇)

本文的主要內容

這里我將說明調試器中的斷點機制是如何實現的。斷點機制是調試器的兩大主要支柱之一 ——另一個是在被調試進程的內存空間中查看變量的值。我們已經在第一篇文章中稍微涉及到了一些監視被調試進程的知識,但斷點機制仍然還是個迷。閱讀完本文之后,這將不再是什么秘密了。

軟中斷

要在x86體系結構上實現斷點我們要用到軟中斷(也稱為“陷阱”trap)。在我們深入細節之前,我想先大致解釋一下中斷和陷阱的概念。

CPU有一個單獨的執行序列,會一條指令一條指令的順序執行。要處理類似IO或者硬件時鍾這樣的異步事件時CPU就要用到中斷。硬件中斷通常是一個 專門的電信號,連接到一個特殊的“響應電路”上。這個電路會感知中斷的到來,然后會使CPU停止當前的執行流,保存當前的狀態,然后跳轉到一個預定義的地 址處去執行,這個地址上會有一個中斷處理例程。當中斷處理例程完成它的工作后,CPU就從之前停止的地方恢復執行。

軟中斷的原理類似,但實際上有一點不同。CPU支持特殊的指令允許通過軟件來模擬一個中斷。當執行到這個指令時,CPU將其當做一個中斷——停止當 前正常的執行流,保存狀態然后跳轉到一個處理例程中執行。這種“陷阱”讓許多現代的操作系統得以有效完成很多復雜任務(任務調度、虛擬內存、內存保護、調 試等)。

一些編程錯誤(比如除0操作)也被CPU當做一個“陷阱”,通常被認為是“異常”。這里軟中斷同硬件中斷之間的界限就變得模糊了,因為這里很難說這種異常到底是硬件中斷還是軟中斷引起的。我有些偏離主題了,讓我們回到關於斷點的討論上來。

關於int 3指令

看過前一節后,現在我可以簡單地說斷點就是通過CPU的特殊指令——int 3來實現的。int就是x86體系結構中的“陷阱指令”——對預定義的中斷處理例程的調用。x86支持int指令帶有一個8位的操作數,用來指定所發生的 中斷號。因此,理論上可以支持256種“陷阱”。前32個由CPU自己保留,這里第3號就是我們感興趣的——稱為“trap to debugger”。

不多說了,我這里就引用“聖經”中的原話吧(這里的聖經就是Intel’s Architecture software developer’s manual, volume2A):

“INT 3指令產生一個特殊的單字節操作碼(CC),這是用來調用調試異常處理例程的。(這個單字節形式非常有價值,因為這樣可以通過一個斷點來替換掉任何指令的第一個字節,包括其它的單字節指令也是一樣,而不會覆蓋到其它的操作碼)。”

上面這段話非常重要,但現在解釋它還是太早,我們稍后再來看。

使用int 3指令

是的,懂得事物背后的原理是很棒的,但是這到底意味着什么?我們該如何使用int 3來實現斷點機制?套用常見的編程問答中出現的對話——請用代碼說話!

實際上這真的非常簡單。一旦你的進程執行到int 3指令時,操作系統就將它暫停。在Linux上(本文關注的是Linux平台),這會給該進程發送一個SIGTRAP信號。

這就是全部——真的!現在回顧一下本系列文章的第一篇,跟蹤(調試器)進程可以獲得所有其子進程(或者被關聯到的進程)所得到信號的通知,現在你知道我們該做什么了吧?

就是這樣,再沒有什么計算機體系結構方面的東東了,該寫代碼了。

手動設定斷點

現在我要展示如何在程序中設定斷點。用於這個示例的目標程序如下:

我現在使用的是匯編語言,這是為了避免當使用C語言時涉及到的編譯和符號的問題。上面列出的程序功能就是在一行中打印“Hello,”,然后在下一行中打印“world!”。這個例子與上一篇文章中用到的例子很相似。

我希望設定的斷點位置應該在第一條打印之后,但恰好在第二條打印之前。我們就讓斷點打在第一個int 0x80指令之后吧,也就是mov edx, len2。首先,我需要知道這條指令對應的地址是什么。運行objdump –d:

通過上面的輸出,我們知道要設定的斷點地址是0x8048096。等等,真正的調試器不是像這樣工作的,對吧?真正的調試器可以根據代碼行數或者函 數名稱來設定斷點,而不是基於什么內存地址吧?非常正確。但是我們離那個標准還差的遠——如果要像真正的調試器那樣設定斷點,我們還需要涵蓋符號表以及調 試信息方面的知識,這需要用另一篇文章來說明。至於現在,我們還必須得通過內存地址來設定斷點。

看到這里我真的很想再扯一點題外話,所以你有兩個選擇。如果你真的對於為什么地址是0x8048096,以及這代表什么意思非常感興趣的話,接着看下一節。如果你對此毫無興趣,只是想看看怎么設定斷點,可以略過這一部分。

題外話——進程地址空間以及入口點

坦白的說,0x8048096本身並沒有太大意義,這只不過是相對可執行鏡像的代碼段(text section)開始處的一個偏移量。如果你仔細看看前面objdump出來的結果,你會發現代碼段的起始位置是0x08048080。這告訴了操作系統 要將代碼段映射到進程虛擬地址空間的這個位置上。在Linux上,這些地址可以是絕對地址(比如,有的可執行鏡像加載到內存中時是不可重定位的),因為在 虛擬內存系統中,每個進程都有自己獨立的內存空間,並把整個32位的地址空間都看做是屬於自己的(稱為線性地址)。

如果我們通過readelf工具來檢查可執行文件的ELF頭,我們將得到如下輸出:

注意,ELF頭的“entry point address”同樣指向的是0x8048080。因此,如果我們把ELF文件中的這個部分解釋給操作系統的話,就表示:

1.  將代碼段映射到地址0x8048080處

2.  從入口點處開始執行——地址0x8048080

但是,為什么是0x8048080呢?它的出現是由於歷史原因引起的。每個進程的地址空間的前128MB被保留給棧空間了(注:這一部分原因可參考 Linkers and Loaders)。128MB剛好是0x80000000,可執行鏡像中的其他段可以從這里開始。0x8048080是Linux下的鏈接器ld所使用的 默認入口點。這個入口點可以通過傳遞參數-Ttext給ld來進行修改。

因此,得到的結論是這個地址並沒有什么特別的,我們可以自由地修改它。只要ELF可執行文件的結構正確且在ELF頭中的入口點地址同程序代碼段(text section)的實際起始地址相吻合就OK了。

通過int 3指令在調試器中設定斷點

要在被調試進程中的某個目標地址上設定一個斷點,調試器需要做下面兩件事情:

1.  保存目標地址上的數據

2.  將目標地址上的第一個字節替換為int 3指令

然后,當調試器向操作系統請求開始運行進程時(通過前一篇文章中提到的PTRACE_CONT),進程最終一定會碰到int 3指令。此時進程停止,操作系統將發送一個信號。這時就是調試器再次出馬的時候了,接收到一個其子進程(或被跟蹤進程)停止的信號,然后調試器要做下面幾 件事:

1.  在目標地址上用原來的指令替換掉int 3

2.  將被跟蹤進程中的指令指針向后遞減1。這么做是必須的,因為現在指令指針指向的是已經執行過的int 3之后的下一條指令。

3.  由於進程此時仍然是停止的,用戶可以同被調試進程進行某種形式的交互。這里調試器可以讓你查看變量的值,檢查調用棧等等。

4.  當用戶希望進程繼續運行時,調試器負責將斷點再次加到目標地址上(由於在第一步中斷點已經被移除了),除非用戶希望取消斷點。


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM