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 );html
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方面的區別。
一、複製的內容不一樣。strcpy只能複製字符串,而memcpy能夠複製任意內容,例如字符數組、整型、結構體、類等。
二、複製的方法不一樣。strcpy不須要指定長度,它遇到被複制字符的串結束符"\0"才結束,因此容易溢出。memcpy則是根據其第3個參數決定複製的長度。
三、用途不一樣。一般在複製字符串時用strcpy,而須要複製其餘類型數據時則通常用memcpylinux
extern char *strtok( char *s, const char *delim );
功能:分解字符串爲一組標記串。s爲要分解的字符串,delim爲分隔符字符串。c++
說明:strtok()用來將字符串分割成一個個片斷。當strtok()在參數s的字符串中發現到參數delim的分割字符時則會將該字符改成 \0 字符。在第一次調用時,strtok()必需給予參數s字符串,日後的調用則將參數s設置成NULL。每次調用成功則返回被分割出片斷的指針。當沒有被分割的串時則返回NULL。全部delim中包含的字符都會被濾掉,並將被濾掉的地方設爲一處分割的節點。git
char * strstr( const char * str1, const char * str2 );
功能:從字符串 str1 中尋找 str2 第一次出現的位置(不比較結束符NULL),若是沒找到則返回NULL。github
char * strchr ( const char *str, int ch );
功能:查找字符串 str 中首次出現字符 ch 的位置
說明:返回首次出現 ch 的位置的指針,若是 str 中不存在 ch 則返回NULL。web
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 );
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 );
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 );
size_t strlen ( const char * str );
功能:計算字符串 str 的長度
說明:返回 str 的長度,不包括結束符NULL。(注意與 sizeof 的區別)
相似的 strnlen:它從內存的某個位置(能夠是字符串開頭,中間某個位置,甚至是某個不肯定的內存區域)開始掃描,直到碰到第一個字符串結束符'\0'或計數器到達如下的maxlen爲止,而後返回計數器值。
size_t strnlen(const char *str, size_t maxlen);
2、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。
類的析構函數爲何設計成虛函數?
析構函數的做用與構造函數正好相反,是在對象的生命期結束時,釋放系統爲對象所分配的空間,即要撤消一個對象。
用對象指針來調用一個函數,有如下兩種狀況:
若是是虛函數,會調用派生類中的版本。(在有派生類的狀況下)
若是是非虛函數,會調用指針所指類型的實現版本。
析構函數也會遵循以上兩種狀況,由於析構函數也是函數嘛,不要把它看得太特殊。 當對象出了做用域或是咱們刪除對象指針,析構函數就會被調用。
當派生類對象出了做用域,派生類的析構函數會先調用,而後再調用它父類的析構函數, 這樣能保證分配給對象的內存獲得正確釋放。
可是,若是咱們刪除一個指向派生類對象的基類指針,而基類析構函數又是非虛的話, 那麼就會先調用基類的析構函數(上面第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爲何能進行斷點單步調式,原理是啥?(騰訊實習生面試也問過這個問題,軟中斷)
要在被調試進程中的某個目標地址上設定一個斷點,調試器須要作下面兩件事情:
1. 保存目標地址上的數據
2. 將目標地址上的第一個字節替換爲int 3指令
而後,當調試器向操做系統請求開始運行進程時(經過前一篇文章中提到的PTRACE_CONT),進程最終必定會碰到int 3指令。此時進程中止,操做系統將發送一個信號。這時就是調試器再次出馬的時候了,接收到一個其子進程(或被跟蹤進程)中止的信號,而後調試器要作下面幾 件事:
1. 在目標地址上用原來的指令替換掉int 3
2. 將被跟蹤進程中的指令指針向後遞減1。這麼作是必須的,由於如今指令指針指向的是已經執行過的int 3以後的下一條指令。
3. 因爲進程此時仍然是中止的,用戶能夠同被調試進程進行某種形式的交互。這裏調試器可讓你查看變量的值,檢查調用棧等等。
4. 當用戶但願進程繼續運行時,調試器負責將斷點再次加到目標地址上(因爲在第一步中斷點已經被移除了),除非用戶但願取消斷點。
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語句刪除指針指向的內存空間。
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< string> ps (new string ("I reigned lonely as a cloud.」);
auto_ptr<string> vocation; vocaticn = ps;
上述賦值語句將完成什麼工做呢?若是ps和vocation是常規指針,則兩個指針將指向同一個string對象。這是不能接受的,由於程序將試圖刪除同一個對象兩次——一次是ps過時時,另外一次是vocation過時時。要避免這種問題,方法有多種:
四種智能指針:
列1 | 列2 |
auto_ptr | 內部使用一個成員變量,指向一塊內存資源(構造函數), 並在析構函數中釋放內存資源。(未實現深複製,所以拷貝一個auto_ptr將會有刪除兩次一個內存的潛在問題) |
unique_ptr | 獨享全部權的智能指針: 一、擁有它指向的對象 二、沒法進行復制構造,沒法進行復制賦值操做。即沒法使兩個unique_ptr指向同一個對象。可是能夠進行移動構造和移動賦值操做(全部權轉讓) 三、保存指向某個對象的指針,當它自己被刪除釋放的時候,會使用給定的刪除器釋放它指向的對象 |
shared_ptr | 使用計數機制來代表資源被幾個指針共享。能夠經過成員函數use_count()來查看資源的全部者個數。 拷貝構造時候,計數器會加一。當咱們調用release()時,當前指針會釋放資源全部權,計數減一。當計數等於0時,資源會被釋放 會有死鎖問題,引入weak_ptr:,若是說兩個shared_ptr相互引用,那麼這兩個指針的引用計數永遠不可能降低爲0,資源永遠不會釋放。 |
weak_ptr | 構造和析構不會引發引用記數的增長或減小。協助shared_ptr,沒有重載*和->但可使用lock得到一個可用的shared_ptr對象 |
Visual C++內置內存泄露檢測工具,可是功能十分有限。VLD就至關強大,能夠定位文件、行號,能夠很是準確地找到內存泄漏的位置,並且還免費、開源!
在使用的時候只要將VLD的頭文件和lib文件放在工程文件中便可。
也能夠一次設置,新工程就不用從新設置了。只介紹在Visual Studio 2003/2005中的設置方法,VC++ 6.0相似:
#include 「vld.h」
順序無所謂,可是必定不能在一些預編譯的文件前(如stdafx.h)。我是加在stdafx.h文件最後。
---------- 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
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)
本文是關於調試器工做原理探究系列的第二篇。在開始閱讀本文前,請先確保你已經讀過本系列的第一篇(基礎篇)。
本文的主要內容
這裏我將說明調試器中的斷點機制是如何實現的。斷點機制是調試器的兩大主要支柱之一 ——另外一個是在被調試進程的內存空間中查看變量的值。咱們已經在第一篇文章中稍微涉及到了一些監視被調試進程的知識,但斷點機制仍然仍是個迷。閱讀完本文以後,這將再也不是什麼祕密了。
軟中斷
要在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信號。
這就是所有——真的!如今回顧一下本系列文章的第一篇,跟蹤(調試器)進程能夠得到全部其子進程(或者被關聯到的進程)所獲得信號的通知,如今你知道咱們該作什麼了吧?
就是這樣,再沒有什麼計算機體系結構方面的東東了,該寫代碼了。
手動設定斷點
如今我要展現如何在程序中設定斷點。用於這個示例的目標程序以下:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
|
section
.
text
;
The
_start
symbol
must
be
declared
for
the
linker
(
ld
)
global
_start
_start
:
;
Prepare
arguments
for
the
sys_write
system
call
:
;
-
eax
:
system
call
number
(
sys_write
)
;
-
ebx
:
file
descriptor
(
stdout
)
;
-
ecx
:
pointer
to
string
;
-
edx
:
string
length
mov
edx
,
len1
mov
ecx
,
msg1
mov
ebx
,
1
mov
eax
,
4
;
Execute
the
sys_write
system
call
int
0x80
;
Now
print
the
other
message
mov
edx
,
len2
mov
ecx
,
msg2
mov
ebx
,
1
mov
eax
,
4
int
0x80
;
Execute
sys_exit
mov
eax
,
1
int
0x80
section
.
data
msg1
db
'Hello,'
,
0xa
len1
equ
$
-
msg1
msg2
db
'world!'
,
0xa
len2
equ
$
-
msg2
|
我如今使用的是彙編語言,這是爲了不當使用C語言時涉及到的編譯和符號的問題。上面列出的程序功能就是在一行中打印「Hello,」,而後在下一行中打印「world!」。這個例子與上一篇文章中用到的例子很類似。
我但願設定的斷點位置應該在第一條打印以後,但剛好在第二條打印以前。咱們就讓斷點打在第一個int 0x80指令以後吧,也就是mov edx, len2。首先,我須要知道這條指令對應的地址是什麼。運行objdump –d:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
|
traced_printer2
:
file
format
elf32
-
i386
Sections
:
Idx
Name
Size
VMA
LMA
File
off
Algn
0
.
text
00000033
08048080
08048080
00000080
2
*
*
4
CONTENTS
,
ALLOC
,
LOAD
,
READONLY
,
CODE
1
.
data
0000000e
080490b4
080490b4
000000b4
2
*
*
2
CONTENTS
,
ALLOC
,
LOAD
,
DATA
Disassembly
of
section
.
text
:
08048080
<
.
text
>
:
8048080
:
ba
07
00
00
00
mov
$
0x7
,
%
edx
8048085
:
b9
b4
90
04
08
mov
$
0x80490b4
,
%
ecx
804808a
:
bb
01
00
00
00
mov
$
0x1
,
%
ebx
804808f
:
b8
04
00
00
00
mov
$
0x4
,
%
eax
8048094
:
cd
80
int
$
0x80
8048096
:
ba
07
00
00
00
mov
$
0x7
,
%
edx
804809b
:
b9
bb
90
04
08
mov
$
0x80490bb
,
%
ecx
80480a0
:
bb
01
00
00
00
mov
$
0x1
,
%
ebx
80480a5
:
b8
04
00
00
00
mov
$
0x4
,
%
eax
80480aa
:
cd
80
int
$
0x80
80480ac
:
b8
01
00
00
00
mov
$
0x1
,
%
eax
80480b1
:
cd
80
int
$
0x80
|
經過上面的輸出,咱們知道要設定的斷點地址是0x8048096。等等,真正的調試器不是像這樣工做的,對吧?真正的調試器能夠根據代碼行數或者函 數名稱來設定斷點,而不是基於什麼內存地址吧?很是正確。可是咱們離那個標準還差的遠——若是要像真正的調試器那樣設定斷點,咱們還須要涵蓋符號表以及調 試信息方面的知識,這須要用另外一篇文章來講明。至於如今,咱們還必須得經過內存地址來設定斷點。
看到這裏我真的很想再扯一點題外話,因此你有兩個選擇。若是你真的對於爲何地址是0x8048096,以及這表明什麼意思很是感興趣的話,接着看下一節。若是你對此毫無興趣,只是想看看怎麼設定斷點,能夠略過這一部分。
題外話——進程地址空間以及入口點
坦白的說,0x8048096自己並無太大意義,這只不過是相對可執行鏡像的代碼段(text section)開始處的一個偏移量。若是你仔細看看前面objdump出來的結果,你會發現代碼段的起始位置是0x08048080。這告訴了操做系統 要將代碼段映射到進程虛擬地址空間的這個位置上。在Linux上,這些地址能夠是絕對地址(好比,有的可執行鏡像加載到內存中時是不可重定位的),由於在 虛擬內存系統中,每一個進程都有本身獨立的內存空間,並把整個32位的地址空間都看作是屬於本身的(稱爲線性地址)。
若是咱們經過readelf工具來檢查可執行文件的ELF頭,咱們將獲得以下輸出:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
|
$
readelf
-
h
traced_printer2
ELF
Header
:
Magic
:
7f
45
4c
46
01
01
01
00
00
00
00
00
00
00
00
00
Class
:
ELF32
Data
:
2'
s
complement
,
little
endian
Version
:
1
(
current
)
OS
/
ABI
:
UNIX
-
System
V
ABI
Version
:
0
Type
:
EXEC
(
Executable
file
)
Machine
:
Intel
80386
Version
:
0x1
Entry
point
address
:
0x8048080
Start
of
program
headers
:
52
(
bytes
into
file
)
Start
of
section
headers
:
220
(
bytes
into
file
)
Flags
:
0x0
Size
of
this
header
:
52
(
bytes
)
Size
of
program
headers
:
32
(
bytes
)
Number
of
program
headers
:
2
Size
of
section
headers
:
40
(
bytes
)
Number
of
section
headers
:
4
Section
header
string
table
index
:
3
|
注意,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. 當用戶但願進程繼續運行時,調試器負責將斷點再次加到目標地址上(因爲在第一步中斷點已經被移除了),除非用戶但願取消斷點。