從字節碼層面剖析以太坊智能合約創建原理

1. 引言

閱讀完本文之后,將能理解一下字節碼含義:

608060405260405160893803806089833981016040819052601e916025565b600055603d565b600060208284031215603657600080fd5b5051919050565b603f80604a6000396000f3fe6080604052600080fdfea26469706673582212204a131c1478e0e7bb29267fd8f6d38a660b40a25888982bd6618b720d4498b6b464736f6c634300080700330000000000000000000000000000000000000000000000000000000000000001

本文解釋了:

  • 以太坊智能合約在 字節碼層面 是如何被構造的,
  • 以及構造函數參數是如何被解釋的。

本文基本結構為:

  • Solidity 中的 creationCode
  • 初始化代碼(Init code)
    • 可支付(payable)構造函數合約
    • 不可支付(non-payable)構造函數合約
  • 運行時代碼(Runtime code)
    • 運行時代碼解析
  • 帶參數的構造函數

從宏觀上看,部署合約的錢包會向 空地址(null address) 發送一筆交易,交易數據分為三部分:

<init code> <runtime code> <constructor parameters>

三者合起來被稱為 創建代碼(creation code)。EVM 會首先執行 init code。如果 init code 編碼正確,這段執行會把運行時代碼存儲到區塊鏈上。

在 EVM 規范中,并沒有強制要求布局必須是 init coderuntime codeconstructor parameters。理論上可以是 init codeconstructor parameters 然后 runtime code。這只是 Solidity 的約定。但是 init code 必須在最前面,否則 EVM 不知道從哪里開始執行。

本文假設讀者已經了解以下內容:

  • Solidity(可參看 Solidity 免費教程)。
  • EVM 操作碼基礎

2. Solidity 中的 creationCode

Solidity 提供了一個機制,可以通過 creationCode 關鍵字獲取合約創建交易中要部署的字節碼。示例如下:

contract ValueStorage {  uint256 public value;  constructor(uint256 value_) {  value = value_;  }  
}contract GetCreationCode {  function get() external returns (bytes memory creationCode) {  creationCode = type(Simple).creationCode;  }  
}

需要注意的是,這 不包含構造函數參數。參數會作為合約部署過程中運行的字節碼一部分附加上去。至于 init codecreationCode)和參數是如何組織的,本文將進行解釋。

3. Init code(初始化代碼)

init code 是創建代碼的一部分,負責部署合約。先看一個最簡單的智能合約。稍后會解釋為什么要加上一個 payable 構造函數。

3.1 Init code(初始化代碼)——可支付(Payable)構造函數合約

pragma solidity 0.8.17; // optimizer: 200 runs
contract Minimal {  constructor() payable {}  
}

要獲得編譯結果,可以在 Remix 部署交易后,復制交易輸入中的 “input” 字段來獲取交易創建字節碼:
在這里插入圖片描述

復制出高亮部分后,得到:

0x6080604052603f8060116000396000f3fe6080604052600080fdfea2646970667358221220d03248cf82928931c158551724bebac67e407e6f3f324f930c4cf1c36e16328764736f6c63430008110033

當然,這串字節碼很難直接閱讀。但可以把它拆成兩部分。
在這里插入圖片描述

看起來似乎在隨機分割字節碼,但稍后會更清楚地解釋。

如果把第一部分復制到 EVM Codes,并把字節碼轉換為助記符(mnemonics),就會得到以下輸出(已添加注釋):

// 分配自由內存指針  
PUSH1 0x80  
PUSH1 0x40  
MSTORE// 運行時代碼的長度  
PUSH1 0x3f   
DUP1// 運行時代碼開始的位置  
PUSH1 0x11   
PUSH1 0x00  // 從 calldata 復制運行時代碼到內存  
CODECOPY// 此步驟會部署運行時代碼  
PUSH1 0x00  
RETURN  
INVALID

