1. 程式人生 > >C/C++ Mono 虛擬機器執行一個可執行的 .NET Assembly

C/C++ Mono 虛擬機器執行一個可執行的 .NET Assembly

    本文是嵌入 Mono 虛擬機器中一片基礎的啟蒙篇幅,它影響到後期對於描述 Mono 虛擬機器引擎嵌入應用方面的內容,另外本文建議讀者至少對 .NET 是什麼有一個清晰的認識。

   參考:

   我們先來琢磨琢磨,嵌入 Mono 虛擬機器引擎大致可以應用於那些方面,我們仔細用小腦袋好好去想一想,其實就能夠發現可以應用到很多方面。

例如:

    1、程式集加固【抗反除錯、反譯編、程式集 dump】

    2、即時偵錯程式(可不利用 PDB,例:dnSpy)

    3、元資料分析

    4、IL 程式碼植入

    5、AppDomain 分析

    6、高效能的熱插拔 .NET 指令碼或外掛化(例:Unity3d)【例:擴充套件 InternalCall 函式】

    ....

   上述的利用方向都無疑是正向友好的,反向當然是幹壞事,不過這不是本文需要探討的內容,想想上面提出的應用點能帶來多少的好處,可以說很多,它能夠解決很多在 .NET /CLR 內不容易解決的問題。

   當然它本身就沒有缺陷了?缺陷倒是有,它相對於 .NET(Win32k)之間效率上還是有一些差距的,當然不會有五倍十倍那麼誇張,另外就是框架庫還存在一些的 BUG,當然這不是主要的,無論是 Java 執行庫還是 .NET 執行庫基本都存在一些 BUG,但是並不是太影響一般性的利用,當然高度複雜的利用,遇到這種坑坑,大概自己造個輪子就解決掉了,要是虛擬機器本身的問題這個就發 issue 或者自己改程式碼然後重新build,所以這並不是什麼太值得令人注意的東西。

   回到正題,既然我們需要執行一個可執行的 .NET Assembly,那麼我們勢必需要先找到具體的程式集的入口點函式,我們從文件中似乎可以看到如下圖所示的函式。

    

   mono_image_get_entry_point 從名稱上看與定義的結構來看 它意思是說從一個 MonoImage 映象中返回入口點函式的 token,這個 token 主要指 metadatatoken = RID,你可以從下述列出的 _CorTokenType 列舉中參考元資料類型範圍的定義。

public enum CorTokenType : uint {     mdtModule = 0u,     mdtTypeRef = 0x1000000,     mdtTypeDef = 0x2000000,     mdtFieldDef = 0x4000000,     mdtMethodDef = 100663296u,     mdtParamDef = 0x8000000,     mdtInterfaceImpl = 150994944u,     mdtMemberRef = 167772160u,     mdtCustomAttribute = 201326592u,     mdtPermission = 234881024u,     mdtSignature = 285212672u,     mdtEvent = 335544320u,     mdtProperty = 385875968u,     mdtModuleRef = 436207616u,     mdtTypeSpec = 452984832u,     mdtAssembly = 0x20000000,     mdtAssemblyRef = 587202560u,     mdtFile = 637534208u,     mdtExportedType = 654311424u,     mdtManifestResource = 671088640u,     mdtGenericParam = 704643072u,     mdtMethodSpec = 721420288u,     mdtGenericParamConstraint = 738197504u,     mdtString = 1879048192u,     mdtName = 1895825408u,     mdtBaseType = 1912602624u }

     mono_image_get_entry_point 函式訪問一個 image 有意思的它可能返回一個 0u 的值(可以視作 NULL)表明它無法獲取到入口點函式的 RID,但是這種情況主要發生在 “.NET Assembly” 並不是一個有效的 “可執行程式” 的上面。

