信息摘要算法之二:SHA1算法分析及实现

SHA算法,即安全散列算法(Secure Hash Algorithm)是一种与MD5同源的数据加密算法,该算法经过加密专家多年来的发展和改进已日益完善,现在已成为公认的最安全的散列算法之一,并被广泛使用。

1、概述

SHA算法能计算出一个数位信息所对应到的,长度固定的字串,又称信息摘要。而且如果输入信息有任何的不同,输出的对应摘要不同的机率非常高。因此SHA算法也是FIPS所认证的五种安全杂凑算法之一。原因有两点:一是由信息摘要反推原输入信息,从计算理论上来说是极为困难的;二是,想要找到两组不同的输入信息发生信息摘要碰撞的几率,从计算理论上来说是非常小的。任何对输入信息的变动,都有很高的几率导致的信息摘要大相径庭。

SHA实际上是一系列算法的统称,分别包括:SHA-1、SHA-224、SHA-256、SHA-384以及SHA-512。后面4中统称为SHA-2,事实上SHA-224是SHA-256的缩减版,SHA-384是SHA-512的缩减版。各中SHA算法的数据比较如下表,其中的长度单位均为位:

类别

SHA-1

SHA-224

SHA-256

SHA-384

SHA-512

消息摘要长度

160

224

256

384

512

消息长度

小于位

小于位

小于位

小于位

小于位

分组长度

512

512

512

1024

1024

计算字长度

32

32

32

64

64

计算步骤数

80

64

64

80

80

SHA-1在许多安全协定中广为使用,包括TLS和SSL、PGP、SSH、S/MIME和IPsec,曾被视为是MD5的后继者。SHA1主要适用于数字签名标准(Digital Signature Standard DSS)里面定义的数字签名算法(Digital Signature Algorithm DSA)。对于长度小于264位的消息,SHA1会产生一个160位的消息摘要。

2、基本原理

前面我们简单的介绍了SHA算法族,接下来我们以SHA-1为例来分析其基本原理。SHA-1是一种数据加密算法,该算法的思想是接收一段明文,然后以一种不可逆的方式将它转换成一段密文,也可以简单的理解为输入一串二进制码,并把它们转化为长度较短、位数固定的输出序列即散列值,也称为信息摘要或信息认证代码的过程。

SHA-1算法输入报文的最大长度不超过264位,产生的输出是一个160位的报文摘要。输入是按512 位的分组进行处理的。SHA-1是不可逆的、防冲突,并具有良好的雪崩效应。

一般来说SHA-1算法包括有如下的处理过程:

2.1、对输入信息进行处理

既然SHA-1算法是对给定的信息进行处理得到相应的摘要,那么首先需要按算法的要求对信息进行处理。那么如何处理呢?对输入的信息按512位进行分组并进行填充。如何填充信息报文呢?其实即使填充报文后使其按512进行分组后,最后正好余448位。那填充什么内容呢?就是先在报文后面加一个1,再加很多个0,直到长度满足对512取模结果为448。到这里可能有人会奇怪,为什么非得是448呢?这是因为在最后会附加上一个64位的报文长度信息,而448+64正好是512。

2.2、填充长度信息

前面已经说过了,最后会补充信息报文使其按512位分组后余448位,剩下的64位就是用来填写报文的长度信息的。至次可能大家也明白了前面说过的报文长度不能超过264位了。填充长度值时要注意必须是低位字节优先。

2.3、信息分组处理

经过添加位数处理的明文,其长度正好为512位的整数倍,然后按512位的长度进行分组,可以得到一定数量的明文分组,我们用Y0,Y1,……YN-1表示这些明文分组。对于每一个明文分组,都要重复反复的处理,这些与MD5都是相同的。

而对于每个512位的明文分组,SHA1将其再分成16份更小的明文分组,称为子明文分组,每个子明文分组为32位,我们且使用M[t](t= 0, 1,……15)来表示这16个子明文分组。然后需要将这16个子明文分组扩充到80个子明文分组,我们将其记为W[t](t= 0, 1,……79),扩充的具体方法是:当0≤t≤15时,Wt = Mt;当16≤t≤79时,Wt = ( Wt-3 ⊕ Wt-8⊕ Wt-14⊕ Wt-16) <<< 1,从而得到80个子明文分组。

