文章目录
- [1 finalize()](#1 finalize())
- [2 insert](#2 insert)
- [3 lookup_instr](#3 lookup_instr)
- [4 preload_at_least](#4 preload_at_least)
1 finalize()
c
DispatchTable* OpcodeTable::finalize() {
if (final) {
return this;
}
instruction_list.clear();
instruction_list.reserve(instructions.size() * 2 + 1);
unsigned upto = 0;
for (const auto& x : instructions) {
auto range = x.second->get_opcode_range();
assert(range.first == x.first);
assert(range.first < range.second);
assert(range.first >= upto);
assert(range.second <= top_opcode);
if (range.first > upto) {
instruction_list.emplace_back(upto, new OpcodeInstrDummy{upto, range.first});
}
instruction_list.emplace_back(x);
upto = range.second;
}
if (upto < top_opcode) {
instruction_list.emplace_back(upto, new OpcodeInstrDummy{upto, top_opcode});
}
instruction_list.shrink_to_fit();
final = true;
return this;
}
这段代码是 OpcodeTable
类中的 finalize
方法的实现,它的作用是完成指令表的构建过程,确保所有的操作码都被正确地映射到对应的指令上,并且没有任何间隙。以下是该方法的详细步骤和逻辑:
-
检查是否已最终确定:
- 如果
final
标志已经设置为true
,表示指令表已经完成最终确定,直接返回this
指针。
- 如果
-
清空并预留空间:
- 清空
instruction_list
,这是一个存储操作码和指令映射的列表。 - 预留足够的空间以减少后续的内存重新分配。预留的空间是
instructions
映射大小的两倍再加1。
- 清空
-
遍历指令映射:
- 遍历
instructions
映射,这个映射存储了操作码和对应的指令对象。 - 对于每个条目,获取指令对象的操作码范围。
- 遍历
-
断言检查:
- 确保操作码范围的起始值等于映射中的键。
- 确保操作码范围是有效的(起始值小于结束值)。
- 确保操作码范围是连续的,没有间隙。
- 确保操作码范围不超过最大操作码值。
-
处理间隙:
- 如果当前操作码范围的起始值大于
upto
,表示存在间隙,需要插入一个OpcodeInstrDummy
对象来表示这个间隙。
- 如果当前操作码范围的起始值大于
-
添加指令到列表:
- 将当前的操作码和指令对象添加到
instruction_list
中。
- 将当前的操作码和指令对象添加到
-
更新
upto
值:- 更新
upto
为当前操作码范围的结束值。
- 更新
-
处理剩余间隙:
- 如果
upto
小于最大操作码值,表示最后还有一段间隙,需要插入一个OpcodeInstrDummy
对象来表示这个间隙。
- 如果
-
优化内存使用:
- 调用
shrink_to_fit
方法来减少instruction_list
占用的内存。
- 调用
-
标记为最终确定:
- 设置
final
标志为true
,表示指令表已经完成最终确定。
- 设置
finalize
方法是构建指令表的关键步骤,它确保了指令表的完整性和连续性。通过这个方法,可以确保:
- 所有的操作码都被正确地映射到对应的指令上。
- 没有任何操作码间隙,如果有,使用
OpcodeInstrDummy
对象来填充。 - 指令表一旦最终确定,就不能再添加新的指令,这有助于提高指令查找的效率。
这个方法是虚拟机启动前必须执行的步骤,确保虚拟机能够正确地执行指令。
2 insert
c
OpcodeTable& OpcodeTable::insert(const OpcodeInstr* instr) {
LOG_IF(FATAL, !insert_bool(instr)) << td::format::lambda([&](auto& sb) {
sb << "cannot insert instruction into table " << name << ": ";
if (!instr) {
sb << "instruction is null";
} else if (final) {
sb << "instruction table already finalized";
} else {
auto range = instr->get_opcode_range();
sb << "opcode range " << td::format::as_hex(range.first) << ".." << td::format::as_hex(range.second - 1)
<< " already occupied or invalid";
}
});
return *this;
}
这段代码是 OpcodeTable
类中的 insert
方法的实现,它用于将一个新的指令(OpcodeInstr
类型)插入到指令表中。如果插入失败,该方法会记录一个致命错误(FATAL
级别的日志),并提供详细的错误信息。以下是该方法的详细步骤和逻辑:
-
插入指令并检查结果:
- 调用
insert_bool
方法尝试将指令instr
插入到指令表中,并检查返回值。 insert_bool
返回true
表示插入成功,返回false
表示插入失败。
- 调用
-
记录错误信息:
- 如果
insert_bool
返回false
,使用LOG_IF
宏记录错误信息。 LOG_IF
宏会在条件为false
时执行日志记录,这里的条件是!insert_bool(instr)
,即插入失败时。
- 如果
-
格式化错误信息:
- 使用
td::format::lambda
和 lambda 表达式来格式化错误信息。 sb
是一个字符串构建器,用于构建错误信息。
- 使用
-
检查指令是否为
null
:- 如果
instr
是null
,记录"instruction is null"。
- 如果
-
检查指令表是否已最终确定:
- 如果指令表已经最终确定(
final
标志为true
),记录"instruction table already finalized"。
- 如果指令表已经最终确定(
-
检查操作码范围是否有效或已占用:
- 如果以上两个条件都不满足,获取指令的操作码范围,并检查是否有效或已占用。
- 将操作码范围格式化为十六进制字符串,并记录错误信息。
insert
方法是指令表管理的关键部分,它确保了指令能够被正确地插入到指令表中。以下是该方法的一些重要作用:
- 确保指令表的完整性:通过检查操作码范围的有效性和是否已占用,确保指令表的完整性和一致性。
- 提供详细的错误信息:如果插入失败,提供详细的错误信息,包括指令表名称、失败原因等,有助于调试和问题排查。
- 防止指令表被错误地修改 :通过检查
final
标志,防止在指令表最终确定后插入新的指令,确保指令表的稳定性。 - 记录致命错误 :使用
FATAL
级别的日志记录错误信息,表明这是一个严重的问题,需要立即解决。
通过这个方法,可以确保指令表的正确性和稳定性,为虚拟机的正确执行提供基础。
3 lookup_instr
c
const OpcodeInstr* OpcodeTable::lookup_instr(const CellSlice& cs, unsigned& opcode, unsigned& bits) const {
bits = max_opcode_bits;
unsigned long long prefetch = cs.prefetch_ulong_top(bits);
opcode = (unsigned)(prefetch >> (64 - max_opcode_bits));
opcode &= (static_cast<int32_t>(static_cast<td::uint32>(-1) << max_opcode_bits) >> bits);
return lookup_instr(opcode, bits);
}
这段代码是 OpcodeTable
类中的 lookup_instr
方法的实现,它用于从 CellSlice
对象中提取操作码和位数,并查找对应的指令。以下是该方法的详细步骤和逻辑:
-
设置位数:
bits = max_opcode_bits;
这行代码将bits
设置为操作码的最大位数,这是预设的操作码长度。
-
预读取数据:
unsigned long long prefetch = cs.prefetch_ulong_top(bits);
这行代码从CellSlice
对象cs
中预读取最多bits
位的数据到prefetch
变量中。prefetch_ulong_top
方法会读取最高位的bits
位数据,形成一个无符号长整型数。
-
提取操作码:
opcode = (unsigned)(prefetch >> (64 - max_opcode_bits));
这行代码将预读取的数据右移64 - max_opcode_bits
位,以将操作码移到最低位,然后转换为unsigned
类型,得到操作码的值。opcode &= (static_cast<int32_t>(static_cast<td::uint32>(-1) << max_opcode_bits) >> bits);
这行代码通过位与操作来清除opcode
中超出max_opcode_bits
位数的高位。这里使用了-1
左移max_opcode_bits
位来创建一个掩码,然后右移bits
位来适配实际的操作码位数。
-
查找指令:
return lookup_instr(opcode, bits);
最后,使用提取的操作码和位数调用另一个lookup_instr
方法来查找对应的指令,并返回找到的指令对象的指针。
这个方法的作用是从 CellSlice
对象中提取操作码和位数,并使用这些信息来查找对应的指令。这是虚拟机执行流程中的关键步骤,因为它允许虚拟机根据指令流中的操作码来确定要执行的具体指令。
- 预读取和提取操作码:这个方法通过预读取和位操作来提取操作码,这是处理指令流的基础。
- 位数适配:通过位与操作来确保操作码只包含有效的位数,这对于处理不同长度的操作码非常重要。
- 指令查找:使用提取的操作码和位数来查找指令,这是虚拟机执行指令前的必要步骤。
这个方法的设计使得虚拟机能够灵活地处理不同长度的操作码,并且能够从数据流中准确地提取出操作码信息,为执行指令做好准备。
这行代码中的 bits
变量代表了操作码的实际位数。在虚拟机的上下文中,操作码的位数通常是指操作码所需的位数,这个位数可能小于或等于操作码可能的最大位数(max_opcode_bits
)。以下是代码中如何确定 bits
的步骤:
-
预设最大位数:
bits = max_opcode_bits;
这行代码将bits
初始化为操作码可能的最大位数,这是一个预设值,用于后续的位操作。
-
预读取数据:
unsigned long long prefetch = cs.prefetch_ulong_top(bits);
这行代码从CellSlice
对象cs
中预读取最多bits
位的数据。这里的bits
还是预设的最大位数。
-
提取操作码:
opcode = (unsigned)(prefetch >> (64 - max_opcode_bits));
这行代码将预读取的数据右移,以便将操作码移到最低位。
-
位数适配:
opcode &= (static_cast<int32_t>(static_cast<td::uint32>(-1) << max_opcode_bits) >> bits);
这行代码通过位与操作来确保opcode
只包含有效的位数。
在位数适配这一步,bits
的值实际上是在之前的步骤中确定的,通常是通过分析指令集架构或运行时环境来确定的。例如,如果指令集架构规定了操作码的最大长度,那么 max_opcode_bits
就会被设置为这个值。在执行期间,虚拟机会根据实际遇到的操作码来确定 bits
的值。
在某些情况下,bits
可能是动态确定的,例如,如果操作码的长度可以根据上下文变化,那么 bits
可能会根据特定的指令或数据来调整。在这种情况下,bits
的值可能会在执行期间通过某种机制(如解析操作码前缀或根据操作码的模式)来确定。
总的来说,bits
的值是由虚拟机的设计和指令集架构决定的,它代表了操作码所需的位数,这个值在执行期间用于正确地提取和解释操作码。
4 preload_at_least
这段代码是 CellSlice
类中的 preload_at_least
方法的实现,它用于确保 CellSlice
对象中至少预加载了 req_bits
位数据。这个方法是私有的,可能是为了内部处理而设计的。以下是该方法的详细步骤和逻辑:
-
参数和状态检查:
assert(req_bits <= 64 && have(req_bits) && ptr);
这行代码断言req_bits
不超过 64,并且至少有req_bits
位数据可用,同时ptr
指针不为空。
-
检查是否已满足要求:
if (req_bits <= zd) { return; }
如果已经预加载的位数zd
大于或等于req_bits
,则不需要进一步预加载。
-
计算剩余位数:
int remain = bits_en - bits_st - zd;
计算剩余的位数,即总位数减去起始位数和已预加载的位数。
-
处理32位数据:
- 如果
zd
小于或等于 32 并且剩余位数remain
大于 24,那么从ptr
指向的位置加载一个32位的数据,并将其左移至z
的正确位置。 - 更新
ptr
指针和zd
(已预加载的位数)。
- 如果
-
处理剩余位数:
- 如果剩余位数
remain
小于或等于 32,那么更新zd
并返回。 - 否则,继续处理剩余的位数。
- 如果剩余位数
-
循环处理剩余位数:
while (zd < req_bits && remain > 0)
循环直到zd
达到req_bits
或者没有剩余位数。- 如果
zd
大于 56,那么从ptr
指向的位置加载数据,并将其右移至z
的正确位置。 - 否则,从
ptr
指向的位置加载数据,并将其左移至z
的正确位置。 - 更新
ptr
指针和zd
。
-
更新剩余位数:
- 如果剩余位数
remain
小于或等于 8,那么更新zd
并返回。 - 否则,更新
zd
和remain
。
- 如果剩余位数
preload_at_least
方法的作用是确保 CellSlice
对象中至少预加载了 req_bits
位数据,这对于后续的位操作和数据提取非常重要。以下是该方法的一些重要作用:
- 确保数据可用性:通过预加载足够的位数,确保后续操作有足够的数据可用。
- 提高效率:通过预加载数据,减少后续操作中的内存访问次数,提高效率。
- 处理不同长度的数据:能够处理不同长度的数据,包括32位和非32位的数据。
这个方法是 CellSlice
类中处理位数据流的关键部分,它为后续的位操作提供了基础。通过预加载足够的数据,可以确保后续操作的正确性和效率。