[原创分享]导出.net dll中的方法,让其他任意语言调用

stherix 2019-04-28 04:45:57
大家都知道.net可以通过PInvoke来调用C/C++/其他本地编译器产生的dll
但是有时候有这样的需求,用c#写dll,其他语言来调用
一般的实现方法都比较麻烦,比如注册成COM组件,用c++/cli再写个dll来export

其实.net framework是支持导出dll里面的方法的,直接就可以用其他语言调用,不过如果手动修改IL和meta data则非常麻烦
这里我们要用到dnlib,是一个类似于mono.cecil的库,可以支持对托管程序集进行读取和修改
其作者也开发了dnspy,de4dot等知名工具,它们用的底层库都是dnlib
dnlib可以在nuget中获取,在github上开源

这是作者自己写的导出文档
引用
dnlib supports exporting managed methods so the managed DLL file can be loaded by native code and then executed. .NET Framework supports this feature, but there's no guarantee that other CLRs (eg. .NET Core or Mono/Unity) support this feature.

The MethodDef class has an ExportInfo property. If it gets initialized, the method gets exported when saving the module. At most 65536 (2^16) methods can be exported. This is a PE file limitation, not a dnlib limitation.

Exported methods should not be generic.

The method's calling convention should be changed to eg. stdcall, or cdecl, by adding an optional modifier to MethodDef.MethodSig.RetType. It must be a System.Runtime.CompilerServices.CallConvCdecl, System.Runtime.CompilerServices.CallConvStdcall, System.Runtime.CompilerServices.CallConvThiscall, or a System.Runtime.CompilerServices.CallConvFastcall, eg.:

var type = method.MethodSig.RetType;
type = new CModOptSig(module.CorLibTypes.GetTypeRef("System.Runtime.CompilerServices", "CallConvCdecl"), type);
method.MethodSig.RetType = type;
Requirements:

