引入

我们如果想hook对象的回调,在上篇文章里我们已经知道了对象回调函数存在一个列表里面,我们通过dt可以看见,这里他是一个LIST_ENTRY结构,但是实际调用的时候,这样是行不通的,说明它结构不对

0: kd> dt _OBJECT_TYPE 86cf5d28
nt!_OBJECT_TYPE+0x000 TypeList         : _LIST_ENTRY [ 0x86cf5d28 - 0x86cf5d28 ]+0x008 Name             : _UNICODE_STRING "Process"+0x010 DefaultObject    : (null) +0x014 Index            : 0x7 ''+0x018 TotalNumberOfObjects : 0x25+0x01c TotalNumberOfHandles : 0xd0+0x020 HighWaterNumberOfObjects : 0x30+0x024 HighWaterNumberOfHandles : 0xf9+0x028 TypeInfo         : _OBJECT_TYPE_INITIALIZER+0x078 TypeLock         : _EX_PUSH_LOCK+0x07c Key              : 0x636f7250+0x080 CallbackList     : _LIST_ENTRY [ 0x996a88c8 - 0x996a88c8 ]

ObRegisterCallBack 分析

函数上来首先是合法性的检查,先检查版本号,然后从CallbackRegistration 取出OperationRegistrationCount也就是所要注册的回调数

inserted = 0;if ( (CallbackRegistration->Version & 0xFF00) != 256 )return 0xC000000D;OperationRegistrationCount = CallbackRegistration->OperationRegistrationCount;if ( !OperationRegistrationCount )return 0xC000000D;

在这之后,函数申请了一个空间,这里需要强调,OperationRegistrationCount*36,也就是每个给回调信息结构的空间有36个字节,但是我们回忆一下之前的知识,回调所对应的结构**_OB_OPERATION_REGISTRATION**只有16个字节,也就是我们需要重新分析这个待会在申请空间中与_OB_OPERATION_REGISTRATION类似的结构

 buf_size = 36 * OperationRegistrationCount + CallbackRegistration->Altitude.Length + 16;Alloc_buf = (DWORD *)ExAllocatePoolWithTag(PagedPool, buf_size, 0x6C46624Fu);Alloc_buf_copy = Alloc_buf;if ( !Alloc_buf )return 0xC000009A;memset(Alloc_buf, 0, buf_size);
typedef struct _OB_OPERATION_REGISTRATION {POBJECT_TYPE                *ObjectType;OB_OPERATION                Operations;POB_PRE_OPERATION_CALLBACK  PreOperation;POB_POST_OPERATION_CALLBACK PostOperation;
} OB_OPERATION_REGISTRATION, *POB_OPERATION_REGISTRATION;

之后我分析我一点一点来,所贴出的代码括号不一定是闭合的

在通过if语句验证了CallbackRegistration->OperationRegistrationCount的合法性后除了去初始化这个Num(用来在While循环中遍历),我们可以看见还有一个**v10 = Alloc_buf_copy + 12;**首先,回忆一下,在上面的代码中,我们知道Alloc_buf_copy是所申请的内存的首地址指针,所以这个v10也就是存放了首地址偏移一段所对应的指针。

但是,这里我要特别指出:这个12是根据我们定义的变量类型来改变的,不同的指针类型,IDA分析出来的伪代码都不太一样,所以这种偏移我们最好都回到汇编代码来进行分析,我就一起附在下面了,从下面那个汇编我们就可以看见,这个偏移实际上是0x30也就是48字节。

