docs(objects): explain executable layout and bytecode verification

This commit is contained in:
2026-11-15 17:24:34 +08:00
parent b793bac407
commit 6d8a5a8fe6
+110 -20
View File
@@ -1,3 +1,7 @@
//! 编译器/宿主提交代码后在此构造不可变执行数据;core::frame 共享代码,eval 按偏移读取指令。
//! 构造先验证名字布局、解码和控制流栈高度;这不是执行器,也不证明运行时值类型或调用一定成功。
//! 常量中的对象句柄在 EndorCode 内只是表示,由堆上 EndorCodeObject 的 trace 建立托管强边。
use std::any::Any;
use std::collections::{BTreeSet, VecDeque};
use std::sync::Arc;
@@ -6,39 +10,58 @@ use crate::builtins::trace_value;
use crate::instruction::{compare_operation, decode, function_counts, invalid};
use crate::{DecodedInstruction, Instruction, ObjectPayload, ObjectRef, Opcode, Value, VmError};
/// Local slots start with positional parameters. Captured locals occupy their
/// original slots as cells; free-variable cells follow all local slots.
/// 编译器生成、执行帧消费的槽布局:参数占局部槽前缀,捕获局部仍占原槽但存 Cell。
/// 外部自由变量 Cell 追加在所有局部槽之后,不能与局部变量重名。
#[derive(Debug, Clone, Default, PartialEq, Eq)]
pub struct CodeLayout {
/// 位置参数名称顺序,必须与 locals 前缀相同。
pub parameters: Vec<String>,
/// 全部局部名称,参数必须是其完整前缀。
pub locals: Vec<String>,
/// 需要提升为 Cell 的局部名称子集,不额外新增局部槽。
pub cell_variables: Vec<String>,
/// 从外层捕获的 Cell 名称,顺序决定闭包传入槽顺序。
pub free_variables: Vec<String>,
/// 全局与属性操作使用的文本名称索引表。
pub names: Vec<String>,
}
/// Source declaration metadata, separate from executable slot layout.
/// 编译器传入的源码类型声明,与执行槽布局分开;类型名称在调用/契约检查时解析。
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct SourceSignature {
/// 位置参数的类型注解顺序,数量必须与代码参数槽数相同。
pub parameters: Vec<String>,
/// 非空的返回类型注解名称,尚不保证该名称可成功解析。
pub return_type: String,
/// 可选方法所属类型名,用于源码方法契约;Some 空串非法。
pub method_owner: Option<String>,
}
/// 已通过构造校验的 Rust 执行数据,尚未分配为托管代码对象;clone 不为常量对象增加根。
#[derive(Debug, Clone)]
pub struct EndorCode {
/// 保留的物理双字节单元编码,含扩展前缀。
bytecode: Vec<u8>,
/// 解码后按偏移排序的逻辑指令列表,执行时无需重新组合前缀。
instructions: Vec<DecodedInstruction>,
/// 常量表示;Rust clone 不为其中的对象句柄 retain。
constants: Vec<Value>,
/// 经 validate_layout 检查的槽和名称布局。
layout: CodeLayout,
/// 从可达控制流计算的最大栈高度,供帧预留容量。
stack_size: usize,
/// 诊断显示的代码名称。
name: String,
/// 诊断显示的源码文件名,不读取该文件。
filename: String,
/// 调用方给出的起始行,供回溯显示,不是逐指令行号表。
first_line: usize,
/// 可选源码声明;不改变已校验的指令布局。
signature: Option<SourceSignature>,
}
impl EndorCode {
/// 宿主从原始字节码建立只有参数局部槽的代码;需要额外局部或本地 Cell 时用 with_layout。
pub fn new(
bytecode: Vec<u8>,
constants: Vec<Value>,
@@ -57,6 +80,7 @@ impl EndorCode {
)
}
/// 编译器与宿主把逻辑指令编码后走统一构造校验;此入口不替调用者修补跳转距离。
pub fn from_instructions(
instructions: &[Instruction],
constants: Vec<Value>,
@@ -65,13 +89,17 @@ impl EndorCode {
Self::with_layout(Instruction::encode_all(instructions)?, constants, layout)
}
/// 全部构造入口汇合处:先检查布局,再解码,最后验证索引、跳转和可达路径栈高度。
pub fn with_layout(
bytecode: Vec<u8>,
constants: Vec<Value>,
layout: CodeLayout,
) -> Result<Self, VmError> {
// 布局错误必须先拒绝,后续槽索引校验依赖这些不变量。
validate_layout(&layout)?;
// 合并 EXTENDED_ARG 并保留物理单元偏移,未知/截断编码在这里失败。
let instructions = decode(&bytecode)?;
// 校验成功后才建立可供执行器使用的代码,栈高只来自可达指令。
let stack_size = verify(&instructions, constants.len(), &layout)?;
Ok(Self {
bytecode,
@@ -86,7 +114,9 @@ impl EndorCode {
})
}
/// 编译器在代码构造后附加声明;这里只检查数量和非空字符串,不解析类型名。
pub fn with_source_signature(mut self, signature: SourceSignature) -> Result<Self, VmError> {
// 参数注解数量必须对应位置参数;每项与可选 owner 都不允许空串。
if signature.parameters.len() != self.parameters().len()
|| signature.return_type.is_empty()
|| signature.parameters.iter().any(String::is_empty)
@@ -102,20 +132,20 @@ impl EndorCode {
self.signature = Some(signature);
Ok(self)
}
/// Store 的源码调用/效果/契约检查借用声明;None 表示没有附加源码类型元数据。
pub fn source_signature(&self) -> Option<&SourceSignature> {
self.signature.as_ref()
}
/// Conservative proof over validated instructions. Local assignment is not
/// automatically pure: shared-reference assignment may update its old value.
/// Unknown calls and writes remain unknown, even when they could be safe for
/// a particular invocation. New opcodes require an explicit audit here.
/// Store/effects 的保守只读证明:所有指令(包括不可达部分)都须在白名单内。
/// 局部赋值可能触发共享引用更新,不自动视为纯;未知调用/写入也不在这里证明安全。
pub fn is_statically_read_only(&self) -> bool {
self.instructions
.iter()
.all(|decoded| read_only_opcode(decoded.instruction.opcode))
}
/// 编译器/宿主附加诊断位置;不校验文件存在或起始行范围,也不改变执行布局。
pub fn with_location(
mut self,
name: impl Into<String>,
@@ -128,55 +158,67 @@ impl EndorCode {
self
}
/// 宿主诊断/序列查看借用原编码;不允许绕过验证修改字节。
pub fn bytecode(&self) -> &[u8] {
&self.bytecode
}
/// eval、效果分析与诊断借用已解码逻辑指令。
pub fn instructions(&self) -> &[DecodedInstruction] {
&self.instructions
}
/// 执行器读取常量,代码载荷 trace 枚举其对象边;返回切片不新增托管根。
pub fn constants(&self) -> &[Value] {
&self.constants
}
/// 帧参数绑定与函数默认值校验读取参数顺序。
pub fn parameters(&self) -> &[String] {
&self.layout.parameters
}
/// 函数闭包校验和帧初始化读取捕获槽顺序。
pub fn free_variables(&self) -> &[String] {
&self.layout.free_variables
}
/// 编译器/帧与 Store 分析借用完整布局。
pub fn layout(&self) -> &CodeLayout {
&self.layout
}
/// 执行帧用可达路径最大栈高预留容量,不是执行步数上限。
pub fn stack_size(&self) -> usize {
self.stack_size
}
/// 错误和代码 repr 读取诊断名称。
pub fn name(&self) -> &str {
&self.name
}
/// 回溯与 repr 读取文件标识,不访问文件系统。
pub fn filename(&self) -> &str {
&self.filename
}
/// 回溯/诊断读取调用方设置的起始行。
pub fn first_line(&self) -> usize {
self.first_line
}
/// 帧创建 localsplus 时计算局部槽加外部自由变量槽的总数。
pub fn slot_count(&self) -> usize {
self.layout.locals.len() + self.layout.free_variables.len()
}
/// Whether a local/free slot holds a managed closure cell.
/// 帧初始化/闭包访问查询槽是否存 Cell,越界槽返回 false。
pub fn is_cell_slot(&self, slot: usize) -> bool {
is_cell_slot(&self.layout, slot)
}
/// Name of a local/free slot used by validated instructions.
/// 执行器为已验证槽生成未绑定变量等诊断时查询名称。
///
/// # Panics
/// Panics if the slot is outside this code's local/free-variable layout.
/// slot 超出局部/自由变量布局时 panic;调用者不能传入未经边界检查的槽。
pub fn slot_name(&self, slot: usize) -> &str {
// 局部槽保留原顺序,自由变量索引从局部槽末尾偏移。
if slot < self.layout.locals.len() {
&self.layout.locals[slot]
} else {
&self.layout.free_variables[slot - self.layout.locals.len()]
}
}
/// Look up a logical instruction by its code-unit offset.
/// eval 按指令指针查询完整逻辑起点;非法偏移返回带位置的字节码错误。
pub fn instruction_at(&self, offset: usize) -> Result<DecodedInstruction, VmError> {
instruction_index(&self.instructions, offset)
.map(|index| self.instructions[index])
@@ -189,45 +231,52 @@ impl EndorCode {
}
}
/// Heap-allocated, immutable and shareable executable. Function objects hold a
/// managed reference to this object rather than embedding a copy of its code.
/// Store::new_code 分配的代码载荷;函数持有其托管引用,帧可共享内部不可变执行数据。
#[derive(Debug, Clone)]
pub struct EndorCodeObject {
/// 共享不可变 Rust 执行数据;Arc 本身不是常量对象的托管根。
code: Arc<EndorCode>,
}
impl EndorCodeObject {
/// Store::new_code 将已校验执行数据包装成待分配载荷,共享数据由 Arc 持有。
pub fn new(code: EndorCode) -> Self {
Self {
code: Arc::new(code),
}
}
/// Store 校验函数引用的代码布局时借用共享执行数据。
pub fn code(&self) -> &EndorCode {
&self.code
}
/// Share immutable executable data without copying constants or wordcode.
/// This Rust Arc does not retain heap objects referenced by constants;
/// callers must keep the managed code object rooted while using them.
/// core::frame 共享执行数据,不深拷贝常量和编码;Arc 不是常量托管对象的拥有根。
/// 调用方使用常量时仍须保持托管代码对象有根,不能只靠此 Arc 延长常量对象寿命。
pub fn shared_code(&self) -> Arc<EndorCode> {
Arc::clone(&self.code)
}
}
impl ObjectPayload for EndorCodeObject {
/// 不可变代码载荷不实现原地赋值,允许只读局部引用重绑定。
fn reference_assignment_only(&self) -> bool {
true
}
/// Store::payload 的 Rust 下转型入口。
fn as_any(&self) -> &dyn Any {
self
}
/// 代码载荷的诊断类别,不是名义类型判断。
fn kind_name(&self) -> &'static str {
"code"
}
/// Heap 分配/替换代码载荷时登记常量对象边;不递归扫描其指令或源码文本。
fn trace(&self, visit: &mut dyn FnMut(ObjectRef)) {
// 仅对象变体贡献边,重复出现的同一对象仍逐次计数。
for value in self.code.constants() {
trace_value(value, visit);
}
}
/// Store::repr 用代码名/文件/起始行生成诊断,不运行代码。
fn repr(&self, _identity: ObjectRef) -> Result<String, VmError> {
Ok(format!(
"<code {} at {}:{}>",
@@ -238,7 +287,8 @@ impl ObjectPayload for EndorCodeObject {
}
}
// Shared by the local proof and call-graph analysis. Unknown opcodes fail closed.
// 白名单之外一律不在此处证明只读;动态调用图分析可以另行判断已知调用。
/// 本地只读证明和 effects 调用图分析共享白名单;新增指令需显式审查其副作用。
pub(crate) fn read_only_opcode(opcode: Opcode) -> bool {
matches!(
opcode,
@@ -269,7 +319,9 @@ pub(crate) fn read_only_opcode(opcode: Opcode) -> bool {
)
}
/// with_layout 在解码前调用,验证各名称表内部唯一性以及局部/捕获槽的交叉关系。
fn validate_layout(layout: &CodeLayout) -> Result<(), VmError> {
// 每类名称表都检查内部唯一性,不能把全部表混在一个 seen 中。
for names in [
&layout.parameters,
&layout.locals,
@@ -277,8 +329,11 @@ fn validate_layout(layout: &CodeLayout) -> Result<(), VmError> {
&layout.free_variables,
&layout.names,
] {
// 每张表单独去重;不同表间允许的重名关系在后面单独检查。
let mut seen = BTreeSet::new();
// 空名称无法可靠绑定,表内重复会使槽或名称索引产生歧义。
for name in names {
// 发现首个违规就退出,构造不产生可执行半成品。
if name.is_empty() || !seen.insert(name) {
return Err(VmError::InvalidArgument(
"empty or duplicate code variable name",
@@ -286,11 +341,13 @@ fn validate_layout(layout: &CodeLayout) -> Result<(), VmError> {
}
}
}
// 帧按局部槽前缀绑定参数,不能仅检查包含关系。
if !layout.locals.starts_with(&layout.parameters) {
return Err(VmError::InvalidArgument(
"parameter names must lead local slots",
));
}
// Cell 局部必须来自 locals;外部自由变量不得同时作为本地变量。
if layout
.cell_variables
.iter()
@@ -307,22 +364,29 @@ fn validate_layout(layout: &CodeLayout) -> Result<(), VmError> {
Ok(())
}
/// 代码公共查询与操作数校验共用:外部自由变量槽全是 Cell,局部槽按名称判断。
fn is_cell_slot(layout: &CodeLayout, slot: usize) -> bool {
// 自由变量段范围内都为 Cell;段外返回 false,避免越界索引。
if slot >= layout.locals.len() {
slot < layout.locals.len() + layout.free_variables.len()
} else {
// 局部槽按名字判断是否提升为 Cell,不改变其原有位置。
layout.cell_variables.contains(&layout.locals[slot])
}
}
/// instruction_at、跳转验证与栈分析用二分查找逻辑起点;前缀中段不是合法目标。
fn instruction_index(instructions: &[DecodedInstruction], offset: usize) -> Option<usize> {
instructions
.binary_search_by_key(&offset, |instruction| instruction.offset)
.ok()
}
/// DecodedInstruction 与 verify 共用相对偏移计算;此助手只算数,不验证目标边界。
pub(crate) fn jump_target(instruction: DecodedInstruction) -> Result<usize, VmError> {
// 操作数是代码单元距离,不是字节数或逻辑指令数量。
let argument = instruction.instruction.argument as usize;
// 仅 JumpBackward 做减法,条件跳转与 JumpForward 做加法;checked 防止绕回。
let target = if instruction.instruction.opcode == Opcode::JumpBackward {
instruction.next_offset.checked_sub(argument)
} else {
@@ -331,17 +395,20 @@ pub(crate) fn jump_target(instruction: DecodedInstruction) -> Result<usize, VmEr
target.ok_or_else(|| invalid(instruction.offset, "jump offset overflow"))
}
/// Validate every operand, then propagate stack heights through reachable
/// control flow. Bad jumps, underflow, mismatched joins and fallthrough fail
/// before execution; loops must return to the same stack height.
/// 拒绝非法索引/跳转、可达栈下溢、路径汇合高度不符和落出代码;循环回边必须保持同高。
/// with_layout 解码后调用;先检查全部指令操作数,再分析入口可达控制流并返回最大栈高。
fn verify(
instructions: &[DecodedInstruction],
constants: usize,
layout: &CodeLayout,
) -> Result<usize, VmError> {
// 操作数检查覆盖全部指令,包括入口不可达部分。
for instruction in instructions {
// 逻辑操作码与完整 u32 参数;前缀已由解码器合并。
let Instruction { opcode, argument } = instruction.instruction;
// 表索引以 usize 使用;其上界由对应布局或常量数量限定。
let index = argument as usize;
// Fast 不能指向 Cell,Deref 必须指向 Cell;跳转必须落在完整逻辑起点。
let valid = match opcode {
Opcode::LoadConst => index < constants,
Opcode::LoadFast | Opcode::StoreFast | Opcode::DeleteFast => {
@@ -368,6 +435,7 @@ fn verify(
Opcode::ExtendedArg => false,
_ => true,
};
// 将错误定位到指令起点,尚未开始传播栈状态。
if !valid {
return Err(invalid(
instruction.offset,
@@ -375,15 +443,24 @@ fn verify(
));
}
}
// None 表示尚未从入口到达,不表示其操作数未检查。
let mut heights = vec![None; instructions.len()];
// decode 保证列表非空;代码入口求值栈为空。
heights[0] = Some(0_usize);
// 工作队列保存待展开的指令索引,每个新到达位置只加入一次。
let mut pending = VecDeque::from([0]);
// 全部可达指令执行前后的最大栈高度。
let mut maximum = 0;
// 前向/回向边共用工作队列,汇合高度一致时不重复展开。
while let Some(index) = pending.pop_front() {
// 队列只保存已赋高度的位置,因此下方 unwrap 由入队不变量保证。
let instruction = instructions[index];
// 当前指令执行前高度,不含任何类型或值内容信息。
let height = heights[index].unwrap();
// 逻辑操作码与完整 u32 参数;前缀已由解码器合并。
let Instruction { opcode, argument } = instruction.instruction;
let argument = argument as usize;
// required 是最深访问需求,popped/pushed 是净变化来源;Copy/Swap 不弹出其深层项。
let (required, popped, pushed) = match opcode {
Opcode::LoadConst
| Opcode::LoadFast
@@ -407,19 +484,23 @@ fn verify(
Opcode::Copy => (argument, 0, 1),
Opcode::Swap => (argument, 0, 0),
Opcode::Call => {
// 显式参数之外还消费函数与接收者/Null 两项,先检查加法溢出。
let consumed = argument
.checked_add(2)
.ok_or_else(|| invalid(instruction.offset, "call operand overflow"))?;
(consumed, consumed, 1)
}
Opcode::BuildMap => {
// 每对键值占两项,先检查乘法溢出。
let consumed = argument
.checked_mul(2)
.ok_or_else(|| invalid(instruction.offset, "map operand overflow"))?;
(consumed, consumed, 1)
}
Opcode::MakeFunction => {
// 函数创建消费默认值、捕获 Cell 和一个代码对象,压回一个函数。
let (defaults, closure) = function_counts(instruction.instruction.argument);
// 两个计数来自 16 位段,总和加代码对象项有界。
let consumed = defaults + closure + 1;
(consumed, consumed, 1)
}
@@ -427,17 +508,21 @@ fn verify(
Opcode::ReturnValue => (1, 1, 0),
_ => (0, 0, 0),
};
// 在减 popped 前检查足够高度,避免栈下溢。
if height < required {
return Err(invalid(instruction.offset, "evaluation stack underflow"));
}
// 返回必须恰好留下一个结果供移出,禁止帧内遗留其他栈项。
if opcode == Opcode::ReturnValue && height != 1 {
return Err(invalid(
instruction.offset,
"RETURN_VALUE must leave an empty frame stack",
));
}
// 校验保证减法安全;随后统计当前和后继高度。
let next_height = height - popped + pushed;
maximum = maximum.max(height).max(next_height);
// 返回无后继;条件跳转同时传播跳转/顺序两条路径,不猜测条件值。
let successors: Vec<usize> = match opcode {
Opcode::ReturnValue => Vec::new(),
Opcode::JumpForward | Opcode::JumpBackward => vec![jump_target(instruction)?],
@@ -446,11 +531,15 @@ fn verify(
}
_ => vec![instruction.next_offset],
};
// 每条后继都必须指向现存逻辑指令,顺序落到代码末尾也属于错误。
for successor in successors {
// 将物理单元偏移转换为逻辑指令索引,拒绝扩展前缀中部。
let target = instruction_index(instructions, successor).ok_or_else(|| {
invalid(instruction.offset, "execution falls off the code object")
})?;
// 已到达位置只能接受同高;新位置设置高度并排队。
match heights[target] {
// 回边或分支汇合必须与第一次到达时的高度完全一致。
Some(existing) if existing != next_height => {
return Err(invalid(
successor,
@@ -458,6 +547,7 @@ fn verify(
));
}
Some(_) => {}
// 首次到达的后继写入高度,之后排队检查它的栈需求。
None => {
heights[target] = Some(next_height);
pending.push_back(target);