MonoMethod* mono_image_get_entry_point_ex2(MonoImage* image)
{
	uint32_t metadataken = image ? mono_image_get_entry_point(image) : 0u;
	if (!mono_metadata_token_index(metadataken))
	{
		return NULL;
	}
	return mono_ldtoken(image, mono_image_get_entry_point(image), NULL, NULL);
}

    但是上面的方法必須要 “.NET Assembly” 內部顯示定義了 entrypoint 入口點函式才可以,但是有的時候編譯的程式碼是包含 Main 函式的,但我們卻不小心編譯成了 DLL 那麼我們應該怎麼去獲取這個未被顯示定義的入口點函式?

    我們知道在 .NET 派系裡面的語言都顯示的約定了一個入口點函式 即 “Main(void) or Main(string[])” 兩個簽名,所以我們只要把這兩個方法在程式集中找出來就可以了,那麼有幾種方式可以選擇 “反射” 、“mono_method_desc_search_in_image” 那麼為什麼不考慮 “mono_method_desc_search_in_class”?這個很好理解,我們都知道 .NET 派系裡的語言並不一定區分 Main 函式需要規定在哪個類裡一個固定的值而是一個未知的值,而 “mono_method_desc_search_in_class” 建立在已知類的 “MonoClass*” 時才成立的,另外這兩個函式支援統配符模糊查詢,用在一般性的查詢上面真的比較方便。

MonoMethod* mono_image_get_entry_point_ex(MonoImage* image)
{
	MonoMethod* method = NULL;
	MonoMethodDesc* desc;
	if (image)
	{
		static char* pattern[] = { "*:Main()", "*:Main(string[])" };
		for (int i = 0; i < 2; i++)
		{
			desc = mono_method_desc_new(pattern[i], TRUE);
			if (!desc)
			{
				continue;
			}
			
			method = mono_method_desc_search_in_image(desc, image);
			mono_method_desc_free(desc);
			if (method)
			{
				break;
			}
		}
	}
	return method;
}

    那麼獲取到入口點的 MonoMethod* 我們應該如何去正確的執行它?其實若僅僅只是執行一個託管的“ 可執行程式” 的話,有一個比較簡單的方法完成。

int main(int argc, char* argv[]) 
{
	MonoAssembly* assembly;
	MonoDomain* domain;
	int retval;

	domain = mono_jit_init(argv[1]);
	assembly = mono_domain_assembly_open(domain, argv[1]);

	mono_jit_exec(domain, assembly, argc, argv);
	retval = mono_environment_exitcode_get();

	mono_jit_cleanup(domain);
	return retval;
}

   如上述程式碼所示,若你僅僅只是利用 mono 執行你的可執行應用程式的話,按上述程式碼就可以了 當然至於 mono 虛擬機器層面的優化,AOT、LLVM 方面的東西,還是需要你自己慢慢處理。

int main(int argc, char* argv[]) 
{
	MonoAssembly* assembly;
	MonoDomain* domain;
	MonoMethod* method;
	MonoImage* image;
	int retval;

	domain = mono_jit_init(argv[1]);
	assembly = mono_domain_assembly_open(domain, argv[1]);
	image = mono_assembly_get_image(assembly);
	method = mono_image_get_entry_point_ex2(image);
	mono_runtime_run_main(method, argc, argv, NULL);

	retval = mono_environment_exitcode_get();

	mono_jit_cleanup(domain);
	return retval;
}

    還有別的方法,例如:mono_runtime_invoke 函式,但本文就不在這裡繼續累贅這些了,有興趣的朋友可以自己私下自己去呼叫來玩玩,或者等本文的後續更新~~~當然到了現在本文都沒有提供 include 方面的宣告,本文在下述把東西給貼上,順帶一提本人的程式碼是連結的 “mono” 靜態庫,若要改成動態連結請注意調整相應的 lib 的 link,另外你可以從官方的提供的現在連接獲取到各個穩定發行版 “https://www.mono-project.com/download/stable/

#include <mono/jit/jit.h>
#include <mono/metadata/object.h>
#include <mono/metadata/environment.h>
#include <mono/metadata/assembly.h>
#include <mono/metadata/debug-helpers.h>

#include <string.h>
#include <stdlib.h>

#pragma comment(lib, "libmono-static-sgen.lib")
#pragma comment(lib, "libmono-static-boehm.lib")

#pragma comment(lib, "ws2_32.lib")
#pragma comment(lib, "msvcrt.lib")
#pragma comment(lib, "winmm.lib")
#pragma comment(lib, "psapi.lib")
#pragma comment(lib, "mincore.lib")

#ifndef FALSE
#define FALSE 0
#endif
#ifndef TRUE
#define TRUE 1
#endif