Acite✨ 小枫叶trall
爱生活,不爱黑眼圈。
关注数: 27 粉丝数: 408 发帖数: 4,846 关注贴吧数: 77
[技术记录]ObRegisterCallbacks注册句柄操作回调与进程保护 简单记录下ObRegisterCallbacks的基本用法,加深一下自己的理解,也希望帮助看到的人少走这方面的弯路。如有错误还望不吝指正。第二个参数指向一个PVOID类型的数组,用于接收此次操作所注册的回调的句柄。一般来说你要注册多少个回调,数组就多长。 重点在于第一个参数,它是一个指向_OB_CALLBACK_REGISTRATION结构的指针。第一个成员Version,msdn说"Drivers should specify OB_FLT_REGISTRATION_VERSION.",那就听他的。 第二个成员OperationRegistrationCount,要注册的回调的数量。不必多说。一般与前面那个数组的长度一样。 第三个成员Altitude,根据msdn上的解释是驱动被加载的优先级。不同类型的驱动有不同的值范围。在后面的例子中我使用了“321000”这个值。(在一个环境下同一个值不能被重复使用,否则调用失败。注册多个回调时要注意这一点) 第四个成员RegistrationContext,当你的回调函数被调用时,系统会把这个参数传递给那个回调函数,如果不需要就填NULL。 第五个成员OperationRegistration,它指向一个_OB_OPERATION_REGISTRATION类型的结构。第一个成员ObjectType,指明了这个回调要处理的对象类型,按照msdn的说法可以是PsProcessType,PsThreadType,ExDesktopObjectType 的其中之一。但其他的类似于SeTokenObjectType,IoFileObjectType也可以用,但因为系统默认是不开启这些回调的,需要先用EnableObType函数启用。 例如:EnableObType(*SeTokenObjectType); 第二个成员Operations,指定要处理的句柄操作类型,可以是OB_OPERATION_HANDLE_CREATE或者OB_OPERATION_HANDLE_DUPLICATE。顾名思义前者代表一个句柄被创建,后者代表句柄被复制。可以用位或操作运算符|进行叠加。 第三个和第四个成员分别是两个回调函数的指针,前者会在操作发生前被调用,后者会在操作发生后被调用。可以为NULL。 再来说一下回调函数的定义。 首先是操作前被调用的。第一个参数是你在_OB_CALLBACK_REGISTRATION中指定的。 第二个参数指向一个_OB_PRE_OPERATION_INFORMATION类型的结构体第一个成员Operation,发生的操作类型,OB_OPERATION_HANDLE_CREATE或者OB_OPERATION_HANDLE_DUPLICATE。 下面那个联合体中,Flags和Reserved都是系统保留的空间,KernelHandle表示这个句柄是不是内核句柄。 Object是将要操作的句柄所对应的对象。比如PEPROCESS,PETHREAD。具体靠下一个成员ObjectType判断。 ObjectType表示这个句柄的类型,同_OB_OPERATION_REGISTRATION的第一个成员。 CallContext默认为NULL,在PreCallback中可以将其设置为其他值,这个值会被传递到PostCallback。 Parameters指向一个_OB_POST_OPERATION_PARAMETERS类型的联合体根据Operation的值来确定访问哪一个,OB_OPERATION_HANDLE_CREATE对应前者。_OB_PRE_CREATE_HANDLE_INFORMATION中定义了被创建的句柄所具有的原始权限和最终申请的权限。我们可以通过修改DesiredAccess的值来修改句柄的权限,比如我不想让我的进程被其他进程Terminate,就可以从句柄中拿掉相关的权限。那些被按位置零的值是ACCESS_MASK中的Specific Right,也就是前16位。对象类型是Process时代表PROCESS_TERMINATE等等。当然我们最好不只把PROCESS_TERMINATE权限拿掉,为了保险也把内存操作,挂起之类的权限拿掉了。 DuplicateHandleInformation与前面的大致相同,不同点是可以获取复制句柄的源进程和操作进程。 其他需要注意的是,你不能给复制的句柄添加原来没有的权限,但可以把已有的权限拿掉。 PostOperationCallback不常用并且用法与PreOperationCallback大致相同就不多说了。 只需要注意你不能在PostOperationCallback回调函数中修改这个句柄的权限。只能通过GrantedAccess获取。 完整例子:
[技术记录]FFMpeg.AutoGen+D2D解码并播放视频(含音频流) 最近在捣鼓FFMpeg这个东西,可惜网上有用的资料除了雷老师的博客之外实在难找,对于c#里面的FFmpeg.AutoGen更是如此。所以走了不少弯路,现开此贴记录。(语言组织能力不太好,这贴的东西会很杂。涉及到d2d绘图的部分,我封装了一个Direct2DImage类,可以把图像动态绘制到Image控件上,具体实现原理不在此贴赘述,请看上一贴和Github源码) FFmpeg.AutoGen这个东西就是把ffmpeg的一些接口封装在一个类里面,本身并不包含解码器,在调用方法上和c区别不大。所以在调用之前必须定位ffmpeg的库,代码如下。 ffmpeg.av_register_all(); ffmpeg.avcodec_register_all(); ffmpeg.avformat_network_init(); 这三个方法调用一次即可,作用是让ffmpeg加载自己的链接库,顺便一提,ffmpeg的链接库在官网就有,download页面选择Shared下载就是链接库。 然后我们需要一个全局性的对象(pFormatContext ),标志着媒体文件,如果说行话叫“解封装”。 var pFormatContext = ffmpeg.avformat_alloc_context(); ffmpeg.avformat_open_input(&pFormatContext, url, null, null); ffmpeg.avformat_find_stream_info(pFormatContext, null); 然后,我们需要从媒体中找到视频和音频流,如下。 for (var i = 0; i < pFormatContext->nb_streams; i++) { if (pFormatContext->streams[i]->codec->codec_type == AVMediaType.AVMEDIA_TYPE_VIDEO) { pStream = pFormatContext->streams[i];//视频流 } else if (pFormatContext->streams[i]->codec->codec_type == AVMediaType.AVMEDIA_TYPE_AUDIO) { aStream = pFormatContext->streams[i];//音频流 } } 我们首先讨论视频的解码部分。 “pStream->codecpar->codec_id”代表视频流所对应的解码器的ID,将他传入以下的函数,可以得到一个解码器对象,随后我们需要使用这个对象获取解码器上下文(pCodecContext )。并使用avcodec_open2打开解码器。 var pCodec = ffmpeg.avcodec_find_decoder(pStream->codecpar->codec_id); pCodecContext = ffmpeg.avcodec_alloc_context3(pCodec); ffmpeg.avcodec_parameters_to_context(pCodecContext, pStream->codecpar); ffmpeg.avcodec_open2(pCodecContext, pCodec, null).ThrowExceptionIfError(); 在很多时候,视频流直接解码出的数据是YUV格式的,这并不便于我们后续的绘图操作。但是好在ffmpeg提供了一组方便的接口让我们可以转换图像的格式: pConvertContext = ffmpeg.sws_getContext(width, height, sourcePixFmt, width, height, destinationPixFmt, ffmpeg.SWS_FAST_BILINEAR, null, null, null); 如上的代码可以获得一个swr对象(pConvertContext ),sourcePixFmt表示源数据的像素格式,可以通过pCodecContext->pix_fmt来获取,目标像素格式一般为BGRA,随后我们需要对swr分配一个帧对象, pConvertedFrame = ffmpeg.av_frame_alloc(); 并为转换后的数据初始化缓冲区, convertedFrameBufferPtr = Marshal.AllocHGlobal(convertedFrameBufferSize); dstData = new byte_ptrArray4(); dstLinesize = new int_array4(); ffmpeg.av_image_fill_arrays(ref dstData, ref dstLinesize, (byte*)convertedFrameBufferPtr, destinationPixFmt, width, height, 1); 至此为止,视频流解码的准备工作已经全部完成,我们再来说一下音频流的处理(播放使用DirectSound,我把相关的播放代码写进了一个dll,源代码位于Shinehelper,需要自取,此贴不对其详细解释)。 回到刚获取到视频和音频流的位置,我们同样需要用流中标识的解码器id来找到并打来解码器: var cedecparA = *aStream->codecpar; var pCodec_A = ffmpeg.avcodec_find_decoder(cedecparA.codec_id); 获取解码器上下文,打开解码器: pcodecContext_A = ffmpeg.avcodec_alloc_context3(pCodec_A); ffmpeg.avcodec_parameters_to_context(pcodecContext_A, &cedecparA); ffmpeg.avcodec_open2(pcodecContext_A, pCodec_A, null); 与视频解码不同的是,随后我们需要定义一些音频的信息,鉴于可能有一些朋友不太了解其意义(我也怕自己忘了),我在这里细说一下。 描述一段原始音频数据(PCM)的信息中,最常用的有采样率(bits_per_coded_sample)、声道数(channels)、样本格式(sample_fmt)、位深度。而其他的基本都可以通过这几位来计算。 那么该如何理解呢?众所周知,视频拍摄的原理就是快速连续的拍照,而拍照的速度就是帧率。帧率为30就是一秒拍照30次,同样播放时每秒也需要展示30帧。那么数字音频有没有类似的概念呢?有的。 音频数据在计算机中是以数字的形式存在的,与模拟信号不同,数字信号不可能完全精确的描述原波形,我们需要的精确度越高,相同时间内的数据就越大。实际上,数字音频采集的原理与视频类似,我们可以理解为在一段时间内连续对原始音频“拍照”,在播放时再按照原来的速度还原,这个速度就是采样率。比如,采样率为44100,就是每秒给原始音频拍44100张照片,而位深度就是每张照片所占用的大小。如果位深度为16bit,每个音频帧就占用两字节。而声道数和样本格式就比较好理解了,通常情况下声道有2个,因为我们需要左右两个喇叭,而样本格式中常用的是S16(有符号16位)。 那么来练习一下,现在有一段100byte大小的16位PCM,声道数为2,采样率为44100,求这段音频的时间。 计算方法: 一个声道的样本数=大小/(位深度/8)/声道数=100/2/2=25 时间=样本数/采样率=25/44100秒 说完这些我们言归正传: ulong out_channel_layout = ffmpeg.AV_CH_LAYOUT_STEREO; //声道描述 out_nb_samples = pcodecContext_A->frame_size; //从视频中读取到的每个音频帧所包含的样本数 bit_per_sample = pcodecContext_A->bits_per_coded_sample; //位深度 AVSampleFormat out_sample_fmt = AVSampleFormat.AV_SAMPLE_FMT_S16; //带符号16位格式 out_sample_rate = pcodecContext_A->sample_rate; //样本率 out_channels = ffmpeg.av_get_channel_layout_nb_channels(out_channel_layout); //声道数 out_buffer_size = ffmpeg.av_samples_get_buffer_size((int*)0, out_channels, out_nb_samples, out_sample_fmt, 1);//缓冲区大小 long in_channel_layout = ffmpeg.av_get_default_channel_layout(pcodecContext_A->channels); //输入的声道描述 因为同样的原因,从视频中解码出的音频数据不便于播放,我们要把他转换为便于播放的数据格式: au_convert_ctx = ffmpeg.swr_alloc(); au_convert_ctx = ffmpeg.swr_alloc_set_opts(au_convert_ctx, (long)out_channel_layout, out_sample_fmt, out_sample_rate, in_channel_layout, pcodecContext_A->sample_fmt, pcodecContext_A->sample_rate, 0, (void*)0); ffmpeg.swr_init(au_convert_ctx); 至此为止,音频解码的准备工作也已经完成,下面要正式开始解码。 一般情况下,我们使用两个线程分别解码并播放音频和视频,但是那样不便于音视频同步,所以我把音频和视频的解码放在同一个线程里面,配合播放的速度解码,而释放解码后的资源的工作则放在播放线程里面。 public struct VideoFrame { public WICBitmap frame; public double time_base; } public struct AudioFrame { public IntPtr data; public double time_base; } public List<VideoFrame?> bits = new List<VideoFrame?>(); public List<AudioFrame?> abits = new List<AudioFrame?>(); 解码线程会实时填充bits和abits这两个容器,并会配合播放的速度,播放线程处理过以后即释放。我在这个贴里面不把完整的解码线程代码放出来,仅解释解码的流程,需要完整代码的可以去github自取。 int error = ffmpeg.av_read_frame(pFormatContext, pPacket); if (error == ffmpeg.AVERROR_EOF) break; 首先我们从媒体流中读取一个数据包(pPacket),然后通过pPacket->stream_index来判断这个数据包是来自视频流还是音频流,并对其采取不同的处理方式。 ffmpeg.avcodec_send_packet(pCodecContext, pPacket).ThrowExceptionIfError(); error = ffmpeg.avcodec_receive_frame(pCodecContext, pDecodedFrame); 以下的代码会把一个数据包(AVPacket)解码为帧(AVFrame),如果是视频,我们这样处理: double timeset = ffmpeg.av_frame_get_best_effort_timestamp(pDecodedFrame) * ffmpeg.av_q2d(pStream->time_base);//该帧的时间,用于音视频同步 ffmpeg.sws_scale(pConvertContext, pDecodedFrame->data, pDecodedFrame->linesize, 0, pCodecContext->height, dstData, dstLinesize); //转换为位数据 ffmpeg.av_packet_unref(pPacket);//释放数据包对象引用 ffmpeg.av_frame_unref(pDecodedFrame);//释放解码帧对象引用 转换出位数据后,我们用其加载一个WIC图像,准备后续的Direct2D绘图。 var m_bitLoads = new WICBitmap(mFty, pCodecContext->width, pCodecContext->height, SharpDX.WIC.PixelFormat.Format32bppBGR, new DataRectangle(convertedFrameBufferPtr, dstLinesize[0])); 将其推入容器: bits.Add(new VideoFrame() { frame = m_bitLoads, time_base = timeset }); 在此处,nFram储存了播放线程所处理过的帧数,预处理120帧。之所以要限制是因为内存不够。 if (bits.Count - nFram >= 120) { while (bits.Count - nFram > 60 && CanRun) Thread.Sleep(1); }; 然后是音频的解码: ffmpeg.avcodec_decode_audio4(pcodecContext_A, pAudioFrame, &got_picture, pPacket); //解码数据包 double timeset = ffmpeg.av_frame_get_best_effort_timestamp(pAudioFrame) * ffmpeg.av_q2d(aStream->time_base); //时间 if (got_picture > 0) { ffmpeg.swr_convert(au_convert_ctx, &out_buffer, 19200, (byte**)&pAudioFrame->data, pAudioFrame->nb_samples); //为原始数据分配内存 var mbuf = Marshal.AllocHGlobal(out_buffer_size); RtlMoveMemory((void*)mbuf, out_buffer, out_buffer_size); abits.Add(new AudioFrame() { data = mbuf, time_base = timeset }); index++; } ffmpeg.av_packet_unref(pPacket);//释放数据包对象引用 ffmpeg.av_frame_unref(pAudioFrame);//释放解码帧对象引用 视频播放的部分如下:此为Direct2DImage类的绘图回调,包含了音视频同步代码,返回值表示要true/false更新前台数据,如果返回false则不计算此帧。Direct2DImage里面有一个额外的时钟稳定帧率。 音频播放:完整代码位于Github:rootacite/Shinengine。
[技术记录]使用Direct2D在Image控件上绘图 Direct2D中,最常用的渲染目标是从窗口句柄建立的HwndRenderTarget,但是有些时候,我们并不希望在整个窗口上绘图,而是在其中的一部分上。这个问题可以通过把创建RenderTarget时引入的窗口句柄设定为某一控件的句柄来解决,但是这有诸多弊端。首先,一些框架的控件没有独立的句柄,例如WPF。其次,在控件重绘时,如果绘图速度不够快可能造成闪烁。 要解决第二个问题,通常的方法是双缓冲,而如果要在解决第二个问题的基础上,同时解决第一个问题,我想到的一个方法就是绘制的结果作为一个Source显示在Image控件上。Direct2D自然没有提供相关的支持,所以我们要自己搭建这个桥梁,造桥材料的其中一个选择是WriteableBitmap。如图的代码可以创建一个新的WriteableBitmap对象。前两个参数是位图的大小,第三、四个参数分别是横纵的dpi,第五个参数是位图的像素格式,最后一个是调色盘,通常留空。 WriteableBitmap类可以作为Image控件的Source,并且可以实时更新内容。然而Direct2D不能直接在它上面绘图,我们需要一个间接的过程,比如使用WIC图片。创建WIC图片的过程不多赘述,但是要注意,WIC图片的各个参数必须与作为缓冲区的WriteableBitmap图片相同。 然后我们要做的就是创建一个WicRenderTarget,使Direct2D绘制的结果保存在其中。WicRenderTarget的第二个参数为创建的WIC对象,剩余参数一般填默认。 在绘制完成后,我们需要把WIC缓冲区中的数据复制到WriteableBitmap中,然而并没有安全的手段来进行这一操作,我们就必须用一些“暴力”的方式。首先,调用WriteableBitmap的Lock方法,开始改写缓冲区。然后再调用WICBitmap的Lock方法,得到缓冲区的地址,并使用CopyMemory把WICBitmap的缓冲区数据复制到WriteableBitmap中,缓冲区的大小可以通过像素高度*每行的字节数来计算。在数据复制完成后要调用WriteableBitmap的AddDirtyRect或者WritePixels方法,来刷新前台的数据,把绘制的结果显示在屏幕上。 总结:这种方法可以实现Direct2D在局部窗口中绘图,并且无需响应WM_PAINT事件,缺点则是对CPU负担较重,在高帧率时性能欠佳。
写了一个文件操作库, 日常使用很方便,来分享一下 最近搞的项目经常会有一些文件数据操作方便的需求。苦于实在不想再成天和那几个winapi打交道,FILE类和fstream又有很多不便。就干脆自己写了一个。 这个库适合操作小文件(最大4G),操作文件时直接把整个文件加载入内存,然后对那段内存进行修改,并提供接口把修改应用到文件。详细如下 基类:MicroFile 抽象类 方法: Load()把文件加载入内存 Clear()清空文件 Save()保存修改到文件 Size()返回文件大小(字节数) Push(LPCVOID,ULONG)把一段数据推入文件末尾 Pop(LPVOID,ULONG)把一段数据从文件末尾弹出 void Sub(LPVOID,int) 从当前指针(这个库有一套自己的文件指针机制)位置截取一段数据 BOOL Gate(LPVOID) 复制整个文件数据 virtual BOOL Get(LPBYTE tart) = 0; 从当前位置获取一个字节 virtual BOOL Set(BYTE sour) = 0; //设置当前位置字节 重载运算符: ++文件指针位置自增1,如果超出文件边界返回0,反之1,下列运算符以此类推 -- 文件指针位置自减1 *返回当前字节的引用 +=n文件指针位置自增n -=n 文件指针位置自减n =设置文件指针位置 除了运算符以外,任何操作不会影响文件指针位置。 派生类MicroBinary 重载 Get,Set方法。 Get(LPBYTE) Get(LPWORD) Get(LPDWORD) Set(LPCBYTE) Set(LPCWORD) Set(LPCDWORD) 派生类 MicroText 重载: Get(LPWSTR) Get(LPSTR) Set(LPCWSTR) Set(LPCSTR) MicroText的Get与Set方法与指针位置无关,直接设置整个文件。 BOOL Load() void Push(LPCSTR,int) 向文件末尾推入字符串 void Push(LPCWSTR,int) void Pop(LPWSTR,int) 从文件末尾弹出字符串 void Pop(LPSTR,int) MicroText类的构造函数的第二个参数是字符格式。 define ENCODE_BYTE 1 define ENCODE_WORD 2 DWORD Size()返回字符个数 运算符以每个字符占的字节数为移动距离 派生类 MicroData 构造函数的第二个参数是结构体的大小。 void Push(LPCVOID) 将数据推入文件 void Pop(LPVOID) 从文件末尾弹出数据 Get Set以及所有运算符根据对象初始化时定义的结合体大小进行读写和移动指针。 源码和本体位于GitHub:
论实现图形界面的方式可以有多么玄学 自己搞出了一个项目,实现了用纯winapi+opencv实现图形界面。 添加了用纯脚本控制界面的代码,是lz自己搞的脚本语法,类似于xml。 吧友们在这个项目中可以找到: 1.Winapi都不需要,从零开始实现控件的代码。原理简单粗暴:监视鼠标事件。 2.不使用任何windows控件,每一个界面都是GDI画上去的。 3.全新的界面实现思路,把每一个页,视作一个“View”。封装在一个名为ViewObject的类内,然后二次封装,形成GameSystem,提供ADV游戏的基本功能。 4.GDI与OpenCV之间的交流,Mat转换Image然后用GDI画。 5.OpenCV实现的基本图像处理。 6.很多玄学的问题解决办法,写出来会被老板开除的那种。 例如:OpenCV搞出来的界面不能截获鼠标消息?OK,把主窗口隐藏,调节成和游戏窗口一样的大小,搞一个线程重复移动到游戏窗口的位置。 7.多线程画界面,内存泄漏的解决办法。 8.解析类XML脚本的代码实现。 同时放出一个Direct2D实现的版本。 鉴于现在网络上Direct2D的教学资源稀缺,楼主身先士卒(误),用D2D实现了与第一个引擎一样的功能,且更加稳定流畅。DX版本没有用到任何变换技术,但是对想要了解D2D的图层技术的吧友,应该...大概...会有借鉴价值吧? 楼主Gayhub的id:rootacite,这两个引擎的代号分别为Clarles和Evelyn。 repositories是公开的。代码商用请打声招呼
浅谈国产Galgame《梦末》(无剧透) 今天在steam上面看见的,我买的是茸雪+梦末+两作ost的捆绑包。两个游戏的本体原价都是18元,打折后12元,加上两个作品的OST,减去捆绑包的优惠是25元,还是十分划算的。 其实我在买之前是想先搜一下贴吧里有没有评测,在G吧吧内搜索,竟然从来没有人水过,那这个活就交给我了。 先说结论(个人角度):推荐。是一个精致的短篇作品。 这是一个末日题材的作品,但是不同于《我和她的世界末日》,这个作品比起解密要素更注重剧情。他讲述了地球即将迎来末日,在最后的时间中依靠“惯性”生存的男主和女主的,一个悲剧的恋爱故事。毕竟是末日题材,这个作品从一开始就不会有一个美好的普遍概念上的好结局。我们总会认为末日题材的作品会给我们带来一种深刻的哲学思考,和对人性的认识,但是这部作品着重表达的很明显不是那些。如果要谈那些的话,3小时的篇幅未免太短了,这部作品在用一种最真挚,最纯粹的爱来感动你。在通关整个作品后,他给我带来的震撼与《我在七年后等你》十分相似。都是在以跨度久远的时间作为媒介,表达男女主的感情(通关后再次点击开始会有一个5分钟的番外)。顺便说一下,这个作品的OST就是在作品中出现的,女主与男主的故事的广播剧(女主魔改后的),极度好评,极度推荐一起购买。 在推完作品后,剧情会让你的心情十分沉重,和那种让人想要哭出来的悲伤完全不一样,因为你从一开始就知道这个故事注定是个悲剧。这里也说一下,虽然不多,但是也有人认为这部作品是给玩家喂屎的烂作。我今天是来吹的,显然我不同意这种观点。鲁迅说过“悲剧就是把美好的事物毁灭给人看”,这个定义与我们一般概念中的“喂屎”在一些方面不谋而合。但是,悲剧与喂屎一定是不同的,并且区别很大。其中最明显的区别就是“喂屎”是不符合逻辑的,比起让人感到悲伤,更让人感到不适甚至是恶心。在这一点上,至少我没有明显感觉到《梦末》在剧情上的任何逻辑漏洞。(相反,《三色绘恋S》(如果/可能)搞(对于没玩前作的路人来说的)有情人突然成兄妹的标准FFF团剧情,那才叫喂屎)。 所以,在长篇作品玩累了,或者时间不多的情况下。享受一下这种精致的短篇作品也是个很好的选择。
【超理】推翻复分解理论 【以下纯属扯淡,意义仅为娱乐,信了你就输了】 对于生活中的种种伪科学,我们要保持一个怀疑的态度。比如,人教版初中教材中对于复分解的定义是:“复分解反应是由两种化合物互相交换成分,生成另外两种化合物的反应。”这种说法已经污染化学界多年,其实仔细想一想,就会发现它其实漏洞百出。 1.我们知道,碳酸钙会与盐酸反应,这个反应的方程式为:CaCO3+2HCl == CaCl2+H2O+CO2↑,这个反应是一个复分解反应,但它却生成了两种以上的物质,很明显与上述的定义不符。 2.再比如说,高锰酸钾与过氧化氢的反应,如果按照上述的定义,那么反应应该是:2KMnO4+H2O2==K2O2+2HMnO4,然而事实不是这样。 综上所述,我要推翻复分解理论,提出大分解理论。 大分解理论的意义如下: 1.可溶性物质溶入一种溶质时,他的分子会被一种能量撕裂成原子的形态,这种能量我将它命名为”超锑能“,比如说硫酸在水中解离的化学式为“H2SO4==2H+ + S⁶+ + 4O²-”而不是“H2SO4==2H+ + SO4²-”。这种状态我们叫“锑离态”,这也就解释了很多反应生成了不止两种物质。 2.如果两种物质在水中同时锑离,那么如果组合成了不能锑离的物质,那么反应就成立。比如:"CuSO4+2HF==SO2+OF2+Cu(OH)2"【然而这个反应纯属超理】。 3.已经在水中锑离的物质,即使把水蒸干,也不一定会得到锑离前的物质。就比如说就算我再怎么蒸馏75%的乙醇,也不能得到无水乙醇。 我已经写信给中科院了,希望引起重视。 【再说一遍,此文纯属搞笑,相信你就输了!】
1 下一页