在上圖中被高亮的部分就是 運行時代碼,其大小為 63 字節(十六進制 0x3f)。它從內存的第 17 個索引(十六進制 0x11)開始。這也解釋了上面助記符解析中 0x3f0x11 的來源。

從宏觀上看,這段 init code 中主要做了三件事:

  • 設置自由內存指針(用于記錄下一個可寫入的內存位置)。
  • 使用 CODECOPY 操作碼把運行時代碼復制到該內存位置。
  • 最后,把包含運行時代碼的內存區域返回給 EVM,EVM 會把它存儲為新合約的運行時字節碼。

3.2 Init code(初始化代碼)——不可支付(Non-payable)構造函數合約

pragma solidity 0.8.17; // optimizer: 200 runs
contract Minimal {constructor() {}
}

來看一下當構造函數不是 payable 時生成的字節碼,并分析其中的區別。以下是編譯器的輸出:

6080604052348015600f57600080fd5b50603f80601d6000396000f3fe6080604052600080fdfea2646970667358221220a6271a05446e269126897aea62fd14e86be796da8d741df53bdefd75ceb4703564736f6c63430008070033

將其拆分為 初始化代碼(init code) 和 運行時代碼(runtime code),如下圖所示:
在這里插入圖片描述

把 payable 和 nonpayable 的 init code 放在一起對比:

0x6080604052603f8060116000396000f3fe        // payable
0x6080604052348015600f57600080fd5b50603f80601d6000396000f3fe // nonpayable

可以注意到:payable 合約的 init code 更短,而 non-payable 的要長一些。

將較長的字節序列(non-payable)放入 EVM playground,得到如下輸出(已加上注釋):

// 初始化自由內存指針  
PUSH1 0x80  
PUSH1 0x40  
MSTORE  // 檢查發送的以太數量(wei)  
CALLVALUE  
DUP1  
ISZERO  // 跳轉到 0x0f(合約部署步驟)  
PUSH1 0x0f  
JUMPI  // 如果發送了 wei > 0,就 revert  
PUSH1 0x00  
DUP1  
REVERT  // 跳轉目標 (0x0f)   
JUMPDEST  
POP  // 運行時代碼長度  
PUSH1 0x3f  
DUP1  // runtime code 開始位置  
PUSH1 0x1d  
PUSH1 0x00  
CODECOPY  
PUSH1 0x00  
RETURN  
INVALID

接下來,將解釋“Payable 與 Non-payable 構造函數的區別”,以便于深入理解以上初始化代碼的區別。

  • 1)非 payable 構造函數會在部署時校驗 callvalue

    • init code 會在 callvalue > 0 時 revert,否則繼續執行。

    • 在 non-payable 的合約中,會額外插入一段 12字節 的字節碼序列:

      348015600f57 600080fd 5b50
      

      它位于初始化內存指針和返回 runtime code 的字節碼之間:

      <init bytecode> <額外12字節序列> <返回runtime字節碼> <runtime字節碼>
      

      這段額外的代碼負責檢查在部署時是否附帶了 value(wei)。

      • 如果發送了 wei,就 revert。
      • 如果沒發送 wei,就繼續部署 runtime code。

      其中:

      • 348015600f57 → 檢查 value 是否為 0,否則跳轉。
      • 600080fd → 否則revert。
      • 5b50 → JUMPDEST + POP。POP 用于丟棄 callvalue(因為它還在棧上,不再需要)。
        JUMPDEST 是跳轉目標,沒有它的話,JUMP 或 JUMPI 無法落地,會導致 revert。
  • 2)運行時代碼的內存偏移不同
    注意:runtime code 的長度沒有變化,但 拷貝的偏移量 變了。
    因為 non-payable 的 init code 更長,所以 runtime code 被推后了。

    • Non-payable 的偏移量:0x1d
    • Payable 的偏移量:0x11

    差值:0x1d – 0x11 = 0x0c(即 12),這正好對應那段額外的檢查字節序列。

4. 運行時代碼

4.1 空合約的運行時代碼