2.4、初始化缓存

所谓初始化缓存就是为链接变量赋初值。前面我们实现MD5算法时,说过由于摘要是128位,以32位为计算单位,所以需要4个链接变量。同样SHA-1采用160位的信息摘要,也以32位为计算长度,就需要5个链接变量。我们记为A、B、C、D、E。其初始赋值分别为:A = 0x67452301、B = 0xEFCDAB89、C = 0x98BADCFE、D = 0x10325476、E = 0xC3D2E1F0。

如果我们对比前面说过的MD5算法就会发现,前4个链接变量的初始值是一样的,因为它们本来就是同源的。

2.5、计算信息摘要

经过前面的准备,接下来就是计算信息摘要了。SHA1有4轮运算,每一轮包括20个步骤,一共80步,最终产生160位的信息摘要,这160位的摘要存放在5个32位的链接变量中。

在SHA1的4论运算中,虽然进行的就具体操作函数不同,但逻辑过程却是一致的。首先,定义5个变量,假设为H0、H1、H2、H3、H4,对其分别进行如下操作:

(A)、将A左移5为与函数的结果求和,再与对应的子明文分组、E以及计算常数求和后的结果赋予H0。

(B)、将A的值赋予H1。

(C)、将B左移30位,并赋予H2。

(D)、将C的值赋予H3。

(E)、将D的值赋予H4。

(F)、最后将H0、H1、H2、H3、H4的值分别赋予A、B、C、D

这一过程表示如下:

 

而在4轮80步的计算中使用到的函数和固定常熟如下表所示:

计算轮次

计算的步数

计算函数

计算常数

第一轮

0≤t≤19步

ft(B,C,D)=(B&C)|(~B&D)

Kt=0x5A827999

第二轮

20≤t≤39步

ft(B,C,D)=B⊕C⊕D

Kt=0x6ED9EBA1

第三轮

40≤t≤59步

ft(B,C,D)=(B&C)|(B&D)|(C&D)

Kt=0x8F188CDC

第四轮

60≤t≤79步

ft(B,C,D)=B⊕C⊕D

Kt=0xCA62C1D6

经过4论80步计算后得到的结果,再与各链接变量的初始值求和,就得到了我们最终的信息摘要。而对于有多个铭文分组的,则将前面所得到的结果作为初始值进行下一明文分组的计算,最终计算全部的明文分组就得到了最终的结果。

3、软件实现

经过前面的分析过程,接下来要具体实现SHA1算法其实已经很简单了!下面来一步步实现它,首先实现初始化函数:

/*  SHA1Reset函数用于初始化SHA1的内容值                          */
/*  参数:context,SHA的内容值,存储计算结果既初始值,输入输出      */
/*  返回值:SHA错误代码                                            */
ErrorCode SHA1Reset(SHA1Context *context)
{if (!context){return shaNull;}context->Length_Low             = 0;context->Length_High            = 0;context->Message_Block_Index    = 0;context->Intermediate_Hash[0]   = 0x67452301;context->Intermediate_Hash[1]   = 0xEFCDAB89;context->Intermediate_Hash[2]   = 0x98BADCFE;context->Intermediate_Hash[3]   = 0x10325476;context->Intermediate_Hash[4]   = 0xC3D2E1F0;context->Computed   = 0;context->Corrupted  = 0;return shaSuccess;
}

接下来实现明文信息的读取及处理函数,该函数读取信息分组,并输入前次计算的结果,对除最后一组信息外的全部分组进行信息摘要计算。

/*  SHA1Input函数,将分组的信息读入并进行摘要计算               */
/*  参数:                                                       */
/*      context,SHA的内容值,存储计算结果既初始值,输入输出     */
/*      message_array,待处理的信息分组的字节数组,输入参数       */
/*      length,message_array数组中信息的长度                      */
/*  返回值:SHA错误代码                                         */
ErrorCode SHA1Input(SHA1Context *context,const uint8_t *message_array,unsigned length)
{if (!length){return shaSuccess;}if (!context || !message_array){return shaNull;}if (context->Computed){context->Corrupted = shaStateError;return shaStateError;}if (context->Corrupted){return (ErrorCode)context->Corrupted;}while(length-- && !context->Corrupted){context->Message_Block[context->Message_Block_Index++] =(*message_array & 0xFF);context->Length_Low += 8;if (context->Length_Low == 0){context->Length_High++;if (context->Length_High == 0){/* 消息长度超过限值 */context->Corrupted = 1;}}if (context->Message_Block_Index == 64){SHA1ProcessMessageBlock(context);}message_array++;}return shaSuccess;
}

