项目地址:https://github.com/HDRorz/WriteFileHook
起因是各系统日志太多,机器也太多,查日志和监控都非常痛苦,所以有意做一套日志收集系统。其实各大公司都有这种东西,现成的也有Elasticsearch + Logstash + Kibana这套解决方案。然而上头就有点推进困难,也畏首畏尾。其实日志系统的demo也做了3、4版了,各种方案都有。
我接到开发任务的时候大概是16年10月份,那时方案设计是直接在日志中间件里埋点,异步调用一个webapi统一存储,同时还实现了服务存活检测(
当然当时我还有另一个脑洞,就是无需修改原系统,直接hook writeFile的API,把读到的内容发到日志收集系统。
原系统架构是.net的,hook部分也使用.net实现。这里使用了nektra公司的Deviare2。(nektra还有SpyStudio这样的好东西,然而并用不大来)
Deviare2官方提供了Demo,不过我还是花了很久才搞成功我想要的功能。
总结一下,这个问题在于对WinApi不熟悉,主要是函数传递是引用还是指针还是值,以及对.net(C#)互操作的不熟悉。
MSDN上的WinApi全家桶,或者找网上下载的中文版手册(chm)
System.Runtime.InteropServices类库和互操作文档
其中最重要的有
字符串
结构体
字符串的难点在于在C++里字符串就有很多种定义形式,比如说char*、char[]、LPSTR,还有t_char、w_char、LPCSTR、LPWSTR、LPCWSTR。然而.net里只有char[],string,stringbuilder。
结构体的难点在于在C++里的结构体在函数上可能是struct或者struct*,然而.net可以是struct或者class。
其中大致结果如下
字符串作为返回或者入参或者结构体成员时 IN char[] [DllImport("kernel32.dll", CharSet = CharSet.Ansi)]、[MarshalAs(UnmanagedType.ByValTStr, SizeConst=128)]String IN w_char[] [DllImport("kernel32.dll", CharSet = CharSet.Unicode)]、[MarshalAs(UnmanagedType.ByValTStr, SizeConst=128)]String IN t_char[] [MarshalAs(UnmanagedType.ByValTStr, SizeConst=128)]String IN LPSTR,char* [MarshalAs(UnmanagedType.LPStr)]String IN LPWSTR,w_char* [MarshalAs(UnmanagedType.LPWStr)]String IN LPTSTR,t_char* [MarshalAs(UnmanagedType.LPTStr)]String 字符串作为函数出参时 OUT char[] [DllImport("kernel32.dll", CharSet = CharSet.Ansi)]、[Out, MarshalAs(UnmanagedType.ByValTStr, SizeConst=128)]StringBuilder OUT w_char[] [DllImport("kernel32.dll", CharSet = CharSet.Unicode)]、[Out, MarshalAs(UnmanagedType.ByValTStr, SizeConst=128)]StringBuilder OUT t_char[] [Out, MarshalAs(UnmanagedType.ByValTStr, SizeConst=128)]StringBuilder OUT LPSTR,char* [Out, MarshalAs(UnmanagedType.LPStr)]StringBuilder OUT LPWSTR,w_char* [Out, MarshalAs(UnmanagedType.LPWStr)]StringBuilder OUT LPTSTR,t_char* [Out, MarshalAs(UnmanagedType.LPTStr)]StringBuilder 结构体值传递和指针时 void A(struct a) void A(struct a) void A(struct* a) void A(ref struct a)
这里在推荐一个站点http://www.pinvoke.net/,里面提供了WinApi 各种类型、枚举、结构体、函数的C++和C#描述。
然而C# Pinvoke互操作并不是万能,任何数组相关的包括字符串,C#都要求预定义数组长度(字符串就是字符数组,字符串也要定义长度),数组长度上限为0x1FFFFFFF。然而即使没定义到上限,在x86下会出现OutOfMemory错误,在x64下也会出现
无法封送处理“parameter #2”: 内部限制: 结构太复杂或太大。
这种时候微软爸爸也有解决方案,有一个C++/CLI的操作可以供我们使用。
C++/CLI这个东西教程真的看不懂。
官方教程 大致是C++/CLI 任务 和 C++/CLI 迁移入门 这两节。
中文博客的话 http://www.cnblogs.com/TianFang/p/4931879.html 这种差不多也是抄了一遍MSDN。
namespace WinApiReader { public ref class SystemHandle { public: unsigned int ProcessId; unsigned char ObjectType; unsigned char Flags; unsigned short Value; unsigned int Address; unsigned int GrantedAccess; }; public ref class SystemHandleInfo { public: int Count; array<SystemHandle^> ^SystemHandles; }; public ref class WinApiReader { public: int QueryProcessHandleInfo(int ProcessId, [System::Runtime::InteropServices::OutAttribute] SystemHandleInfo^% HandleInfo); }; } int WinApiReader::WinApiReader::QueryProcessHandleInfo(int ProcessId, [System::Runtime::InteropServices::OutAttribute] SystemHandleInfo^% HandleInfo) HandleInfo = gcnew SystemHandleInfo(); HandleInfo->SystemHandles = gcnew array<SystemHandle^>(outCount);
总结一下,大致是修改了托管类声明时的语法,用了gcnew关键字替代了C#里的new关键字,用来实例化托管类。在标注托管类型时会在类型后面再加一个^符号,假如是托管类型引用传递时,还会在后面再跟一个%符号。别的语法就看VS的自动提示了,实在不会就google(baidu真的救不了你