-
如果在linux下指定在不被4整除的内存地址上写入int型变量会发生什么
2018-04-02 11:13:34总结:(1)linux下内存存储的一般都是低位在前,高位在后。(2)linux默认对齐字节都是按照int型变量占的字节个数对齐,... 假设指定一内存地址:0x0001603a (注意此数不能被4整除) 0x0001603a=90170, 90170/4=22...总结:(1)linux下内存存储的一般都是低位在前,高位在后。
(2)linux默认对齐字节都是按照int型变量占的字节个数对齐,32系统int型占4个字节,所以此系统默认对齐是4个字节。
(3)如果指定在不被4整除的内存地址上存放int型变量,会按照下面图逆序和拼接产生数据。
假设指定一内存地址:0x0001603a (注意此数不能被4整除)
0x0001603a=90170, 90170/4=22542.5
那么如果指定在0x0001603a位置上存放一个结构体 此结构体是3个int型的变量组成
如:
typedef struct
{
UINT32 is_available; /* whether blcok is avaiable */
UINT32 prior_blocksize; /* size of prior block */
}mem_control_block;
mem_control_block * mcb = NULL;
mcb = (mem_control_block *)0x0001603a;
mcb->is_available = 0x01020304;
mcb->prior_blocksize = 0x05060708;
printf("***************************mcb->is_available=0x%8x*******************\n",mcb->is_available);
printf("***************************mcb->prior_blocksize=0x%8x*******************\n",mcb->prior_blocksize);
CHAR *p=(CHAR *)malloc_addr;
printf("*(p-1)=0x%2x*******0x%8x************\n",*(p-1),(p-1));
printf("*(p-2)=0x%2x********0x%8x***********\n",*(p-2),(p-2));
printf("*(p-3)=0x%2x*******0x%8x************\n",*(p-3),(p-3));
printf("*(p-4)=0x%2x*******0x%8x************\n",*(p-4),(p-4));
printf("*(p)=0x%2x********0x%8x***********\n",*p,p);
printf("*(p+1)=0x%2x*******0x%8x************\n",*(p+1),(p+1));
printf("*(p+2)=0x%2x******0x%8x*************\n",*(p+2),(p+2));
printf("*(p+3)=0x%2x******0x%8x*************\n",*(p+3),(p+3));
printf("*(p+4)=0x%2x*******0x%8x************\n",*(p+4),(p+4));
printf("*(p+5)=0x%2x*******0x%8x************\n",*(p+5),(p+5));
printf("*(p+6)=0x%2x*******0x%8x************\n",*(p+6),(p+6));
printf("*(p+7)=0x%2x*******0x%8x************\n",*(p+7),(p+7));
printf("*(p+8)=0x%2x*******0x%8x************\n",*(p+8),(p+8));
printf("***************************mcb->is_available=0x%8x*******************\n",mcb->is_available);
printf("***************************mcb->prior_blocksize=0x%8x*******************\n",mcb->prior_blocksize);
打印结果会是什么呢? 出乎意料
*(p-1)=0x 3*******0x 16039************
*(p-2)=0x 4********0x 16038***********
*(p-3)=0x 0*******0x 16037************
*(p-4)=0x 0*******0x 16036************
*(p)=0x 2********0x 1603a***********
*(p+1)=0x 1*******0x 1603b************
*(p+2)=0x 8******0x 1603c*************
*(p+3)=0x 7******0x 1603d*************
*(p+4)=0x 6*******0x 1603e************
*(p+5)=0x 5*******0x 1603f************
*(p+6)=0x 0*******0x 16040************
*(p+7)=0x 0*******0x 16041************
*(p+8)=0x 0*******0x 16042************
***************************mcb->is_available=0x 3040102*******************
***************************mcb->prior_blocksize=0x 7080506*******************
你会发现设置的mcb->is_available值mcb->is_available = 0x01020304; mcb->prior_blocksize = 0x05060708;
,跟读取处理的值mcb->is_available=0x 3040102,mcb->prior_blocksize=0x 7080506都不一样,为什么?
主要原因就是因为没在被4整除的内存地址上写。如下图:产生的结果会是16038和16039两个地址的变量高低位变化下就是 0304,而1603a 1603b再颠倒下是0102,再拼接下就是03040102;
实际存放的是低位在前高位在后,如果指向不是第一个被4整除的地址放int变量,需要拼接,比如此图:
指向第3个则,第 1 第2个颠倒一下,然后第3 第4颠倒一下,再拼接,0x03040102;
就这样产生这样的错误了。
切记:在内存中是低位在先,高位在后。
同理:如果指定在1603b地址上开始写入这个结构体,又会发生什么
结果如下:
*(p-1)=0x 2*******0x 1603a************
*(p-2)=0x 3********0x 16039***********
*(p-3)=0x 4*******0x 16038************
*(p-4)=0x 0*******0x 16037************
*(p)=0x 1********0x 1603b***********
*(p+1)=0x 8*******0x 1603c************
*(p+2)=0x 7******0x 1603d*************
*(p+3)=0x 6******0x 1603e*************
*(p+4)=0x 5*******0x 1603f************
*(p+5)=0x 0*******0x 16040************
*(p+6)=0x 0*******0x 16041************
*(p+7)=0x 0*******0x 16042************
*(p+8)=0x 0*******0x 16043************
***************************mcb->is_available=0x 2030401*******************
***************************mcb->prior_blocksize=0x 6070805*******************
同理如图:
实际存放的是低位在前高位在后,如果指向不是第一个被4整除的地址放int变量,需要拼接,比如此图:
指向第4个则,第 1 第2个第3个颠倒一下,然后与第4拼接,0x02030401
上述提到的问题如何避免,可以强制一字节对齐,解决了上述问题。这样就会实际存放的和我想存放的位置是一样的。
第一种方法:举例此地址0x0001603a(不能被4整除)放入int变量
直接强制1字节对齐,不管外部,则想存放位置和实际存放的位置是一样的。
(注意A:强制4字节对齐#pragma pack(4)没有作用,因为系统本来就是默认4字节对齐
B:强制2字节对齐#pragma pack(2)有作用,但是只针对偶数地址0x0001603a有作用,但是如果对0x0001603b奇数指定地址写int类型也是不行的。
总结:指定的内存地址一定是能被对齐字节整除才可以。
)
#pragma pack(1)
typedef struct
{
UINT32 is_available; /* whether blcok is avaiable */
UINT32 prior_blocksize; /* size of prior block */
UINT32 current_blocksize; /* block size */
}mem_control_block;
#pragma pack()
第二种方法: 放在被4 整除的地址上,
测试用例test_ydt_mallocandfree:----自实现malloc函数初始化空间首地址---=0x0001603c
*(p-1)=0x 0*******0x 1603b************
*(p-2)=0x 0********0x 1603a***********
*(p-3)=0x 0*******0x 16039************
*(p-4)=0x 0*******0x 16038************
*(p)=0x 4********0x 1603c***********
*(p+1)=0x 3*******0x 1603d************
*(p+2)=0x 2******0x 1603e*************
*(p+3)=0x 1******0x 1603f*************
*(p+4)=0x 8*******0x 16040************
*(p+5)=0x 7*******0x 16041************
*(p+6)=0x 6*******0x 16042************
*(p+7)=0x 5*******0x 16043************
*(p+8)=0x 0*******0x 16044************
***************************mcb->is_available=0x 1020304*******************
***************************mcb->prior_blocksize=0x 5060708*******************
-
内存不能“read”
2017-11-26 21:17:00内存不能被读取和写入,我想大家一定见过这种类似的问题。最近公司一直出现这种问题: 而且有的时候桌面上的任何图标都是显示不完整,从事件查看器中出现很多莫名的提示错误。这个真的很头疼,为了弄清事件的来源...内存不能“read,writen”内存不能被读取和写入,我想大家一定见过这种类似的问题。最近公司一直出现这种问题:
而且有的时候桌面上的任何图标都是显示不完整,从事件查看器中出现很多莫名的提示错误。这个真的很头疼,为了弄清事件的来源(是否是系统出现问题)每一次都是重新启动。每次重启完成之后,系统就正常啦。但这也不能解决实际问题啊。问题好像就是系统在启动的时候,有些文件没有加载上或者是加载并不完整。
下面我希望和大家分享一下我的所得,有更好的方法大家可以一起去探讨啊!一般情况下出现这种问题有两根方面的原因:一是.硬件即内存方面的问题;二是软件,这个方面可就多啦。电脑硬件一般不是特别爱出现问题,可能的情况是内存条坏啦,存在质量问题;内存条和插槽接触不是很好,还有就是兼容性的问题。当然我们可以下载一个软件来检测内存的使用情况。现在就来说说软件的问题吧,原理大致是这样的,当系统中的某个程序把数据放置内存中的缓存区域的时候,程序需要操作系统提供的“功能函数”来为其分配内存空间。如果分配成功的话,函数将会把新开辟的内存地址返回给应用程序,应用程序就可以根据这个地址来使用这个内存空间。这就是“动态内存分配”。出现错误的原因就是内存并不是永远能够分配成功的。当分配失败的时候系统函数将会返回一个0值。而程序应用程序在每一次申请内存后都会检查返回值是否为“0”。如果是的话那就好说啦,应用程序一般都会采取一些自救的措施。如果应用程序没有检查这个错误呢,它就是认为这个“0”地址就是它将要使用的内存空间地址。实际上真正的“0”地址内存区域存储的是计算机系统中重要的“中断描述符表”,绝对不能够被应用程序占用。在windows操作系统中这个操作会被系统自我保护机制所捕获,结果就是这个应用程序将会被强行关闭。这个时候就会出现像上面的情况一样:内存不能被“read”,并指出被引用的内存地址不能为“0x000xxxxx”的错误。内存分配失败的原因很多:内存不足,系统中的函数出现错误等。这种情况多出现于系统已经使用很长时间,安装的很多应用软件,结果造成修给了大量的系统参数等。在动态分配应用程序的过程中,有可能会出现这样的情况:应用程序被分配的内存地址已经消失,程序本身在某个时间“注销”了这个内存空间。当这个内存空间被系统收回的时候,这个内存空间的访问权已经不再属于该应用程序,所以这个程序的读写操作同样会被系统终止掉!
基本上面是我自己的理解,下面是我个人的一些建议:首先先保证自己的系统是一个比较干净的系统,查杀系统中的病毒,让系统检查被攻击的可能。平时的使用中,对系统修复一些漏洞。使用一些稳定版的应用软件。还有就是一种:先停止掉“windows management instrumentation”这个服务,在运行里面输入“services.msc"打开服务管理界面--将这个服务停止。然后进到这个文件夹中:c:\windows\system32\Wbem\Repository 删除里面的所有文件(要是自己是在没有把握的话,可以先对这些文件做好备份),然后启动刚才停止的服务或者重新启动电脑也可以。被删除的文件会在注册表中的 HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\WBEM\CIMOM\Autorecover MOFs 这个信息重新创建出来。
本文转自 位鹏飞 51CTO博客,原文链接:http://blog.51cto.com/weipengfei/390461,如需转载请自行联系原作者
-
“内存不能为read/written”是什么原理?
2020-12-29 09:27:07翻译错误,原文:This memory cannot be read/written,应当翻译为内存不能被读取/写入 内存有个存放数据的地方叫缓冲区。 当程序把数据放在缓冲区,需要操作系统提供的“功能函数”来申请,如果内存分配成功,函数...“内存不能为read/written”是什么原理:
翻译错误,原文:
This memory cannot be read/written
,应当翻译为内存不能被读取/写入
内存有个存放数据的地方叫缓冲区。
当程序把数据放在缓冲区,需要操作系统提供的“功能函数”来申请,如果内存分配成功,函数就会将所新开辟的内存区地址返回给应用程序,应用程序就可以通过这个地址使用这块内存。这就是“动态内存分配”,内存地址也就是编程中的“光标”。内存不是永远都招之即来、用之不尽的,有时候内存分配也会失败。
当分配失败时系统函数会返回一个0值,这时返回值“0”已不表示新启用的光标,而是系统向应用程序发出的一个通知,告知出现了错误。作为应用程序,在每一次申请内存后都应该检查返回值是否为0,如果是,则意味着出现了故障,应该采取一些措施挽救,这就增强了程序的“健壮性”。
若应用程序没有检查这个错误,它就会按照“思维惯性”认为这个值是给它分配的可用光标,继续在之后的执行中使用这块内存。真正的“0”地址内存区储存的是计算机系统中最重要的“中断描述符表”,绝对不允许应用程序使用。在没有保护机制的操作系统下(如DOS),写数据到这个地址会导致立即当机,而在健壮的操作系统中,如Windows等,这个操作会马上被系统的保护机制捕获,其结果就是由操作系统强行关闭出错的应用程序,以防止其错误扩大。
这时候,就会出现上述的内存不能为“read”错误,并指出被引用的内存地址为“0x00000000“。内存分配失败故障的原因很多,内存不够、系统函数的版本不匹配等都可能有影响。因此,这种分配失败多见于操作系统使用很长时间后,安装了多种应用程序(包括无意中“安装”的病毒程序),更改了大量的系统参数和系统档案之后。
在使用动态分配的应用程序中,有时会有这样的情况出现:程序试图读写一块“应该可用”的内存,但不知为什么,这个预料中可用的光标已经失效了。有可能是“忘记了”向操作系统要求分配,也可能是程序自己在某个时候已经注销了这块内存而“没有留意”等等。注销了的内存被系统回收,其访问权已经不属于该应用程序,因此读写操作也同样会触发系统的保护机制,企图“违法”的程序唯一的下场就是被操作终止执行,回收全部资源。计算机世界的法律还是要比人类有效和严厉得多啊!像这样的情况都属于程序自身的BUG,你往往可在特定的操作顺序下重现错误。
-
内存
2020-08-01 20:35:30内存的物理结构 内存的内部是由各种ic电路组成的,种类庞大。主要分为三种储存器: 随机储存器(RAM)重要...当cpu向内存中写入数据时,这些数据也会被写入高速缓存中。当cpu读取数据时,会直接从高速缓存器中读取,如内存的物理结构
内存的内部是由各种ic电路组成的,种类庞大。主要分为三种储存器:
- 随机储存器(RAM)重要的一种,表示既可以从中读取数据也可以写入数据,当容器关闭时,内存中的信息会丢失。
- 只读储存器:ROM一般只能用于数据的读取,不能写入数据,但是当机器停电时,这些数据不会丢失。
- 高速缓存:Cache分为一级缓存、二级缓存、三级缓存这些数据时,它位于内存和cpu之间,是一个读写速度比内存更快的储存器。当cpu向内存中写入数据时,这些数据也会被写入高速缓存中。当cpu读取数据时,会直接从高速缓存器中读取,如果Cache中没有,cpu回再去读取内存中的数据。
内存ic
内部有:电源、地址信号、数据信号、控制信号和用于寻址的ic引脚来进行数据的读写。
首先给VCC接通+5v的电源,给GND接通0v的电源,使用A0-A9来指定数据的储存场所,然后再把数据的值输入给D0-D7的数据信号,并把WR的值置位1,执行完这些操作后,即可以向内存中ic中写入数据。
读出数据时,只需要通过A0-A9的地址信号指定数据的储存场所,然后将RD的值置位1即可。
RD和WR称为控制信号。其中当WR和RD都为0时,无法进行写入和读写操作。
程序中的数据不仅只有数值,还有数据类型的概念,即使物理上强制以1个字节位单位来逐一读写数据的内存,在程序中通过指定数据数据类型,也能实现以特定字节数为单位进行读写。
低字节序列:将数据地位储存在内存地位地址
高字节序列:将数据的高位储存在内存地位的方式称为高字节序列
-
解决python写入文件数据不全的问题
2019-05-15 17:08:35如果利用语句 f = open('test.txt','a') 向txt文件中写入内容时,...将缓存在内存中的内容全部写入到文件中 且能对test.txt文件进行删除等其他操作, 不必担心是否文件被程序占用的问题。 另一个解决方案就... -
写入到段合并&对写入的优化
2019-07-17 17:27:49但是此时并不能搜索到。每当内存buffer满了或者达到refreshi interval 时,向操作系统的文件缓存中写入一个段,此时文档就可以被检索到。但是这时候文档并没有持久化到磁盘,只是写入了操作系统的文件缓存中【操作... -
python垃圾回收 采用方式_python 写入文本Python内存管理方式和垃圾回收算法解析...
2020-12-03 13:19:50有 __del__ 方法的实例会以健全的方式被处理。给新类型添加GC支持是很容易的。支持GC的Python与常规的Python是二进制兼容的。分代式回收能运行工作(目前是三个分代)。由 pybench 实测的结果是大约有百分之四的开销。... -
汇编怎么从内存地址写入连续的数字_计算机自制操作系统(四):汇编语言烧脑的ORG问题...
2020-11-21 11:52:26有读者给我留言,由于MBR程序是被加载到7C00H之处的,...这里我先给出结论,再来大篇幅的解释原理:是因为我已经熟知ORG的工作原理,所以不需要在程序里明文出现ORG的伪指令,也同样能达到程序成功运行和数据成功... -
Java-内存溢出和内存泄露
2020-09-22 11:37:47内存泄漏:程序用new申请了一块内存,后来很长时间都不再使用了(按理应该释放),但是因为一直被某个或某些实例所持有导致 GC 不能回收,也就是该被释放的对象没有释放。 内存溢出 java.lang.OutOfMemoryError,是... -
关于内存泄漏和内存溢出
2020-07-20 18:23:482、内存泄漏:你用新建一个对象,申请了一块内存,后来这个对象长时间都不再使用了(按理应该释放),但是因为一直被某个或某些实例所持有导致 GC 不能回收,也就是该被释放的对象没有释放。 下面具体介绍。 1.1 ... -
ElasticSearch数据写入流程
2020-12-25 01:06:05此时数据还不能被search到,通过refresh操作将JVM堆内存中的indexing buffer中的新数据刷到OS的filesystem cache上面,这时在filesystem上的数据会形成segment(基于lucence的倒排索引文件)可以被search到。... -
关于内存泄漏与内存溢出
2018-08-04 11:38:152、内存泄漏:你用new申请了一块内存,后来很长时间都不再使用了(按理应该释放),但是因为一直被某个或某些实例所持有导致 GC 不能回收,也就是该被释放的对象没有释放。 2.1出现内存泄漏与内存溢出的原因? 为... -
Mysql日志-写入机制
2020-04-21 16:22:04一个事务的binlog是不能被拆开的,因此不论这个事务多大,也要确保一次性写入。 系统给binlog cache分配了一片内存,每个线程一个,参数 binlog_cache_size用于控制单个线程内binlog cache所占内存的大小。如果超过... -
内存技术资料大全
2020-11-18 11:04:50一、基本知识 内存是内部存储器的简称,它用来存放当前要用的数据,其存取速度快,但存储容量小,通常CPU的操作都需要经过内存,从内存中取程序和数据,计算...这些信息只能读出,不能写入,而且即使机器掉电,数据 -
java内存泄露
2019-11-12 14:58:582,内存泄漏:你用new申请了一块内存,后来很长时间都不再使用了(按理应该释放),但是因为一直被某个或某些实例所持有导致 GC 不能回收,也就是该被释放的对象没有释放。 二,内存溢出 java.lang.... -
解决在linux服务器上部署定时自动查找cpu,内存,磁盘使用量,并将查询结果写入数据库的脚本,只能手动运行...
2019-05-25 10:14:17问题描述:将脚本名命名为mortior.sh(以下简称mo),手动执行脚本后查询数据库,表中有相应的信息,放入自动...当设置为自动执行时,位于linux下,在当前路径下PASH中没有操作命令解析文件的路径,所以不能实现添加... -
书生笔记-binlog 的写入机制
2020-11-17 17:21:42一个事务的 binlog 是不能被拆开的,因此不论这个事务多大,也要确保一次性写入。这就涉及到了 binlog cache 的保存问题。 系统给 binlog cache 分配了一片内存,每个线程一个,参数 binlog_cache_size 用于控制... -
Java内存泄漏
2019-06-12 19:02:352、内存泄漏:你用new申请了一块内存,后来很长时间都不再使用了(按理应该释放),但是因为一直被某个或某些实例所持有导致 GC 不能回收,也就是该被释放的对象没有释放。 下面具体介绍。 1.1 内存溢出 java.lang... -
内存知识集
2019-09-20 22:14:43RAM与ROMRAM(Random Access Memory)的全名为随机存取记忆体(可称作系统内存),不过,当电源关闭时RAM不能保留数据,如果需要保存数据,就必须把它们写入到一个长期的存储器中(例如硬盘),RAM内存可以进一步分为静态RAM... -
Java内存泄漏的排查总结
2018-06-23 10:52:082、内存泄漏:你用new申请了一块内存,后来很长时间都不再使用了(按理应该释放),但是因为一直被某个或某些实例所持有导致 GC 不能回收,也就是该被释放的对象没有释放。下面具体介绍。1.1 内存溢出java.lang.... -
Java内存泄漏排查
2019-01-03 16:34:58内存泄漏:new申请了一块内存,后来很长时间都不再使用了(按理应该释放),但是因为一直被某个或某些实例所持有导致 GC 不能回收,也就是该被释放的对象没有释放。 1.1内存溢出(Out Of Memory) 产生该错误的原因... -
python内存管理_Python内存管理(一):预备知识
2020-12-07 11:23:51由于他们之间不能彼此覆盖,他们在开始写入之前一定要向这本书的管理者申请,由管理者来决定他们写入到哪里。由于这本书会存在很长的时间,书中的很多故事可能已经不再有意义。当没有人读或者引用故事,这些无意义的... -
final 的内存语义
2018-10-08 09:44:00写内存语义:在构造函数内对一个 final 域的写入,与随后将对象引用赋值给引用变量,这两个操作不能重排序。 读内存语义:初次读一个包含 final 域的对象的引用,与随后初次读这个 final 域,这两个操作不能重排序。... -
redis内存淘汰策略
2020-07-16 10:29:11① noeviction:禁止驱逐数据,当内存不足时,新写入操作就会报错,请求可以继续进行,线上任务也不能持续进行,可以保证数据不被丢失,这是系统默认的一种淘汰策略。 ② allkeys-lru:从所有的数据中根据LRU(最近... -
java内存泄露排查总结
2021-01-18 11:14:24内存泄露:你用new申请了一块内存,后来很长时间都不使用了,但是因为一直被某个或者某些实例所持有导致GC不能回收掉,也就是该释放的对象没有释放,则出现泄露。 1.1 内存溢出 java.lang.OutOfMemoryError:是指... -
SXSSFWorkbook写入大量数据的两点处理
2018-08-05 18:03:04初用SXSSFWorkbook,感觉以读写大量数据为买点...这个参数,会指定一个sheet可读取的row数目,超过该数目的row,会被写入到磁盘文件中,进而不能在通过getRow访问到,通过这种方式,内存使用空间就缩小很多了。 需... -
MySQL---binlog和redo log的写入机制
2021-01-27 10:43:20一个事务的 binlog 是不能被拆开的,因此不论这个事务多大,也要确保一次性写入。这就涉及到了 binlog cache 的保存问题。系统给 binlog cache 分配了一片内存,每个线程一个,参数 binlog_cache_size 用于控制单个... -
Java内存模型之final的内存语义和happens-before
2019-01-26 22:36:311)在构造函数内对一个final域的写入,与随后把这个被构造对象的引用赋值给一个引用变量,这两个操作之间不能重排序。(即对构造函数的写入要在这个对象被其他线程看到之后) 2)初次读一个包含final域的对象的引用,... -
dotnet 双缓存数据结构设计 下载库的文件写入缓存框架
2020-09-22 00:49:28但是文件写入只能支持单线程,我不想让网络下载需要等待磁盘写入,因此我需要先在内存做缓存,然后让磁盘写入。配合 DirectX 渲染的设计方法,采用双缓存数据结构设计,也就是有两个集合,其中一个集合用来被其他...
-
QT实现基于TCP连接的聊天室
-
100道Java高频面试题(阿里面试官整理)
-
物联网之mqtt实现(emqx+springboot+mqtt附源码)
-
log宏与条件式编译
-
一天学完MySQL数据库
-
切片与MapTask并行度决定机制
-
PTA 基础编程题目集 7-38 数列求和-加强版 (20 分)
-
基于采用级联调制器的光电振荡器的自振荡光学频率梳状发生器
-
支付宝架构师眼里的高可用与容灾架构演进
-
P1005 [NOIP2007 提高组] 矩阵取数游戏(区间dp+__int128)
-
Samba 服务配置与管理
-
关于绝热演化的一般模型
-
全局绝热搜索算法的电路模型
-
FastDFS 分布式文件系统部署
-
pcl交互式ICP
-
java - 适配者模式
-
自适应极限学习机
-
Vue中的动画封装(5-7)
-
全光子晶体光纤单级直接放大产生34 W高功率飞秒脉冲
-
Java并发之CompletionService详解