即便合約是空的(沒有函數),runtime code 依然不是空的。

原因是 solidity 編譯器會在運行時代碼后附加合約的元數據。

  • fe(INVALID) 會被加在元數據之前,防止其被當成可執行字節碼。

更多合約元數據信息,可參看:Solidity metadata.json playground。

(在 Solidity 0.8.18 中,引入了 --no-cbor-metadata 編譯選項,可以不在合約字節碼中附加這些元數據。)

4.1.1 在純 Yul 合約中,編譯器默認不會添加元數據

如果合約是用純 Yul 編寫的,那么它將沒有元數據。
但是,可以通過在全局對象中包含 .metadata 來添加元數據。

// 該合約的編譯輸出默認不會包含元數據  
object "Simple" {  code {  datacopy(0, dataoffset("runtime"), datasize("runtime"))  return(0, datasize("runtime"))          }object "runtime" {  code {  mstore(0x00, 2)  return(0x00, 0x20)  }  }  
}

編譯器的輸出如下:

6000600d60003960006000f3fe

轉換為助記符(mnemonics)后,得到:

// 將 runtime 代碼復制到內存  
PUSH1	00  
PUSH1	0d  
PUSH1	00  
CODECOPY	// 返回一個零大小的區域,因為沒有 runtime 代碼  
PUSH1	00  
PUSH1	00  
RETURN	  
INVALID

在這種情況下,返回的內存區域大小為零,因為既沒有 runtime 代碼,也沒有元數據。

(編譯器從 0x0d 開始,復制 0x00 字節的 runtime 代碼到內存偏移 0x00 的位置,然后返回 0x00 字節。)

4.2 非空合約的 Runtime 代碼

現在給合約加上最簡單的邏輯。

pragma solidity 0.8.7;
contract Runtime {  address lastSender;  constructor () payable {}receive() external payable {  lastSender = msg.sender;  }  
}

其輸出的創建代碼為:

608060405260578060116000396000f3fe608060405236601c57600080546001600160a01b03191633179055005b600080fdfea2646970667358221220e9b731ab28726d97cbf5219f1e5eaec508f23254c60b15ed1d3456572547c5bf64736f6c63430008070033

可以拆分為:
在這里插入圖片描述
接下來將詳細看看其中的 runtime 代碼。

由于這是一個 Solidity 合約,可以像之前解釋的那樣,將其分為 可執行字節碼 和 合約元數據。

Runtime code :=
0x608060405236601c57600080546001600160a01b03191633179055005b600080fdfeMetadata :=
0xa2646970667358221220e9b731ab28726d97cbf5219f1e5eaec508f23254c60b15ed1d3456572547c5bf64736f6c63430008070033

使用 evm.codes 輸出 來分析 runtime 代碼。它已被拆分以便于理解。

首先,初始化空閑內存指針:

[00] PUSH1      80  
[02] PUSH1      40  
[04] MSTORE

接下來檢查交易是否帶有數據,如果有,則跳轉到程序計數器 (PC) 0x1c 并執行回退(revert)。
合約能夠接收數據的合法方式只有兩種:調用函數和 fallback。
這里只有 receive 函數,所以沒有合法方式接收 calldata。

[05] CALLDATASIZE	  
[06] PUSH1      1c  
[08] JUMPI

然后是存儲 msg.sender 的代碼:

[09] PUSH1      00  
[0b] DUP1	  
[0c] SLOAD	  
[0d] PUSH1      01  
[0f] PUSH1      01  
[11] PUSH1      a0  
[13] SHL	  
[14] SUB	  
[15] NOT	  
[16] AND  
[17] CALLER	  
[18] OR	  
[19] SWAP1	  
[1a] SSTORE  
[1b] STOP

最后,這是當 calldata 被傳入時跳轉到的 JUMPDEST 0x1c,交易會回退:

[1c] JUMPDEST	  
[1d] PUSH1      00  
[1f] DUP1	  
[20] REVERT	  
[21] INVALID