然后实现结果输出函数,该函数对信息的最后部分进行处理并返回160位的信息摘要到Message_Digest数组,该数组作为参数有调用者输入。第一个元素存第一个字节,依次20个字节。

/*  SHA1Result函数,对信息的最后部分进行处理并输出最终计算结果 */
/*  参数:                                                     */
/*      context,SHA的内容值,存储计算结果既初始值,输入输出   */
/*      Message_Digest,信息摘要的返回值,输出参数              */
/*  返回值:SHA错误代码                                       */
ErrorCode SHA1Result( SHA1Context *context,uint8_t Message_Digest[SHA1HashSize])
{int i;if (!context || !Message_Digest){return shaNull;}if (context->Corrupted){return (ErrorCode)context->Corrupted;}if (!context->Computed){SHA1PadMessage(context);for(i=0; i<64; ++i){/* 处理完毕,清除消息分组 */context->Message_Block[i] = 0;}context->Length_Low = 0;    /* 清除长度数据 */context->Length_High = 0;context->Computed = 1;}for(i = 0; i < SHA1HashSize; ++i){Message_Digest[i] = context->Intermediate_Hash[i>>2]>>8*(3-(i&0x03));}return shaSuccess;
}

还需要实现消息分组的处理函数。该函数处理存储于Message_Block数组中的,待处理的512位的明文分组,将其处理为80个子明文分组,并进行4轮80步运算,返回相应的摘要值。

/*  SHA1ProcessMessageBlock函数,处理消息分组                    */
/*  描述:                                                       */
/*  参数:                                                       */
/*      context,SHA的内容值,存储计算结果既初始值,输入输出     */
/*  返回值:无                                                   */
static void SHA1ProcessMessageBlock(SHA1Context *context)
{/* SHA-1计算中用到的常数定义 */const uint32_t K[]={0x5A827999,0x6ED9EBA1,0x8F1BBCDC,0xCA62C1D6};int t;                         /* 循环变量 */uint32_t temp;                /* 临时变量,存计算值 */uint32_t W[80];               /* 子明文分组数组 */uint32_t A, B, C, D, E;       /* 初始值缓存变量 *//* 初始化子明文分组W的前16个字 */for(t = 0; t < 16; t++){W[t] = context->Message_Block[t * 4] << 24;W[t] |= context->Message_Block[t * 4 + 1] << 16;W[t] |= context->Message_Block[t * 4 + 2] << 8;W[t] |= context->Message_Block[t * 4 + 3];}for(t = 16; t < 80; t++){W[t] = SHA1CircularShift(1,W[t-3] ^ W[t-8] ^ W[t-14] ^ W[t-16]);}A = context->Intermediate_Hash[0];B = context->Intermediate_Hash[1];C = context->Intermediate_Hash[2];D = context->Intermediate_Hash[3];E = context->Intermediate_Hash[4];/*第1轮20步计算*/for(t = 0; t < 20; t++){temp =  SHA1CircularShift(5,A)+((B & C) | ((~B) & D)) + E + W[t] + K[0];E = D;D = C;C = SHA1CircularShift(30,B);B = A;A = temp;}/*第2轮20步计算*/for(t = 20; t < 40; t++){temp = SHA1CircularShift(5,A) + (B ^ C ^ D) + E + W[t] + K[1];E = D;D = C;C = SHA1CircularShift(30,B);B = A;A = temp;}/*第3轮20步计算*/for(t = 40; t < 60; t++){temp = SHA1CircularShift(5,A)+((B & C) | (B & D) | (C & D)) + E + W[t] + K[2];E = D;D = C;C = SHA1CircularShift(30,B);B = A;A = temp;}/*第4轮20步计算*/for(t = 60; t < 80; t++){temp = SHA1CircularShift(5,A) + (B ^ C ^ D) + E + W[t] + K[3];E = D;D = C;C = SHA1CircularShift(30,B);B = A;A = temp;}context->Intermediate_Hash[0] += A;context->Intermediate_Hash[1] += B;context->Intermediate_Hash[2] += C;context->Intermediate_Hash[3] += D;context->Intermediate_Hash[4] += E;context->Message_Block_Index = 0;
}