继续向下看,进入了While循环开始遍历这个回调列表,取出回调结构信息到v11这个变量,复制一份给v25(这里我还没有重命名,变量名字可能不同但是相对位置应该是一样的)

  CallbackRegistrationa = 0;if ( CallbackRegistration->OperationRegistrationCount ){Num = 0;v10 = Alloc_buf_copy + 12;while ( 1 ){v11 = &CallbackRegistration->OperationRegistration[Num];v25 = v11;if ( !v11->Operations || ((*v11->ObjectType)->TypeInfo.ObjectTypeFlags & 0x40) == 0 )break;
PAGE:006D62B0 8D 7E 30                      lea     edi, [esi+30h]

上面代码的if ( !v11->Operations || ((*v11->ObjectType)->TypeInfo.ObjectTypeFlags & 0x40) == 0 )检查了两个东西,第一个是我们之前学过的Operations

typedef struct _OB_OPERATION_REGISTRATION {POBJECT_TYPE                *ObjectType;OB_OPERATION                Operations;POB_PRE_OPERATION_CALLBACK  PreOperation;POB_POST_OPERATION_CALLBACK PostOperation;
} OB_OPERATION_REGISTRATION, *POB_OPERATION_REGISTRATION;

第二个我们可以用windbg来看看是什么值,这里找到对应的TypeInfo里面的ObjectTypeFlags,源代码里面异或的是0x40,也就是01000000,对应的就是SupportsObjectCallbacks,通过之前的学习我们知道,这里是对象是否支持回调的验证位,也就是现在还是合法性检查.

0: kd> dt _OBJECT_TYPE
nt!_OBJECT_TYPE........................+0x028 TypeInfo         : _OBJECT_TYPE_INITIALIZER //这里+0x078 TypeLock         : _EX_PUSH_LOCK..........................
0: kd> dt _OBJECT_TYPE_INITIALIZER
nt!_OBJECT_TYPE_INITIALIZER+0x000 Length           : Uint2B+0x002 ObjectTypeFlags  : UChar+0x002 CaseInsensitive  : Pos 0, 1 Bit+0x002 UnnamedObjectsOnly : Pos 1, 1 Bit+0x002 UseDefaultObject : Pos 2, 1 Bit+0x002 SecurityRequired : Pos 3, 1 Bit+0x002 MaintainHandleCount : Pos 4, 1 Bit+0x002 MaintainTypeList : Pos 5, 1 Bit+0x002 SupportsObjectCallbacks : Pos 6, 1 Bit.............................................

我们继续回到代码分析上来,可以看见之后是用于检查前后回调的合法性

  if ( v11->PreOperation ){if ( !MmVerifyCallbackFunction((unsigned int)v11->PreOperation) )goto LABEL_21;v11 = v25;}else if ( !v11->PostOperation ){break;}if ( v11->PostOperation ){if ( !MmVerifyCallbackFunction((unsigned int)v11->PostOperation) ){
LABEL_21:inserted = 0xC0000022;goto LABEL_22;

再往下,我们看见了一个类似结构体初始化的操作,这个v10我们回忆一下,就是 **v10 = Alloc_buf_copy + 12;*这里的v10,我们已经知道它偏移了申请空间48字节。这里如果有细心的人来看,就会发现少了一个(v10 - 5)的赋值,这里我们后面再说,但是如果加上这个复制,这里的结构是36字节

      *v10 = 0;*(v10 - 7) = (DWORD)(v10 - 8);*(v10 - 8) = (DWORD)(v10 - 8);*(v10 - 6) = v11->Operations;*(v10 - 4) = (DWORD)Alloc_buf_copy;*(v10 - 3) = (DWORD)*v11->ObjectType;*(v10 - 2) = (DWORD)v11->PreOperation;*(v10 - 1) = (DWORD)v11->PostOperation;

如果对最开始我们内存分配方式还有印象的人应该就可以回想起这个36,它正是我们为每个回调结构申请的大小,同时,还可以想起来申请的时候,申请的空间还加了16个字节,在这里也对上了。这里我需要强调的是这个结构的头两个成员,也是指针。

我们在上面强调过,指针相关的偏移是按照我们的定义来算的,所以,这里的减8实际上减的是DWORD*8,也就是32字节。进一步讲,这两个成员装的是我们结构的首地址,其实这里剧透一下,这就是在初始化链表

还需要注意的是第5个成员指向的是我们申请空间的首地址

在这里插入图片描述

有细心的人在读到这里之前可能就有疑问,之前还有一段代码为什么没有介绍,这一段代码经过本人初学的分析,就是在给上图的16个字节赋值,具体的图我放下来了,这里每个框是两个字节

其中,Length指的是海拔字符串的长度,buffer指向的是存放这段字符串的首地址。

*(_WORD *)Alloc_buf_copy = 256;Alloc_buf_copy[1] = (DWORD)CallbackRegistration->RegistrationContext;Length = CallbackRegistration->Altitude.Length;*((_WORD *)Alloc_buf_copy + 5) = Length;*((_WORD *)Alloc_buf_copy + 4) = Length;v9 = (char *)Alloc_buf_copy + buf_size - Length;//这里可以看出来算出了这个字符串的首地址v22 = *((unsigned __int16 *)Alloc_buf_copy + 4);Alloc_buf_copy[3] = (DWORD)v9;memcpy(v9, CallbackRegistration->Altitude.Buffer, v22);

在这里插入图片描述

我们继续跟着代码往下走,看见调用了一个函数,从语义上来说,这是将回调按照海拔(优先级)插入回调链表的函数,这里我在放一下前面文章提到过的_OBJECT_TYPE结构,也就是插入这个结构0x80的位置

inserted = ObpInsertCallbackByAltitude((int *)v10 - 8, *(v10 - 3));
0: kd> dt _OBJECT_TYPE
nt!_OBJECT_TYPE+0x000 TypeList         : _LIST_ENTRY+0x008 Name             : _UNICODE_STRING+0x010 DefaultObject    : Ptr32 Void+0x014 Index            : UChar+0x018 TotalNumberOfObjects : Uint4B+0x01c TotalNumberOfHandles : Uint4B+0x020 HighWaterNumberOfObjects : Uint4B+0x024 HighWaterNumberOfHandles : Uint4B+0x028 TypeInfo         : _OBJECT_TYPE_INITIALIZER+0x078 TypeLock         : _EX_PUSH_LOCK+0x07c Key              : Uint4B+0x080 CallbackList     : _LIST_ENTRY

这两个参数,向上翻翻就知道一个是链表头,一个是ObjectType,这里不过多分析了。

然后我们进入这个插入函数,来看看是怎么插入的。

我们已经知道了,传入的第一个参数是我们之前分析结构的首地址,也是一个双向链表头

00000000 _OB_LIST_ENTRY struc ; (sizeof=0x24, mappedto_1307)
00000000 callbackList _LIST_ENTRY ?
00000008 Operation dd ?
0000000C UNKNOW dd ?
00000010 Self dd ?
00000014 Object_Type dd ?
00000018 PreOperation dd ?
0000001C PostOperation dd ?
00000020 UNKNOW1 dd ?
00000024 _OB_LIST_ENTRY ends

在知道我们传入的参数a1的结构是分析出来的_OB_LIST_ENTRY参数a2是_OBJECT_TYPE之后,我们的分析就清晰很多了,首先是线程的锁,这个这里不细说

v17 = 0;CurrentThread = KeGetCurrentThread();--CurrentThread->SpecialApcDisable;p_TypeLock = &a2->TypeLock;if ( _interlockedbittestandset((volatile signed __int32 *)&a2->TypeLock, 0) )ExfAcquirePushLockExclusive();

以后我大概以注释的形式来解释代码,可以看见最后的结果就是将我们选中的链表插入这个callbacklist中,并按照海拔降序排列(其实就是优先级)

p_CallbackList = &a2->CallbackList;//取出链表Flink = a2->CallbackList.Flink;if ( Flink == p_CallbackList )//如果没有其他链表goto LABEL_10;//直接插入Altitude2 = (const UNICODE_STRING *)(a1->Self + 8);//取出海拔while ( 1 ){v7 = RtlCompareAltitudes((PCUNICODE_STRING)&Flink[2].Flink[1], Altitude2);//比较海拔,如果当前所要插入链表头的海拔大于所遍历的链表的海拔,则插入。这样就可以保证回调链表的降序,始终是高海拔的huii'dv8 = v7 == 0;if ( v7 <= 0 )//如果break;Flink = Flink->Flink;if ( Flink == p_CallbackList ){v8 = v7 == 0;break;}}if ( !v8 ){
LABEL_10://插入操作Blink = Flink->Blink;v10 = Blink->Flink;a1->callbackList.Flink = Blink->Flink;a1->callbackList.Blink = Blink;v10->Blink = &a1->callbackList;Blink->Flink = &a1->callbackList;}else{v17 = 0xC01C0011;//如果海拔相同就报这个错}Value = p_TypeLock->Value;锁相关这里不细说if ( (p_TypeLock->Value & 0xFFFFFFF0) <= 0x10 )v12 = 0;elsev12 = Value - 16;if ( (Value & 2) != 0 || _InterlockedCompareExchange((volatile signed __int32 *)p_TypeLock, v12, Value) != Value )//ExfReleasePushLockShared(p_TypeLock);v13 = KeGetCurrentThread();if ( !++v13->SpecialApcDisable && ($ECEA6BAF150BF07A2F607C21A5294F19 *)v13->ApcState.ApcListHead[0].Flink != &v13->64 )KiCheckForKernelApcDelivery();return v17;
}

回到ObRegisterCallBack函数,这里代码大概意思就是移动到下一个结构体,继续重复刚刚的操作

 if ( inserted < 0 )goto LABEL_22;++*((_WORD *)Alloc_buf_copy + 1);CallbackRegistrationa = (_OB_CALLBACK_REGISTRATION *)((char *)CallbackRegistrationa + 1);++Num;v10 += 9;if ( (unsigned int)CallbackRegistrationa >= CallbackRegistration->OperationRegistrationCount )goto LABEL_29;}inserted = 0xC000000D;

最后就是我们之前提到在_OB_LIST_ENTRY中的一个位,它当时没有被置位,在最后一段中,每个_OB_LIST_ENTRY的这个位(我们后面称为Flags),都被或了一个1.最后返回了,所申请内存空间的首地址,作为RegistrationHandle,也就是ObRegisterCallBack的第二个参数。

到这里我们就分析完了

LABEL_30:v19 = 0;if ( *((_WORD *)Alloc_buf_copy + 1) )//OperationRegistrationCount{v20 = Alloc_buf_copy + 7;//这个就是之前我们标记UNKNOW的位置,我们以后记为FLag位do{*v20 |= 1u;//或了一个1++v19;v20 += 9;//这里的九指的是_OB_LIST_ENTRY 里面的9,这样等价于跳到了下个相同结构的Flag,然后重复这个循环}while ( v19 < *((unsigned __int16 *)Alloc_buf_copy + 1) );}*RegistrationHandle = Alloc_buf_copy;//最后,这就是我们返回的RegistrationHandle}return inserted;
}

实践一下,验证逆向结果

在之前我们对象回调的代码基础上,打印出RegistrationHandle的位置

0: kd> g
[+] The state is 0
[+] RegistrationHandle is in 996a88b8

在这里插入图片描述

dd来看一下,为了方便我把图又放了一次

0: kd> dd 996a88b8
ReadVirtual: 996a88b8 not properly sign extended
996a88b8  00010100 00000000 000c000c 996a88ec
996a88c8  86cf5da8 86cf5da8 00000003 00000001
996a88d8  996a88b8 86cf5d28 98c08150 98c08140//这里的996a88b8指向了自己的首地址,
996a88e8  00000000 00320031 00340033 00360035
996a88f8  06040209 74416553 00000000 996a8904
996a8908  996a8904 00000000 996a8910 996a8910
996a8918  06080204 61564d43 002c0000 80c4d874
996a8928  00176b76 80000004 ffffffff 00000004

首先便是一个00010100,0001指的是我们回调数量,为1;0100是256,符合我们的预期

然后就是RegistrationContext这是我们函数参数,我没传所以是00000000,然后是海拔的bufffer 996a88ec,db看一下这个值,可以看见123456,符合预期。这16字节正确之后,继续向下走。

0: kd> db 996a88ec
ReadVirtual: 996a8938 not properly sign extended
996a88ec  31 00 32 00 33 00 34 00-35 00 36 00 09 02 04 06  1.2.3.4.5.6.....
996a88fc  53 65 41 74 00 00 00 00-04 89 6a 99 04 89 6a 99  SeAt......j...j.
996a890c  00 00 00 00 10 89 6a 99-10 89 6a 99 04 02 08 06  ......j...j.....
996a891c  43 4d 56 61 00 00 2c 00-74 d8 c4 80 76 6b 17 00  CMVa..,.t...vk..
996a892c  04 00 00 80 ff ff ff ff-04 00 00 00 01 00 35 00  ..............5.
996a893c  4d 69 6e 50 6f 73 31 32-37 34 78 31 31 39 39 78  MinPos1274x1199x
996a894c  39 36 28 31 29 2e 79 00-00 00 00 00 08 02 04 06  96(1).y.........
996a895c  53 65 41 74 00 00 00 00-64 89 6a 99 64 89 6a 99  SeAt....d.j.d.j.

再接下来的两个86cf5da8其实是链表头链表尾,但是我们只有一个函数所以一致

这个结构应该就是我们分析的_OB_LIST_ENTRY,首先是链表头链表尾996a88c8 996a88c8 ,对照上面,这里确实是我们这个_OB_LIST_ENTRY的地址

0: kd> dd 86cf5da8
ReadVirtual: 86cf5da8 not properly sign extended
86cf5da8  996a88c8 996a88c8 04190019 d46a624f
86cf5db8  8dc05880 00080006 8dc011f8 00000000
86cf5dc8  86cf5d00 86cf5f08 00000000 00000000
86cf5dd8  00000002 00000000 00000000 13030002
86cf5de8  00000000 00000000 86cf5df0 86cf5df0
86cf5df8  00080006 8dc011f8 00000000 00000006
86cf5e08  00000002 00000002 00000004 00000004
86cf5e18  00080050 00000000 00000000 00020004

接下来是86cf5d28,这个应该是我们的OBJECT_TYPE,可以看见是进程对象,完美,并且最下面0x80的链表也对上了.

0: kd> dt _OBJECT_TYPE 86cf5d28
nt!_OBJECT_TYPE+0x000 TypeList         : _LIST_ENTRY [ 0x86cf5d28 - 0x86cf5d28 ]+0x008 Name             : _UNICODE_STRING "Process"+0x010 DefaultObject    : (null) +0x014 Index            : 0x7 ''+0x018 TotalNumberOfObjects : 0x25+0x01c TotalNumberOfHandles : 0xd0+0x020 HighWaterNumberOfObjects : 0x30+0x024 HighWaterNumberOfHandles : 0xf9+0x028 TypeInfo         : _OBJECT_TYPE_INITIALIZER+0x078 TypeLock         : _EX_PUSH_LOCK+0x07c Key              : 0x636f7250+0x080 CallbackList     : _LIST_ENTRY [ 0x996a88c8 - 0x996a88c8 ]

在接着就是98c08150 98c08140这两个我们注册的前后回调,这里没必要演示了,最后就是我们之前分析的v10 = 0,整个结构体结束.

到这里我们就验证了我们结构体的正确性

完结,撒花😀

最后的结果

00000000 _OB_LIST_ENTRY struc ; (sizeof=0x24, mappedto_1307)
00000000                                         ; XREF: _OB_HANDLE/r
00000000 callbackList _LIST_ENTRY ?
00000008 Operation dd ?
0000000C Flag dd ?
00000010 Self dd ?
00000014 Object_Type dd ?
00000018 PreOperation dd ?
0000001C PostOperation dd ?
00000020 UNKNOW1 dd ?
00000024 _OB_LIST_ENTRY ends
00000024
00000000 ; ---------------------------------------------------------------------------
00000000
00000000 _OB_HANDLE struc ; (sizeof=0x34, mappedto_1308)
00000000 Version dw ?
00000002 OperationRegistrationCount dw ?
00000004 RegistrationContext dd ?
00000008 Length1 dw ?
0000000A Length2 dw ?
0000000C buffer dd ?
00000010 CallBackList _OB_LIST_ENTRY ?
00000034 _OB_HANDLE ends
00000034

RegistrationHandle实际的结构就是上面写的 _OB_HANDLE,然后回调链表的实际结构就是 _OB_LIST_ENTRY

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。
如若转载,请注明出处:http://www.pswp.cn/pingmian/85005.shtml
繁体地址,请注明出处:http://hk.pswp.cn/pingmian/85005.shtml
英文地址,请注明出处:http://en.pswp.cn/pingmian/85005.shtml

如若内容造成侵权/违法违规/事实不符,请联系英文站点网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

Nginx-3 Nginx 的负载均衡策略

Nginx-3 Nginx 的负载均衡策略 Nginx 的负载均衡其实就是指将请求按照一定的策略转发给服务集群中的一台&#xff0c;提高了服务集群的可用性&#xff0c;解决数据流量过大、网络负荷过重的问题。 AKF 扩展立方体 分为 3 个方向负载&#xff1a; x 轴&#xff1a;增加实例数…

Wiiu平台RetroArch全能模拟器美化整合包v1.18

这款WiiU平台RetroArch全能模拟器美化整合包v1.18的亮点包括&#xff1a; 1. 18款平台完美兼容&#xff1a;无论你是喜欢NES时代的经典游戏&#xff0c;还是钟爱SNES、GBA等平台的大作&#xff0c;这款整合包都能满足你的需求&#xff0c;让你尽情畅玩游戏。 2. 三款自制主题&a…

MyBatis原理

Mybatis执行过程为&#xff1a;接口代理->sqlSession会话->executor执行器->JDBC操作 一、接口代理 Mybatis根据Mapper接口&#xff0c;动态生成相应实现类 二、SqlSession介绍 MyBatis核心对象SqlSession介绍 - MyBatis中文官网 三、Executor执行器介绍 精通My…

升级内核4.19-脚本

#bash cd /root yum remove -y kernel-tools-3.10.0-1160.el7.x86_64 yum remove -y kernel-tools-libs-3.10.0-1160.el7.x86_64tar -xvf rhel-7-amd64-rpms.tar.gz cd /root/rhel-7-amd64-rpms #安装依赖、包括socat&conntrack yum localinstall -y *.rpm --skip-broken#升…

全面理解 JVM 垃圾回收(GC)机制:原理、流程与实践

JVM 的 GC&#xff08;Garbage Collection&#xff09;机制是 Java 程序性能的关键支柱。本文将从堆内存布局、回收原理、GC 算法、流程细节、并发收集器机制等维度&#xff0c;系统讲清楚 GC 的底层运作原理和优化思路。 一、JVM 堆内存结构 Java 堆是 GC 管理的主要区域&am…

runas命令让其他用户以管理员权限运行程序

RUNAS 用法: RUNAS使用示例&#xff1a; runas /noprofile /user:mymachine\administrator cmd #本机Administrator管理员身份执行CMD&#xff0c;/noprofile为不加载该用户的配置信息。runas /profile /env /user:mydomain\admin “mmc %windir%\system32\dsa.msc” #本机上…

实战指南:部署MinerU多模态文档解析API与Dify深度集成(实现解析PDF/JPG/PNG)

MinerU web api部署 MinerU 能够将包含图片、公式、表格等元素的多模态 PDF、PPT、DOCX 等文档转化为易于分析的 Markdown 格式。 克隆 MinerU 的仓库 git clone https://github.com/opendatalab/MinerU.gitcd 到 projects/web-api cd projects/web-api在可以科学上网的情况下…

向量外积与秩1矩阵的关系

向量外积与秩1矩阵的关系 flyfish 向量外积是构造秩1矩阵的基本工具&#xff0c;其本质是用两组向量的线性组合刻画矩阵的行和列相关性&#xff1b;任意秩1矩阵必可表示为外积&#xff0c;而低秩矩阵&#xff08;秩 k k k&#xff09;可分解为 k k k 个外积矩阵的和&#x…

设计模式-创建型模式(详解)

创建型模式 单例模式 一个类只允许创建一个对象&#xff0c;称为单例。 单例体现&#xff1a;配置类、连接池、全局计数器、id生成器、日志对象。 懒汉式 (线程不安全) 单例&#xff1a;【不可用】 用到该单例对象的时候再创建。但存在很大问题&#xff0c;单线程下这段代…

什么是BI?有哪些应用场景

BI&#xff08;Business Intelligence&#xff0c;商业智能&#xff09;是通过技术手段对海量业务数据进行采集、整合、分析和可视化的过程&#xff0c;旨在帮助企业从数据中获取洞察&#xff0c;支持决策。其核心是通过工具&#xff08;如Quick BI&#xff09;将原始数据转化为…

从零开始:使用Vite和Vue.js搭建一个空项目

进入node.js官网 https://nodejs.org/zh-cn 下载node.js 点击进行安装&#xff0c; 完成之后&#xff0c;按住shift鼠标右键&#xff0c;打开powershell窗口 输入node -v &#xff0c;出现版本号就是成功了 node -v 接下来&#xff0c;打开设置&#xff0c;搜索开发者设置&…

Redis 核心数据类型及典型使用场景详解

在日常开发中&#xff0c;Redis 不仅是缓存利器&#xff0c;更是一套高性能的数据结构服务。你是否真的了解 Redis 提供的五种核心数据类型&#xff1f;它们各自的底层结构和适用场景又有哪些差异&#xff1f;本篇博客将深入解析 Redis 的数据类型及其典型应用&#xff0c;助你…

threejs webVR获取相机正前方向量

通常获取相机正前方可以使用camera.getWorldDirection(new Vector3()) 函数来得到&#xff0c;但是在threejs0.139.2版本中进入VR后使用上面函数获取的数据是固定不变的&#xff0c;不管是否旋转了头盔&#xff0c;经过一番研究发现必须使用renderer.xr.getCamera() 此函数获取…

华为OD-2024年E卷-字符统计及重排[100分] -- python

问题描述&#xff1a; 给出一个仅包含字母的字符串&#xff0c;不包含空格&#xff0c;统计字符串中各个字母(区分大小写)出现的次数&#xff0c;并按照字母出现次数从大到小的顺序输出各个字母及其出现次数。如果次数相同&#xff0c;按照自然顺序进行排序&#xff0c;且小写…

MCP(模型上下文协议)协议和Http协议对比

MCP&#xff08;Model Context Protocol&#xff0c;模型上下文协议&#xff09;和 HTTP&#xff08;HyperText Transfer Protocol&#xff0c;超文本传输协议&#xff09;是两种定位完全不同的协议&#xff0c;主要区别如下&#xff1a; 1. 核心定位 HTTP 通用网络通信协议&am…

C++打印乘法口诀表

int main()​​&#xff1a; 这是C 程序的入口点。每个C 程序都必须有一个 main 函数&#xff0c;程序从这里开始执行。 ​​外层 for 循环​​&#xff1a; for (int i 1; i < 10; i) { int i 1&#xff1a;定义并初始化循环变量 i 为 1。这里的 i 代表乘法表中的行…

RoGBAG 与 MCAP

RoGBAG 和 MCAP 都是机器人领域常用的二进制数据格式&#xff0c;用于存储传感器数据、控制命令和状态信息。两者主要区别在于&#xff1a; RoGBAG&#xff1a;ROS 1/2 的标准日志格式&#xff0c;采用 LZF/LZ4 压缩&#xff0c;适合中小型数据集 MCAP&#xff1a;新一代机器人…

Ubuntu 空间占用情况排查常用命令

查看当前目录总大小及子目录占用详情 du -sh * | sort -hr ​​du​​&#xff1a;磁盘使用统计命令​​-s​​&#xff1a;显示每个参数的总计&#xff08;不递归子目录&#xff09;​​-h​​&#xff1a;以人类可读格式&#xff08;KB/MB/GB&#xff09;显示​​*​​&…

C语言编译优化实战与技巧

一.概述 1.C语言编译优化介绍 C语言编译优化是提升程序性能的核心手段&#xff0c;涉及从源代码到机器码的多层次转换&#xff0c;下面从优化级别、常用技术、内存管理、指令调度等多个维度详细介绍。 2.编译器优化等级&#xff08;GCC/Clang&#xff09; 二.常用优化技术 1…