5. 帶參數的構造函數

帶構造函數參數的合約在編碼時會有一些不同。構造函數的參數會被追加到創建代碼(creation code)的末尾(在運行時代碼 runtime code 之后),并且是以 ABI 編碼 的形式追加的。

Solidity 還會額外增加一個檢查:

  • 確保構造函數參數的長度至少等于預期的構造函數參數長度,否則會直接 revert。

來看一個簡單的例子。這里不包含任何運行時代碼(runtime code),只在構造函數中寫邏輯(構造函數代碼不會存在于運行時代碼中):

// optimizer: 200
contract MinimalLogic {  uint256 private x;  constructor (uint256 _x) payable {  x = _x;  }  
}

生成的創建代碼是:

608060405260405160893803806089833981016040819052601e916025565b600055603d565b600060208284031215603657600080fd5b5051919050565b603f80604a6000396000f3fe6080604052600080fdfea26469706673582212204a131c1478e0e7bb29267fd8f6d38a660b40a25888982bd6618b720d4498b6b464736f6c63430008070033

拆解:

  • Init code(初始化代碼):
    0x608060405260405160893803806089833981016040819052601e916025565b600055603d565b600060208284031215603657600080fd5b5051919050565b603f80604a6000396000f3fe
  • Runtime code(僅包含 metadata):
    0x6080604052600080fdfea26469706673582212204a131c1478e0e7bb29267fd8f6d38a660b40a25888982bd6618b720d4498b6b464736f6c63430008070033
  • 缺少構造函數參數!

如果直接執行這份創建代碼,會在 init code 中失敗(revert),因為它期望在運行時代碼后至少有 32 字節的數據來作為 uint256 _x 的值。

為了解決這個問題,可以把 ABI 編碼后的 uint256(1) 追加到創建代碼末尾,作為構造函數參數_x

修正后的字節碼:

608060405260405160893803806089833981016040819052601e916025565b600055603d565b600060208284031215603657600080fd5b5051919050565b603f80604a6000396000f3fe6080604052600080fdfea26469706673582212204a131c1478e0e7bb29267fd8f6d38a660b40a25888982bd6618b720d4498b6b464736f6c634300080700330000000000000000000000000000000000000000000000000000000000000001