还需要实现一个对消息进行补位和追加消息长度并进行处理的函数。根据标准,消息必须被填充到一个剩至512位。第一个填充位必须是'1'。最后64位表示原始消息的长度。中间的所有位都应该是0。该函数将根据这些规则填充消息,并相应地填充Message_Block数组。

/*  SHA1PadMessage函数,补全消息,并添加长度,计算最终结果      */
/*  参数:                                                       */
/*      context,SHA的内容值,存储计算结果既初始值,输入输出     */
/*  返回值:无                                                   */
static void SHA1PadMessage(SHA1Context *context)
{/* 检查当前的消息分组,如果小于等于55个字节,则直接添加补位和长度信息。否则,如果大于55个字节,我们填充块到512位,并处理它,然后继续填充到第二个块中直道448位,最后填写长度信息。*/if (context->Message_Block_Index > 55){context->Message_Block[context->Message_Block_Index++] = 0x80;while(context->Message_Block_Index < 64){context->Message_Block[context->Message_Block_Index++] = 0;}SHA1ProcessMessageBlock(context);while(context->Message_Block_Index < 56){context->Message_Block[context->Message_Block_Index++] = 0;}}else{context->Message_Block[context->Message_Block_Index++] = 0x80;while(context->Message_Block_Index < 56){context->Message_Block[context->Message_Block_Index++] = 0;}}/* 将明文长度填入到最后8个字节中 */context->Message_Block[56] = context->Length_High >> 24;context->Message_Block[57] = context->Length_High >> 16;context->Message_Block[58] = context->Length_High >> 8;context->Message_Block[59] = context->Length_High;context->Message_Block[60] = context->Length_Low >> 24;context->Message_Block[61] = context->Length_Low >> 16;context->Message_Block[62] = context->Length_Low >> 8;context->Message_Block[63] = context->Length_Low;SHA1ProcessMessageBlock(context);
}

至此SHA1散列算法就全部实现完了,需要说明一下的是相应的结构体定义和错误代码的定义如下:

/*  定义SHA-1内容保存结构体  */
typedef struct SHA1Context
{uint32_t Intermediate_Hash[SHA1HashSize/4];         /* 信息摘要 */uint32_t Length_Low;                                /* 按位计算的信息长度低字 */uint32_t Length_High;                               /* 按位计算的信息长度高字 */int_least16_t Message_Block_Index;                  /* 信息分组数组的索引 */uint8_t Message_Block[64];                          /* 512位信息分组 */int Computed;                                       /* 摘要计算标识 */int Corrupted;                                      /* 信息摘要损坏标识 */
} SHA1Context;typedef enum
{shaSuccess = 0,     /* 处理成功 */shaNull,            /* 指针参数为NUll */shaInputTooLong,    /* 输入消息长度超范围 */shaStateError       /* 在处理完毕后,未经初始化直接调用输入处理 */
}ErrorCode;

4、总结

我们已经实现了SHA1这一散列算法,接下来我们验证一下它的效果如何。首先我们输入信息“abcdef”,计算结果,并使用通用工具验算。

 

以上2图我们可以看到结果是一致的,接下来我们输入信息:“a1b23c4d5e6f7g8h9i0j”,计算结果如下:

 

对比上述2图的结果也是一致的。接下来我们分别测试长度448位、长度超过448位、长度超过512位的明文信息,所得的结果也是正确的,说明我们的实现没有问题。

前面我说了SHA-1与MD5是同源的散列算法,那他们究竟有何区别于联系呢?接下来我们简单的比较一下这两种算法:

(1)、因为二者均由MD4导出,SHA-1和MD5彼此很相似。相应的,他们的强度和其他特性也是相似,但还有以下几点不同:

(2)、对强行供给的安全性:最显著和最重要的区别是SHA-1摘要比MD5摘要长32 位。使用强行技术,产生任何一个报文使其摘要等于给定报摘要的难度对MD5是2^128数量级的操作,而对SHA-1则是2^160数量级的操作。这样,SHA-1对强行攻击有更大的强度。

