找回密码
 立即注册
查看: 650|回复: 0

[笔记] Unity mono代码结构分析及阅读(四)开始调试

[复制链接]
发表于 2020-12-2 12:25 | 显示全部楼层 |阅读模式
这是基于unity mono代码阅读的第四篇。
本文主要介绍怎样从CS代码生成我们需要的包含CLR字节码的exe和dll文件。
上文已经介绍了怎样创建一个可以调试的mono工程。现在我们的测试工程已经可以运行包含入口的CLR,现在我们需要做的构建一个可以测试的C#生成CLR的环境。
其实本章内容要说简单也可以很简单。。。
有同学说,我直接用VS2017或者19创建一个C#工程,生成一个EXE不就行了么。是的,你没说错,.Net是一个标准,所以无论是VS,还是MonoDevlop还是unity调用的msc都是可以生成符合标准可运行的CIL字节码的。。
打开vs2017 创建一个C#控制台程序,然后框架选择.net2.0然后简单打一个HelloWorld。
using System;
using System.Collections.Generic;
using System.Text;

namespace TestCPlus
{
    class Program
    {
        static void Main(/*string[] args 注 默认参数需要注视掉不然堆栈溢出的报错。。*/)
        {
            System.Console.WriteLine("Hello World.");
        }
    }
}
编译生成。然后将我们一开始代码里面加载CIL的代码改成我们新生成的这个exe看一下。
assembly = mono_domain_assembly_open(domain, "D:\\MonoTest\\TestCPlus\\bin\\Debug\\TestCPlus.exe");
编译运行
不用理会上面的assertion,Hello World出现啦。
可见,即使用VS2017,只要你选择对了.net版本,那么也是可以在我们的调试环境中运行的。
当然,如果只想了解这些,我们的确可以不用继续往下看了。不过作为一个unity程序员,我觉得我们还是需要了解一下,Unity是怎样生成CIL字节码的。
稍微有点了解unity安卓包的同学可能知道。
在最终打包的untiy工程中,C#最终会被编译成Assembly-CSharp.dll打包在我们的apk或者windows发布包内(在没选il2cpp的情况下)。那么我们怎样将我们的代码生成一个Dll并且像Unity一样调用里面一堆monobehavior呢?
下面我们一个一个来看,
1.生成一个DLL后缀的文件
这个比较简单。如果你用VS在生成部分改成Dll是可以完成这个需求的。但是Unity发布的时候是没有强制要求VS的,你可以用MonoDevlop,甚至你用Notepad,Vim来写代码都没有问题。这又是怎么做到的呢?
这里面的道理其实很简单,在本系列的第一篇文章里面就有介绍。
Mono自带了一套完全用C#写的C#编译器,unity就是调用这几个mono提供的编译器来编译C#代码的。本文现在用适合.net2.0的gmcs编译器为例重新编译一下刚刚我们生产的VS2017C#工程内的Hello World。
gmsc文件在Unity文件夹下的Mono\lib\mono\2.0 目录下
E:\trunk\Data\Mono\bin\mono.exe "E:\trunk\Data\Mono\lib\mono\2.0\gmcs.exe" TestCPlus.cs很快我们就生成了TestCPlus.exe。用我们刚刚生成的测试环境跑一下
完美,可见unity目录自带的mono .net2.0C#编译器gmcs跟vs2017一样都可以生成符合CIL标准的字节码达到一样的效果。
接下来我们看一下Unity引擎自带的mono编译器包含哪些参数
  -target:KIND         Specifies the format of the output assembly (short: -t),KIND can be one of: exe, winexe, library, module