字節碼解析(通過 EVM playground 來演示):

  • 1)Step 1: 初始化 free memory pointer
    和所有 solidity 合約一樣,先用 6080604052 初始化空閑內存指針。

  • 2)Step 2: 獲取構造函數參數的長度

    // 6040 51 6089 38 03
    PC   OPCODE[05] PUSH1 40
    [07] MLOAD
    [08] PUSH1 89
    [0a] CODESIZE
    [0b] SUB
    

    這里 PUSH1 40 MLOAD 會將 自由內存指針(free memory pointer)加載到棧上,以便后續使用。接著通過 PUSH1 89 將 創建代碼長度(不包含構造函數參數部分) 壓入棧中,然后調用 CODESIZE(它包含了構造函數參數)。通過兩者相減來得到 構造函數參數的長度。

  • 3)Step 3: 把構造函數參數復制到內存

    // 80 6089 83 39
    PC   OPCODE[0c] DUP1
    [0d] PUSH1 89
    [0f] DUP4
    [10] CODECOPY
    

    這里為 CODECOPY 做棧準備。首先使用 DUP1 復制前面減法得到的結果,然后通過 PUSH1 89 將 創建代碼長度(不包含構造函數參數) 壓入棧中。最后,使用 DUP4 將內存偏移量放到棧頂。現在調用 CODECOPY,把構造函數參數復制到內存中的 自由內存指針 位置。

  • 4)Step 4:更新自由內存指針
    在把代碼寫入內存后,Solidity 會按如下方式更新 自由內存指針:

    // 81 01 6040 81 90 52  
    PC   OPCODE[11] DUP2  
    [12] ADD  
    [13] PUSH1 40  
    [15] DUP2  
    [16] SWAP1  
    [17] MSTORE
    

    這里通過把之前復制的 構造函數參數長度 (0x20) 加到 自由內存指針 (0x80) 上來實現更新。然后用 Dup1Swap1 來調整棧結構,最后調用 MSTORE 40 把新的值 (0xa0) 存儲為新的自由內存指針。

    接下來會有一系列的 動態操作和 JUMP,這些不會順序執行,而是取決于條件分支。
    這些步驟都有編號,可以按編號順序理解,而不需要自己去找對應的 JUMPDEST。

    也可以用這個 playground 鏈接 來運行這個字節碼。

  • 5)Step 5:跳轉到 SSTORE 的 JUMPDEST

    // 601e 91 6025 56  
    PC   OPCODE[18] PUSH1 1e  
    [1a] SWAP2  
    [1b] PUSH1 25  
    [1d] JUMP // jump to JUMPDEST 0x25
    

    在此,想跳轉到執行存儲構造函數參數的代碼位置。
    這里先把 1e 壓入棧中。1e 對應的正是 SSTORE 實際執行的位置。但在這之前,需要先檢查構造函數參數是否至少有 32 字節。這個檢查過程從 PC = 0x25(即上面的 JUMPDEST)開始。

  • 8)Step 8:把構造函數參數存儲到存儲槽 0
    這是 JUMPDEST 0x1e。不過要注意,JUMPDEST 0x25 會先執行(見下面步驟 6)。
    另外要注意,這里是 Step 8,而上一個還是 Step 5。它之所以亂序,是因為只有 Step 6 和Step 7 條件滿足后,這里才會真正執行。為了和編譯后的字節碼保持一致,在這里提前引入。

    // 5b 6000 55 603d 56  
    PC   OPCODE[1e] JUMPDEST  
    [1f] PUSH1 00  
    [21] SSTORE  
    [22] PUSH1 3d  
    [24] JUMP
    

    這里把 0x00 壓棧,作為存儲槽編號,把 _x 存入其中,然后調用 SSTORE。接著壓入 0x3d,作為最后一次 CODECOPY 和 RETURN 的跳轉目標。

  • 6)Step 6:檢查構造函數參數是否至少 32 字節
    這是 JUMPDEST 0x25

    // 5b 6000 6020 82 84  
    PC   OPCODE[25] JUMPDEST  
    [26] PUSH1 00  
    [28] PUSH1 20  
    [2a] DUP3  
    [2b] DUP5// continue
    // 03 12 15 6036 57  
    [2c] SUB  
    [2d] SLT  
    [2e] ISZERO  
    [2f] PUSH1 36  
    [31] JUMPI // Jump to 0x36 if ISZERO returns 1// else continue and revert
    // 6000 80 fd
    [32] PUSH1 00  
    [34] DUP1  
    [35] REVERT
    

    這里檢查構造函數參數的大小是否至少為 32 字節。

    1. 先把 0x00 壓入棧(稍后會用到),再壓入最小接受長度 0x20 (32 字節)
    2. 然后通過之前壓入棧的 offset自由內存指針 來計算參數實際長度。
      • DUP3 得到 offset
      • DUP5 得到當前自由內存指針
    3. SUB 相減得到長度并壓棧。
    4. SLT 檢查是否小于 32 字節,返回 0(不小于)或 1(小于)。
    5. ISZERO 對結果取反:如果參數長度 >= 32,則結果為 1。
    6. 壓入跳轉目標 0x36,如果條件滿足就跳轉,否則執行 REVERT 來避免無效輸入。
  • 7)Step 7:加載參數到棧,并準備寫入存儲
    這是 JUMPDEST 0x36

    // 5b 50 51 91 90 50 56  
    PC   OPCODE[36] JUMPDEST  
    [37] POP  
    [38] MLOAD  
    [39] SWAP2  
    [3a] SWAP1  
    [3b] POP  
    [3c] JUMP // jump to 0x1e
    

    這里先把之前壓棧的 0(來自Step 6 的 [26])彈出,因為已經不需要了。
    然后:

    • 使用 MLOAD 把構造函數參數加載到棧頂
    • 清理掉構造函數參數的內存偏移(已無用)
    • 最后 JUMP0x1e,進入 Step 8,執行真正的存儲操作。
  • 9)Step 9:將運行時代碼拷貝到內存并返回
    這是 JUMPDEST 0x3d,不過上面 JUMPDEST 0x1e 先執行。

    // 5b 603f 80 604a 6000 39 6000 f3 fe  
    PC   OPCODE[3d] JUMPDEST  
    [3e] PUSH1 3f  
    [40] DUP1  
    [41] PUSH1 4a  
    [43] PUSH1 00  
    [45] CODECOPY  
    [46] PUSH1 00  
    [48] RETURN  
    [49] INVALID
    

    無執行能力的代碼(合約元數據):

    0x6080604052600080fdfea26469706673582212204a131c1478e0e7bb29267fd8f6d38a660b40a25888982bd6618b720d4498b6b464736f6c63430008070033
    

    這里按慣例從內存中返回合約的運行時代碼。