(3)、对密码分析的安全性:由于MD5的设计,易受密码分析的攻击,SHA-1显得不易受这样的攻击。

(4)、速度:在相同的硬件上,SHA-1的运行速度比MD5慢。

欢迎关注:

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

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

相关文章

2018数学建模A题的简单指导

之前写过一篇博客&#xff0c;介绍如何使用差分格式求解热传导方程 今天打开博客&#xff0c;突然发现评论区被这篇文章霸屏了 询问实验室的小伙伴才知&#xff0c;原来是被可爱的建模学子们攻占了 经过简单的了解&#xff0c;发现今年建模的A题的核心就是求解一个热传导方程…

PID控制器开发笔记之五:变积分PID控制器的实现

在普通的PID控制算法中&#xff0c;由于积分系数Ki是常数&#xff0c;所以在整个控制过程中&#xff0c;积分增量是不变的。然而&#xff0c;系统对于积分项的要求是&#xff0c;系统偏差大时&#xff0c;积分作用应该减弱甚至是全无&#xff0c;而在偏差小时&#xff0c;则应该…

使用SIFT匹配金馆长表情包

python使用opencv计算SIFT特征点的示例前言潜在的问题记录demo1&#xff1a;计算并绘制特征点demo2&#xff1a;使用SIFT匹配两幅图像参考文章地址前言 SIFT&#xff08;Scale-invariant feature transform&#xff09;是2004年提出的&#xff0c;至今已经经受住各种考验&…

PID控制器开发笔记之六:不完全微分PID控制器的实现

从PID控制的基本原理我们知道&#xff0c;微分信号的引入可改善系统的动态特性&#xff0c;但也存在一个问题&#xff0c;那就是容易引进高频干扰&#xff0c;在偏差扰动突变时尤其显出微分项的不足。为了解决这个问题人们引入低通滤波方式来解决这一问题。 1、不完全微分的基…

使用Python实现简易的数据标注工具

使用Python实现简易的数据标注工具 以增加工作效率为目的&#xff0c;最近一直在着手构建一个AI ToolBox 这两天&#xff0c;我为其中的预处理工具目录添加了数据标注模块&#xff0c;本文所介绍内容的代码见这里 该数据标注模块包含以下几个demo gui_tkinter_exercise.py …

PID控制器开发笔记之七:微分先行PID控制器的实现

前面已经实现了各种的PID算法&#xff0c;然而在某些给定值频繁且大幅变化的场合&#xff0c;微分项常常会引起系统的振荡。为了适应这种给定值频繁变化的场合&#xff0c;人们设计了微分先行算法。 1、微分先行算法的思想 微分先行PID控制是只对输出量进行微分&#xff0c;而…

python实现视频关键帧提取(基于帧间差分)

python实现视频关键帧提取&#xff08;基于帧间差分&#xff09; 在很多场景下&#xff0c;我们不想或者不能处理视频的每一帧图片&#xff0c;这时我们希望能够从视频中提取出一些重要的帧进行处理&#xff0c;这个过程我们称为视频关键帧提取。 关键帧提取算法多种多样&…

PID控制器开发笔记之八:带死区的PID控制器的实现

在计算机控制系统中&#xff0c;由于系统特性和计算精度等问题&#xff0c;致使系统偏差总是存在&#xff0c;系统总是频繁动作不能稳定。为了解决这种情况&#xff0c;我们可以引入带死区的PID算法。 1、带死区PID的基本思想 带死区的PID控制算法就是检测偏差值&#xff0c;…

在多任务(RTOS)环境中使用看门狗

最近在SEGGER的博客上看到一篇有关在实时操作系统使用看门狗的文章。从一个失败的太空项目出发&#xff0c;分析了看门狗的作用及使用&#xff0c;自我感觉很有启发&#xff0c;特此翻译此文并推荐给各位同仁。为了阅读方便&#xff0c;有些航天领域名词本人添加了注释&#xf…

天池竞赛-津南数字制造算法挑战赛【赛场二】解决方案分享

天池竞赛-津南数字制造算法挑战赛【赛场二】解决方案分享 一、前言 竞赛页面 团队名BugFlow&#xff0c;最终排名35/2157 虽然成绩一般&#xff0c;但是作为一支目标检测领域的新手队伍&#xff0c;仅仅有一块1070显卡&#xff0c;从零开始拿到这个排名&#xff0c;也算有一…

