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;限定参与输出的光学结构表面区…