RETURN 執行前的內存布局:

  • 0x000x40:空的運行時代碼和元數據字節碼
  • 0x40:自由內存指針
  • 0x80:構造函數參數 uint256(1)

內存詳細分布:

0x00 <- 0x20 = 0x6080604052600080fdfea26469706673582212208f9ffa7a3ab43f0ff61d3033
0x20 <- 0x40 = 0x624bf0e9d398f9a91213656b13d9ffc8fd90fdbc64736f6c63430008070033
0x40 <- 0x60 = 0x0000000000000000000000000000000000000000000000000000000000000a0
0x60 <- 0x80 = 0x00000000000000000000000000000000000000000000000000000000000000
0x80 <- 0xa0 = 0x0000000000000000000000000000000000000000000000000000000000000001

6. 總結

智能合約部署包含了一些低級操作,這些通常被高級語言封裝起來。
本文學習了:

  1. 智能合約如何通過發送 創建代碼到零地址 來執行
  2. 創建代碼的不同組成部分及其在部署中的作用
  3. 各部分如何協同工作完成合約部署
  4. 構造函數參數如何存儲、驗證,并用于初始化合約

通過這個流程,可以對智能合約的底層執行有更清晰的理解。

參考資料

[1] RareSkills團隊2023年2月博客 Ethereum smart contract creation code

本文來自互聯網用戶投稿,該文觀點僅代表作者本人,不代表本站立場。本站僅提供信息存儲空間服務,不擁有所有權,不承擔相關法律責任。
如若轉載,請注明出處:http://www.pswp.cn/pingmian/94192.shtml
繁體地址,請注明出處:http://hk.pswp.cn/pingmian/94192.shtml
英文地址,請注明出處:http://en.pswp.cn/pingmian/94192.shtml

如若內容造成侵權/違法違規/事實不符,請聯系多彩編程網進行投訴反饋email:809451989@qq.com,一經查實,立即刪除!

相關文章

typora無需激活版及最新激活版方法!雙擊安裝就能用

介紹 Typora 是一款Markdown編輯器&#xff0c;支持實時預覽&#xff0c;所見即所得。跨平臺&#xff0c;支持Windows、macOS、Linux。適合寫作、筆記、技術文檔等。本教程將提供合法安全的安裝方案&#xff0c;并解決常見問題&#xff0c;助你高效完成部署&#xff01; 直接…

基于Java、GeoTools與PostGIS的對跖點求解研究

目錄 前言 一、對跖點簡介 1、地理學定義 2、人生哲學含義 二、對跖點求解 1、Java求解 2、Geotools求解 3、PostGIS求解 4、三種計算方法的對比 5、Leaflet展示對跖點 三、總結 前言 在地理信息系統&#xff08;GIS&#xff09;領域&#xff0c;對跖點&#xff08;A…

