C 中的函式名稱粉碎機制和它的逆向應用

2021-08-15 16:49:14 字數 4439 閱讀 9980

**: c++中的函式名稱粉碎機制和它的逆向應用

在c語言的語法中,函式名稱是乙個函式的唯一標識,如果乙個檔案內含有兩個名稱相同的函式,編譯器就會報「函式已有主體」的錯誤;在多個檔案鏈結時,如果發現有兩個名稱相同的函式,鏈結器就會報「符號重定義」的錯誤。

具有多型特性的c++支援函式的過載,函式不再以函式名稱作為唯一標識。只要滿足構成過載的條件,兩個(或多個)功能不同的函式可以有相同的函式名稱。這樣一來,函式的呼叫者會獲得多型性帶來的極大方便(雖然函式的編寫者的工作量沒有改變,所有的同名函式仍需要乙個乙個地去編寫)。構成函式過載的條件是:

1.作用域相同

2.函式名稱相同

3.引數不同(型別,個數,順序)

(另外:返回值型別、呼叫約定型別並不作為參考)

為了支援函式過載這一新特性,編譯器的開發者們大多選擇使用名稱粉碎機制,即把函式的原有名稱和引數型別、個數、順序等資訊融合成乙個新的函式名稱。這個新的名稱就是此函式的唯一標識。有了它,之後的工作就可以繼續沿用c語言的套路(在編譯、連線過程中,若發現新名稱存在重複現象,仍會發出「函式已有主體」或「符號重定義」資訊)。

值得一提的是,c++標準中只是說明了函式過載的定義,但並沒有提出「名稱粉碎機制」這種概念。由於名稱粉碎機制的直觀、高效、易於相容以前的c版本的特點,所以各編譯器作者不約而同地選擇用名稱粉碎機制來實現函式過載。雖然各編譯器的思路是一致的,但是由於沒有統一的標準,所以各編譯器的名稱粉碎結果也自然是五花八門。下面我們來觀察微軟vc編譯器的名稱粉碎細節。

在一般情況下,我們是看不到名稱粉碎機制的細節的(因為我們沒有必要知道編譯器內部的操作)。為了看到這些細節,我們必須進入編譯生成的obj檔案中探索。例如這次定義兩個名為test_add的函式,分別用於計算整形資料相加之和和雙精度浮點型資料相加之和:

#include "stdafx.h"

int test_add(int n1, int n2)

double test_add(double d1, double d2)

在標頭檔案中宣告這兩個函式:

//檔案test.h

#pragma once

int test_add(int n1, int n2);

double test_add(double d1, double d2);

在main函式中呼叫test_add函式分別求整形和浮點數的和:

//檔案main.cpp

#include "stdafx.h"

#include "test.h"

int _tmain(int argc, _tchar* argv)

此時程式可以正常編譯編譯執行。來到工程目錄下,使用winhex開啟test.cpp所生成的obj檔案(名稱為test.obj)。使用文字搜尋功能搜尋「test_add」,可以在檔案快結束的地方發現被粉碎後的新函式名稱,如圖所示:

除了在obj檔案中探索,其實還可以通過人為製造編譯錯誤的方法,快速地讓編譯器告訴我們粉碎後的新的函式名稱。在原有**的基礎上,注釋掉test.cpp檔案中關於兩個test_add函式的定義部分:

#include "stdafx.h"

/*int test_add(int n1, int n2)

double test_add(double d1, double d2)

*/

這樣一來,雖然編譯過程不會報錯,但是在鏈結的時候,因為main函式中需要執行test_add函式的**卻無法找到它的定義,就會發生錯誤:

1>main.obj : error lnk2019: 無法解析的外部符號 "int __cdecl test_add(int,int)" (?test_add@@yahhh@z),該符號在函式 _wmain 中被引用

1>main.obj : error lnk2019: 無法解析的外部符號 "double __cdecl test_add(double,double)" (?test_add@@yannn@z),該符號在函式 _wmain 中被引用

錯誤資訊向我們揭示了:兩個add_test函式因為引數的不同,被名稱粉碎機制賦予了新的函式名稱,分別是?test_add@@yahhh@z?test_add@@yannn@z。通過這個人為製造錯誤的方法,我們可以繼續測試不同型別的引數會對名稱粉碎造成什麼樣的影響。在微軟的名稱粉碎機制中,除了函式的引數型別之外,函式的呼叫約定、返回值型別和作用域都被整合到了粉碎後的新名稱之中。

