整合營(yíng)銷服務(wù)商

          電腦端+手機(jī)端+微信端=數(shù)據(jù)同步管理

          免費(fèi)咨詢熱線:

          「在 Nervos CKB 上做開發(fā)」CKB腳本編程

          「在 Nervos CKB 上做開發(fā)」CKB腳本編程簡(jiǎn)介「2」:腳本基礎(chǔ)

          KB腳本編程簡(jiǎn)介[2]:腳本基礎(chǔ)

          原文作者:Xuejie原文鏈接:

          https://xuejie.space/2019_07_13_introduction_to_ckb_script_programming_script_basics/本文譯者:Shooter,Jason,Orange (排名不分先后)

          上一篇我們介紹了當(dāng)前 CKB 的驗(yàn)證模型。這一篇會(huì)更加有趣一點(diǎn),我們要向大家展示如何將腳本代碼真正部署到 CKB 網(wǎng)絡(luò)上去。我希望在你看完本文后,你可以有能力自行去探索 CKB 的世界并按照你自己的意愿去編寫新的腳本代碼。

          需要注意的是,盡管我相信目前的 CKB 的編程模型已經(jīng)相對(duì)穩(wěn)定了,但是開發(fā)仍在進(jìn)行中,因此未來(lái)還可能會(huì)有一些變化。我將盡力確保本文始終處于最新的狀態(tài),但是如果在過(guò)程到任何疑惑,本文以 https://github.com/nervosnetwork/ckb/commit/80b51a9851b5d535625c5d144e1accd38c32876b 作為依據(jù)。

          警告:這是一篇很長(zhǎng)的文章,因?yàn)槲蚁霝橄轮芨腥さ脑掝}提供充足的內(nèi)容。所以如果你沒有充足的時(shí)間,你不必馬上完成它。我在試著把它分成幾個(gè)獨(dú)立的不凡,這樣你就可以一次嘗試一個(gè)。

          語(yǔ)法

          在繼續(xù)之前,我們先來(lái)區(qū)分兩個(gè)術(shù)語(yǔ):腳本(script)和腳本代碼(script code)

          在本文以及整個(gè)系列文章內(nèi),我們將區(qū)分腳本和腳本代碼。腳本代碼實(shí)際上是指你編寫和編譯并在 CKB 上運(yùn)行的程序。而腳本,實(shí)際上是指 CKB 中使用的腳本數(shù)據(jù)結(jié)構(gòu),它會(huì)比腳本代碼稍微多一點(diǎn)點(diǎn):

          pub struct Script {
           pub args: Vec<Bytes>,
           pub code_hash: H256,
           pub hash_type: ScriptHashType,
           }
          

          我們目前可以先忽略hash_type,之后的文章再來(lái)解釋什么是hash_type以及它有什么有趣的用法。在這篇文章的后面,我們會(huì)說(shuō)明code_hash實(shí)際上是用來(lái)標(biāo)識(shí)腳本代碼的,所以目前我們可以只把它當(dāng)成腳本代碼。那腳本還包括什么呢?腳本還包括args這個(gè)部分,它是用來(lái)區(qū)分腳本和腳本代碼的。args在這里可以用來(lái)給一個(gè) CKB 腳本提供額外的參數(shù),比如:雖然大家可能都會(huì)使用相同的默認(rèn)的 lock script code,但是每個(gè)人可能都有自己的 pubkey hash,args 就是用來(lái)保存 pubkey hash 的位置。這樣,每一個(gè)CKB 的用戶都可以擁有不同的 lock script ,但是卻可以共用同樣的 lock script code。

          請(qǐng)注意,在大多數(shù)情況下,腳本和腳本代碼是可以互換使用的,但是如果你在某些地方感到了困惑,那么你可能有必要考慮一下兩者間的區(qū)別。

          一個(gè)最小的 CKB 腳本代碼

          你可能之前就已經(jīng)聽所過(guò)了,CKB (編者注:此處指的應(yīng)該是 CKB VM)是基于開源的 RISC-V 指令集編寫的。但這到底意味著什么呢?用我自己的話來(lái)說(shuō),這意味著我們(在某種程度上)在 CKB 中嵌入了一臺(tái)真正的微型計(jì)算機(jī),而不是一臺(tái)虛擬機(jī)。一臺(tái)真正的計(jì)算機(jī)的好處是,你可以用任何語(yǔ)言編寫任何你想寫的邏輯。在這里,我們展示的前面幾個(gè)例子將會(huì)用 C語(yǔ)言編寫,以保持簡(jiǎn)單性(我是說(shuō)工具鏈中的簡(jiǎn)單性,而不是http://blog.llvm.org/2011/05/what-every-c-programmer-should-know.html),之后我們還會(huì)切換到基于 JavaScript 的腳本代碼,并希望在本系列中展示更多的語(yǔ)言。記住,在 CKB 上有無(wú)限的可能!

          正如我們提到的,CKB VM 更像是一臺(tái)真正的微型計(jì)算機(jī)。CKB 的代碼腳本看起來(lái)也更像是我們?cè)陔娔X上跑的一個(gè)常見的 Unix 風(fēng)格的可執(zhí)行程序。

          int main(int argc, char* argv[])
          {
           return 0;
          }
          

          當(dāng)你的代碼通過(guò) C 編譯器編譯時(shí),它將成為可以在 CKB 上運(yùn)行的腳本代碼。換句話說(shuō),CKB 只是采用了普通的舊式 Unix 風(fēng)格的可執(zhí)行程序(但使用的是 RISC-V 體系結(jié)構(gòu),而不是流行的 x86 體系結(jié)構(gòu)),并在虛擬機(jī)環(huán)境中運(yùn)行它。如果程序的返回代碼是 0 ,我們認(rèn)為腳本成功了,所有非零的返回代碼都將被視為失敗腳本。

          在上面的例子中,我們展示了一個(gè)總是成功的腳本代碼。因?yàn)榉祷卮a總是 0。但是請(qǐng)不要使用這個(gè)作為您的 lock script code ,否則您的 token 可能會(huì)被任何人拿走。

          但是顯然上面的例子并不有趣,這里我們從一個(gè)有趣的想法開始:我個(gè)人不是很喜歡胡蘿卜。我知道胡蘿卜從營(yíng)養(yǎng)的角度來(lái)看是很好的,但我還是想要避免它的味道。如果現(xiàn)在我想設(shè)定一個(gè)規(guī)則,比如我想讓我在 CKB 上的 Cell 里面都沒有以carrot開頭的數(shù)據(jù)?讓我們編寫一個(gè)腳本代碼來(lái)實(shí)現(xiàn)這一點(diǎn)。

          為了確保沒有一個(gè) cell 在 cell data中包含carrot,我們首先需要一種方法來(lái)讀取腳本中的 cell data。CKB 提供了syscalls來(lái)幫助解決這個(gè)問(wèn)題。

          為了確保 CKB 腳本的安全性,每個(gè)腳本都必須在與運(yùn)行 CKB 的主計(jì)算機(jī)完全分離的隔離環(huán)境中運(yùn)行。這樣它就不能訪問(wèn)它不需要的數(shù)據(jù),比如你的私鑰或密碼。然而,要使得腳本有用,必須有特定的數(shù)據(jù)要訪問(wèn),比如腳本保護(hù)的 cell 或腳本驗(yàn)證的事務(wù)。CKB 提供了syscalls來(lái)確保這一點(diǎn),syscalls是在 RISC-V 的標(biāo)準(zhǔn)中定義的,它們提供了訪問(wèn)環(huán)境中某些資源的方法。在正常情況下,這里的環(huán)境指的是操作系統(tǒng),但是在 CKB VM 中,環(huán)境指的是實(shí)際的 CKB 進(jìn)程。使用syscalls, CKB腳本可以訪問(wèn)包含自身的整個(gè)事務(wù),包括輸入(inputs)、輸出(outpus)、見證(witnesses)和 deps。

          好消息是,我們已經(jīng)將syscalls封裝在了一個(gè)易于使用的頭文件中,非常歡迎您在這里https://github.com/nervosnetwork/ckb-system-scripts/blob/66d7da8ec72dffaa7e9c55904833951eca2422a9/c/ckb_syscalls.h,了解如何實(shí)現(xiàn)syscalls。最重要的是,您可以只獲取這個(gè)頭文件并使用包裝函數(shù)來(lái)創(chuàng)建您想要的系統(tǒng)調(diào)用。

          現(xiàn)在有了syscalls,我們可以從禁止使用carrot的腳本開始:

          #include <memory.h>
          #include "ckb_syscalls.h"
          int main(int argc, char* argv[]) {
           int ret;
           size_t index=0;
           volatile uint64_t len=0; /* (1) */
           unsigned char buffer[6];
           while (1) {
           len=6;
           memset(buffer, 0, 6);
           ret=ckb_load_cell_by_field(buffer, &len, 0, index, CKB_SOURCE_OUTPUT,
           CKB_CELL_FIELD_DATA); /* (2) */
           if (ret==CKB_INDEX_OUT_OF_BOUND) { /* (3) */
           break;
           }
           if (memcmp(buffer, "carrot", 6)==0) {
           return -1;
           }
           index++;
           }
           return 0;
          }
          

          以下幾點(diǎn)需要解釋一下:

          1. 由于 C 語(yǔ)言的怪癖,len字段需要標(biāo)記為volatile。我們會(huì)同時(shí)使用它作為輸入和輸出參數(shù),CKB VM 只能在它還保存在內(nèi)存中時(shí),才可以把它設(shè)置輸出參數(shù)。而volatile可以確保 C 編譯器將它保存為基于 RISC-V 內(nèi)存的變量。
          2. 在使用syscall時(shí),我們需要提供以下功能:一個(gè)緩沖區(qū)來(lái)保存syscall提供的數(shù)據(jù);一個(gè)len字段,來(lái)表示系統(tǒng)調(diào)用返回的緩沖區(qū)長(zhǎng)度和可用數(shù)據(jù)長(zhǎng)度;一個(gè)輸入數(shù)據(jù)緩沖區(qū)中的偏移量,以及幾個(gè)我們?cè)诮灰字行枰@取的確切字段的參數(shù)。詳情請(qǐng)參閱我們的https://github.com/nervosnetwork/rfcs/blob/master/rfcs/0009-vm-syscalls/0009-vm-syscalls.md。
          3. 為了保證最大的靈活性,CKB 使用系統(tǒng)調(diào)用的返回值來(lái)表示數(shù)據(jù)抓取狀態(tài):0 (or CKB_SUCCESS) 意味著成功,1 (or CKB_INDEX_OUT_OF_BOUND) 意味著您已經(jīng)通過(guò)一種方式獲取了所有的索引,2 (orCKB_ITEM_MISSING) 意味著不存在一個(gè)實(shí)體,比如從一個(gè)不包含該 type 腳本的 cell 中獲取該 type 的腳本。

          概況一下,這個(gè)腳本將循環(huán)遍歷交易中的所有輸出 cells,加載每個(gè) cell data 的前6個(gè)字節(jié),并測(cè)試這些字節(jié)是否和carrot匹配。如果找到匹配,腳本將返回-1,表示錯(cuò)誤狀態(tài);如果沒有找到匹配,腳本將返回0退出,表示執(zhí)行成功。

          為了執(zhí)行該循環(huán),該腳本將保存一個(gè)index變量,在每次循環(huán)迭代中,它將試圖讓 syscall 獲取 cell 中目前采用的index值,如果 syscall 返回 CKB_INDEX_OUT_OF_BOUND,這意味著腳本已經(jīng)遍歷所有的 cell,之后會(huì)退出循環(huán);否則,循環(huán)將繼續(xù),每測(cè)試 cell data 一次,index變量就會(huì)遞增一次。

          這是第一個(gè)有用的 CKB 腳本代碼!在下一節(jié)中,我們將看到我們是如何將其部署到 CKB 中并運(yùn)行它的。

          將腳本部署到 CKB 上

          首先,我們需要編譯上面寫的關(guān)于胡蘿卜的源代碼。由于 GCC 已經(jīng)提供了 RISC-V 的支持,您當(dāng)然可以使用官方的 GCC 來(lái)創(chuàng)建腳本代碼。或者你也可以使用我們準(zhǔn)備的 https://hub.docker.com/r/nervos/ckb-riscv-gnu-toolchain來(lái)避免編譯 GCC 的麻煩:

          $ ls
          carrot.c ckb_consts.h ckb_syscalls.h
          $ sudo docker run --rm -it -v `pwd`:/code nervos/ckb-riscv-gnu-toolchain:xenial bash
          root@dc2c0c209dcd:/# cd /code
          root@dc2c0c209dcd:/code# riscv64-unknown-elf-gcc -Os carrot.c -o carrot
          root@dc2c0c209dcd:/code# exit
          exit
          $ ls
          carrot* carrot.c ckb_consts.h ckb_syscalls.h
          

          就是這樣,CKB 可以直接使用 GCC 編譯的可執(zhí)行文件作為鏈上的腳本,無(wú)需進(jìn)一步處理。我們現(xiàn)在可以在鏈上部署它了。注意,我將使用 CKB 的 Ruby SDK,因?yàn)槲以?jīng)是一名 Ruby 程序員,當(dāng)然 Ruby 對(duì)我來(lái)說(shuō)是最自然的(但不一定是最好的)。如何設(shè)置請(qǐng)參考https://github.com/nervosnetwork/ckb-sdk-ruby/blob/develop/README.md。

          要將腳本部署到 CKB,我們只需創(chuàng)建一個(gè)新的 cell,把腳本代碼設(shè)為 cell data 部分:

          pry(main)> data=File.read("carrot")
          pry(main)> data.bytesize=> 6864
          pry(main)> carrot_tx_hash=wallet.send_capacity(wallet.address, CKB::Utils.byte_to_shannon(8000), CKB::Utils.bin_to_hex(data))
          

          在這里,我首先要通過(guò)向自己發(fā)送 token 來(lái)創(chuàng)建一個(gè)容量足夠的新的 cell。現(xiàn)在我們可以創(chuàng)建包含胡蘿卜腳本代碼的腳本:

          pry(main)> carrot_data_hash=CKB::Blake2b.hexdigest(data)
          pry(main)> carrot_type_script=CKB::Types::Script.new(code_hash: carrot_data_hash, args: [])
          

          回憶一下腳本數(shù)據(jù)結(jié)構(gòu):

          pub struct Script {
           pub args: Vec<Bytes>,
           pub code_hash: H256,
           pub hash_type: ScriptHashType,
           }
          

          我們可以看到,我們沒有直接將腳本代碼嵌入到腳本數(shù)據(jù)結(jié)構(gòu)中,而是只包含了代碼的哈希,這是實(shí)際腳本二進(jìn)制代碼的 Blake2b 哈希。由于胡蘿卜腳本不使用參數(shù),我們可以對(duì)args部分使用空數(shù)組。

          注意,這里仍然忽略了 hash_type,我們將在后面的文章中通過(guò)另一種方式討論指定代碼哈希。現(xiàn)在,讓我們盡量保持簡(jiǎn)單。

          要運(yùn)行胡蘿卜腳本,我們需要?jiǎng)?chuàng)建一個(gè)新的交易,并將胡蘿卜 type 腳本設(shè)置為其中一個(gè)輸出 cell 的 type 腳本:

          pry(main)> tx=wallet.generate_tx(wallet2.address, CKB::Utils.byte_to_shannon(200))
          pry(main)> tx.outputs[0].instance_variable_set(:@type, carrot_type_script.dup)
          

          我們還需要進(jìn)行一個(gè)步驟:為了讓 CKB 可以找到胡蘿卜腳本,我們需要在一筆交易的 deps 中引用包含胡蘿卜腳本的 cell:

          pry(main)> carrot_out_point=CKB::Types::OutPoint.new(cell: CKB::Types::CellOutPoint.new(tx_hash: carrot_tx_hash, index: 0))
          pry(main)> tx.deps.push(carrot_out_point.dup)
          

          現(xiàn)在我們準(zhǔn)備簽名并發(fā)送交易:

          [44] pry(main)> tx.witnesses[0].data.clear
          [46] pry(main)> tx=tx.sign(wallet.key, api.compute_transaction_hash(tx))
          [19] pry(main)> api.send_transaction(tx)=> "0xd7b0fea7c1527cde27cc4e7a2e055e494690a384db14cc35cd2e51ec6f078163"
          

          由于該交易的 cell 中沒有任何一個(gè)的 cell data 包含carrot,因此 type 腳本將驗(yàn)證成功。現(xiàn)在讓我們嘗試一個(gè)不同的交易,它確實(shí)含有一個(gè)以carrot開頭的 cell:

          pry(main)> tx2=wallet.generate_tx(wallet2.address, CKB::Utils.byte_to_shannon(200))
          pry(main)> tx2.deps.push(carrot_out_point.dup)
          pry(main)> tx2.outputs[0].instance_variable_set(:@type, carrot_type_script.dup)
          pry(main)> tx2.outputs[0].instance_variable_set(:@data, CKB::Utils.bin_to_hex("carrot123"))
          pry(main)> tx2.witnesses[0].data.clear
          pry(main)> tx2=tx2.sign(wallet.key, api.compute_transaction_hash(tx2))
          pry(main)> api.send_transaction(tx2)
          CKB::RPCError: jsonrpc error: {:code=>-3, :message=>"InvalidTx(ScriptFailure(ValidationFailure(-1)))"}
          from /home/ubuntu/code/ckb-sdk-ruby/lib/ckb/rpc.rb:164:in `rpc_request'
          

          我們可以看到,我們的胡蘿卜腳本拒絕了一筆生成的 cell 中包含胡蘿卜的交易。現(xiàn)在我可以使用這個(gè)腳本來(lái)確保所有的 cell 中都不含胡蘿卜!

          所以,總結(jié)一下,部署和運(yùn)行一個(gè) type 腳本的腳本,我們需要做的是:

          1. 將腳本編譯為 RISC-V 可執(zhí)行的二進(jìn)制文件
          2. 在 cell 的 data 部分部署二進(jìn)制文件
          3. 創(chuàng)建一個(gè) type 腳本數(shù)據(jù)結(jié)構(gòu),使用二進(jìn)制文件的 blake2b 散列作為code hash,補(bǔ)齊args部分中腳本代碼的需要的參數(shù)
          4. 用生成的 cell 中設(shè)置的 type 腳本創(chuàng)建一個(gè)新的交易
          5. 將包含腳本代碼的 cell 的 outpoint 寫入到一筆交易的 deps 中去

          這就是你所有需要的!如果您的腳本遇到任何問(wèn)題,您需要檢查這些要點(diǎn)。

          雖然在這里我們只討論了 type 腳本,但是 lock 腳本的工作方式完全相同。您惟一需要記住的是,當(dāng)您使用特定的 lock 腳本創(chuàng)建 cell 時(shí),lock 腳本不會(huì)在這里運(yùn)行,它只在您使用 cell 時(shí)運(yùn)行。因此, type 腳本可以用于構(gòu)造創(chuàng)建 cell 時(shí)運(yùn)行的邏輯,而 lock 腳本用于構(gòu)造銷毀 cell 時(shí)運(yùn)行的邏輯。考慮到這一點(diǎn),請(qǐng)確保您的 lock 腳本是正確的,否則您可能會(huì)在以下場(chǎng)景中丟失 token:

          您的 lock 腳本有一個(gè)其他人也可以解鎖您的 cell 的 bug。

          您的 lock 腳本有一個(gè) bug,任何人(包括您)都無(wú)法解鎖您的 cell。

          在這里我們可以提供的一個(gè)技巧是,始終將您的腳本作為一個(gè) type 腳本附加到你交易的一個(gè) output cell 中去進(jìn)行測(cè)試,這樣,發(fā)生錯(cuò)誤時(shí),您可以立即知道,并且您的 token 可以始終保持安全。

          分析默認(rèn) lock 腳本代碼

          根據(jù)已經(jīng)掌握的知識(shí),讓我們看看 CKB 中包含的默認(rèn)的 lock 腳本代碼。 為了避免混淆,我們正在查看 lock 腳本代碼在 https://github.com/nervosnetwork/ckb-system-scripts/blob/66e2b3fc4fa3e80235e4b4f94a16e81352a812f7/c/secp256k1_blake160_sighash_all.c。

          默認(rèn)的 lock 腳本代碼將循環(huán)遍歷與自身具有相同 lock 腳本的所有的 input cell,并執(zhí)行以下步驟:

          • 它通過(guò)提供的 syscall 獲取當(dāng)前的交易 hash
          • 它獲取相應(yīng)的 witness 數(shù)據(jù)作為當(dāng)前輸入
          • 對(duì)于默認(rèn) lock 腳本,假設(shè) witness 中的第一個(gè)參數(shù)包含由 cell 所有者簽名的可恢復(fù)簽名,其余參數(shù)是用戶提供的可選參數(shù)
          • 默認(rèn)的 lock 腳本運(yùn)行 由交易 hash 鏈接的二進(jìn)制程序的 blake2b hash, 還有所有用戶提供的參數(shù)(如果存在的話)
          • 將 blake2b hash 結(jié)果用作 secp256k1 簽名驗(yàn)證的消息部分。注意,witness 數(shù)據(jù)結(jié)構(gòu)中的第一個(gè)參數(shù)提供了實(shí)際的簽名。
          • 如果簽名驗(yàn)證失敗,腳本退出并返回錯(cuò)誤碼。否則它將繼續(xù)下一個(gè)迭代。

          注意,我們?cè)谇懊嬗懻摿四_本和腳本代碼之間的區(qū)別。每一個(gè)不同的公鑰 hash 都會(huì)產(chǎn)生不同的 lock 腳本,因此,如果一個(gè)交易的輸入 cell 具有相同的默認(rèn) lock 腳本代碼,但具有不同的公鑰 hash(因此具有不同的 lock 腳本),將執(zhí)行默認(rèn) lock 腳本代碼的多個(gè)實(shí)例,每個(gè)實(shí)例都有一組共享相同 lock 腳本的 cell。

          現(xiàn)在我們可以遍歷默認(rèn) lock 腳本代碼的不同部分:

          if (argc !=2) {
           return ERROR_WRONG_NUMBER_OF_ARGUMENTS;
          }
          secp256k1_context context;
          if (secp256k1_context_initialize(&context, SECP256K1_CONTEXT_VERIFY)==0) {
           return ERROR_SECP_INITIALIZE;
          }
          len=BLAKE2B_BLOCK_SIZE;
          ret=ckb_load_tx_hash(tx_hash, &len, 0);
          if (ret !=CKB_SUCCESS) {
           return ERROR_SYSCALL;
          }
          

          當(dāng)參數(shù)包含在 Script數(shù)據(jù)結(jié)構(gòu)的 args部分, 它們通過(guò) Unix 傳統(tǒng)的arc/argv方式發(fā)送給實(shí)際運(yùn)行的腳本程序。為了進(jìn)一步保持約定,我們?cè)赼rgv[0] 處插入一個(gè)偽參數(shù),所以 第一個(gè)包含的參數(shù)從argv[1]開始。在默認(rèn) lock 腳本代碼的情況下,它接受一個(gè)參數(shù),即從所有者的私鑰生成的公鑰 hash。

          ret=ckb_load_input_by_field(NULL, &len, 0, index, CKB_SOURCE_GROUP_INPUT,
           CKB_INPUT_FIELD_SINCE);
          if (ret==CKB_INDEX_OUT_OF_BOUND) {
           return 0;
          }
          if (ret !=CKB_SUCCESS) {
           return ERROR_SYSCALL;
          }
          

          使用與胡蘿卜這個(gè)例子相同的技術(shù),我們檢查是否有更多的輸入 cell 要測(cè)試。與之前的例子有兩個(gè)不同:

          • 如果我們只想知道一個(gè) cell 是否存在并且不需要任何數(shù)據(jù),我們只需要傳入NULL 作為數(shù)據(jù)緩沖區(qū),一個(gè) len 變量的值是 0。
          • 通過(guò)這種方式,syscall 將跳過(guò)數(shù)據(jù)填充,只提供可用的數(shù)據(jù)長(zhǎng)度和正確的返回碼用于處理。
          • 在這個(gè) carrot 的例子中,我們循環(huán)遍歷交易中的所有輸入, 但這里我們只關(guān)心具有相同 lock 腳本的輸入cell。 CKB將具有相同鎖定(或類型)腳本的cell命名為group。 我們可以使用 CKB_SOURCE_GROUP_INPUT 代替 CKB_SOURCE_INPUT, 來(lái)表示只計(jì)算同一組中的 cell,舉個(gè)例子,即具有與當(dāng)前 cell 相同的 lock 腳本的 cells。
          len=WITNESS_SIZE;
          ret=ckb_load_witness(witness, &len, 0, index, CKB_SOURCE_GROUP_INPUT);
          if (ret !=CKB_SUCCESS) {
           return ERROR_SYSCALL;
          }
          if (len > WITNESS_SIZE) {
           return ERROR_WITNESS_TOO_LONG;
          }
          if (!(witness_table=ns(Witness_as_root(witness)))) {
           return ERROR_ENCODING;
          }
          args=ns(Witness_data(witness_table));
          if (ns(Bytes_vec_len(args)) < 1) {
           return ERROR_WRONG_NUMBER_OF_ARGUMENTS;
          }
          

          繼續(xù)沿著這個(gè)路徑,我們正在加載當(dāng)前輸入的 witness。 對(duì)應(yīng)的 witness 和輸入具有相同的索引。現(xiàn)在 CKB 在 syscalls 中使用flatbuffer作為序列化格式,所以如果你很好奇,https://github.com/dvidelabs/flatcc是你最好的朋友。

          /* Load signature */
          len=TEMP_SIZE;
          ret=extract_bytes(ns(Bytes_vec_at(args, 0)), temp, &len);
          if (ret !=CKB_SUCCESS) {
           return ERROR_ENCODING;
          }
          /* The 65th byte is recid according to contract spec.*/
          recid=temp[RECID_INDEX];
          /* Recover pubkey */
          secp256k1_ecdsa_recoverable_signature signature;
          if (secp256k1_ecdsa_recoverable_signature_parse_compact(&context, &signature, temp, recid)==0) {
           return ERROR_SECP_PARSE_SIGNATURE;
          }
          blake2b_state blake2b_ctx;
          blake2b_init(&blake2b_ctx, BLAKE2B_BLOCK_SIZE);
          blake2b_update(&blake2b_ctx, tx_hash, BLAKE2B_BLOCK_SIZE);
          for (size_t i=1; i < ns(Bytes_vec_len(args)); i++) {
           len=TEMP_SIZE;
           ret=extract_bytes(ns(Bytes_vec_at(args, i)), temp, &len);
           if (ret !=CKB_SUCCESS) {
           return ERROR_ENCODING;
           }
           blake2b_update(&blake2b_ctx, temp, len);
          }
          blake2b_final(&blake2b_ctx, temp, BLAKE2B_BLOCK_SIZE);
          

          witness 中的第一個(gè)參數(shù)是要加載的簽名,而其余的參數(shù)(如果提供的話)被附加到用于 blake2b 操作的交易 hash 中。

          secp256k1_pubkey pubkey;
          if (secp256k1_ecdsa_recover(&context, &pubkey, &signature, temp) !=1) {
           return ERROR_SECP_RECOVER_PUBKEY;
          }
          

          然后使用哈希后的 blake2b 結(jié)果作為信息,進(jìn)行 secp256 簽名驗(yàn)證。

          size_t pubkey_size=PUBKEY_SIZE;
          if (secp256k1_ec_pubkey_serialize(&context, temp, &pubkey_size, &pubkey, SECP256K1_EC_COMPRESSED) !=1 ) {
           return ERROR_SECP_SERIALIZE_PUBKEY;
          }
          len=PUBKEY_SIZE;
          blake2b_init(&blake2b_ctx, BLAKE2B_BLOCK_SIZE);
          blake2b_update(&blake2b_ctx, temp, len);
          blake2b_final(&blake2b_ctx, temp, BLAKE2B_BLOCK_SIZE);
          if (memcmp(argv[1], temp, BLAKE160_SIZE) !=0) {
           return ERROR_PUBKEY_BLAKE160_HASH;
          }
          

          最后同樣重要的是,我們還需要檢查可恢復(fù)簽名中包含的 pubkey 確實(shí)是用于生成 lock 腳本參數(shù)中包含的 pubkey hash 的 pubkey。否則,可能會(huì)有人使用另一個(gè)公鑰生成的簽名來(lái)竊取你的 token。

          簡(jiǎn)而言之,默認(rèn) lock 腳本中使用的方案與現(xiàn)在https://bitcoin.org/en/transactions-guide#p2pkh-script-validation非常相似。

          介紹 Duktape

          我相信你和我現(xiàn)在的感覺一樣: 我們可以用 C 語(yǔ)言寫合約,這很好,但是 C 語(yǔ)言總是讓人覺得有點(diǎn)乏味,而且,讓我們面對(duì)現(xiàn)實(shí),它很危險(xiǎn)。有更好的方法嗎?

          當(dāng)然! 我們上面提到的 CKB VM 本質(zhì)上是一臺(tái)微型計(jì)算機(jī),我們可以探索很多解決方案。 我們?cè)谶@里做的一件事是,使用 JavaScript 編寫 CKB 腳本代碼。 是的,你說(shuō)對(duì)了,簡(jiǎn)單的 ES5 (是的,我知道,但這只是一個(gè)例子,你可以使用轉(zhuǎn)換器) JavaScript。

          這怎么可能呢? 由于我們有 C 編譯器,我們只需為嵌入式系統(tǒng)使用一個(gè) JavaScript 實(shí)現(xiàn),在我們的例子中,https://duktape.org/ 將它從 C 編譯成 RISC-V 二進(jìn)制文件,把它放在鏈上,我們就可以在 CKB 上運(yùn)行 JavaScript 了!因?yàn)槲覀兪褂玫氖且慌_(tái)真正的微型計(jì)算機(jī),所以沒有什么可以阻止我們將另一個(gè) VM 作為 CKB 腳本嵌入到 CKB VM 中,并在 VM 路徑上探索這個(gè) VM。

          從這條路徑展開,我們可以通過(guò) duktape 在 CKB 上使用 JavaScript,我們也可以通過(guò) https://github.com/mruby/mruby在 ckb 上使用 Ruby, 我們甚至可以將比特幣腳本或EVM放到鏈上,我們只需要編譯他們的虛擬機(jī),并把它放在鏈上。這確保了 CKB VM 既能幫助我們保存資產(chǎn),又能構(gòu)建一個(gè)多樣化的生態(tài)系統(tǒng)。所有的語(yǔ)言都應(yīng)該在 CKB 上被平等對(duì)待,自由應(yīng)該掌握在區(qū)塊鏈合約的開發(fā)者手中。

          在這個(gè)階段,你可能想問(wèn): 是的,這是可能的,但是 VM 之上的 VM 不會(huì)很慢嗎? 我相信這取決于你的例子是否很慢。我堅(jiān)信,基準(zhǔn)測(cè)試沒有任何意義,除非我們將它放在具有標(biāo)準(zhǔn)硬件需求的實(shí)際用例中。 所以我們需要有時(shí)間檢驗(yàn)這是否真的會(huì)成為一個(gè)問(wèn)題。 在我看來(lái),高級(jí)語(yǔ)言更可能用于 type scripts 來(lái)保護(hù) cell 轉(zhuǎn)換,在這種情況下,我懷疑它會(huì)很慢。此外,我們也在這個(gè)領(lǐng)域努力工作,以優(yōu)化 CKB VM 和 VMs 之上的 CKB VM,使其越來(lái)越快,:P

          要在 CKB 上使用 duktape,首先需要將 duktape 本身編譯成 RISC-V 可執(zhí)行二進(jìn)制文件:

          $ git clone https://github.com/nervosnetwork/ckb-duktape
          $ cd ckb-duktape
          $ sudo docker run --rm -it -v `pwd`:/code nervos/ckb-riscv-gnu-toolchain:xenial bash
          root@0d31cad7a539:~# cd /code
          root@0d31cad7a539:/code# make
          riscv64-unknown-elf-gcc -Os -DCKB_NO_MMU -D__riscv_soft_float -D__riscv_float_abi_soft -Iduktape -Ic -Wall -Werror c/entry.c -c -o build/entry.o
          riscv64-unknown-elf-gcc -Os -DCKB_NO_MMU -D__riscv_soft_float -D__riscv_float_abi_soft -Iduktape -Ic -Wall -Werror duktape/duktape.c -c -o build/duktape.o
          riscv64-unknown-elf-gcc build/entry.o build/duktape.o -o build/duktape -lm -Wl,-static -fdata-sections -ffunction-sections -Wl,--gc-sections -Wl,-s
          root@0d31cad7a539:/code# exit
          exit
          $ ls build/duktape
          build/duktape*
          

          與 carrot 示例一樣,這里的第一步是在 CKB cell 中部署 duktape 腳本代碼:

          pry(main)> data=File.read("../ckb-duktape/build/duktape")
          pry(main)> duktape_data.bytesize=> 269064
          pry(main)> duktape_tx_hash=wallet.send_capacity(wallet.address, CKB::Utils.byte_to_shannon(280000), CKB::Utils.bin_to_hex(duktape_data))
          pry(main)> duktape_data_hash=CKB::Blake2b.hexdigest(duktape_data)
          pry(main)> duktape_out_point=CKB::Types::OutPoint.new(cell: CKB::Types::CellOutPoint.new(tx_hash: duktape_tx_hash, index: 0))
          

          與 carrot 的例子不同,duktape 腳本代碼現(xiàn)在需要一個(gè)參數(shù): 要執(zhí)行的 JavaScript 源代碼:

          pry(main)> duktape_hello_type_script=CKB::Types::Script.new(code_hash: duktape_data_hash, args: [CKB::Utils.bin_to_hex("CKB.debug(\"I'm running in JS!\")")])
          

          注意,使用不同的參數(shù),你可以為不同的用例創(chuàng)建不同的 duktape 支持的 type script:

          pry(main)> duktape_hello_type_script=CKB::Types::Script.new(code_hash: duktape_data_hash, args: [CKB::Utils.bin_to_hex("var a=1;\nvar b=a + 2;")])
          

          這反映了上面提到的腳本代碼與腳本之間的差異:這里 duktape 作為提供 JavaScript 引擎的腳本代碼,而不同的腳本利用 duktape 腳本代碼在鏈上提供不同的功能。

          現(xiàn)在我們可以創(chuàng)建一個(gè) cell 與 duktape 的 type script 附件:

          pry(main)> tx=wallet.generate_tx(wallet2.address, CKB::Utils.byte_to_shannon(200))
          pry(main)> tx.deps.push(duktape_out_point.dup)
          pry(main)> tx.outputs[0].instance_variable_set(:@type, duktape_hello_type_script.dup)
          pry(main)> tx.witnesses[0].data.clear
          pry(main)> tx=tx.sign(wallet.key, api.compute_transaction_hash(tx))
          pry(main)> api.send_transaction(tx)=> "0x2e4d3aab4284bc52fc6f07df66e7c8fc0e236916b8a8b8417abb2a2c60824028"
          

          我們可以看到腳本執(zhí)行成功,如果在ckb.toml 文件中將 ckb-script日志模塊的級(jí)別設(shè)置為debug,你可以看到以下日志:

          2019-07-15 05:59:13.551 +00:00 http.worker8 DEBUG ckb-script script group: c35b9fed5fc0dd6eaef5a918cd7a4e4b77ea93398bece4d4572b67a474874641 DEBUG OUTPUT: I'm running in JS!
          

          現(xiàn)在您已經(jīng)成功地在 CKB 上部署了一個(gè) JavaScript 引擎,并在 CKB 上運(yùn)行基于 JavaScript 的腳本!

          你可以在這里嘗試認(rèn)識(shí)的 JavaScript 代碼。

          一道思考題

          現(xiàn)在你已經(jīng)熟悉了 CKB 腳本的基礎(chǔ)知識(shí),下面是一個(gè)思考:在本文中,您已經(jīng)看到了一個(gè) always-success 的腳本是什么樣子的,但是一個(gè) always-failure 的腳本呢?一個(gè) always-failure 腳本(和腳本代碼)能有多小?

          提示:這不是 gcc 優(yōu)化比賽,這只是一個(gè)思考。

          下集預(yù)告

          我知道這是一個(gè)很長(zhǎng)的帖子,我希望你已經(jīng)嘗試過(guò),并成功地部署了一個(gè)腳本到 CKB。在下一篇文章中,我們將介紹一個(gè)重要的主題:如何在 CKB 定義自己的用戶定義 token(UDT)。CKB 上 udt 最好的部分是,每個(gè)用戶都可以將自己的 udt 存儲(chǔ)在自己的 cell 中,這與 Ethereum 上的 ERC20 令牌不同,在 Ethereum 上,每個(gè)人的 token 都必須位于 token 發(fā)起者的單個(gè)地址中。所有這些都可以通過(guò)單獨(dú)使用 type script 來(lái)實(shí)現(xiàn)。

          如果你感興趣,請(qǐng)繼續(xù)關(guān)注 :)

          加入 Nervos Community

          Nervos Community 致力于成為最好的 Nervos 社區(qū),我們將持續(xù)地推廣和普 及 Nervos 技術(shù),深入挖掘 Nervos 的內(nèi)在價(jià)值,開拓 Nervos 的無(wú)限可能, 為每一位想要深入了解 Nervos Network 的人提供一個(gè)優(yōu)質(zhì)的平臺(tái)。

          添加微信號(hào):BitcoinDog 即可加入 Nervos Community,如果是程序員請(qǐng)備注,還會(huì)將您拉入開發(fā)者群。


          者:Xuejie原文鏈接:https://xuejie.space/20191018introductiontockbscriptprogrammingdebugging/

          Nervos CKB 腳本編程簡(jiǎn)介[5]:調(diào)試 debug

          事實(shí)上,CKB 腳本工作的層級(jí)要比其他智能合約低很多,因此 CKB 的調(diào)試過(guò)程就顯得相當(dāng)神秘。在本文中,我們將展示如何調(diào)試 CKB 腳本。你會(huì)發(fā)現(xiàn),其實(shí)調(diào)試 CKB 腳本和你日常調(diào)試程序并沒有太大區(qū)別。

          本文建立在 ckb v0.23.0 之上。具體的,我在每個(gè)項(xiàng)目中使用的是如下版本的 commit:

          • ckb: 7e2ad2d9ed6718360587f3762163229eccd2cf10
          • ckb-sdk-ruby: 18a89d8c69e173ad59ce3e3b3bf79b5d11c5f8f8
          • ckb-duktape:347bf730c08eb0aab7e56e0357945a4d6cee109a
          • ckb-standalone-debugger: 2379e89ae285e4e639b961756c22d8e4fde4d6ab

          使用 GDB 調(diào)試 C 程序

          CKB 腳本調(diào)試的第一種方案,通常適用于 C、Rust 等編程語(yǔ)言。也許你已經(jīng)習(xí)慣了寫 C 的程序,而 GDB 也是你的好搭檔。你想知道是不是可以用 GDB 來(lái)調(diào)試 C 程序,答案當(dāng)然是:Yes!你肯定可以通過(guò) GDB 來(lái)調(diào)試用 C 編寫的 CKB 腳本!讓我來(lái)演示一下:

          首先,我們還是用之前文章中用到的關(guān)于 carrot 的例子:

          #include <memory.h>#include "ckb_syscalls.h"
          int main(int argc, char* argv[]) {
           int ret;
           size_t index=0;
           uint64_t len=0;
           unsigned char buffer[6];
           while (1) {
           len=6;
           memset(buffer, 0, 6);
           ret=ckb_load_cell_data(buffer, &len, 0, index, CKB_SOURCE_OUTPUT);
           if (ret==CKB_INDEX_OUT_OF_BOUND) {
           break;
           }
           int cmp=memcmp(buffer, "carrot", 6);
           if (cmp) {
           return -1;
           }
           index++;
           }
           return 0;
          }

          這里我進(jìn)行了兩處修改:

          首先我更新了這個(gè)腳本,讓它可以兼容 ckb v0.23.0。在這個(gè)版本中,我們可以使用 ckbloadcell_data 來(lái)獲取 cell 的數(shù)據(jù)。

          我還在這段代碼中加入了一個(gè)小 bug,這樣我們等會(huì)兒就可以進(jìn)行調(diào)試的工作流程。如果你非常熟悉 C,你可能已經(jīng)注意到了,當(dāng)然你沒有在意到的話也完全不用擔(dān)心,稍后我會(huì)解釋的。

          和往常一樣,我們使用官方的 toolchain 來(lái)將其編譯成 RISC-V 的代碼:

          $ ls
          carrot.c
          $ git clone https://github.com/nervosnetwork/ckb-system-scripts
          $ cp ckb-system-scripts/c/ckb_*.h ./
          $ ls
          carrot.c ckb_consts.h ckb_syscalls.h ckb-system-scripts/
          $ sudo docker run --rm -it -v `pwd`:/code nervos/ckb-riscv-gnu-toolchain:bionic-20191012 bash
          root@3efa454be9af:/# cd /code
          root@3efa454be9af:/code# riscv64-unknown-elf-gcc carrot.c -g -o carrot
          root@3efa454be9af:/code# exit

          請(qǐng)注意,當(dāng)我編譯腳本的時(shí)候,我添加了 -g,以便生成調(diào)試信息,這在 GDB 中非常有用。對(duì)于實(shí)際使用的腳本,你總是希望盡量地完善它們來(lái)盡量節(jié)省存儲(chǔ)在鏈上的空間。

          現(xiàn)在,讓我們將腳本部署到 CKB 上。保持 CKB 節(jié)點(diǎn)處于運(yùn)行狀態(tài),并啟動(dòng) Ruby SDK:

          pry(main)> api=CKB::API.new
          pry(main)> wallet=CKB::Wallet.from_hex(api, "<your private key>")
          pry(main)> wallet2=CKB::Wallet.from_hex(api, CKB::Key.random_private_key)
          pry(main)> carrot_data=File.read("carrot")
          pry(main)> carrot_data.bytesize=> 19296
          pry(main)> carrot_tx_hash=wallet.send_capacity(wallet2.address, CKB::Utils.byte_to_shannon(20000), CKB::Utils.bin_to_hex(carrot_data), fee: 21000)
          pry(main)> carrot_data_hash=CKB::Blake2b.hexdigest(carrot_data)
          pry(main)> carrot_type_script=CKB::Types::Script.new(code_hash: carrot_data_hash, args: "0x")
          pry(main)> carrot_cell_dep=CKB::Types::CellDep.new(out_point: CKB::Types::OutPoint.new(tx_hash: carrot_tx_hash, index: 0))

          現(xiàn)在鏈上有了 carrot 的腳本,我們可以創(chuàng)建一筆交易來(lái)測(cè)試這個(gè) carrot 腳本:

          pry(main)> tx=wallet.generate_tx(wallet2.address, CKB::Utils.byte_to_shannon(100), use_dep_group: false, fee: 5000)
          pry(main)> tx.outputs[0].type=carrot_type_script
          pry(main)> tx.cell_deps << carrot_cell_dep
          pry(main)> tx.witnesses[0]="0x"
          pry(main)> tx=tx.sign(wallet.key, api.compute_transaction_hash(tx))
          pry(main)> api.send_transaction(tx)
          CKB::RPCError: jsonrpc error: {:code=>-3, :message=>"Script(ValidationFailure(-1))"}

          如果你仔細(xì)檢查這筆交易,你會(huì)發(fā)現(xiàn)在輸出的 cell 中,并沒有以 carrot 開頭的數(shù)據(jù)。然而我們運(yùn)行之后仍然是驗(yàn)證失敗,這意味著我們的腳本一定存在 bug。先前,沒什么別的辦法,你可能需要返回去檢查代碼,希望可以找到出錯(cuò)的地方。但現(xiàn)在沒有這個(gè)必要了,你可以跳過(guò)這里的交易,然后將其輸入到一個(gè)獨(dú)立的 CKB 調(diào)試器開始調(diào)試它!

          首先,讓我們將這筆交易連同使用的環(huán)境,都轉(zhuǎn)存到一個(gè)本地文件中:

          pry(main)> CKB::MockTransactionDumper.new(api, tx).write("carrot.json")

          在這里你還需要跟蹤 carrot 類型腳本的哈希:

          pry(main)> carrot_type_script.compute_hash=> "0x039c2fba64f389575cdecff8173882b97be5f8d3bdb2bb0770d8a7e265b91933"

          請(qǐng)注意,你可能會(huì)得到和我這里不一樣的哈希,這得看你使用的環(huán)境。

          現(xiàn)在,讓我們來(lái)試試 ckb-standalone-debugger:

          $ git clone https://github.com/nervosnetwork/ckb-standalone-debugger
          $ cd ckb-standalone-debugger/bins
          $ cargo build --release
          $ ./target/release/ckb-debugger -l 0.0.0.0:2000 -g type -h 0x039c2fba64f389575cdecff8173882b97be5f8d3bdb2bb0770d8a7e265b91933 -t carrot.json

          注意,你可能需要根據(jù)你的環(huán)境,調(diào)整 carrot 類型腳本的哈希或者 carrot.json 的路徑。現(xiàn)在讓我們?cè)囋囋谝粋€(gè)不同的終端內(nèi)通過(guò) GDB 連接調(diào)試器:

          $ sudo docker run --rm -it -v `pwd`:/code nervos/ckb-riscv-gnu-toolchain:bionic-20191012 bash
          root@66e3b39e0dfd:/# cd /code
          root@66e3b39e0dfd:/code# riscv64-unknown-elf-gdb carrot
          GNU gdb (GDB) 8.3.0.20190516-git
          Copyright (C) 2019 Free Software Foundation, Inc.
          License GPLv3+: GNU GPL version 3 or later <http://gnu.org/licenses/gpl.html>
          This is free software: you are free to change and redistribute it.
          There is NO WARRANTY, to the extent permitted by law.
          Type "show copying" and "show warranty" for details.
          This GDB was configured as "--host=x86_64-pc-linux-gnu --target=riscv64-unknown-elf".
          Type "show configuration" for configuration details.
          For bug reporting instructions, please see:
          <http://www.gnu.org/software/gdb/bugs/>.
          Find the GDB manual and other documentation resources online at:
           <http://www.gnu.org/software/gdb/documentation/>.
          
          For help, type "help".
          Type "apropos word" to search for commands related to "word"...
          Reading symbols from carrot...
          (gdb) target remote 192.168.1.230:2000
          Remote debugging using 192.168.1.230:2000
          0x00000000000100c6 in _start ()
          (gdb)

          注意,這里的 192.168.1.230 是我的工作站在本地網(wǎng)絡(luò)中的 IP 地址,你可能需要調(diào)整該地址,因?yàn)槟愕挠?jì)算機(jī)可能是不同的 IP 地址。現(xiàn)在我們可以試一下常見的 GDB 調(diào)試過(guò)程:

          (gdb) b main
          Breakpoint 1 at 0x106b0: file carrot.c, line 6.
          (gdb) c
          Continuing.
          
          Breakpoint 1, main (argc=0, argv=0x400000) at carrot.c:6
          6 size_t index=0;
          (gdb) n
          7 uint64_t len=0;
          (gdb) n
          11 len=6;
          (gdb) n
          12 memset(buffer, 0, 6);
          (gdb) n
          13 ret=ckb_load_cell_data(buffer, &len, 0, index, CKB_SOURCE_OUTPUT);
          (gdb) n
          14 if (ret==CKB_INDEX_OUT_OF_BOUND) {
          (gdb) n
          18 int cmp=memcmp(buffer, "carrot", 6);
          (gdb) n
          19 if (cmp) {
          (gdb) p cmp
          $1=-99
          (gdb) p buffer[0]
          $2=0 '\000'
          (gdb) n
          20 return -1;

          這里我們可以看到哪里出問(wèn)題了:buffer 中第一個(gè)字節(jié)的值是 0,這和 c 不同,因此我們的 buffer 和 carrot 不同。條件 if (cap) { 沒有跳轉(zhuǎn)到下一個(gè)循環(huán),而是跳到了 true 的情況,返回了 -1,表明與 carrot 匹配。出現(xiàn)這樣問(wèn)題的原因是,當(dāng)兩個(gè) buffers 相等的時(shí)候,memcmp 將會(huì)返回 0,當(dāng)它們不相等的時(shí)候,將返回非零值。但是我們沒有測(cè)試 memcmp 的返回值是否為 0,就直接在 if 條件中使用了它,這樣 C 會(huì)把所有的非零值都視為 true,這里返回的 -99 就會(huì)被判斷為 true。對(duì)于初學(xué)者而言,這是在 C 中會(huì)遇到的典型的錯(cuò)誤,我希望你不會(huì)再犯這樣的錯(cuò)誤。

          現(xiàn)在我們知道了錯(cuò)誤的原因,接下來(lái)去修復(fù) carrot 腳本中的錯(cuò)誤就非常簡(jiǎn)單了。但是正如你看到的,我們?cè)O(shè)法從 CKB 上獲取一筆錯(cuò)誤交易在運(yùn)行時(shí)的狀態(tài),然后通過(guò) GDB(一個(gè)業(yè)界常見的工具)來(lái)對(duì)其進(jìn)行調(diào)試。而且您在 GDB 上現(xiàn)有的工作流程和工具也可以在這里使用,是不是很棒?

          基于 REPL 的開發(fā)/調(diào)試

          然而,GDB 僅僅是現(xiàn)代軟件開發(fā)中的一部分。動(dòng)態(tài)語(yǔ)言在很大程度上占據(jù)了主導(dǎo)地位,很多程序員都使用基于 REPL 的開發(fā)/調(diào)試工作流。這與編譯語(yǔ)言中的 GDB 完全不同,基本上你需要的是一個(gè)運(yùn)行的環(huán)境,你可以輸入任何你想要與環(huán)境進(jìn)行交互的代碼,然后得到不同的結(jié)果。正如我們將在這里展示的,CKB 也會(huì)支持這種類型的開發(fā)/調(diào)試工作流。

          在這里,我們將使用 ckb-duktape 來(lái)展示基于 JavaScript 的 REPL。但是請(qǐng)注意,這只是一個(gè) demo 用來(lái)演示一下工作流程,沒有任何東西阻止您將自己喜愛的動(dòng)態(tài)語(yǔ)言(不管是 Ruby、Rython、Lisp 等等)移植到 CKB 中去,并為該語(yǔ)言啟動(dòng) REPL。

          首先,讓我們嘗試編譯 duktape:

          $ git clone https://github.com/nervosnetwork/ckb-duktape
          $ cd ckb-duktape
          $ sudo docker run --rm -it -v `pwd`:/code nervos/ckb-riscv-gnu-toolchain:bionic-20191012 bash
          root@982d1e906b76:/# cd /code
          root@982d1e906b76:/code# make
          riscv64-unknown-elf-gcc -Os -DCKB_NO_MMU -D__riscv_soft_float -D__riscv_float_abi_soft -Iduktape -Ic -Wall -Werror c/entry.c -c -o build/entry.o
          riscv64-unknown-elf-gcc -Os -DCKB_NO_MMU -D__riscv_soft_float -D__riscv_float_abi_soft -Iduktape -Ic -Wall -Werror duktape/duktape.c -c -o build/duktape.o
          riscv64-unknown-elf-gcc build/entry.o build/duktape.o -o build/duktape -lm -Wl,-static -fdata-sections -ffunction-sections -Wl,--gc-sections -Wl,-s
          riscv64-unknown-elf-gcc -Os -DCKB_NO_MMU -D__riscv_soft_float -D__riscv_float_abi_soft -Iduktape -Ic -Wall -Werror c/repl.c -c -o build/repl.o
          riscv64-unknown-elf-gcc build/repl.o build/duktape.o -o build/repl -lm -Wl,-static -fdata-sections -ffunction-sections -Wl,--gc-sections -Wl,-s
          root@982d1e906b76:/code# exit

          你需要在這里生成 build/repl 二進(jìn)制文件。和 carrot 的例子類似,我們先將 duktape REPL 的二進(jìn)制文件部署在 CKB 上:

          pry(main)> api=CKB::API.new
          pry(main)> wallet=CKB::Wallet.from_hex(api, "<your private key>")
          pry(main)> wallet2=CKB::Wallet.from_hex(api, CKB::Key.random_private_key)
          pry(main)> duktape_repl_data=File.read("build/repl")
          pry(main)> duktape_repl_data.bytesize=> 283048
          pry(main)> duktape_repl_tx_hash=wallet.send_capacity(wallet2.address, CKB::Utils.byte_to_shannon(300000), CKB::Utils.bin_to_hex(duktape_repl_data), fee: 310000)
          pry(main)> duktape_repl_data_hash=CKB::Blake2b.hexdigest(duktape_repl_data)
          pry(main)> duktape_repl_type_script=CKB::Types::Script.new(code_hash: duktape_repl_data_hash, args: "0x")
          pry(main)> duktape_repl_cell_dep=CKB::Types::CellDep.new(out_point: CKB::Types::OutPoint.new(tx_hash: duktape_repl_tx_hash, index: 0))

          我們還需要?jiǎng)?chuàng)建一筆包含 duktape 腳本的交易,我這里使用一個(gè)非常簡(jiǎn)單的腳本,當(dāng)然你可以加入更多的數(shù)據(jù),這樣你就可以在 CKB 上玩起來(lái)了!

          pry(main)> tx=wallet.generate_tx(wallet2.address, CKB::Utils.byte_to_shannon(100), use_dep_group: false, fee: 5000)
          pry(main)> tx.outputs[0].type=duktape_repl_type_script
          pry(main)> tx.cell_deps << duktape_repl_cell_dep
          pry(main)> tx.witnesses[0]="0x"

          然后讓我們把它轉(zhuǎn)存到文件中,并檢查 duktape 類型腳本的哈希:

          pry(main)> CKB::MockTransactionDumper.new(api, tx).write("duktape.json")=> 2765824
          pry(main)> duktape_repl_type_script.compute_hash=> "0xa8b79392c857e29cb283e452f2cd48a8e06c51af64be175e0fe0e2902c482837"

          與上面不同的是,我們不需要啟動(dòng) GDB,而是可以直接啟動(dòng)程序:

          $ ./target/release/ckb-debugger -g type -h 0xa8b79392c857e29cb283e452f2cd48a8e06c51af64be175e0fe0e2902c482837 -t duktape.json
          duk>

          你可以看到一個(gè) duk> 提示你輸入 JS 代碼!同樣,如果遇到錯(cuò)誤,請(qǐng)檢查是否需要更改類型腳本的哈希,或者使用正確的 duktape.json 路徑。我們看到常見的 JS 代碼可以在這里工作運(yùn)行:

          duk> print(1 + 2)
          3=undefined
          duk> function foo(a) { return a + 1; }=undefined
          duk> foo(123)=124

          您還可以使用與 CKB 相關(guān)的功能:

          duk> var hash=CKB.load_script_hash()=undefined
          duk> function buf2hex(buffer) { return Array.prototype.map.call(new Uint8Array(buffer), function(x) { return ('00' + x.toString(16)).slice(-2); }).join(''); }=undefined
          duk> buf2hex(hash)=a8b79392c857e29cb283e452f2cd48a8e06c51af64be175e0fe0e2902c482837

          請(qǐng)注意,我們?cè)谶@里得到的腳本哈希正是我們當(dāng)前執(zhí)行的類型腳本的哈希!這將證明 CKB 系統(tǒng)調(diào)試在這里是有效的,我們也可以嘗試更多有趣的東西:

          duk> print(CKB.SOURCE.OUTPUT)
          2=undefined
          duk> print(CKB.CELL.CAPACITY)
          0=undefined
          duk> capacity_field=CKB.load_cell_by_field(0, 0, CKB.SOURCE.OUTPUT, CKB.CELL.CAPACITY)=[object ArrayBuffer]
          duk> buf2hex(capacity_field)=00e40b5402000000

          這個(gè) 00e40b5402000000 可能在一開始看起來(lái)有點(diǎn)神秘,但是請(qǐng)注意 RISC-V 使用的是 little endian(低字節(jié)序),所以如果在這里我們將字節(jié)序列顛倒,我們將得到 00000002540be400,在十進(jìn)制中正好是 10000000000。還要記住,在 CKB 中容量使用的單位是 shannons,所以 10000000000 正好是 100 個(gè)字節(jié),這正是我們生成上面的交易時(shí),想要發(fā)送的代幣的數(shù)量!現(xiàn)在你看到了如何在 duktape 環(huán)境中與 CKB 愉快地玩耍了 。

          結(jié)論

          我們已經(jīng)介紹了兩種不同的在 CKB 中調(diào)試的過(guò)程,你可以隨意使用其中一種(或者兩種)。我已經(jīng)迫不及待地想看你們?cè)?CKB 上玩出花來(lái)啦!

          加入 Nervos Community

          Nervos Community 致力于成為最好的 Nervos 社區(qū),我們將持續(xù)地推廣和普 及 Nervos 技術(shù),深入挖掘 Nervos 的內(nèi)在價(jià)值,開拓 Nervos 的無(wú)限可能, 為每一位想要深入了解 Nervos Network 的人提供一個(gè)優(yōu)質(zhì)的平臺(tái)。

          背景簡(jiǎn)介

          手機(jī)淘寶客戶端在歷史上接過(guò)多種多樣的腳本引擎,用于支持的語(yǔ)言包括:js/python/wasm/lua,其中js引擎接過(guò)的就有:javascriptcore/duktape/v8/quickjs 等多個(gè)。眾多的引擎會(huì)面臨共同面臨包大小及性能相關(guān)的問(wèn)題,我們是否可以提供一套方案,在能支持業(yè)務(wù)需求的前提下,用一個(gè)引擎來(lái)支持盡可能多的語(yǔ)言,能較好的兼顧包大小較小和性能優(yōu)異。為了解決這個(gè)問(wèn)題,我們開始了 hyengine 的探索。

          二 設(shè)計(jì)簡(jiǎn)介

          "有hyengine就夠全家用了" - hyengine是為統(tǒng)一移動(dòng)技術(shù)所需的各種腳本語(yǔ)言(wasm/js/python 等)執(zhí)行引擎而生,以輕量級(jí)、高性能、多語(yǔ)言支持為設(shè)計(jì)和研發(fā)目標(biāo)。目前已通過(guò)對(duì) wasm3/quickjs 的 jit 編譯及 runtime 優(yōu)化,以極小包體積的代價(jià)實(shí)現(xiàn)了 wasm/js 執(zhí)行速度 2~3 倍的提升,未來(lái)將通過(guò)實(shí)現(xiàn)自有字節(jié)碼和 runtime 增加對(duì) python 及其他語(yǔ)言的支持。

          注:由于當(dāng)前手機(jī)絕大多數(shù)都已支持 arm64,hyengine 僅支持 arm64 的 jit 實(shí)現(xiàn)。

          注:由于 ios 不支持 jit,目前 hyengine 只有 android 版本。

          hyengine 整體分為兩大塊,編譯(compiler)部分及引擎(vm)部分。

          compiler 部分分為前端、中端、后端,其中前端部分復(fù)用現(xiàn)有腳本引擎的實(shí)現(xiàn),比如 js 使用 quickjs,wasm 使用 emscripten,中端計(jì)劃實(shí)現(xiàn)一套自己的字節(jié)碼、優(yōu)化器及字節(jié)碼轉(zhuǎn)換器,后端實(shí)現(xiàn)了 quickjs 和 wasm 的 jit 及匯編器和優(yōu)化器。

          vm 分為解釋器、runtime、api、調(diào)試、基礎(chǔ)庫(kù),由于人力有限,目前VM暫無(wú)完整的自有實(shí)現(xiàn),復(fù)用quickjs/wasm3 的代碼,通過(guò)實(shí)現(xiàn)一套自己的內(nèi)分配器及gc,和優(yōu)化現(xiàn)有runtime實(shí)現(xiàn)來(lái)提升性能。

          業(yè)務(wù)代碼(以wasm為例)通過(guò)下圖所示的流程,被編譯為可執(zhí)行代碼:

          c/c++ 代碼經(jīng)過(guò) emscripten 編譯變?yōu)?wasm 文件,wasm 經(jīng)過(guò) hyengine(wasm3) 加載并編譯為 arm64 指令,arm64 指令經(jīng)過(guò) optimizer 優(yōu)化產(chǎn)出優(yōu)化后的 arm64 指令,業(yè)務(wù)方通過(guò)調(diào)用入口 api 來(lái)執(zhí)行對(duì)應(yīng)代碼。

          注:hyengine 本身期望沉淀一套自己的底層(匯編級(jí)別)的基礎(chǔ)能力庫(kù),除了用于 jit 相關(guān)用途外,還計(jì)劃用于手機(jī)客戶端的包大小、性能優(yōu)化、調(diào)試輔助等場(chǎng)景。

          注:本方案業(yè)界的方舟編譯器和 graalvm 可能有一定相似度。

          三 實(shí)現(xiàn)介紹

          1 編譯(compiler)部分

          為了讓實(shí)現(xiàn)方案較為簡(jiǎn)單,hyengine 的編譯采用直接翻譯的方式,直接翻譯出來(lái)的代碼性能一般較慢,需要經(jīng)過(guò)優(yōu)化器的優(yōu)化來(lái)提升性能。下面是相關(guān)模塊的具體實(shí)現(xiàn):

          匯編器

          為了生成 cpu 能執(zhí)行的代碼,我們需要實(shí)現(xiàn)一個(gè)匯編器,將相關(guān)腳本的 opcode 翻譯成機(jī)器碼。

          匯編器的核心代碼基于 golang 的 arch 項(xiàng)目已有的指令數(shù)據(jù)根據(jù)腳本生成,并輔佐人工修正及對(duì)應(yīng)的工具代碼。

          單個(gè)匯編代碼示例如下:

          // Name: ADC
          // Arch: 32-bit variant
          // Syntax: ADC <Wd>, <Wn>, <Wm>
          // Alias: 
          // Bits: 0|0|0|1|1|0|1|0|0|0|0|Rm:5|0|0|0|0|0|0|Rn:5|Rd:5
          static inline void ADC_W_W_W(uint32_t *buffer, int8_t rd, int8_t rn, int8_t rm) {
              uint32_t code=0b00011010000000000000000000000000;
              code |=IMM5(rm) << 16;
              code |=IMM5(rn) << 5;
              code |=IMM5(rd);
              *buffer=code;
          }

          代碼的作用是匯編ADC , , 指令,第一個(gè)參數(shù)是存放機(jī)器碼的 buffer ,后三個(gè)參數(shù)分別為匯編指令的操作數(shù)Wd/Wn/Wm。代碼中第7行的 code 為機(jī)器碼的固定部分,第 8~10 行為將操作數(shù)對(duì)應(yīng)的寄存器編號(hào)放入機(jī)器碼對(duì)應(yīng)的位置(詳見注釋種的 Bits 部分),第 9 行為將機(jī)器碼放入 buffer 。其中IMM5表示取數(shù)值的低 5 位,因?yàn)榧拇嫫魇且粋€(gè) 5bits 長(zhǎng)的數(shù)字。這樣命名的好處是,可以直觀的將匯編器的方法名和其產(chǎn)生的機(jī)器碼的助記詞形式相關(guān)聯(lián)。

          其中IMM5實(shí)現(xiàn)如下:

          #define IMM5(v) (v & 0b11111)

          為了保證匯編方法的正確性,我們基于 golang 的 arch 項(xiàng)目中的gnucases.txt,采取機(jī)器生成 + 人工修正的方式,產(chǎn)出了如下格式的單測(cè)用例:

              // 0a011f1a|  adc w10, w8, wzr
              ADC_W_W_W(&buffer, R10, R8, RZR);
              assert(buffer==bswap32(0x0a011f1a));

          第一行注釋中前半部分為機(jī)器碼的大端表示,后半部分為機(jī)器碼對(duì)應(yīng)的匯編代碼。第二行為匯編器的匯編方法調(diào)用。第三行為匯編結(jié)果檢查,確認(rèn)結(jié)果和注釋中的機(jī)器碼一致,由于注釋中的機(jī)器碼為大端表示,需要做 byte swap 才和匯編結(jié)果匹配。

          反匯編器

          這里的反匯編器不包含完整的反匯編功能,目的是為了用于在優(yōu)化器中識(shí)別機(jī)器碼,取機(jī)器碼中的參數(shù)使用。簡(jiǎn)單舉例:

          #define IS_MOV_X_X(ins) \
              (IMM11(ins >> 21)==IMM11(HY_INS_TEMPLATE_MOV_X_X >> 21) && \
              IMM11(ins >> 5)==IMM11(HY_INS_TEMPLATE_MOV_X_X >> 5))

          這條指令就可以在優(yōu)化器中判斷某條指令是不是mov xd, xm,進(jìn)而可以通過(guò)如下代碼取出 xd 中 d 的具體數(shù)值:

          #define RM(ins) IMM5(ins >> 16)
          
          #define RN(ins) IMM5(ins >> 5)
          
          #define RD(ins) IMM5(ins)

          同樣的,我們?yōu)榉磪R編器也做了對(duì)應(yīng)的單測(cè):

          // e7031eaa|    mov x7, x30
          assert(IS_MOV_X_X(bswap32(0xe7031eaa)));

          wasm編譯

          編譯時(shí)我們會(huì)遍歷 wasm 模塊的每個(gè)方法,估算存放產(chǎn)物代碼所需的內(nèi)存空間,然后將方法中的字節(jié)碼翻譯為機(jī)器碼。

          其中核心的翻譯的整體實(shí)現(xiàn)是一個(gè)大的循環(huán) + switch,每遍歷一個(gè) opcode 即生成一段對(duì)應(yīng)的機(jī)器碼,代碼示例如下:

          M3Result h3_JITFunction(IH3JITState state, IH3JITModule hmodule,
                                  IH3JITFunction hfunction) {
              uint32_t *alloc=state->code + state->codeOffset;
              
              ......
              
              // prologue
              // stp        x28, x27, [sp, #-0x60]!
              // stp        x26, x25, [sp, #0x10]!
              // stp        x24, x23, [sp, #0x20]
              // stp        x22, x21, [sp, #0x30]
              // stp        x20, x19, [sp, #0x40]
              // stp        x29, x30, [sp, #0x50]
              // add        x20, sp, #0x50
              STP_X_X_X_I_PR(alloc + codeOffset++, R28, R27, RSP, -0x60);
              STP_X_X_X_I(alloc + codeOffset++, R26, R25, RSP, 0x10);
              STP_X_X_X_I(alloc + codeOffset++, R24, R23, RSP, 0x20);
              STP_X_X_X_I(alloc + codeOffset++, R22, R21, RSP, 0x30);
              STP_X_X_X_I(alloc + codeOffset++, R20, R19, RSP, 0x40);
              STP_X_X_X_I(alloc + codeOffset++, R29, R30, RSP, 0x50);
              ADD_X_X_I(alloc + codeOffset++, R29, RSP, 0x50);
              
              ......
              
              for (bytes_t i=wasm; i < wasmEnd; i +=opcodeSize) {
                  uint32_t index=(uint32_t)(i - wasm) / sizeof(u8);
                  uint8_t opcode=*i;
                  
                  ......
                  
                  switch (opcode) {
                  case OP_UNREACHABLE: {
                      BRK_I(alloc + codeOffset++, 0);
                      break;
                  }
          
                  case OP_NOP: {
                      NOP(alloc + codeOffset++);
                      break;
                  }
                  
                  ......
                  
                  case OP_REF_NULL:
                  case OP_REF_IS_NULL:
                  case OP_REF_FUNC:
                  default:
                      break;
                  }
          
                  if (spOffset > maxSpOffset) {
                      maxSpOffset=spOffset;
                  }
          
              }
              
              ......
              
              // return 0(m3Err_none)
              MOV_X_I(alloc + codeOffset++, R0, 0);
              // epilogue
              // ldp        x29, x30, [sp, #0x50]
              // ldp        x20, x19, [sp, #0x40]
              // ldp        x22, x21, [sp, #0x30]
              // ldp        x24, x23, [sp, #0x20]
              // ldp        x26, x25, [sp, #0x10]
              // ldp        x28, x27, [sp], #0x60
              // ret
              LDP_X_X_X_I(alloc + codeOffset++, R29, R30, RSP, 0x50);
              LDP_X_X_X_I(alloc + codeOffset++, R20, R19, RSP, 0x40);
              LDP_X_X_X_I(alloc + codeOffset++, R22, R21, RSP, 0x30);
              LDP_X_X_X_I(alloc + codeOffset++, R24, R23, RSP, 0x20);
              LDP_X_X_X_I(alloc + codeOffset++, R26, R25, RSP, 0x10);
              LDP_X_X_X_I_PO(alloc + codeOffset++, R28, R27, RSP, 0x60);
              RET(alloc + codeOffset++);
              
              ......
              
              return m3Err_none;
          }

          上述代碼會(huì)先生成方法的 prologue,然后 for 循環(huán)遍歷 wasm 字節(jié)碼,生產(chǎn)對(duì)應(yīng)的 arm64 機(jī)器碼,最后加上方法的 epilogue。

          字節(jié)碼生成機(jī)器碼以 wasm 的 opcode i32.add 為例:

          case OP_I32_ADD: {
              LDR_X_X_I(alloc + codeOffset++, R8, R19, (spOffset - 2) * sizeof(void *));
              LDR_X_X_I(alloc + codeOffset++, R9, R19, (spOffset - 1) * sizeof(void *));
              ADD_W_W_W(alloc + codeOffset++, R9, R8, R9);
              STR_X_X_I(alloc + codeOffset++, R9, R19, (spOffset - 2) * sizeof(void *));
              spOffset--;
              break;
          }

          代碼中的alloc是當(dāng)前正在編譯的方法的機(jī)器碼存放首地址,codeOffset是當(dāng)前機(jī)器碼相對(duì)于首地址的偏移,R8/R9代表我們約定的兩個(gè)臨時(shí)寄存器,R19存放的棧底地址,spOffset是運(yùn)行到當(dāng)前 opcode 時(shí)棧相對(duì)于棧底的偏移。

          這段代碼會(huì)生成 4 條機(jī)器碼,分別用于加載位于棧上spOffset - 2和spOffset - 1位置的兩條數(shù)據(jù),然后相加,再把結(jié)果存放到棧上spOffset - 2位置。由于 i32.add 指令會(huì)消耗 2 條棧上數(shù)據(jù),并生成 1 條棧上數(shù)據(jù),最終棧的偏移就要 -1。

          上述代碼生成的機(jī)器碼及其對(duì)應(yīng)助記形式如下:

          f9400a68: ldr    x8, [x19, #0x10]
          f9400e69: ldr    x9, [x19, #0x18]
          0b090109: add    w9, w8, w9
          f9000a69: str    x9, [x19, #0x10]

          x表示64位寄存器,w表示 64 位寄存器的低 32 位,由于 i32.add 指令是做 32 位加法,這里只需要加低 32 位即可。

          以如下 fibonacci 的 c 代碼:

          uint32_t fib_native(uint32_t n) {
              if (n < 2) return n;
              return fib_native(n - 1) + fib_native(n - 2);
          }

          編譯產(chǎn)生的 wasm 代碼:

              parse  |  load module: 61 bytes
              parse  |  found magic + version
              parse  |  ** Type [1]
              parse  |      type  0: (i32) -> i32
              parse  |  ** Function [1]
              parse  |  ** Export [1]
              parse  |      index:   0; kind: 0; export: 'fib'; 
              parse  |  ** Code [1]
              parse  |      code size: 29  
            compile  |  compiling: 'fib'; wasm-size: 29; numArgs: 1; return: i32
            compile  |  estimated constant slots: 3
            compile  |  start stack index: 1
            compile  |     0 | 0x20  .. local.get
            compile  |     1 | 0x41  .. i32.const
            compile  |       | .......... (const i32=2)
            compile  |     2 | 0x49  .. i32.lt_u
            compile  |     3 | 0x04  .. if
            compile  |     4 | 0x20  .... local.get
            compile  |     5 | 0x0f  .... return
            compile  |     6 | 0x0b  .. end
            compile  |     7 | 0x20  .. local.get
            compile  |     8 | 0x41  .. i32.const
            compile  |       | .......... (const i32=2)
            compile  |     9 | 0x6b  .. i32.sub
            compile  |    10 | 0x10  .. call
            compile  |       | .......... (func='fib'; args=1)
            compile  |    11 | 0x20  .. local.get
            compile  |    12 | 0x41  .. i32.const
            compile  |       | .......... (const i32=1)
            compile  |    13 | 0x6b  .. i32.sub
            compile  |    14 | 0x10  .. call
            compile  |       | .......... (func='fib'; args=1)
            compile  |    15 | 0x6a  .. i32.add
            compile  |    16 | 0x0f  .. return
            compile  |    17 | 0x0b   end
            compile  |  unique constant slots: 2; unused slots: 1
            compile  |  max stack slots: 7

          經(jīng)過(guò) hyengine jit 編譯的產(chǎn)出代碼如下:

              0x107384000: stp    x28, x27, [sp, #-0x60]!
              0x107384004: stp    x26, x25, [sp, #0x10]
              0x107384008: stp    x24, x23, [sp, #0x20]
              0x10738400c: stp    x22, x21, [sp, #0x30]
              0x107384010: stp    x20, x19, [sp, #0x40]
              0x107384014: stp    x29, x30, [sp, #0x50]
              0x107384018: add    x29, sp, #0x50            ;=0x50 
              0x10738401c: mov    x19, x0
              0x107384020: ldr    x9, [x19]
              0x107384024: str    x9, [x19, #0x8]
              0x107384028: mov    w9, #0x2
              0x10738402c: str    x9, [x19, #0x10]
              0x107384030: mov    x9, #0x1
              0x107384034: ldr    x10, [x19, #0x8]
              0x107384038: ldr    x11, [x19, #0x10]
              0x10738403c: cmp    w10, w11
              0x107384040: csel   x9, x9, xzr, lo
              0x107384044: str    x9, [x19, #0x8]
              0x107384048: ldr    x9, [x19, #0x8]
              0x10738404c: cmp    x9, #0x0                  ;=0x0 
              0x107384050: b.eq   0x107384068
              0x107384054: ldr    x9, [x19]
              0x107384058: str    x9, [x19, #0x8]
              0x10738405c: ldr    x9, [x19, #0x8]
              0x107384060: str    x9, [x19]
              0x107384064: b      0x1073840dc
              0x107384068: ldr    x9, [x19]
              0x10738406c: str    x9, [x19, #0x10]
              0x107384070: mov    w9, #0x2
              0x107384074: str    x9, [x19, #0x18]
              0x107384078: ldr    x8, [x19, #0x10]
              0x10738407c: ldr    x9, [x19, #0x18]
              0x107384080: sub    w9, w8, w9
              0x107384084: str    x9, [x19, #0x10]
              0x107384088: add    x0, x19, #0x10            ;=0x10 
              0x10738408c: bl     0x10738408c
              0x107384090: ldr    x9, [x19]
              0x107384094: str    x9, [x19, #0x18]
              0x107384098: mov    w9, #0x1
              0x10738409c: str    x9, [x19, #0x20]
              0x1073840a0: ldr    x8, [x19, #0x18]
              0x1073840a4: ldr    x9, [x19, #0x20]
              0x1073840a8: sub    w9, w8, w9
              0x1073840ac: str    x9, [x19, #0x18]
              0x1073840b0: add    x0, x19, #0x18            ;=0x18 
              0x1073840b4: bl     0x1073840b4
              0x1073840b8: ldr    x8, [x19, #0x10]
              0x1073840bc: ldr    x9, [x19, #0x18]
              0x1073840c0: add    w9, w8, w9
              0x1073840c4: str    x9, [x19, #0x10]
              0x1073840c8: ldr    x9, [x19, #0x10]
              0x1073840cc: str    x9, [x19]
              0x1073840d0: b      0x1073840dc
              0x1073840d4: ldr    x9, [x19, #0x10]
              0x1073840d8: str    x9, [x19]
              0x1073840dc: mov    x0, #0x0
              0x1073840e0: ldp    x29, x30, [sp, #0x50]
              0x1073840e4: ldp    x20, x19, [sp, #0x40]
              0x1073840e8: ldp    x22, x21, [sp, #0x30]
              0x1073840ec: ldp    x24, x23, [sp, #0x20]
              0x1073840f0: ldp    x26, x25, [sp, #0x10]
              0x1073840f4: ldp    x28, x27, [sp], #0x60
              0x1073840f8: ret

          這段代碼運(yùn)行fib_native(40)耗時(shí) 1716ms,而 wasm3 解釋執(zhí)行 wasm 運(yùn)行同樣代碼耗時(shí) 3637ms,耗時(shí)只有解釋執(zhí)行的約 47%,但這夠快嗎?

          優(yōu)化器

          上面的代碼看起來(lái)似乎感覺沒什么大毛病,但是和 llvm 編譯出來(lái)的 native 代碼一比較,差距就出來(lái)的了。fib_native的 c 代碼經(jīng)過(guò) llvm 編譯的反匯編代碼如下:

          hyengine 產(chǎn)出的指令有 63 條,而 llvm 產(chǎn)出的指令只有 17 條,指令數(shù)量是 llvm 的約 3.7 倍!而實(shí)際運(yùn)行性能差距更大,hyengine 產(chǎn)出的代碼運(yùn)行fib_native(40)耗時(shí) 1716ms,llvm 產(chǎn)出的代碼耗時(shí) 308ms,耗時(shí)是 llvm 的約 5.57 倍。

          為了縮小差距,是時(shí)候做一些優(yōu)化了。

          1)優(yōu)化器的主要流程

          優(yōu)化器的主要流程如下:

          先將整個(gè)方法體的代碼按照跳轉(zhuǎn)指令(如:b/cbz 等)及其跳轉(zhuǎn)目標(biāo)地址做拆分,將方法體拆為多個(gè)代碼塊。然后對(duì)每個(gè)塊跑一下優(yōu)化的 pass ,對(duì)塊內(nèi)代碼進(jìn)行優(yōu)化。最后將優(yōu)化后的指令塊重新合并為一個(gè)方法體,優(yōu)化完成。

          需要把方法體拆分為塊的原因之一在于,優(yōu)化器可能會(huì)刪除或者增加代碼,這樣跳轉(zhuǎn)指令的跳轉(zhuǎn)目標(biāo)地址就會(huì)發(fā)生改變,需要重新計(jì)算跳轉(zhuǎn)目標(biāo),拆成塊后跳轉(zhuǎn)目標(biāo)比較容易計(jì)算。

          在塊拆分及優(yōu)化 pass 的實(shí)現(xiàn)中,會(huì)用到前面提到反匯編器和匯編器,這也是整個(gè)優(yōu)化器的核心依賴。

          我們以前文中的代碼的一部分為例子,做優(yōu)化流程的介紹,首先是塊拆分:

              ; --- code block 0 ---
              0x107384048: ldr    x9, [x19, #0x8]
              0x10738404c: cmp    x9, #0x0                  ;=0x0 
           -- 0x107384050: b.eq   0x107384068               ; b.eq 6
          |    
          |    ; --- code block 1 ---
          |   0x107384054: ldr    x9, [x19]
          |   0x107384058: str    x9, [x19, #0x8]
          |   0x10738405c: ldr    x9, [x19, #0x8]
          |   0x107384060: str    x9, [x19]
          |   0x107384064: b      0x1073840dc
          |    
          |    ; --- code block 2 ---
           -> 0x107384068: ldr    x9, [x19]
              0x10738406c: str    x9, [x19, #0x10]

          這里會(huì)根據(jù)代碼中的第四行的b.eq指令及其跳轉(zhuǎn)的目標(biāo)地址第 14 行作拆分,代碼為拆為了 3 個(gè)塊。原本第 11 行的 b 指令也要做一次拆分,但前面的b.eq已經(jīng)拆過(guò)了,就不再拆了。

          接下對(duì)會(huì)對(duì)拆分成塊后的代碼跑一堆優(yōu)化的 pass ,跑完后的結(jié)果如下:

              ; --- code block 0 ---
              0x104934020: cmp    w9, #0x2                  ;=0x2 
           -- 0x104934024: b.hs   0x104934038               ; b.hs 5
          |   
          |   ; --- code block 1 ---
          |   0x104934028: mov    x9, x20
          |   0x10493402c: mov    x21, x9
          |   0x104934030: mov    x20, x9
          |   0x104934034: b      0x104934068
          |
          |    ; --- code block 2 ---
           -> 0x104934038: sub    w22, w20, #0x2            ;=0x2

          在跑完一堆 pass 后代碼完全變了樣(關(guān)鍵優(yōu)化的實(shí)現(xiàn)請(qǐng)看下一節(jié)內(nèi)容),但可以看出 code block 1 的代碼從 5 條指令變成了 4 條,之前的b.eq被優(yōu)化為了b.hs跳轉(zhuǎn)的目標(biāo)地址的偏移也少 1,從 6 變?yōu)?5。

          最后把塊重新合并成為新的方法體指令:

              0x104934020: cmp    w9, #0x2                  ;=0x2 
              0x104934024: b.hs   0x104934038               ; b.hs 5
              0x104934028: mov    x9, x20
              0x10493402c: mov    x21, x9
              0x104934030: mov    x20, x9
              0x104934034: b      0x104934068
              0x104934038: sub    w22, w20, #0x2            ;=0x2

          2)關(guān)鍵優(yōu)化之寄存器分配

          3.7 倍代碼量的速度慢 5.57 倍的一個(gè)主要原因在于,我們生產(chǎn)的代碼中數(shù)據(jù)完全存放在棧中,棧在內(nèi)存上,各種ldr/str指令對(duì)內(nèi)存的訪問(wèn),就算數(shù)據(jù)在 cpu 的 l1 cache 上,也比對(duì)寄存器的訪問(wèn)慢 4 倍。為此,如果我們將數(shù)據(jù)盡量放在寄存器,減少對(duì)內(nèi)存的訪問(wèn),就可以進(jìn)一步提升性能。

          寄存器分配有一些較為成熟的方案,常用的包括:基于 live range 的線性掃描內(nèi)存分配,基于 live internal 的線性掃描內(nèi)存分配,基于圖染色的內(nèi)存分配等。在常見 jit 實(shí)現(xiàn),會(huì)采用基于 live internal 的線性掃描內(nèi)存分配方案,來(lái)做到產(chǎn)物性能和寄存器分配代碼的時(shí)間復(fù)雜度的平衡。

          為了實(shí)現(xiàn)的簡(jiǎn)單性,hyengine 使用了一種非主流的極簡(jiǎn)方案,基于代碼訪問(wèn)次數(shù)的線性掃描內(nèi)存分配,用人話說(shuō)就是:給代碼中出現(xiàn)次數(shù)最多的棧偏移分配寄存器。

          假設(shè)代碼如下(節(jié)選自 hyengine jit 產(chǎn)出代碼):

              0x107384020: ldr    x9, [x19]
              0x107384024: str    x9, [x19, #0x8]
              0x107384028: mov    w9, #0x2
              0x10738402c: str    x9, [x19, #0x10]
              0x107384030: mov    x9, #0x1
              0x107384034: ldr    x10, [x19, #0x8]
              0x107384038: ldr    x11, [x19, #0x10]

          對(duì)假設(shè)代碼的分配寄存器后代碼如下:

              0x107384020: ldr    x9, [x19]        ; 偏移0沒變
              0x107384024: mov    x20, x9          ; 偏移8變成x20
              0x107384028: mov    w9, #0x2
              0x10738402c: mov    x21, x9          ; 偏移16變成x21
              0x107384030: mov    x9, #0x1
              0x107384034: mov    x10, x20         ; 偏移8變成x20
              0x107384038: mov    x11, x21         ; 偏移16變成x21

          之前的 jit 產(chǎn)物代碼優(yōu)化后如下(注:做了少量指令融合):

              0x102db4000: stp    x28, x27, [sp, #-0x60]!
              0x102db4004: stp    x26, x25, [sp, #0x10]
              0x102db4008: stp    x24, x23, [sp, #0x20]
              0x102db400c: stp    x22, x21, [sp, #0x30]
              0x102db4010: stp    x20, x19, [sp, #0x40]
              0x102db4014: stp    x29, x30, [sp, #0x50]
              0x102db4018: add    x29, sp, #0x50            ;=0x50 
              0x102db401c: mov    x19, x0
              0x102db4020: ldr    x9, [x19]
              0x102db4024: mov    x20, x9
              0x102db4028: mov    x21, #0x2
              0x102db402c: mov    x9, #0x1
              0x102db4030: cmp    w20, w21
              0x102db4034: csel   x9, x9, xzr, lo
              0x102db4038: mov    x20, x9
              0x102db403c: cmp    x9, #0x0                  ;=0x0 
              0x102db4040: b.eq   0x102db4054
              0x102db4044: ldr    x9, [x19]
              0x102db4048: mov    x20, x9
              0x102db404c: str    x20, [x19]
              0x102db4050: b      0x102db40ac
              0x102db4054: ldr    x9, [x19]
              0x102db4058: mov    x21, x9
              0x102db405c: mov    x22, #0x2
              0x102db4060: sub    w9, w21, w22
              0x102db4064: mov    x21, x9
              0x102db4068: add    x0, x19, #0x10            ;=0x10 
              0x102db406c: str    x21, [x19, #0x10]
              0x102db4070: bl     0x102db4070
              0x102db4074: ldr    x21, [x19, #0x10]
              0x102db4078: ldr    x9, [x19]
              0x102db407c: mov    x22, x9
              0x102db4080: mov    x23, #0x1
              0x102db4084: sub    w9, w22, w23
              0x102db4088: mov    x22, x9
              0x102db408c: add    x0, x19, #0x18            ;=0x18 
              0x102db4090: str    x22, [x19, #0x18]
              0x102db4094: bl     0x102db4094
              0x102db4098: ldr    x22, [x19, #0x18]
              0x102db409c: add    w9, w21, w22
              0x102db40a0: mov    x21, x9
              0x102db40a4: str    x21, [x19]
              0x102db40a8: nop    
              0x102db40ac: mov    x0, #0x0
              0x102db40b0: ldp    x29, x30, [sp, #0x50]
              0x102db40b4: ldp    x20, x19, [sp, #0x40]
              0x102db40b8: ldp    x22, x21, [sp, #0x30]
              0x102db40bc: ldp    x24, x23, [sp, #0x20]
              0x102db40c0: ldp    x26, x25, [sp, #0x10]
              0x102db40c4: ldp    x28, x27, [sp], #0x60
              0x102db40c8: ret

          優(yōu)化后的代碼量從 63 條減少到 51 條,且內(nèi)存訪問(wèn)數(shù)量明顯減少,耗時(shí)也減少到 1361ms,耗時(shí)減少到 llvm 的約 4.42 倍。

          3)關(guān)鍵優(yōu)化之寄存器參數(shù)傳遞

          在寄存器分配優(yōu)化的最后一條中提到,在方法調(diào)用時(shí)需要把寄存器的值拷回棧,額外增加了內(nèi)存訪問(wèn)的開銷。其相關(guān)匯編代碼為:

              0x102db4068: add    x0, x19, #0x10            ;=0x10 
              0x102db406c: str    x21, [x19, #0x10]
              0x102db4070: bl     0x102db4070
              0x102db4074: ldr    x21, [x19, #0x10]

          而 arm64 的調(diào)用約定中,參數(shù)傳遞是通過(guò)寄存器來(lái)做的,這樣每次方法調(diào)用可以減少兩次內(nèi)存訪問(wèn)。
          這里把 wasm 的棧作為放入x0, 第一個(gè)參數(shù)x22直接放入x1,方法調(diào)用后的返回值x0直接放入x22,優(yōu)化后代碼如下:

              0x1057e405c: add    x0, x19, #0x10            ;=0x10 
              0x1057e4060: mov    x1, x22
              0x1057e4064: bl     0x1057e4064
              0x1057e4068: mov    x22, x0

          注:這里因?yàn)榻o棧偏移 0 也分配了寄存器,所以寄存器的編號(hào)比優(yōu)化前的代碼多 1 。
          同時(shí)將方法頭部取參數(shù)的代碼從:

              0x102db4020: ldr    x9, [x19]
              0x102db4024: mov    x20, x9

          優(yōu)化為:

              0x1057e4020: mov    x20, x1

          這里又減少了一次內(nèi)存訪問(wèn)和一條指令。
          優(yōu)化后最終完整的代碼如下:

              0x1057e4000: stp    x28, x27, [sp, #-0x60]!
              0x1057e4004: stp    x26, x25, [sp, #0x10]
              0x1057e4008: stp    x24, x23, [sp, #0x20]
              0x1057e400c: stp    x22, x21, [sp, #0x30]
              0x1057e4010: stp    x20, x19, [sp, #0x40]
              0x1057e4014: stp    x29, x30, [sp, #0x50]
              0x1057e4018: add    x29, sp, #0x50            ;=0x50 
              0x1057e401c: mov    x19, x0
              0x1057e4020: mov    x20, x1
              0x1057e4024: mov    x21, x20
              0x1057e4028: mov    x22, #0x2
              0x1057e402c: mov    x9, #0x1
              0x1057e4030: cmp    w21, w22
              0x1057e4034: csel   x9, x9, xzr, lo
              0x1057e4038: mov    x21, x9
              0x1057e403c: cmp    x9, #0x0                  ;=0x0 
              0x1057e4040: b.eq   0x1057e404c
              0x1057e4044: mov    x21, x20
              0x1057e4048: b      0x1057e409c
              0x1057e404c: mov    x22, x20
              0x1057e4050: mov    x23, #0x2
              0x1057e4054: sub    w9, w22, w23
              0x1057e4058: mov    x22, x9
              0x1057e405c: add    x0, x19, #0x10            ;=0x10 
              0x1057e4060: mov    x1, x22
              0x1057e4064: bl     0x1057e4064
              0x1057e4068: mov    x22, x0
              0x1057e406c: mov    x23, x20
              0x1057e4070: mov    x24, #0x1
              0x1057e4074: sub    w9, w23, w24
              0x1057e4078: mov    x23, x9
              0x1057e407c: add    x0, x19, #0x18            ;=0x18 
              0x1057e4080: mov    x1, x23
              0x1057e4084: bl     0x1057e4084
              0x1057e4088: mov    x23, x0
              0x1057e408c: add    w9, w22, w23
              0x1057e4090: mov    x22, x9
              0x1057e4094: mov    x20, x22
              0x1057e4098: nop    
              0x1057e409c: mov    x0, x20
              0x1057e40a0: ldp    x29, x30, [sp, #0x50]
              0x1057e40a4: ldp    x20, x19, [sp, #0x40]
              0x1057e40a8: ldp    x22, x21, [sp, #0x30]
              0x1057e40ac: ldp    x24, x23, [sp, #0x20]
              0x1057e40b0: ldp    x26, x25, [sp, #0x10]
              0x1057e40b4: ldp    x28, x27, [sp], #0x60
              0x1057e40b8: ret

          優(yōu)化后的代碼量從 51 條減少到 47 條,耗時(shí)也減少到 687ms,耗時(shí)減少到 llvm 的約 2.23 倍。雖然代碼量只減少了 4 條,但耗時(shí)顯著減少了約 50%。

          注:這個(gè)優(yōu)化僅對(duì)方法體比較短且調(diào)用頻繁的方法有顯著跳過(guò),方法體比較長(zhǎng)的代碼效果不明顯。

          4)關(guān)鍵優(yōu)化之特征匹配

          特征匹配就是在代碼中遍歷預(yù)設(shè)的代碼特征,對(duì)符合特征的代碼做相應(yīng)的優(yōu)化。
          比如上面代碼中的:

              0x1057e404c: mov    x22, x20
              0x1057e4050: mov    x23, #0x2
              0x1057e4054: sub    w9, w22, w23
              0x1057e4058: mov    x22, x9

          可以被優(yōu)化為:

              0x104934038: sub    w22, w20, #0x2            ;=0x2

          4 條指令變 1 條。

          5)優(yōu)化結(jié)果

          經(jīng)過(guò)上述多種優(yōu)化后,代碼變?yōu)椋?/span>

              0x104934000: stp    x24, x23, [sp, #-0x40]!
              0x104934004: stp    x22, x21, [sp, #0x10]
              0x104934008: stp    x20, x19, [sp, #0x20]
              0x10493400c: stp    x29, x30, [sp, #0x30]
              0x104934010: add    x29, sp, #0x30            ;=0x30 
              0x104934014: mov    x19, x0
              0x104934018: mov    x20, x1
              0x10493401c: mov    x9, x20
              0x104934020: cmp    w9, #0x2                  ;=0x2 
              0x104934024: b.hs   0x104934038
              0x104934028: mov    x9, x20
              0x10493402c: mov    x21, x9
              0x104934030: mov    x20, x9
              0x104934034: b      0x104934068
              0x104934038: sub    w22, w20, #0x2            ;=0x2 
              0x10493403c: add    x0, x19, #0x10            ;=0x10 
              0x104934040: mov    x1, x22
              0x104934044: bl     0x104934000
              0x104934048: mov    x22, x0
              0x10493404c: sub    w23, w20, #0x1            ;=0x1 
              0x104934050: add    x0, x19, #0x18            ;=0x18 
              0x104934054: mov    x1, x23
              0x104934058: bl     0x104934000
              0x10493405c: add    w9, w22, w0
              0x104934060: mov    x22, x9
              0x104934064: mov    x20, x9
              0x104934068: mov    x0, x20
              0x10493406c: ldp    x29, x30, [sp, #0x30]
              0x104934070: ldp    x20, x19, [sp, #0x20]
              0x104934074: ldp    x22, x21, [sp, #0x10]
              0x104934078: ldp    x24, x23, [sp], #0x40
              0x10493407c: ret

          優(yōu)化后的代碼量從 63 條減少到 32 條,耗時(shí)從 1716ms 減少到 493ms ,耗時(shí)減少到 llvm 的約 1.6 倍。

          quickjs 編譯

          注:js 的主要耗時(shí)在 runtime,所以目前 jit 只占 js 整體性能優(yōu)化的約 20%,后續(xù)將引入更多 jit 優(yōu)化細(xì)節(jié)。

          quickjs 的編譯流程和 wasm 類似,只是對(duì) opcode 的實(shí)現(xiàn)上會(huì)稍微復(fù)雜一些,以O(shè)P_object為例:

              // *sp++=JS_NewObject(ctx);
              // if (unlikely(JS_IsException(sp[-1])))
              //     goto exception;
          case OP_object: {
              MOV_FUNCTION_ADDRESS_TO_REG(R8, JS_NewObject);
              MOV_X_X(NEXT_INSTRUCTION, R0, CTX_REG);
              BLR_X(NEXT_INSTRUCTION, R8);
              STR_X_X_I(NEXT_INSTRUCTION, R0, R26, SP_OFFSET(0));
              CHECK_EXCEPTION(R0, R9);
              
              break;
          }

          這里首先通過(guò)MOV_FUNCTION_ADDRESS_TO_REG宏把要調(diào)用的JS_NewObject方法地址放入R8寄存器:

          #define MOV_FUNCTION_ADDRESS_TO_REG(reg, func)                                 \
          {                                                                          \
                  uintptr_t func##Address=(uintptr_t)func;                             \
                  MOVZ_X_I_S_I(NEXT_INSTRUCTION, reg, IMM16(func##Address), LSL, 0);     \
                  if (IMM16(func##Address >> 16) !=0) {                                 \
                      MOVK_X_I_S_I(NEXT_INSTRUCTION, reg, IMM16(func##Address >> 16),    \
                                   LSL, 16);                                             \
                  } else {                                                               \
                      NOP(NEXT_INSTRUCTION);                                             \
                  }                                                                      \
                  if (IMM16(func##Address >> 32) !=0) {                                 \
                      MOVK_X_I_S_I(NEXT_INSTRUCTION, reg, IMM16(func##Address >> 32),    \
                                   LSL, 32);                                             \
                  } else {                                                               \
                      NOP(NEXT_INSTRUCTION);                                             \
                  }                                                                      \
                  if (IMM16(func##Address >> 48) !=0) {                                 \
                      MOVK_X_I_S_I(NEXT_INSTRUCTION, reg, IMM16(func##Address >> 48),    \
                                   LSL, 48);                                             \
                  } else {                                                               \
                      NOP(NEXT_INSTRUCTION);                                             \
                  }                                                                      \
              }

          然后將CTX_REG(里面存的 ctx 地址)放入R0作為第一個(gè)參數(shù),并調(diào)用JS_NewObject,然后結(jié)果存入 js 棧的SP_OFFSET(0)位置。然后通過(guò)CHECK_EXCEPTION判斷結(jié)果是否存在異常:

          #define EXCEPTION(tmp)                                                     \
              LDR_X_X_I(NEXT_INSTRUCTION, tmp, CTX_REG, HYJS_BUILTIN_OFFSET(0));     \
              MOV_X_I(NEXT_INSTRUCTION, R0, SP_OFFSET(0));                           \
              BLR_X(NEXT_INSTRUCTION, tmp);
          
          #define CHECK_EXCEPTION(reg, tmp)                                          \
              MOV_X_I(NEXT_INSTRUCTION, tmp, ((uint64_t)JS_TAG_EXCEPTION<<56));      \
              CMP_X_X_S_I(NEXT_INSTRUCTION, reg, tmp, LSL, 0);                       \
              B_C_L(NEXT_INSTRUCTION, NE, 4 * sizeof(uint32_t));                     \
              EXCEPTION(tmp)

          就這一個(gè) opcode 生成的 arm64 機(jī)器碼就多達(dá) 13 條!而且這還不算多的。

          同樣是 fibonacci 的實(shí)現(xiàn),wasm 的 jit 產(chǎn)物代碼只有 32 條,而 quickjs 的有 467 條!!!又想起了被匯編所支配的恐懼。

          注:這么指令源于對(duì) builtin 的調(diào)用、引用計(jì)數(shù)、類型判斷。后面 vm 優(yōu)化將引用計(jì)數(shù)干掉后代碼量減少到 420 條。

          2 引擎(vm)部分

          因?yàn)?wasm 本身是強(qiáng)類型的字節(jié)碼,runtime 本身提供的能力較少,性能瓶頸也主要在代碼的解釋執(zhí)行,所以 vm 部分的基本沒有做優(yōu)化。而 quickjs 的字節(jié)碼作為弱類型的字節(jié)碼,其主要功能需要依賴 runtime 來(lái)實(shí)現(xiàn),同時(shí)由于語(yǔ)言本身接管了內(nèi)存管理,由此帶來(lái)的 gc 也開銷也比較明顯。

          在之前對(duì)某業(yè)務(wù)js代碼的性能分析后發(fā)現(xiàn),超過(guò) 50% 的性能開銷在內(nèi)存分配及 gc 上,為此引擎部分將主要介紹對(duì) quickjs 的內(nèi)存分配和 gc 優(yōu)化,部分 runtime 的 builtin 的快路徑、inline cache 目前優(yōu)化占比不高,僅做少量介紹。

          內(nèi)存分配器 hymalloc

          為了實(shí)現(xiàn) hyengine 對(duì) quickjs 性能優(yōu)化,同時(shí)兼顧 gc 優(yōu)化所需要的對(duì)內(nèi)存的管理權(quán),需要設(shè)計(jì)一套更快速(無(wú)鎖,非線程安全)的內(nèi)存分配器。同時(shí)需要考慮面向其他引擎可能需要的定制,來(lái)做到 hymalloc 的盡量通用。

          1)實(shí)現(xiàn)簡(jiǎn)介

          hymalloc 將內(nèi)存分為 19 個(gè)區(qū)(region),18 個(gè) small region/1 個(gè) large region。small region主要用來(lái)存放規(guī)則內(nèi)存,每個(gè)區(qū)的大小分從為 116 至 1916 bytes;large region 用于存放大于 9*16 bytes 的內(nèi)存。

          每個(gè)區(qū)可包含多個(gè)池(pool),每個(gè)池里面可包含多個(gè)目標(biāo)大小的條目(item)。large region 比較特殊,每個(gè) pool 里只有 1 個(gè)條目。在向系統(tǒng)申請(qǐng)內(nèi)存時(shí),按 pool 來(lái)做申請(qǐng),之后再將 pool 拆分成對(duì)應(yīng)的 item。

          每個(gè) small region 初始化有一個(gè)池,池的大小可配置,默認(rèn)為 1024 個(gè) item;large region 默認(rèn)是空的。

          區(qū)/塊/池的示意圖如下:

          這里對(duì)最關(guān)鍵的兩個(gè)數(shù)據(jù)結(jié)構(gòu)做下簡(jiǎn)單介紹:

          // hymalloc item
          struct HYMItem {
              union {
                  HYMRegion* region;     // set to region when allocated
                  HYMItem*   next;       // set to next free item when freed
              };
              size_t  flags;
              uint8_t ptr[0];
          };
          
          // hymalloc pool
          struct HYMPool {
              HYMRegion *region;
              HYMPool   *next;
              size_t    item_size;
          };

          其中 HYMItem 是前面提到的 item 的數(shù)據(jù)結(jié)構(gòu),這里的 item 的大小不固定,數(shù)據(jù)結(jié)構(gòu)本身更像是 item header描述,其中 flags 目前作為 gc 的特別標(biāo)記存在,ptr 用于取 item 的實(shí)際可用部分內(nèi)存的地址(通過(guò)&item->ptr獲取)。union 中的 region/next 是一個(gè)用來(lái)省內(nèi)存的設(shè)計(jì),在 item 被分配出去之前,next 的值指向 region 的下一個(gè)空閑 item;在 item 被分配出去之后,region 被設(shè)定為 item 所屬的 region 地址。
          region 的空閑 item 鏈表示意圖如下:

          在內(nèi)存分配時(shí),取鏈表的首個(gè) item 作為分配結(jié)果,鏈表如果為空,則向系統(tǒng)申請(qǐng)一個(gè)新的 pool 并把 pool 的item 放入鏈表,分配示意圖如下:

          分配代碼如下:

          static void* _HYMallocFixedSize(HYMRegion *region, size_t size) {
              // allocate new pool, if no free item exists
              if (region->free_item_list==NULL) {
                  // notice: large region's item size is 0, use 'size' instead
                  size_t item_size=region->item_size ? region->item_size : size;
                  int ret=_HYMAllocPool(region, region->pool_initial_item_count, item_size);
                  if (!ret) {
                      return NULL;
                  }
              }
              
              // get free list item head, and set region to item's region
              HYMItem *item=region->free_item_list;
              region->free_item_list=item->next;
              item->region=region;
              item->flags=0;
              
              return &item->ptr;
          }

          在內(nèi)存釋放時(shí),將 item 插入所屬 region 的空閑鏈表的頭部即可:

          void HYMFree(void *ptr) {
              HYMItem *item=(HYMItem *)((uint8_t *)ptr - HYM_ITEM_SIZE_OVERHEAD);
              
              // set item as head of region's free item list
              HYMRegion *region=item->region;
              HYMItem *first_item_in_region=region->free_item_list;
              region->free_item_list=item;
              item->next=first_item_in_region;
          
          }

          上述實(shí)現(xiàn)在簡(jiǎn)單的內(nèi)存分配/釋放測(cè)試 case 中,在 macbook m1 設(shè)備上比系統(tǒng)提供的 malloc/free 快約4倍。

          2)內(nèi)存 compact + update

          為了減少內(nèi)存占用,hymalloc 實(shí)現(xiàn)了部分內(nèi)存 compact ,可以清理完全未使用的 small region中的 pool 和 large region 的所有 pool。但目前沒有實(shí)現(xiàn) update 功能,無(wú)法做到真正的將不同 pool 之間的 item 相互拷貝,來(lái)做到更多內(nèi)存的節(jié)省。

          但從客戶端的使用場(chǎng)景來(lái)看,運(yùn)行代碼的內(nèi)存用量本身不高,compact + update 完整組合的實(shí)現(xiàn)復(fù)雜度較高,性價(jià)比不足。后續(xù)根據(jù)實(shí)際業(yè)務(wù)的使用情況,再評(píng)估實(shí)現(xiàn)完整 compact + update 的必要性。

          3)hymalloc 的局限性

          為了提升分配和釋放性能,hymalloc 的每個(gè) item 都有 header,需要額外占用內(nèi)存空間,這會(huì)導(dǎo)致一定的內(nèi)存浪費(fèi)。

          而且雖然 hymalloc 提供了 compact 方法來(lái)釋放空閑的內(nèi)存,但由于按照 pool 來(lái)批量申請(qǐng)內(nèi)存,只要 pool 中有一個(gè) item 被使用,那么這個(gè) pool 就不會(huì)被釋放,導(dǎo)致內(nèi)存不能被完全高效的釋放。

          另外,考慮到內(nèi)存被復(fù)用的概率,large region 的內(nèi)存會(huì)默認(rèn)按 256bytes 對(duì)齊來(lái)申請(qǐng),同樣可能存在浪費(fèi)。

          上述問(wèn)題可以通過(guò)設(shè)定更小的 pool 的默認(rèn) item 數(shù)量,及更小的對(duì)齊尺寸,犧牲少量性能,來(lái)減少內(nèi)存浪費(fèi)。

          后續(xù)可以引入更合理的數(shù)據(jù)結(jié)構(gòu),以及更完善的 compact + update 機(jī)制,來(lái)減少內(nèi)存浪費(fèi)。

          垃圾回收器 hygc

          quickjs 的原本的gc基于引用計(jì)數(shù) + mark sweep,設(shè)計(jì)和實(shí)現(xiàn)本身比較簡(jiǎn)潔高效,但未實(shí)現(xiàn)分代、多線程、compact、閑時(shí) gc、拷貝 gc,使得 gc 在整體執(zhí)行耗時(shí)中的占比較高,同時(shí)也存在內(nèi)存碎片化帶來(lái)的潛在性能降低。另外由于引用計(jì)數(shù)的存在,jit 生成的代碼中會(huì)存在大量的引用計(jì)數(shù)操作的指令,使得代碼體積較大。

          為了實(shí)現(xiàn) hyengine 對(duì) quickjs 性能優(yōu)化,減少 gc 在整體耗時(shí)種的占比,減少 gc 可能導(dǎo)致的長(zhǎng)時(shí)間運(yùn)行停止。參考 v8 等其他先進(jìn)引擎的 gc 設(shè)計(jì)思路,實(shí)現(xiàn)一套適用于移動(dòng)端業(yè)務(wù)的,輕量級(jí)、高性能、實(shí)現(xiàn)簡(jiǎn)單的 gc。

          注:本實(shí)現(xiàn)僅僅針對(duì)于 quickjs,后續(xù)可能會(huì)衍生出通用的 gc 實(shí)現(xiàn)。

          注:為了保障業(yè)務(wù)體驗(yàn)不出現(xiàn)卡頓,需要將 gc 的暫停時(shí)間控制在 30ms 內(nèi)。

          1)常用垃圾回收實(shí)現(xiàn)

          常用的垃圾回收主要有 3 大類:

          • 引用計(jì)數(shù)給每個(gè)對(duì)象加一個(gè)引用數(shù)量,多一個(gè)引用數(shù)量 +1,少一個(gè)引用數(shù)量 -1,如果引用數(shù)量為 0 則釋放。弊端:無(wú)法解決循環(huán)引用問(wèn)題。
          • mark sweep遍歷對(duì)象,標(biāo)記對(duì)象是否有引用,如果沒有請(qǐng)用則清理掉。
          • 拷貝 gc遍歷對(duì)象,標(biāo)記對(duì)象是否有引用,把有引用的對(duì)象拷貝一份新的,丟棄所有老的內(nèi)存。

          基于這三大類會(huì)有一些衍生,來(lái)實(shí)現(xiàn)多線程等支持,比如:

          • 三色標(biāo)記 gc遍歷對(duì)象,標(biāo)記對(duì)象是否有引用,狀態(tài)比單純的有引用(黑色)和無(wú)引用(白色)多一個(gè)中間狀態(tài)標(biāo)記中/不確定(灰色),可支持多線程。

          為了盡可能減少 gc 暫停時(shí)間并減少 js 執(zhí)行耗時(shí),hygc 采用多線程三色 gc 方案。在業(yè)務(wù) case 測(cè)試中,發(fā)現(xiàn)本身內(nèi)存使用量并不大,故沒有引入分代支持。

          2)hygc 的業(yè)務(wù)策略

          hygc 計(jì)劃將策略可以暴露給用戶,用于滿足不同使用場(chǎng)景的性能需求,提供:無(wú) gc、閑時(shí) gc、多線程 gc 三種選項(xiàng),應(yīng)對(duì)不同場(chǎng)景對(duì)內(nèi)存和性能的不同訴求。業(yè)務(wù)根據(jù)實(shí)際需求選擇 gc 策略,建議對(duì) gc 策略設(shè)置開關(guān),避免所選的 gc 策略可能導(dǎo)致非預(yù)期的結(jié)果。

          • 無(wú) gc運(yùn)行期不觸發(fā) gc 操作。待代碼完全運(yùn)行完畢銷毀 runtime 時(shí)做一次 full gc 整體釋放內(nèi)存。
          • 閑時(shí) gc運(yùn)行期不觸發(fā) gc 操作,運(yùn)行結(jié)束后在異步線程做 gc。代碼完全運(yùn)行完畢銷毀 runtime 時(shí)做一次 full gc 整體釋放內(nèi)存。
          • 默認(rèn) gc運(yùn)行期會(huì)觸發(fā) gc。代碼完全運(yùn)行完畢銷毀 runtime 時(shí)做一次 full gc 整體釋放內(nèi)存。

          我們的某個(gè)業(yè)務(wù)case就可以設(shè)定無(wú) gc 或閑時(shí) gc,因?yàn)榇a運(yùn)行期間沒有內(nèi)存能被回收,gc 是在浪費(fèi)時(shí)間。

          3)hygc 的實(shí)現(xiàn)方案

          quickjs 原本采用引用計(jì)數(shù) + mark sweep 結(jié)合的 gc 方案,在 gc 優(yōu)化時(shí)被移除,并替換為新的多線程三色標(biāo)記gc 方案。hygc 的實(shí)現(xiàn)復(fù)用了部分原本 quickjs 的代碼,做到盡可能簡(jiǎn)單的實(shí)現(xiàn)所需功能。

          hygc 的三色標(biāo)記流程(單線程版本):

          首先,收集根對(duì)象的主要操作是掃描 js 線程的棧,并將線程棧上的 js 對(duì)象和 js 調(diào)用棧關(guān)聯(lián)的對(duì)象收集起來(lái),作為三色標(biāo)記的根對(duì)象。然后,從根對(duì)象作為標(biāo)記入口,依次遞歸標(biāo)記子對(duì)象。遍歷 gc_obj_list(quickjs 的所有需要 gc 的對(duì)象都在這個(gè)雙向鏈表上),將沒有被標(biāo)記到的對(duì)象放入 tmp_obj_list。最后,釋放 tmp_obj_list 中的對(duì)象。

          單線程的 gc 會(huì)在 gc 過(guò)程中完全暫停 js 的執(zhí)行,存在潛在的業(yè)務(wù)卡頓風(fēng)險(xiǎn)(僅僅是潛在,由于實(shí)際業(yè)務(wù)的內(nèi)存使用量較小,暫并未出現(xiàn)由 gc 導(dǎo)致的卡頓),并且會(huì)讓js的執(zhí)行時(shí)間相對(duì)較長(zhǎng)。為此 hygc 引入了多線程的三色標(biāo)記,其流程如下:

          在多線程版本中,存在 js 和 gc 兩個(gè)線程,js 線程完成根對(duì)象收集及老對(duì)象轉(zhuǎn)移到異步 gc 鏈表,然后 js 繼續(xù)執(zhí)行。gc 線程會(huì)先將老對(duì)象的三色標(biāo)記全設(shè)為 0,然后開始標(biāo)記存活對(duì)象,然后對(duì)垃圾對(duì)象進(jìn)行收集。這里將垃圾對(duì)象的釋放拆分成了 2 個(gè)階段,一個(gè)是可以在 gc 線程執(zhí)行的垃圾對(duì)象相關(guān)屬性修改及置空,另一個(gè)是需要在 js 線程做的內(nèi)存釋放,這么做的原因是 hymalloc 不是線程安全的。這樣 js 線程中的 gc 操作就只剩下相對(duì)不耗時(shí)的根對(duì)象收集、老對(duì)象轉(zhuǎn)移、內(nèi)存釋放三個(gè)操作。

          注:令人悲傷的是,由于 mark 和垃圾回收仍然只在單獨(dú)一個(gè)線程完成,這里只用到了兩種顏色做標(biāo)記,灰色實(shí)際上沒用到。后續(xù)優(yōu)化讓 hygc 實(shí)現(xiàn)和 quickjs 原本的 gc 能夠共存,讓 gc 的遷移風(fēng)險(xiǎn)更低。

          4)hygc 的局限性

          hygc 的異步線程在做垃圾回收時(shí),僅僅會(huì)對(duì)老對(duì)象做 gc,在完成老對(duì)象轉(zhuǎn)移后的新對(duì)象將不會(huì)參與 gc,可能會(huì)造成內(nèi)存使用峰值的提升,提升程度與 gc 線程的執(zhí)行耗時(shí)相關(guān)。

          此問(wèn)題后續(xù)也將根據(jù)實(shí)際情況,判斷是否進(jìn)行方案優(yōu)化來(lái)解決。

          其他優(yōu)化舉例

          1)global 對(duì)象的 inline cache

          quickjs 的 global 對(duì)象的操作被單獨(dú)編譯為了OP_get_var/OP_put_var等 op ,而這兩個(gè) op 的實(shí)現(xiàn)格外的慢,為此我們對(duì) global object 訪問(wèn)加上了 inline cache。對(duì) js 的對(duì)象屬性訪問(wèn)可以簡(jiǎn)化理解為在遍歷數(shù)組來(lái)找到想要的屬性,inline cache 的目的就是緩存住某段代碼訪問(wèn)的屬性所在的數(shù)組中的偏移,這樣下次取就直接用偏移來(lái)取了,不用再做重復(fù)的屬性數(shù)組遍歷。

          global inline cache 的數(shù)據(jù)結(jié)構(gòu)如下:

          typedef struct {
              JSAtom prop;  // property atom
              int offset;   // cached property offset
              void *obj;    // global_obj or global_var_obj
          } HYJSGlobalIC;

          這里的第 4 行的void *obj比較特殊,原因在于 quickjs 的 global 可能存在 context 對(duì)象的 global_obj 或 global_var_obj 中,具體存在哪個(gè)里面需要一并放入 cache 中。
          具體代碼實(shí)現(xiàn)如下:

          case OP_get_var: { // 73
              
              JSAtom atom=get_u32(buf + i + 1);
              
              uint32_t cache_index=hyjs_GetGlobalICOffset(ctx, atom);
              JSObject obj;
              JSShape shape;
          
              LDR_X_X_I(NEXT_INSTRUCTION, R8, CTX_REG, (int32_t)((uintptr_t)&ctx->global_ic - (uintptr_t)ctx));
              ADD_X_X_I(NEXT_INSTRUCTION, R8, R8, cache_index * sizeof(HYJSGlobalIC));
              LDP_X_X_X_I(NEXT_INSTRUCTION, R0, R9, R8, 0);
              CBZ_X_L(NEXT_INSTRUCTION, R9, 12 * sizeof(uint32_t)); // check cache exsits
              LSR_X_X_I(NEXT_INSTRUCTION, R1, R0, 32); // get offset
              LDR_X_X_I(NEXT_INSTRUCTION, R2, R9, (int32_t)((uintptr_t)&obj.shape - (uintptr_t)&obj)); // get shape
              ADD_X_X_I(NEXT_INSTRUCTION, R2, R2, (int32_t)((uintptr_t)&shape.prop - (uintptr_t)&shape)); // get prop
              LDR_X_X_W_E_I(NEXT_INSTRUCTION, R3, R2, R1, UXTW, 3); // get prop
              LSR_X_X_I(NEXT_INSTRUCTION, R3, R3, 32);
              CMP_W_W_S_I(NEXT_INSTRUCTION, R0, R3, LSL, 0);
              B_C_L(NEXT_INSTRUCTION, NE, 5 * sizeof(uint32_t));
              LDR_X_X_I(NEXT_INSTRUCTION, R2, R9, (int32_t)((uintptr_t)&obj.prop - (uintptr_t)&obj)); // get prop
              LSL_W_W_I(NEXT_INSTRUCTION, R1, R1, 4); // R1 * sizeof(JSProperty)
              LDR_X_X_W_E_I(NEXT_INSTRUCTION, R0, R2, R1, UXTW, 0); // get value
              
              B_L(NEXT_INSTRUCTION, 17 * sizeof(uint32_t));
          
              MOV_FUNCTION_ADDRESS_TO_REG(R8, HYJS_GetGlobalVar);
              
              MOV_X_X(NEXT_INSTRUCTION, R0, CTX_REG);
              MOV_IMM32_TO_REG(R1, atom);
              MOV_X_I(NEXT_INSTRUCTION, R2, opcode - OP_get_var_undef);
              MOV_X_I(NEXT_INSTRUCTION, R3, cache_index);
              BLR_X(NEXT_INSTRUCTION, R8);
          
              CHECK_EXCEPTION(R0, R9);
          
              STR_X_X_I(NEXT_INSTRUCTION, R0, R26, SP_OFFSET(0));
              
              i +=4;
              break;
          }

          首先是第5行的hyjs_GetGlobalICOffset,這個(gè)方法會(huì)為當(dāng)前 opcode 分配一個(gè) inline cache 的 cache_index,這個(gè) cache_index 會(huì)在第 31 行設(shè)定為HYJS_GetGlobalVar方法調(diào)用的第 4 個(gè)參數(shù)。代碼的第 9 行到第 19 行,會(huì)根據(jù) cache_index 取 cache,并根據(jù) cache 中的 offset,取 global 對(duì)象對(duì)應(yīng)偏移里存的 prop(也就是屬性 id,數(shù)據(jù)類型是 atom),和當(dāng)前需要取的對(duì)象的屬性的 atom 比較,確認(rèn) cache 是否仍然有效。如果 cache 有效則通過(guò)第 20-22 行代碼直接取對(duì)象屬性數(shù)組,如果無(wú)效則走到第 26 行的慢路徑,遍歷屬性數(shù)組,并更新 inline cache。

          2)builtin 的快路徑優(yōu)化

          快路徑優(yōu)化是將代碼中的某些執(zhí)行概率更高的部分,單獨(dú)提出來(lái),來(lái)避免冗余代碼的執(zhí)行拖慢性能。

          以 Array.indexOf 的實(shí)現(xiàn)為例:

          static JSValue hyjs_array_indexOf(JSContext *ctx, JSValueConst func_obj,
                                            JSValueConst obj,
                                            int argc, JSValueConst *argv, int flags)
          {
              ......
          
              res=-1;
              if (len > 0) {
          
                  ......
                  
                  // fast path
                  if (JS_VALUE_GET_TAG(element)==JS_TAG_INT) {
                      for (; n < count; n++) {
                          if (JS_VALUE_GET_PTR(arrp[n])==JS_VALUE_GET_PTR(element)) {
                              res=n;
                              goto done;
                          }
                      }
                      goto property_path;
                  }
                  
                  // slow path
                  for (; n < count; n++) {
                      
                      if (js_strict_eq2(ctx, JS_DupValue(ctx, argv[0]),
                                        JS_DupValue(ctx, arrp[n]), JS_EQ_STRICT)) {
                          res=n;
                          goto done;e
                      }
                  }
                  
                  ......
              }
           done:
              return JS_NewInt64(ctx, res);
          
           exception:
              return JS_EXCEPTION;
          }

          原本的實(shí)現(xiàn)是從第 23 行開始的慢路徑,這里需要調(diào)用js_strict_eq2方法來(lái)判斷數(shù)組 index 是否相等,這個(gè)比較方法會(huì)相對(duì)比較重。而實(shí)際上 index 絕大多數(shù)情況都是 int 類型,所以提出來(lái)第 12 行的快路徑,如果 index 本身是 int 類型,那么直接做 int 類型數(shù)據(jù)的比較,就會(huì)比調(diào)用 js_strict_eq2 來(lái)比較要快。

          四 優(yōu)化結(jié)果

          性能測(cè)試設(shè)備基于 m1(arm64) 芯片的 macbook ,wasm 業(yè)務(wù)性能測(cè)試基于 huawei mate 8 手機(jī);測(cè)試結(jié)果選擇方法為每個(gè) case 跑 5 次,取排第 3 位的結(jié)果;測(cè)試 case 選擇為斐波那契數(shù)列、benchmark、業(yè)務(wù) case 三種,以評(píng)估不同場(chǎng)景下優(yōu)化帶來(lái)的性能變化。

          1 wasm 性能

          注:在業(yè)務(wù) case 中得出的時(shí)間是單幀渲染的整體耗時(shí),包括 wasm 執(zhí)行和渲染耗時(shí)兩部分。

          注:coremark hyengine jit 耗時(shí)是 llvm 編譯版本的約 3 倍,原因在于對(duì)計(jì)算指令優(yōu)化不足,后續(xù)可在優(yōu)化器中對(duì)更多計(jì)算指令進(jìn)行優(yōu)化。

          注:上述測(cè)試編譯優(yōu)化選項(xiàng)為 O3。

          2 js性能

          注:microbench 的部分單項(xiàng)在 gc 優(yōu)化上有負(fù)向的優(yōu)化,使得整體優(yōu)化的提升并不明顯,但改單項(xiàng)對(duì)業(yè)務(wù)影響不大。

          注:從業(yè)務(wù) case 上可以看出,vm 優(yōu)化所帶來(lái)的提升遠(yuǎn)大于目前 jit 帶來(lái)的提升,原因在于 jit 目前引入的優(yōu)化方式較少,仍有大量的優(yōu)化空間。另外 case 1 在 v8 上,jit 比 jitless 帶來(lái)的提升也只有 30% 左右。在 jit 的實(shí)現(xiàn)中,單項(xiàng)的優(yōu)化單來(lái)可能帶來(lái)的提升只有 1% 不到,需要堆幾十上百個(gè)不同的優(yōu)化,來(lái)讓性能做到比如 30% 的提升,后續(xù)會(huì)更具性能需求及開發(fā)成本來(lái)做均衡選擇。
          注:上述測(cè)試編譯優(yōu)化選項(xiàng)為 Os。

          五 后續(xù)計(jì)劃

          后續(xù)計(jì)劃主要分為 2 個(gè)方向:性能優(yōu)化、多語(yǔ)言支持,其中性能優(yōu)化將會(huì)持續(xù)進(jìn)行。
          性能優(yōu)化點(diǎn)包括:

          • 編譯器優(yōu)化,引入自有字節(jié)碼支持。
          • 優(yōu)化器優(yōu)化,引入更多優(yōu)化pass。
          • 自有 runtime,熱點(diǎn)方法匯編實(shí)現(xiàn)。

          六 參考內(nèi)容

          • wasm3: https://github.com/wasm3/wasm3
          • quickjs: https://bellard.org/quickjs/
          • v8: https://chromium.googlesource.com/v8/v8.git
          • javascriptcore: https://opensource.apple.com/tarballs/JavaScriptCore/
          • golang/arch: https://github.com/golang/arch
          • libmalloc: https://opensource.apple.com/tarballs/libmalloc/
          • Trash talk: the Orinoco garbage collector: https://v8.dev/blog/trash-talk
          • JavaScript engine fundamentals: Shapes and Inline Caches:https://mathiasbynens.be/notes/shapes-ics
          • cs143: https://web.stanford.edu/class/cs143/
          • C in ASM(ARM64):https://www.zhihu.com/column/c_142064221

          作者 | 知兵

          原文鏈接:https://developer.aliyun.com/article/848417?utm_content=g_1000316862

          本文為阿里云原創(chuàng)內(nèi)容,未經(jīng)允許不得轉(zhuǎn)載。


          主站蜘蛛池模板: 性无码免费一区二区三区在线| 91精品一区国产高清在线| 视频在线观看一区| 国产在线视频一区| 国产成人av一区二区三区在线| 精品一区二区三区高清免费观看 | 亚洲熟妇无码一区二区三区导航| 国产成人精品无码一区二区三区| 国产精品熟女一区二区| 大帝AV在线一区二区三区| 无码国产精品一区二区免费vr | 蜜桃传媒视频麻豆第一区| 亚洲熟妇av一区二区三区漫画| 日本人的色道www免费一区| 国产天堂一区二区综合| 国产在线乱子伦一区二区| 人妻少妇久久中文字幕一区二区| 无码精品一区二区三区免费视频| 亚洲av无码一区二区三区乱子伦 | aⅴ一区二区三区无卡无码| 精品国产高清自在线一区二区三区| 亚洲欧美日韩中文字幕一区二区三区| 亚洲一区二区三区AV无码| 国产一区二区精品久久| 国产一区二区在线|播放| 色窝窝免费一区二区三区| 国产精品美女一区二区| 国产日韩AV免费无码一区二区三区| 国产免费无码一区二区| 亚洲高清日韩精品第一区| 久久中文字幕无码一区二区| 国产精品第一区第27页| 亚洲AV本道一区二区三区四区| 成人国内精品久久久久一区| 波多野结衣高清一区二区三区| 无码av不卡一区二区三区| 在线观看日韩一区| 国产一区二区三区国产精品| 亚洲AV永久无码精品一区二区国产| 中文无码精品一区二区三区 | 国产SUV精品一区二区88|