根据这个参数,我们可以尝试生成一下跟unity引擎以前的dll试试。
E:\trunk\Data\Mono\bin\mono.exe "E:\trunk\Data\Mono\lib\mono\2.0\gmcs.exe" TestCPlus.cs -target:library生成dll后,我们尝试运行一下我们刚刚的mono调试环境试试。在正常win32编程中,其实dll和exe主要的差别在于入口点的不同,也就是说exe自带入口点,程序运行会自动搜寻main进行运行。dll有点区别,dll一般可以从dllmain运行,也可以只加载,加载后,通过调用dll提供的各个接口进行工作。
int retval = mono_jit_exec(domain, assembly, argc - 1, argv + 1);
我们刚刚一直用的是mono_jit_exec运行exe,现在exe变成dll,应该会存在一定的入口点找不到问题。不过我们先试一下。
assembly = mono_domain_assembly_open(domain, "D:\\MonoTest\\TestCPlus\\bin\\Debug\\TestCPlus.dll");
我们将测试工程内的assembly由TestCPlus.exe改成TestCPlus.dll后,编译运行。
果然,这次与我们想象的一样找不到入口点。接下来就是我们要解决的第二个问题
2.对于dll类型的CIL字节码,我们要怎样像unity一样找到入口点并运行。
这里有几个思路可以参考。
1.老子有unity源码,老子直接翻代码不就得了。(是,如果你有unity源码,无论是泄露的还是掏钱买的,这都是比较正经的解决方案,其实也不那么正经,你会发现unity代码里面想找点东西还不如下面几种方法)
2.我google一下,查查文档,看看有没有别人处理过。
这个是最靠谱的一个方法,如果你用心搜索,只要姿势正确,一定是可以找到如下几个函数
//Get a image from the assembly
MonoImage* image;
image = mono_assembly_get_image(assembly);
if (!image)
{
    std::cout << "mono_assembly_get_image failed" << std::endl;
    system("pause");
     return;
}