此外,微軟還為我們提供了乙個「反名稱粉碎」工具undname,用於快速地把粉碎後的函式名稱還原成本來的樣子。開啟vs2012工具命令提示(位於 開始選單->microsoft visual studio 2012->visual studio tools),輸入undname即可開啟這個工具。我們可以利用它來直接翻譯粉碎後的名稱,如圖所示,函式的返回值型別,呼叫約定,引數的型別、個數、順序都被翻譯出來了。

舉個具體例子,乙個正在合作同乙個專案的程式設計師,在完成了自己負責的那一部分功能後,因為想保護自己的原始碼,所以只共享了編譯後生成的obj檔案和乙份配套的文件,文件裡說明了怎麼去呼叫此obj裡的函式。

現在有了undname這個工具,配合之前對obj檔案的文字搜尋經驗,我們就可以嘗試探索並呼叫obj檔案裡的所有可用函式,而不是被侷限於文件的說明。

假設乙個程式設計師寫了乙個cpp檔案,並且只在文件裡說明了test_open函式,卻隱藏了test_hiden函式的說明:

注:**中英語「隱藏」拼寫錯誤,應該是hidden,而非hiden。

//在文件內提供次函式說明

int test_open(int n)

//文件裡沒有提到此函式

int test_hiden(int n)

編譯之後,obj檔案和test_open函式的呼叫說明(test_open函式的呼叫說明也可以是標頭檔案的形式)被共享出去。主函式的編寫者拿到obj檔案之後,雖然他不知道test_open函式的原始碼,但是按照呼叫說明還是可以呼叫這個函式:

#include int test_open(int n); //obj檔案內的函式的宣告;也可以是包含乙個標頭檔案的形式。

int main(int argc, char* argv)

現在只要編譯此main.cpp檔案,就可以拿到main.obj檔案。然後把main.obj檔案和程式設計合作者提供的test.obj檔案鏈結,我們就會得到有功能的main.exe檔案了。main.exe的執行結果為15,符合test_open函式的功能,整個過程如下圖所示:

現在我們用winhex來開啟程式設計合作者發來的test.obj檔案,嘗試探索一些他在文件裡沒有說明的資訊(即test_hiden函式)。如圖所示,按照之前的經驗,可以在檔案末尾發現被名稱粉碎的函式資訊。

對於我們感興趣的test_hiden函式,複製它的資訊,然後用undname工具還原一下本來的面貌,如圖所示:

原來,這是乙個c呼叫約定的函式,它的返回值是int,需要1個int型的引數。知道了這些資訊,我們就可以嘗試在main函式裡去呼叫它。修改main函式**(test_hiden的函式宣告裡:形參名字可以任意寫,甚至可以不寫,編譯器在乎的只是形參的型別;c呼叫約定是預設呼叫約定,所以不用寫):

#include int test_open(int n);

int test_hiden(int nsecret);

int main(int argc, char* argv)

這樣,我們就呼叫了這個沒有被文件說明的函式:

對於c語法下的帶有static修飾符的靜態函式,這種方法還是無能為力的,因為static函式的資訊不會出現在obj檔案中。

對於c++語法下類(class)裡面的private/protected函式(包括帶有static修飾符的靜態函式),雖然我們能通過obj檔案和undname工具還原它們的函式資訊,但由於它們的private/protected屬性,還是無法從外部對它們進行呼叫。如果函式是public屬性,那麼無論他是普通函式,還是static修飾的靜態函式,都可以用本文的方法還原函式資訊然後呼叫。

繼承中的函式名稱遮掩

在程式中有作用域的概念,當編譯器處在某個函式的作用域時,當需要需要查詢某個變數或者某個函式時,總是從最裡面的作用域開始查詢,當查詢不到時,才向外圍繼續查詢,其中尤為需要注意的是 c 中的名稱遮掩規則所做的唯一的事情就是 遮掩名稱 而不管型別是否相同。例如 class base class deriv...

C 中虛函式功能的實現機制

c 中虛函式功能的實現機制 要理解c 中虛函式是如何工作的,需要回答四個問題。1 什麼是虛函式。虛函式由於必須是在類中宣告的函式,因此又稱為虛方法。所有以virtual修飾符開始的成員函式都成為虛方法。此時注意是virtual修飾的成員函式不是virtual修飾的成員函式名。例如 基類中定義 vir...

C 中虛函式功能的實現機制

要理解c 中虛函式是如何工作的,需要回答四個問題。1 什麼是虛函式。虛函式由於必須是在類中宣告的函式,因此又稱為虛方法。所有以virtual修飾符開始的成員函式都成為虛方法。此時注意是virtual修飾的成員函式不是virtual修飾的成員函式名。例如 基類中定義 virtual void show...