Linux-函數的使用-編寫監控腳本

Linux-函數的使用-編寫監控腳本前言一、監控cpu二、采集內存的使用信息三、采集磁盤和分區的使用信息四、顯示進程的信息前言 編寫監控腳本實現以下功能 監控cpu&#xff0c;內存&#xff0c;磁盤&#xff0c;進程等信息&#xff0c;每隔5分鐘記錄這些信息到日志文件里perform…

Authelia:開源雙因素認證與單點登錄解決方案

項目標題與描述 Authelia是一個開源的認證和授權服務器&#xff0c;專注于為應用程序提供雙因素認證&#xff08;2FA&#xff09;和單點登錄&#xff08;SSO&#xff09;功能。通過Web門戶&#xff0c;Authelia能夠作為身份和訪問管理&#xff08;IAM&#xff09;系統&#xff…

Apache Ozone 介紹與部署使用(最新版2.0.0)

目錄 一、軟件介紹 二、軟件架構 Ozone Manager&#xff08;OM&#xff09; Storage Container Manager&#xff08;SCM&#xff09; Containers Datanodes Storage Containers Recon Recon 和 Ozone Manager Recon 和 Storage Container Manager 三、安裝部署 準備…

Review --- Linux

Review — Linux Linux 是一種開源的類 Unix 操作系統內核&#xff0c;廣泛應用于服務器、嵌入式設備和個人計算機中。其核心特點是開源、穩定、安全和高度的可定制性。對于大學畢業生而言&#xff0c;掌握 Linux 的基本操作和原理是進入 IT 行業的重要技能之一。 Linux 的基本…

【msyql 】占用硬盤太大 ,那些文件可以清理

從目錄內容來看&#xff0c;這臺 MySQL 服務器上主要是 xxl-job 調度平臺的數據庫。占用空間最大的是&#xff1a;24G xxl_job_log.ibd這個文件是 xxl-job 的任務執行日志表&#xff0c;隨著時間推移&#xff0c;日志量會非常大。可以清理的文件和方法1. 清理 xxl_job_log 表數…

58 C++ 現代C++編程藝術7-模板友元

C 現代C編程藝術7-模板友元 文章目錄C 現代C編程藝術7-模板友元一、基礎應用場景 &#x1f9e9;1. 模板類聲明友元函數2. 普通類聲明模板函數為友元二、模板類互訪場景 ??1. 同類模板互訪&#xff08;一對一&#xff09;2. 異類模板互訪&#xff08;多對多&#xff09;三、高…

Undertow —— JBOSS 的社區版,redhat 下場維護的開源項目,頂頂好用的 Java web server

Undertow JBoss Community Undertow Undertow is a flexible performant web server written in java, providing both blocking and non-blocking API’s based on NIO. Undertow 是一個用 Java 編寫的靈活高性能 Web 服務器&#xff0c;提供基于 NIO 的阻塞和非阻塞 API。…

【AI智能體】Dify 搭建業務單據差異核對助手實戰詳解

目錄 一、前言 二、Dify介紹 2.1 Dify 是什么 2.2 Dify 核心特性 2.2.1 Dify特點 2.2.2 Dify 多模型支持 2.2.3 Dify 適應場景 2.2.4 基于Dify 搭建發票識別應用優勢 三、Dify 搭建業務單據核對助手實戰過程 3.1 前置準備 3.1.1 安裝必要的插件 3.2 完整操作步驟 3…

Centos編譯安裝Python3.10

gcc編譯源碼包 下載python源碼包并解壓 wget https://www.python.org/ftp/python/3.10.18/Python-3.10.18.tgz tar -xf Python-3.10.18.tgz cd Python-3.10.18系統編譯依賴環境安裝 sudo yum install zlib-devel ncurses-devel gdbm-devel nss-devel openssl-devel readline-de…

Maya 3D建模 導入參考圖、鎖定參考圖