信息摘要算法之三:SHA256算法分析与实现

前面一篇中我们分析了SHA的原理&#xff0c;并且以SHA1为例实现了相关的算法&#xff0c;在这一片中我们将进一步分析SHA2并实现之。 1、SHA简述 前面的篇章中我们已经说明过&#xff0c;SHA实际包括有一系列算法&#xff0c;分别是SHA-1、SHA-224、SHA-256、SHA-384以及SHA-…

focal loss的几种实现版本(Keras/Tensorflow)

起源于在工作中使用focal loss遇到的一个bug&#xff0c;我仔细的学习多个靠谱的focal loss讲解及实现版本 通过测试&#xff0c;我发现了这样一个奇怪的现象&#xff0c;几乎每个版本的focal loss实现对同样的输入计算出的loss都是不同的。 通过仔细的比对和思考&#xff0c…

基于ARM Cortex-M和Eclipse的SWO单总线输出

最近在MCU on Eclipse网站上看到Erich Styger所写的一篇有关通过SWD的跟踪接口SWO获取ARM Cortex-M相关信息的文章&#xff0c;文章结构明晰&#xff0c;讲解透彻&#xff0c;本人深受启发&#xff0c;特意将其翻译过来供各位同仁参考。当然限于个人水平&#xff0c;有不当之处…

PID控制器开发笔记之九:基于前馈补偿的PID控制器的实现

对于一般的时滞系统来说&#xff0c;设定值的变动会产生较大的滞后才能反映在被控变量上&#xff0c;从而产生合理的调节。而前馈控制系统是根据扰动或给定值的变化按补偿原理来工作的控制系统&#xff0c;其特点是当扰动产生后&#xff0c;被控变量还未变化以前&#xff0c;根…

借助百度识图爬取数据集

背景 一个能够实际应用的深度学习模型&#xff0c;背后的数据集往往都花费了大量的人力财力&#xff0c;通过聘用标注团队对真实场景数据进行标注生产出来&#xff0c;大多数情况不太可能使用网络来源的图片。但在项目初期的demo阶段&#xff0c;或者某些特定的场合下&#xf…

通过printf从目标板到调试器的输出

最近在SEGGER的博客上看到Johannes Lask写的一篇关于在调试时使用printf函数从目标MCU输出信息到调试器的文章&#xff0c;自我感觉很有启发&#xff0c;特此翻译此文并推荐给各位同仁。当然限于个人水平&#xff0c;有不当之处恳请指正。原文网址&#xff1a;https://blog.seg…

小心使用tf.image.resize_images,填坑经验分享给你

上上周&#xff0c;我在一个项目上线前对模型进行测试时出现了问题&#xff0c;这个问题困扰了我近两周&#xff0c;终于找到了问题根源&#xff0c;做个简短总结分享给你&#xff0c;希望对大家有帮助。 问题描述&#xff1a; 线上线下测试结果不一致&#xff0c;且差异很大…

PID控制器开发笔记之十:步进式PID控制器的实现

对于一般的PID控制系统来说&#xff0c;当设定值发生较大的突变时&#xff0c;很容易产生超调而使系统不稳定。为了解决这种阶跃变化造成的不利影响&#xff0c;人们发明了步进式PID控制算法。 1、步进式PID的基本思想 所谓步进式PID算法&#xff0c;实际就是在设定值发生阶跃…

AutoML 与 Bayesian Optimization 概述

1. AutoML 概述 AutoML是指对于一个超参数优化任务&#xff08;比如规定计算资源内&#xff0c;调整网络结构找到准确率最高的网络&#xff09;&#xff0c;尽量减少人为干预&#xff0c;使用某种学习机制&#xff0c;来调节这些超参数&#xff0c;使得目标问题达到最优。 这…

使用Eclipse进行Makefile项目

最近在MCU on Eclipse网站上看到Erich Styger所写的一篇有关在Eclipse中使用Makefile创建项目的文章&#xff0c;文章讲解清晰明了非常不错&#xff0c;所以呢没人将其翻译过来供各位同仁参考。当然限于个人水平&#xff0c;有不当之处恳请指正。原文网址&#xff1a;https://m…