//Build a method description object
MonoMethodDesc* TypeMethodDesc;
const char* TypeMethodDescStr = "TestCPlus.Program:Main()";
TypeMethodDesc = mono_method_desc_new(TypeMethodDescStr, NULL);
if (!TypeMethodDesc)
{
      std::cout << "mono_method_desc_new failed" << std::endl;
      system("pause");
       return;
}
//Search the method in the image
MonoMethod* method;
method = mono_method_desc_search_in_image(TypeMethodDesc, image);
if (!method)
{
      std::cout << "mono_method_desc_search_in_image failed" << std::endl;
      system("pause");
      return;
}
//run the method
std::cout << "Running the static method: " << TypeMethodDescStr << std::endl;
mono_runtime_invoke(method, nullptr, nullptr, nullptr);
3.看代码,看为什么mono_jit_exec这个函数失效了
本文会从mono_jit_exec开始做起,其实mono_jit_exec的代码比较简单,大概猜一下,就是完成我们上一步2搜索到的操作,只是他把2的操作合并了一下,为一些exe直接运行提供了方便。但是猜归猜。一切都在代码之中。打开我们用来编译mono.dll的vs工程,开始吧。
/**
* mono_jit_exec:
* @assembly: reference to an assembly
* @argc: argument count
* @argv: argument vector
*
* Start execution of a program.
*/
int
mono_jit_exec (MonoDomain *domain, MonoAssembly *assembly, int argc, char *argv[])
{
        MonoImage *image = mono_assembly_get_image (assembly);
        MonoMethod *method;
        guint32 entry = mono_image_get_entry_point (image);

        if (!entry) {
                g_print ("Assembly '%s' doesn't have an entry point.\n", mono_image_get_filename (image));
                /* FIXME: remove this silly requirement. */
                mono_environment_exitcode_set (1);
                return 1;
        }

        method = mono_get_method (image, entry, NULL);
        if (method == NULL){
                g_print ("The entry point method could not be loaded\n");
                mono_environment_exitcode_set (1);
                return 1;
        }
       
        return mono_runtime_run_main (method, argc, argv, NULL);
}
mono_jit_exec的代码跟我们想的差不多,他通过函数mono_image_get_entry_point来获取在exe中描述的入口点。我们一般以为入口点就是写好的mian函数而已,其实
/**
* mono_image_get_entry_point:
* @image: the image where the entry point will be looked up.
*
* Use this routine to determine the metadata token for method that
* has been flagged as the entry point.
*
* Returns: the token for the entry point method in the image
*/
guint32
mono_image_get_entry_point (MonoImage *image)
{
        return ((MonoCLIImageInfo*)image->image_info)->cli_cli_header.ch_entry_point;
}
代码告诉我们,这个是在image里面定义的,也就是说,你可以随便选择一个入口点,只要在exe内指定即可,这个实际上也可以在vs里面进行操作,C#工程的属性就可以指定入口点。
如果找不到入口点,就像刚刚我们拿了一个dll来鱼目混珠,就会被检查出来并提示报错。
首先是通过mono_get_method获取到MonoMethod指针,没有找到这个入口点方法的话也是一样报错,然后就会进入mono_runtime_run_main。
现在入口点方法已经找到了,我们继续往下看方法怎么被运行。
/**
* mono_runtime_run_main:
* @method: the method to start the application with (usually Main)
* @argc: number of arguments from the command line
* @argv: array of strings from the command line
* @exc: excetption results
*
* Execute a standard Main() method (argc/argv contains the
* executable name). This method also sets the command line argument value
* needed by System.Environment.
*
*
*/
int
mono_runtime_run_main (MonoMethod *method, int argc, char* argv[],
                       MonoObject **exc)
{
        int i;
        MonoArray *args = NULL;
        MonoDomain *domain = mono_domain_get ();

        int result;

        g_assert (method != NULL);
       
        mono_thread_set_main (mono_thread_current ());

        mono_set_commandline_arguments(argc, argv,method->klass->image->assembly->basedir);

        argc--;
        argv++;
        if (mono_method_signature (method)->param_count) {
                args = (MonoArray*)mono_array_new (domain, mono_defaults.string_class, argc);
                for (i = 0; i < argc; ++i) {
                        /* The encodings should all work, given that
                         * we've checked all these args for the
                         * main_args array.
                         */
                        gchar *str = mono_utf8_from_external (argv );
                        MonoString *arg = mono_string_new (domain, str);
                        mono_array_setref (args, i, arg);
                        g_free (str);
                }
        } else {
                args = (MonoArray*)mono_array_new (domain, mono_defaults.string_class, 0);
        }

        mono_assembly_set_main (method->klass->image->assembly);

        result = mono_runtime_exec_main (method, args, exc);
        fire_process_exit_event ();
        return result;
}
mono_runtime_run_main里面是个巨大的宝库,mono的函数命名很好,从这几个函数的命名上面我们就可以看出当前的这几个函数作用。
首先我们会设置当前线程为mono的主线程。然后我们会获取命令行参数,然后我们就会给运行的exe生成当前需要的命令行参数。
最后就是设置当前mian assembly,然后调用mono_runtime_exec_main来运行。
运行结束后,调用fire_process_exit_event退出。
上面涉及到的几个函数,都是跟mono运行环境初始化相关的函数,有兴趣的就可以深入进去再看一下。
然后就是运行部分了
我们进入mono_runtime_exec_main看一下,代码较多,我进行了一下删减。
/*
* Execute a standard Main() method (args doesn't contain the
* executable name).
*/
int
mono_runtime_exec_main (MonoMethod *method, MonoArray *args, MonoObject **exc)
{
        MonoDomain *domain;
        gpointer pa [1];
        int rval;
        MonoCustomAttrInfo* cinfo;
        gboolean has_stathread_attribute;
        MonoThread* thread = mono_thread_current ();

        g_assert (args);

        pa [0] = args;
        
        ....

        /* FIXME: check signature of method */
        if (mono_method_signature (method)->ret->type == MONO_TYPE_I4) {
                MonoObject *res;
                res = mono_runtime_invoke (method, NULL, pa, exc);
                if (!exc || !*exc)
                        rval = *(guint32 *)((char *)res + sizeof (MonoObject));
                else
                        rval = -1;

                mono_environment_exitcode_set (rval);
        } else {
                mono_runtime_invoke (method, NULL, pa, exc);
                if (!exc || !*exc)
                        rval = 0;
                else {
                        /* If the return type of Main is void, only
                         * set the exitcode if an exception was thrown
                         * (we don't want to blow away an
                         * explicitly-set exit code)
                         */
                        rval = -1;
                        mono_environment_exitcode_set (rval);
                }
        }
        return rval;
}
mono_runtime_exec_main主要就是设置一下运行前的环境,然后设置一下线程相关的参数。最后是根据方法里面描述的字节编码相关信息,最终调用mono_runtime_invoke。
看到mono_runtime_invoke我们就可以稍微休息一下了。是不是跟我们想的一样,运行exe与运行dll并没有太大的差别,只是exe在他的描述文件内说明了他的入口点,并且会涉及到一些命令行传入参数和线程相关的设置。而dll没有这么麻烦,我们只需要把dll加载进来,然后通过上面学到的函数,找到我们要调用的方法名,用mono_runtime_invoke调用就好了。
好的,那么我们现在就按我们上文学到的方法,来试着调用一下我们刚刚生成的dll看一下。
void RunCILInDll(MonoAssembly *assembly)
{
    //Get a image from the assembly
    MonoImage* image;
    image = mono_assembly_get_image(assembly);
    if (!image)
    {
        std::cout << "mono_assembly_get_image failed" << std::endl;
        system("pause");
        return;
    }
#pragma endregion

#pragma region Run a static method
    {
        //Build a method description object
        MonoMethodDesc* TypeMethodDesc;
        const char* TypeMethodDescStr = "TestCPlus.Program:Main()";
        TypeMethodDesc = mono_method_desc_new(TypeMethodDescStr, NULL);
        if (!TypeMethodDesc)
        {
            std::cout << "mono_method_desc_new failed" << std::endl;
            system("pause");
            return;
        }

        //Search the method in the image
        MonoMethod* method;
        method = mono_method_desc_search_in_image(TypeMethodDesc, image);
        if (!method)
        {
            std::cout << "mono_method_desc_search_in_image failed" << std::endl;
            system("pause");
            return;
        }

        //run the method
        std::cout << "Running the static method: " << TypeMethodDescStr << std::endl;
        mono_runtime_invoke(method, nullptr, nullptr, nullptr);
    }
}
我们添加一个新的RunCILInDll函数用来调用dll中的方法。
其实这里面有个方法名的问题,有同学可能会问,在哪里找到这个TestCPlus.Program:Main方法名的呢?其实经验丰富的C#开发者可以通过命名空间类名,方法名这样来写。
不过笔者还是推荐一个神器,ildasm.exe这个是微软官方提供的il反汇编器。可以将编译好的il字节码反编译成可读的il代码
是的,我们可以通过ildasm.exe来获取编译后的方法名。
这里还有个地方要注意一下,其中的一些定义会引入新的include文件。
#include <mono/metadata/assembly.h>
#include <mono/metadata/class.h>
#include <mono/metadata/debug-helpers.h>
说实话,这个也是笔者挺讨厌c++的地方,现在都2020年了,各大编辑器还不能智能补全include,当然也许会有人说,工程情况复杂,多版本兼容,同名函数冲突这些。我觉得C++就是历史包袱太重了。。我不太喜欢。。。
然后我们把一开始的jit_exec改成我们现在的RunCILInDll
//int retval = mono_jit_exec(domain, assembly, argc - 1, argv + 1);
RunCILInDll(assembly);
运行


完美。
好了,本章到此就结束了,至于mono_runtime_invoke的实现,我们留到下面章节进行分析。

本帖子中包含更多资源

您需要 登录 才可以下载或查看,没有账号?立即注册

×
懒得打字嘛,点击右侧快捷回复 【右侧内容,后台自定义】
您需要登录后才可以回帖 登录 | 立即注册

本版积分规则

小黑屋|手机版|Unity开发者联盟 ( 粤ICP备20003399号 )

GMT+8, 2024-9-28 05:59 , Processed in 0.066973 second(s), 24 queries .

Powered by Discuz! X3.5 Licensed

© 2001-2024 Discuz! Team.

快速回复 返回顶部 返回列表