1 導入參考圖切換到 前視圖 或者 側視圖 導入 &#xff08;根據參考圖片類別去選擇&#xff09;方法1&#xff1a;視圖--圖像平面--導入圖像方法2&#xff1a;直接點 圖像平面 備注&#xff1a;誤操作導致看不到 解決辦法&#xff1a;顯示--視口 找對應的2 鎖定參考圖目的&…

基于單片機智能加濕器/空氣加濕器

傳送門 &#x1f449;&#x1f449;&#x1f449;&#x1f449;其他作品題目速選一覽表 &#x1f449;&#x1f449;&#x1f449;&#x1f449;其他作品題目功能速覽 概述 基于單片機的智能加濕器通過集成溫濕度傳感器、控制模塊和霧化裝置&#xff0c;實現環境濕度的自…

SNDR:高精度ADC系統的綜合性能標尺

SNDR&#xff1a;高精度ADC系統的綜合性能標尺 一、SNDR的本質定義與理論基礎 信噪失真比(Signal-to-Noise-and-Distortion Ratio) 是評估ADC系統綜合性能的核心指標&#xff0c;定義為信號功率與噪聲及失真功率之和的比值&#xff1a; SNDRdB10log?10(PsignalPnoisePdistorti…

2025年滲透測試面試題總結-31(題目+回答)

安全領域各種資源&#xff0c;學習文檔&#xff0c;以及工具分享、前沿信息分享、POC、EXP分享。不定期分享各種好玩的項目及好用的工具&#xff0c;歡迎關注。 目錄 一、代碼審計核心思路&#xff08;261&#xff09; 二、MySQL Getshell前提&#xff08;262&#xff09; …

[創業之路-560]:機械、電氣、自控、電子、軟件、信息、通信、大數據、人工智能,上述技術演進過程

上述關鍵詞反映的技術演進過程可梳理為一條從機械執行到智能決策的遞進式發展主線&#xff0c;各技術領域在不同階段相互滲透、共同推動機器人技術從功能替代向認知革命躍遷。以下是具體演進邏輯與趨勢分析&#xff1a;一、技術演進的三階段遞進機械主導階段&#xff08;工業革…

芋道前端項目部署后刷新 404 的解決辦法(Nginx 配置教程)

很多同學在把 芋道前端項目 部署到服務器后&#xff0c;會遇到一個奇怪的問題&#xff1a; &#x1f449; 項目首頁能正常訪問&#xff0c;但一旦在瀏覽器里手動刷新某個頁面&#xff0c;就會報 404 Not Found 錯誤。 這到底是為什么呢&#xff1f;又該怎么解決呢&#xff1f;下…

更適合后端寶寶的前端三件套之HTML

文章目錄&#x1f4d5;1. HTML基礎??1.1 什么是HTML??1.2 認識HTML標簽??1.3 HTML文件基本結構??1.4 標簽層次結構&#x1f4d5;2. HTML常見標簽??2.1 標題標簽??2.2 段落標簽??2.3 換行標簽??2.4 圖片標簽??2.5 超鏈接標簽??2.6 表格標簽&#x1f4d5;3. …

【JVM內存結構系列】四、不同垃圾回收器與堆內存的適配關系:從分代GC到Region GC

在JVM內存體系中&#xff0c;堆內存的“分代結構”與“對象流轉規則”是通用基礎&#xff0c;但垃圾回收器&#xff08;GC&#xff09;是決定堆內存實際表現的核心變量——不同GC為實現“低延遲”“高吞吐量”等目標&#xff0c;會對堆的劃分方式、對象管理邏輯、參數配置規則進…

Zemax光學設計輸出3D

輸出立體數據文件&#xff08;IGES/STEP/SAT/STL 格式&#xff09;的參數設置界面&#xff0c;各參數含義如下&#xff1a;1. 起始面/終止面&#xff1a;設定要輸出立體數據對應的光學表面范圍&#xff0c;從第 0 個表面到第 9 個表面 &#xff0c;限定參與輸出的光學結構表面區…