The assembly platform must be x86, x64, IA-64 or ARM (ARM64 isn't supported at the moment). AnyCPU assemblies are not supported. This is as simple as changing (if needed) ModuleWriterOptions.PEHeadersOptions.Machine when saving the file. x86 files should set 32-bit required flag and clear 32-bit preferred flag in the COR20 header.
ModuleWriterOptions.Cor20HeaderOptions.Flags: The IL Only bit must be cleared.
It must be a DLL file (see ModuleWriterOptions.PEHeadersOptions.Characteristics). The file will fail to load at runtime if it's an EXE file.


下面是个简单的例子

先创建一个c#的动态库TestDll,用于导出方法,注意exe文件无法用于导出
里面就一个类Class1
写一个方法,必须是静态的,因为只有静态方法才能被导出
public static int Add(int value1, int value2) => value1 + value2;
然后CPU架构必须手工选择x86或者x64, AnyCPU的话无法被导出,然后选择Release编译模式,我自己测试的时候Debug也是无法正常调用的

下面是用dnlib修改导出的代码
byte[] data = File.ReadAllBytes("TestDll.dll");
ModuleDefMD module = ModuleDefMD.Load(data);
TypeDef Class1 = module.Find("TestDll.Class1", false);
foreach (MethodDef method in Class1.Methods.Where(x => x.IsStatic))
{
method.ExportInfo = new MethodExportInfo();
TypeSig type = method.MethodSig.RetType;
type = new CModOptSig(module.CorLibTypes.GetTypeRef("System.Runtime.CompilerServices", "CallConvStdcall"), type);
method.MethodSig.RetType = type;
}

var options = new dnlib.DotNet.Writer.ModuleWriterOptions(module);
options.Cor20HeaderOptions.Flags = module.Cor20HeaderFlags & ~ComImageFlags.ILOnly;
module.Write("TestDll_Exported.dll", options);


首先是将dll用dnlib加载进来,找到类(TestDll.Class1)
然后循环处理类里面的所有静态方法
method.ExportInfo = new MethodExportInfo(); //这里是声明此方法需要被导出,一般来初始化ExportInfo就可以了,如果需要自定义导出函数名或者顺序之类,那么可以对其成员进行赋值
之后还要修改导出函数的返回值,用System.Runtime.CompilerServices.CallConvStdcall修饰它,声明这是用于stdcall调用
当然你也可以用System.Runtime.CompilerServices.CallConvCdecl,对应的是cdecl调用

最后是将这个修改后的dll保存,这里需要注意的是需要将Cor20HeaderOptions里的ILOnly去掉,因为导出的dll不完全是托管代码了,可以用非托管代码访问

修改完后,可以用Depends查看这个dll,可以看到导出表里已经有Add函数

接下来就是调用了
[DllImport("TestDll_Exported.dll", CallingConvention = CallingConvention.StdCall)]
public static extern int Add(int value1, int value2);

别吐槽为什么用c#来调用,这只是个例子,用c++也是同样的效果
#include <Windows.h>
#include <stdio.h>

typedef int(__stdcall *AddFunc)(int);
int main() {
HMODULE handle = LoadLibraryA("TestDll_Exported.dll");
if (handle) {
AddFunc Add = (AddFunc)GetProcAddress(handle, "IncNumber");
if (Add) {
printf("%d", Add(1));
}
}
FreeLibrary(handle);
return 0;
}

有一点是,如果你的dll是x86的,那么调用者也必须是x86,x64的同理,否则会报错
...全文
655 4 打赏 收藏 转发到动态 举报
写回复
用AI写文章
4 条回复
切换为时间正序
请发表友善的回复…
发表回复
stherix 2019-04-28
  • 打赏
  • 举报
回复
忘了说了 调用导出的dll,还是需要.net framework支持,并不是说完全转成native code了
果然C 2019-04-28
  • 打赏
  • 举报
回复
OrdinaryCoder 2019-04-28
  • 打赏
  • 举报
回复
谢谢分享,感觉这个特别有用
stherix 2019-04-28
  • 打赏
  • 举报
回复
最后c++的函数签名和名称没写对,不过改一下就能正常运行
Reflexil是一个.NET程序集编辑器,方便开发人员对.NET程序进行修改;可以作为一个Reflector插件修改程序集的IL并保存到磁盘文件,也可以在自己的.NET程序调用进行更为灵活的.NET程序集修改。Reflexil使用的Mono.Cecil。   下面介绍在Reflector使用Reflexil插件进行反编   也许有人会说Reflector不就是反编译了么,为什么还要用Reflexil这个插件? Reflector 仅仅是反编译出源码给你看,并不能在修改后编译回去。所以我们需要使用Reflexil这个插件进行反编译回去。 .NET 三种反编译方式 这里顺便介绍下我所知道的三种反编译回去的方式: 直接修改IL代码(当然,这比较难,需要熟悉IL汇编,但是也是最实用的。也许我会在以后的文章详细阐述) 使用Reflector进行整个代码项目的导出,然后用VS进行修改后编译(在大型项目,如果有很多其他的dll引用,也许会让你寸步难行,无法使用VS编译通过,所以大型项目直接用IL汇编反编译关键代码即可) 使用Reflexil插件进行直接反编译(似乎这种要比第二种方法要好的多,至少很省事。其实实际使用起来也不是特别的方便,本文会进行一个稍微详细的使用介绍) Reflexil 安装 在此之前你需要先去下载Reflector和Reflexil插件,这些你可以去百度或谷歌上面下载到。(如果你不会…那下面的文章不用看了) 下载好之后首先安装Reflector程序,安装完毕后在把Reflexil插件放到Reflector程序目录下面(其实是个dll文件而已),如下图所示 然后打开Reflector,在工具栏依次点击 Tools -> Add-Ins... ,如下图所示 然后在弹出的界面点击 号选择刚刚拷贝到Reflector的Reflexil插件的dll文件,如下图所示 然后点击Close ,此时程序的 Tools 栏目下已经有了 Reflexil 插件选项。 要反编译的示例项目 本文反编译一个Winform程序(当然,你也可以反编译.NET 的其他 dll 文件),程序如下所示: 图 代码     public partial class Form1 : Form     {         public Form1()         {             InitializeComponent();         }         private void btnTest_Click(object sender, EventArgs e)         {             MessageBox.Show("你好");         }     } 我们把这个exe程序进行反编译吧 开始反编译工作 首先在Reflector工具栏依次点击 Tools -> Reflexil 打开 Reflexil 的窗口,如下图所示 然后打开你要反编译的程序集,我们编译自己写的Winform的exe程序作为测试吧,如下图所示。 然后我们选我们要反编译的方法,在Reflexil插件进行反编译,如下图所示。 可以直接在“你好”的那一栏进行点击右键选择 “Edit” 进行直接的编辑 也可以在图的这个选项卡( Instructions ) 的表的任意一栏点击右键选择,如下图所示 你可以直接Edit编辑单个文本,也可以Replace all with code... 编辑整个class的内容。 如果你选择的是Replace all with code... 来编辑整个内容的话,你需要在编辑完成后,在界面的左下角点击Complile 做编译操作。 但是似乎这个插件的编辑整个class的功能有缺陷,有些代码识别不出来。所以还是看情况使用吧,如果Replace all with code..不行,就直接选 Edit 编辑我们所选的内容即可。  我这里将“你好” 修改为 “你好,欢迎访问我的博客,www.wxzzz.com”。如下所示 修改完成后,就万事俱备 只欠东风了。 最后一步操作,依次在Reflector项目点击右键 -> 选择 Reflexil -> 选择 Save as.. 保存即可,这样就完成了修改代码,编译成功。如图 然后就ok了,运行看看:     标签:reflexil  反编译

17,748

社区成员

发帖
与我相关
我的任务
社区描述
.NET技术 .NET Framework
社区管理员
  • .NET Framework社区
加入社区
  • 近7日
  • 近30日
  • 至今
社区公告
暂无公告

试试用AI创作助手写篇文章吧