2405 lines
88 KiB
Rust
2405 lines
88 KiB
Rust
use std::collections::{BTreeMap, HashMap, HashSet, VecDeque};
|
|
use std::iter::FromIterator;
|
|
use std::mem;
|
|
use std::ptr::NonNull;
|
|
|
|
use analysis::AnalysisType;
|
|
use codegen;
|
|
use control_flow;
|
|
use control_flow::WasmStructure;
|
|
use cpu::cpu;
|
|
use cpu::global_pointers;
|
|
use cpu::memory;
|
|
use cpu_context::CpuContext;
|
|
use jit_instructions;
|
|
use opstats;
|
|
use page::Page;
|
|
use profiler;
|
|
use profiler::stat;
|
|
use state_flags::CachedStateFlags;
|
|
use util::SafeToU16;
|
|
use wasmgen::wasm_builder::{Label, WasmBuilder, WasmLocal};
|
|
|
|
#[derive(Copy, Clone, Eq, Hash, PartialEq)]
|
|
#[repr(transparent)]
|
|
pub struct WasmTableIndex(u16);
|
|
impl WasmTableIndex {
|
|
pub fn to_u16(self) -> u16 { self.0 }
|
|
}
|
|
|
|
mod unsafe_jit {
|
|
use jit::{CachedStateFlags, WasmTableIndex};
|
|
|
|
extern "C" {
|
|
pub fn codegen_finalize(
|
|
wasm_table_index: WasmTableIndex,
|
|
phys_addr: u32,
|
|
state_flags: CachedStateFlags,
|
|
ptr: u32,
|
|
len: u32,
|
|
);
|
|
pub fn jit_clear_func(wasm_table_index: WasmTableIndex);
|
|
}
|
|
}
|
|
|
|
fn codegen_finalize(
|
|
wasm_table_index: WasmTableIndex,
|
|
phys_addr: u32,
|
|
state_flags: CachedStateFlags,
|
|
ptr: u32,
|
|
len: u32,
|
|
) {
|
|
unsafe { unsafe_jit::codegen_finalize(wasm_table_index, phys_addr, state_flags, ptr, len) }
|
|
}
|
|
|
|
pub fn jit_clear_func(wasm_table_index: WasmTableIndex) {
|
|
unsafe { unsafe_jit::jit_clear_func(wasm_table_index) }
|
|
}
|
|
|
|
// less branches will generate if-else, more will generate brtable
|
|
pub const BRTABLE_CUTOFF: usize = 10;
|
|
|
|
pub const WASM_TABLE_SIZE: u32 = 900;
|
|
|
|
pub const CHECK_JIT_STATE_INVARIANTS: bool = false;
|
|
|
|
pub const JIT_USE_LOOP_SAFETY: bool = true;
|
|
|
|
pub const JIT_THRESHOLD: u32 = 200 * 1000;
|
|
|
|
pub const MAX_EXTRA_BASIC_BLOCKS: usize = 250;
|
|
|
|
const MAX_INSTRUCTION_LENGTH: u32 = 16;
|
|
|
|
#[allow(non_upper_case_globals)]
|
|
static mut jit_state: NonNull<JitState> =
|
|
unsafe { NonNull::new_unchecked(mem::align_of::<JitState>() as *mut _) };
|
|
|
|
pub fn get_jit_state() -> &'static mut JitState { unsafe { jit_state.as_mut() } }
|
|
|
|
#[no_mangle]
|
|
pub fn rust_init() {
|
|
dbg_assert!(std::mem::size_of::<[Option<NonNull<cpu::Code>>; 0x100000]>() == 0x100000 * 4);
|
|
|
|
let x = Box::new(JitState::create_and_initialise());
|
|
unsafe {
|
|
jit_state = NonNull::new(Box::into_raw(x)).unwrap()
|
|
}
|
|
|
|
use std::panic;
|
|
|
|
panic::set_hook(Box::new(|panic_info| {
|
|
console_log!("{}", panic_info.to_string());
|
|
}));
|
|
}
|
|
|
|
struct PageInfo {
|
|
wasm_table_index: WasmTableIndex,
|
|
hidden_wasm_table_indices: Vec<WasmTableIndex>,
|
|
entry_points: Vec<(u16, u16)>,
|
|
state_flags: CachedStateFlags,
|
|
}
|
|
|
|
enum CompilingPageState {
|
|
Compiling { pages: HashMap<Page, PageInfo> },
|
|
CompilingWritten,
|
|
}
|
|
|
|
pub struct JitState {
|
|
wasm_builder: WasmBuilder,
|
|
|
|
// as an alternative to HashSet, we could use a bitmap of 4096 bits here
|
|
// (faster, but uses much more memory)
|
|
// or a compressed bitmap (likely faster)
|
|
// or HashSet<u32> rather than nested
|
|
entry_points: HashMap<Page, (u32, HashSet<u16>)>,
|
|
pages: HashMap<Page, PageInfo>,
|
|
wasm_table_index_free_list: Vec<WasmTableIndex>,
|
|
compiling: Option<(WasmTableIndex, CompilingPageState)>,
|
|
}
|
|
|
|
pub fn check_jit_state_invariants(ctx: &mut JitState) {
|
|
if !CHECK_JIT_STATE_INVARIANTS {
|
|
return;
|
|
}
|
|
|
|
match &ctx.compiling {
|
|
Some((_, CompilingPageState::Compiling { pages })) => {
|
|
dbg_assert!(pages.keys().all(|page| ctx.entry_points.contains_key(page)));
|
|
},
|
|
_ => {},
|
|
}
|
|
|
|
let free: HashSet<WasmTableIndex> =
|
|
HashSet::from_iter(ctx.wasm_table_index_free_list.iter().cloned());
|
|
let used = HashSet::from_iter(ctx.pages.values().map(|info| info.wasm_table_index));
|
|
let compiling = HashSet::from_iter(ctx.compiling.as_ref().map(|&(index, _)| index));
|
|
dbg_assert!(free.intersection(&used).next().is_none());
|
|
dbg_assert!(used.intersection(&compiling).next().is_none());
|
|
dbg_assert!(free.len() + used.len() + compiling.len() == (WASM_TABLE_SIZE - 1) as usize);
|
|
|
|
match &ctx.compiling {
|
|
Some((_, CompilingPageState::Compiling { pages })) => {
|
|
dbg_assert!(pages.keys().all(|page| ctx.entry_points.contains_key(page)));
|
|
},
|
|
_ => {},
|
|
}
|
|
|
|
for i in 0..unsafe { cpu::valid_tlb_entries_count } {
|
|
let page = unsafe { cpu::valid_tlb_entries[i as usize] };
|
|
let entry = unsafe { cpu::tlb_data[page as usize] };
|
|
if 0 != entry {
|
|
let tlb_physical_page = Page::of_u32(
|
|
(entry as u32 >> 12 ^ page as u32) - (unsafe { memory::mem8 } as u32 >> 12),
|
|
);
|
|
let w = match unsafe { cpu::tlb_code[page as usize] } {
|
|
None => None,
|
|
Some(c) => unsafe {
|
|
Some(c.as_ref().wasm_table_index)
|
|
},
|
|
};
|
|
let tlb_has_code = entry & cpu::TLB_HAS_CODE == cpu::TLB_HAS_CODE;
|
|
let infos = ctx.pages.get(&tlb_physical_page);
|
|
let entry_points = ctx.entry_points.get(&tlb_physical_page);
|
|
dbg_assert!(tlb_has_code || !w.is_some());
|
|
dbg_assert!(tlb_has_code || !infos.is_some());
|
|
dbg_assert!(tlb_has_code || !entry_points.is_some());
|
|
//dbg_assert!((w.is_some() || page.is_some() || entry_points.is_some()) == tlb_has_code); // XXX: check this
|
|
}
|
|
}
|
|
}
|
|
|
|
impl JitState {
|
|
pub fn create_and_initialise() -> JitState {
|
|
// don't assign 0 (XXX: Check)
|
|
let wasm_table_indices = (1..=(WASM_TABLE_SIZE - 1) as u16).map(|x| WasmTableIndex(x));
|
|
|
|
JitState {
|
|
wasm_builder: WasmBuilder::new(),
|
|
|
|
entry_points: HashMap::new(),
|
|
pages: HashMap::new(),
|
|
|
|
wasm_table_index_free_list: Vec::from_iter(wasm_table_indices),
|
|
compiling: None,
|
|
}
|
|
}
|
|
}
|
|
|
|
#[derive(PartialEq, Eq)]
|
|
pub enum BasicBlockType {
|
|
Normal {
|
|
next_block_addr: Option<u32>,
|
|
jump_offset: i32,
|
|
jump_offset_is_32: bool,
|
|
},
|
|
ConditionalJump {
|
|
next_block_addr: Option<u32>,
|
|
next_block_branch_taken_addr: Option<u32>,
|
|
condition: u8,
|
|
jump_offset: i32,
|
|
jump_offset_is_32: bool,
|
|
},
|
|
// Set eip to an absolute value (ret, jmp r/m, call r/m)
|
|
AbsoluteEip,
|
|
Exit,
|
|
}
|
|
|
|
pub struct BasicBlock {
|
|
pub addr: u32,
|
|
pub virt_addr: i32,
|
|
pub last_instruction_addr: u32,
|
|
pub end_addr: u32,
|
|
pub is_entry_block: bool,
|
|
pub ty: BasicBlockType,
|
|
pub has_sti: bool,
|
|
pub number_of_instructions: u32,
|
|
}
|
|
|
|
#[derive(Copy, Clone, PartialEq)]
|
|
pub struct CachedCode {
|
|
pub wasm_table_index: WasmTableIndex,
|
|
pub initial_state: u16,
|
|
}
|
|
|
|
impl CachedCode {
|
|
pub const NONE: CachedCode = CachedCode {
|
|
wasm_table_index: WasmTableIndex(0),
|
|
initial_state: 0,
|
|
};
|
|
}
|
|
|
|
#[derive(PartialEq)]
|
|
pub enum InstructionOperandDest {
|
|
WasmLocal(WasmLocal),
|
|
Other,
|
|
}
|
|
#[derive(PartialEq)]
|
|
pub enum InstructionOperand {
|
|
WasmLocal(WasmLocal),
|
|
Immediate(i32),
|
|
Other,
|
|
}
|
|
impl InstructionOperand {
|
|
pub fn is_zero(&self) -> bool {
|
|
match self {
|
|
InstructionOperand::Immediate(0) => true,
|
|
_ => false,
|
|
}
|
|
}
|
|
}
|
|
impl Into<InstructionOperand> for InstructionOperandDest {
|
|
fn into(self: InstructionOperandDest) -> InstructionOperand {
|
|
match self {
|
|
InstructionOperandDest::WasmLocal(l) => InstructionOperand::WasmLocal(l),
|
|
InstructionOperandDest::Other => InstructionOperand::Other,
|
|
}
|
|
}
|
|
}
|
|
pub enum Instruction {
|
|
Cmp {
|
|
dest: InstructionOperandDest,
|
|
source: InstructionOperand,
|
|
opsize: i32,
|
|
},
|
|
Sub {
|
|
dest: InstructionOperandDest,
|
|
source: InstructionOperand,
|
|
opsize: i32,
|
|
is_dec: bool,
|
|
},
|
|
Add {
|
|
dest: InstructionOperandDest,
|
|
source: InstructionOperand,
|
|
opsize: i32,
|
|
is_inc: bool,
|
|
},
|
|
AdcSbb {
|
|
dest: InstructionOperandDest,
|
|
source: InstructionOperand,
|
|
opsize: i32,
|
|
},
|
|
NonZeroShift {
|
|
dest: InstructionOperandDest,
|
|
opsize: i32,
|
|
},
|
|
Bitwise {
|
|
dest: InstructionOperandDest,
|
|
opsize: i32,
|
|
},
|
|
Other,
|
|
}
|
|
|
|
pub struct JitContext<'a> {
|
|
pub cpu: &'a mut CpuContext,
|
|
pub builder: &'a mut WasmBuilder,
|
|
pub register_locals: &'a mut Vec<WasmLocal>,
|
|
pub start_of_current_instruction: u32,
|
|
pub exit_with_fault_label: Label,
|
|
pub exit_label: Label,
|
|
pub current_instruction: Instruction,
|
|
pub previous_instruction: Instruction,
|
|
pub instruction_counter: WasmLocal,
|
|
}
|
|
impl<'a> JitContext<'a> {
|
|
pub fn reg(&self, i: u32) -> WasmLocal { self.register_locals[i as usize].unsafe_clone() }
|
|
}
|
|
|
|
pub const JIT_INSTR_BLOCK_BOUNDARY_FLAG: u32 = 1 << 0;
|
|
|
|
pub fn is_near_end_of_page(address: u32) -> bool {
|
|
address & 0xFFF >= 0x1000 - MAX_INSTRUCTION_LENGTH
|
|
}
|
|
|
|
pub fn jit_find_cache_entry(phys_address: u32, state_flags: CachedStateFlags) -> CachedCode {
|
|
// TODO: dedup with jit_find_cache_entry_in_page?
|
|
// NOTE: This is currently only used for invariant/missed-entry-point checking
|
|
let ctx = get_jit_state();
|
|
|
|
match ctx.pages.get(&Page::page_of(phys_address)) {
|
|
Some(PageInfo {
|
|
wasm_table_index,
|
|
state_flags: s,
|
|
entry_points,
|
|
hidden_wasm_table_indices: _,
|
|
}) => {
|
|
if *s == state_flags {
|
|
let page_offset = phys_address as u16 & 0xFFF;
|
|
if let Some(&(_, initial_state)) =
|
|
entry_points.iter().find(|(p, _)| p == &page_offset)
|
|
{
|
|
return CachedCode {
|
|
wasm_table_index: *wasm_table_index,
|
|
initial_state,
|
|
};
|
|
}
|
|
}
|
|
},
|
|
None => {},
|
|
}
|
|
|
|
return CachedCode::NONE;
|
|
}
|
|
|
|
#[no_mangle]
|
|
pub fn jit_find_cache_entry_in_page(
|
|
virt_address: u32,
|
|
wasm_table_index: WasmTableIndex,
|
|
state_flags: u32,
|
|
) -> i32 {
|
|
// TODO: generate code for this
|
|
profiler::stat_increment(stat::INDIRECT_JUMP);
|
|
|
|
let state_flags = CachedStateFlags::of_u32(state_flags);
|
|
|
|
unsafe {
|
|
match cpu::tlb_code[(virt_address >> 12) as usize] {
|
|
None => {},
|
|
Some(c) => {
|
|
let c = c.as_ref();
|
|
if state_flags == c.state_flags && wasm_table_index == c.wasm_table_index {
|
|
let state = c.state_table[virt_address as usize & 0xFFF];
|
|
if state != u16::MAX {
|
|
return state.into();
|
|
}
|
|
}
|
|
},
|
|
}
|
|
}
|
|
|
|
profiler::stat_increment(stat::INDIRECT_JUMP_NO_ENTRY);
|
|
|
|
return -1;
|
|
}
|
|
|
|
// Maximum number of pages per wasm module. Necessary for the following reasons:
|
|
// - There is an upper limit on the size of a single function in wasm (currently ~7MB in all browsers)
|
|
// See https://github.com/WebAssembly/design/issues/1138
|
|
// - v8 poorly handles large br_table elements and OOMs on modules much smaller than the above limit
|
|
// See https://bugs.chromium.org/p/v8/issues/detail?id=9697 and https://bugs.chromium.org/p/v8/issues/detail?id=9141
|
|
// Will hopefully be fixed in the near future by generating direct control flow
|
|
const MAX_PAGES: usize = 3;
|
|
|
|
fn jit_find_basic_blocks(
|
|
ctx: &mut JitState,
|
|
entry_points: HashSet<i32>,
|
|
cpu: CpuContext,
|
|
) -> Vec<BasicBlock> {
|
|
fn follow_jump(
|
|
virt_target: i32,
|
|
ctx: &mut JitState,
|
|
pages: &mut HashSet<Page>,
|
|
page_blacklist: &mut HashSet<Page>,
|
|
max_pages: usize,
|
|
marked_as_entry: &mut HashSet<i32>,
|
|
to_visit_stack: &mut Vec<i32>,
|
|
) -> Option<u32> {
|
|
if is_near_end_of_page(virt_target as u32) {
|
|
return None;
|
|
}
|
|
let phys_target = match cpu::translate_address_read_no_side_effects(virt_target) {
|
|
Err(()) => {
|
|
dbg_log!("Not analysing {:x} (page not mapped)", virt_target);
|
|
return None;
|
|
},
|
|
Ok(t) => t,
|
|
};
|
|
|
|
let phys_page = Page::page_of(phys_target);
|
|
|
|
if !pages.contains(&phys_page) && pages.len() == max_pages
|
|
|| page_blacklist.contains(&phys_page)
|
|
{
|
|
return None;
|
|
}
|
|
|
|
if !pages.contains(&phys_page) {
|
|
// page seen for the first time, handle entry points
|
|
if let Some((hotness, entry_points)) = ctx.entry_points.get_mut(&phys_page) {
|
|
let existing_entry_points = match ctx.pages.get(&phys_page) {
|
|
Some(PageInfo { entry_points, .. }) => {
|
|
HashSet::from_iter(entry_points.iter().map(|x| x.0))
|
|
},
|
|
None => HashSet::new(),
|
|
};
|
|
|
|
if entry_points
|
|
.iter()
|
|
.all(|entry_point| existing_entry_points.contains(entry_point))
|
|
{
|
|
page_blacklist.insert(phys_page);
|
|
return None;
|
|
}
|
|
|
|
// XXX: Remove this paragraph
|
|
//let old_length = entry_points.len();
|
|
//entry_points.extend(existing_entry_points);
|
|
//dbg_assert!(
|
|
// entry_points.union(&existing_entry_points).count() == entry_points.len()
|
|
//);
|
|
|
|
*hotness = 0;
|
|
|
|
for &addr_low in entry_points.iter() {
|
|
let addr = virt_target & !0xFFF | addr_low as i32;
|
|
to_visit_stack.push(addr);
|
|
marked_as_entry.insert(addr);
|
|
}
|
|
}
|
|
else {
|
|
// no entry points: ignore this page?
|
|
page_blacklist.insert(phys_page);
|
|
return None;
|
|
}
|
|
|
|
pages.insert(phys_page);
|
|
dbg_assert!(pages.len() <= max_pages);
|
|
}
|
|
|
|
to_visit_stack.push(virt_target);
|
|
Some(phys_target)
|
|
}
|
|
|
|
let mut to_visit_stack: Vec<i32> = Vec::new();
|
|
let mut marked_as_entry: HashSet<i32> = HashSet::new();
|
|
let mut basic_blocks: BTreeMap<u32, BasicBlock> = BTreeMap::new();
|
|
let mut pages: HashSet<Page> = HashSet::new();
|
|
let mut page_blacklist = HashSet::new();
|
|
|
|
// 16-bit doesn't not work correctly, most likely due to instruction pointer wrap-around
|
|
let max_pages = if cpu.state_flags.is_32() { MAX_PAGES } else { 1 };
|
|
|
|
for virt_addr in entry_points {
|
|
let ok = follow_jump(
|
|
virt_addr,
|
|
ctx,
|
|
&mut pages,
|
|
&mut page_blacklist,
|
|
max_pages,
|
|
&mut marked_as_entry,
|
|
&mut to_visit_stack,
|
|
);
|
|
dbg_assert!(ok.is_some());
|
|
dbg_assert!(marked_as_entry.contains(&virt_addr));
|
|
}
|
|
|
|
while let Some(to_visit) = to_visit_stack.pop() {
|
|
let phys_addr = match cpu::translate_address_read_no_side_effects(to_visit) {
|
|
Err(()) => {
|
|
dbg_log!("Not analysing {:x} (page not mapped)", to_visit);
|
|
continue;
|
|
},
|
|
Ok(phys_addr) => phys_addr,
|
|
};
|
|
|
|
if basic_blocks.contains_key(&phys_addr) {
|
|
continue;
|
|
}
|
|
|
|
if is_near_end_of_page(phys_addr) {
|
|
// Empty basic block, don't insert
|
|
profiler::stat_increment(stat::COMPILE_CUT_OFF_AT_END_OF_PAGE);
|
|
continue;
|
|
}
|
|
|
|
let mut current_address = phys_addr;
|
|
let mut current_block = BasicBlock {
|
|
addr: current_address,
|
|
virt_addr: to_visit,
|
|
last_instruction_addr: 0,
|
|
end_addr: 0,
|
|
ty: BasicBlockType::Exit,
|
|
is_entry_block: false,
|
|
has_sti: false,
|
|
number_of_instructions: 0,
|
|
};
|
|
loop {
|
|
let addr_before_instruction = current_address;
|
|
let mut cpu = &mut CpuContext {
|
|
eip: current_address,
|
|
..cpu
|
|
};
|
|
let analysis = ::analysis::analyze_step(&mut cpu);
|
|
current_block.number_of_instructions += 1;
|
|
let has_next_instruction = !analysis.no_next_instruction;
|
|
current_address = cpu.eip;
|
|
|
|
dbg_assert!(Page::page_of(current_address) == Page::page_of(addr_before_instruction));
|
|
let current_virt_addr = to_visit & !0xFFF | current_address as i32 & 0xFFF;
|
|
|
|
match analysis.ty {
|
|
AnalysisType::Normal | AnalysisType::STI => {
|
|
dbg_assert!(has_next_instruction);
|
|
dbg_assert!(!analysis.absolute_jump);
|
|
|
|
if current_block.has_sti {
|
|
// Convert next instruction after STI (i.e., the current instruction) into block boundary
|
|
|
|
marked_as_entry.insert(current_virt_addr);
|
|
to_visit_stack.push(current_virt_addr);
|
|
|
|
current_block.last_instruction_addr = addr_before_instruction;
|
|
current_block.end_addr = current_address;
|
|
break;
|
|
}
|
|
|
|
if analysis.ty == AnalysisType::STI {
|
|
current_block.has_sti = true;
|
|
|
|
dbg_assert!(
|
|
!is_near_end_of_page(current_address),
|
|
"TODO: Handle STI instruction near end of page"
|
|
);
|
|
}
|
|
else {
|
|
// Only split non-STI blocks (one instruction needs to run after STI before
|
|
// handle_irqs may be called)
|
|
|
|
if basic_blocks.contains_key(¤t_address) {
|
|
current_block.last_instruction_addr = addr_before_instruction;
|
|
current_block.end_addr = current_address;
|
|
dbg_assert!(!is_near_end_of_page(current_address));
|
|
current_block.ty = BasicBlockType::Normal {
|
|
next_block_addr: Some(current_address),
|
|
jump_offset: 0,
|
|
jump_offset_is_32: true,
|
|
};
|
|
break;
|
|
}
|
|
}
|
|
},
|
|
AnalysisType::Jump {
|
|
offset,
|
|
is_32,
|
|
condition: Some(condition),
|
|
} => {
|
|
dbg_assert!(!analysis.absolute_jump);
|
|
// conditional jump: continue at next and continue at jump target
|
|
|
|
let jump_target = if is_32 {
|
|
current_virt_addr + offset
|
|
}
|
|
else {
|
|
cpu.cs_offset as i32
|
|
+ (current_virt_addr - cpu.cs_offset as i32 + offset & 0xFFFF)
|
|
};
|
|
|
|
dbg_assert!(has_next_instruction);
|
|
to_visit_stack.push(current_virt_addr);
|
|
|
|
let next_block_addr = if is_near_end_of_page(current_address) {
|
|
None
|
|
}
|
|
else {
|
|
Some(current_address)
|
|
};
|
|
|
|
current_block.ty = BasicBlockType::ConditionalJump {
|
|
next_block_addr,
|
|
next_block_branch_taken_addr: follow_jump(
|
|
jump_target,
|
|
ctx,
|
|
&mut pages,
|
|
&mut page_blacklist,
|
|
max_pages,
|
|
&mut marked_as_entry,
|
|
&mut to_visit_stack,
|
|
),
|
|
condition,
|
|
jump_offset: offset,
|
|
jump_offset_is_32: is_32,
|
|
};
|
|
|
|
current_block.last_instruction_addr = addr_before_instruction;
|
|
current_block.end_addr = current_address;
|
|
|
|
break;
|
|
},
|
|
AnalysisType::Jump {
|
|
offset,
|
|
is_32,
|
|
condition: None,
|
|
} => {
|
|
dbg_assert!(!analysis.absolute_jump);
|
|
// non-conditional jump: continue at jump target
|
|
|
|
let jump_target = if is_32 {
|
|
current_virt_addr + offset
|
|
}
|
|
else {
|
|
cpu.cs_offset as i32
|
|
+ (current_virt_addr - cpu.cs_offset as i32 + offset & 0xFFFF)
|
|
};
|
|
|
|
if has_next_instruction {
|
|
// Execution will eventually come back to the next instruction (CALL)
|
|
marked_as_entry.insert(current_virt_addr);
|
|
to_visit_stack.push(current_virt_addr);
|
|
}
|
|
|
|
current_block.ty = BasicBlockType::Normal {
|
|
next_block_addr: follow_jump(
|
|
jump_target,
|
|
ctx,
|
|
&mut pages,
|
|
&mut page_blacklist,
|
|
max_pages,
|
|
&mut marked_as_entry,
|
|
&mut to_visit_stack,
|
|
),
|
|
jump_offset: offset,
|
|
jump_offset_is_32: is_32,
|
|
};
|
|
current_block.last_instruction_addr = addr_before_instruction;
|
|
current_block.end_addr = current_address;
|
|
|
|
break;
|
|
},
|
|
AnalysisType::BlockBoundary => {
|
|
// a block boundary but not a jump, get out
|
|
|
|
if has_next_instruction {
|
|
// block boundary, but execution will eventually come back
|
|
// to the next instruction. Create a new basic block
|
|
// starting at the next instruction and register it as an
|
|
// entry point
|
|
marked_as_entry.insert(current_virt_addr);
|
|
to_visit_stack.push(current_virt_addr);
|
|
}
|
|
|
|
if analysis.absolute_jump {
|
|
current_block.ty = BasicBlockType::AbsoluteEip;
|
|
}
|
|
|
|
current_block.last_instruction_addr = addr_before_instruction;
|
|
current_block.end_addr = current_address;
|
|
break;
|
|
},
|
|
}
|
|
|
|
if is_near_end_of_page(current_address) {
|
|
current_block.last_instruction_addr = addr_before_instruction;
|
|
current_block.end_addr = current_address;
|
|
profiler::stat_increment(stat::COMPILE_CUT_OFF_AT_END_OF_PAGE);
|
|
break;
|
|
}
|
|
}
|
|
|
|
let previous_block = basic_blocks
|
|
.range(..current_block.addr)
|
|
.next_back()
|
|
.filter(|(_, previous_block)| (!previous_block.has_sti))
|
|
.map(|(_, previous_block)| previous_block.clone());
|
|
|
|
if let Some(previous_block) = previous_block {
|
|
if current_block.addr < previous_block.end_addr {
|
|
// If this block overlaps with the previous block, re-analyze the previous block
|
|
to_visit_stack.push(previous_block.virt_addr);
|
|
|
|
let addr = previous_block.addr;
|
|
let old_block = basic_blocks.remove(&addr);
|
|
dbg_assert!(old_block.is_some());
|
|
|
|
// Note that this does not ensure the invariant that two consecutive blocks don't
|
|
// overlay. For that, we also need to check the following block.
|
|
}
|
|
}
|
|
|
|
dbg_assert!(current_block.addr < current_block.end_addr);
|
|
dbg_assert!(current_block.addr <= current_block.last_instruction_addr);
|
|
dbg_assert!(current_block.last_instruction_addr < current_block.end_addr);
|
|
|
|
basic_blocks.insert(current_block.addr, current_block);
|
|
}
|
|
|
|
dbg_assert!(pages.len() <= max_pages);
|
|
|
|
for block in basic_blocks.values_mut() {
|
|
if marked_as_entry.contains(&block.virt_addr) {
|
|
block.is_entry_block = true;
|
|
}
|
|
}
|
|
|
|
let basic_blocks: Vec<BasicBlock> = basic_blocks.into_iter().map(|(_, block)| block).collect();
|
|
|
|
for i in 0..basic_blocks.len() - 1 {
|
|
let next_block_addr = basic_blocks[i + 1].addr;
|
|
let next_block_end_addr = basic_blocks[i + 1].end_addr;
|
|
let next_block_is_entry = basic_blocks[i + 1].is_entry_block;
|
|
let block = &basic_blocks[i];
|
|
dbg_assert!(block.addr < next_block_addr);
|
|
if next_block_addr < block.end_addr {
|
|
dbg_log!(
|
|
"Overlapping first=[from={:x} to={:x} is_entry={}] second=[from={:x} to={:x} is_entry={}]",
|
|
block.addr,
|
|
block.end_addr,
|
|
block.is_entry_block as u8,
|
|
next_block_addr,
|
|
next_block_end_addr,
|
|
next_block_is_entry as u8
|
|
);
|
|
}
|
|
}
|
|
|
|
basic_blocks
|
|
}
|
|
|
|
#[no_mangle]
|
|
#[cfg(debug_assertions)]
|
|
pub fn jit_force_generate_unsafe(virt_addr: i32) {
|
|
dbg_assert!(
|
|
!is_near_end_of_page(virt_addr as u32),
|
|
"cannot force compile near end of page"
|
|
);
|
|
jit_increase_hotness_and_maybe_compile(
|
|
virt_addr,
|
|
cpu::translate_address_read(virt_addr).unwrap(),
|
|
cpu::get_seg_cs() as u32,
|
|
cpu::get_state_flags(),
|
|
JIT_THRESHOLD,
|
|
);
|
|
dbg_assert!(get_jit_state().compiling.is_some());
|
|
}
|
|
|
|
#[inline(never)]
|
|
fn jit_analyze_and_generate(
|
|
ctx: &mut JitState,
|
|
virt_entry_point: i32,
|
|
phys_entry_point: u32,
|
|
cs_offset: u32,
|
|
state_flags: CachedStateFlags,
|
|
) {
|
|
let page = Page::page_of(phys_entry_point);
|
|
|
|
dbg_assert!(ctx.compiling.is_none());
|
|
|
|
let (_, entry_points) = match ctx.entry_points.get(&page) {
|
|
None => return,
|
|
Some(entry_points) => entry_points,
|
|
};
|
|
|
|
let existing_entry_points = match ctx.pages.get(&page) {
|
|
Some(PageInfo { entry_points, .. }) => HashSet::from_iter(entry_points.iter().map(|x| x.0)),
|
|
None => HashSet::new(),
|
|
};
|
|
|
|
if entry_points
|
|
.iter()
|
|
.all(|entry_point| existing_entry_points.contains(entry_point))
|
|
{
|
|
profiler::stat_increment(stat::COMPILE_SKIPPED_NO_NEW_ENTRY_POINTS);
|
|
return;
|
|
}
|
|
|
|
// XXX: check and remove
|
|
//let old_length = entry_points.len();
|
|
//entry_points.extend(existing_entry_points);
|
|
//dbg_log!(
|
|
// "{} + {} = {}",
|
|
// entry_points.len(),
|
|
// existing_entry_points.len(),
|
|
// entry_points.union(&existing_entry_points).count()
|
|
//);
|
|
//dbg_assert!(entry_points.union(&existing_entry_points).count() == entry_points.len());
|
|
|
|
profiler::stat_increment(stat::COMPILE);
|
|
|
|
let cpu = CpuContext {
|
|
eip: 0,
|
|
prefixes: 0,
|
|
cs_offset,
|
|
state_flags,
|
|
};
|
|
|
|
dbg_assert!(
|
|
cpu::translate_address_read_no_side_effects(virt_entry_point).unwrap() == phys_entry_point
|
|
);
|
|
let virt_page = Page::page_of(virt_entry_point as u32);
|
|
let entry_points: HashSet<i32> = entry_points
|
|
.iter()
|
|
.map(|e| virt_page.to_address() as i32 | *e as i32)
|
|
.collect();
|
|
let basic_blocks = jit_find_basic_blocks(ctx, entry_points, cpu.clone());
|
|
|
|
let mut pages = HashSet::new();
|
|
|
|
for b in basic_blocks.iter() {
|
|
// Remove this assertion once page-crossing jit is enabled
|
|
dbg_assert!(Page::page_of(b.addr) == Page::page_of(b.end_addr));
|
|
pages.insert(Page::page_of(b.addr));
|
|
}
|
|
|
|
let print = false;
|
|
|
|
for b in basic_blocks.iter() {
|
|
if !print {
|
|
break;
|
|
}
|
|
let last_instruction_opcode = memory::read32s(b.last_instruction_addr);
|
|
let op = opstats::decode(last_instruction_opcode as u32);
|
|
dbg_log!(
|
|
"BB: 0x{:x} {}{:02x} {} {}",
|
|
b.addr,
|
|
if op.is_0f { "0f" } else { "" },
|
|
op.opcode,
|
|
if b.is_entry_block { "entry" } else { "noentry" },
|
|
match &b.ty {
|
|
BasicBlockType::ConditionalJump {
|
|
next_block_addr: Some(next_block_addr),
|
|
next_block_branch_taken_addr: Some(next_block_branch_taken_addr),
|
|
..
|
|
} => format!(
|
|
"0x{:x} 0x{:x}",
|
|
next_block_addr, next_block_branch_taken_addr
|
|
),
|
|
BasicBlockType::ConditionalJump {
|
|
next_block_addr: None,
|
|
next_block_branch_taken_addr: Some(next_block_branch_taken_addr),
|
|
..
|
|
} => format!("0x{:x}", next_block_branch_taken_addr),
|
|
BasicBlockType::ConditionalJump {
|
|
next_block_addr: Some(next_block_addr),
|
|
next_block_branch_taken_addr: None,
|
|
..
|
|
} => format!("0x{:x}", next_block_addr),
|
|
BasicBlockType::ConditionalJump {
|
|
next_block_addr: None,
|
|
next_block_branch_taken_addr: None,
|
|
..
|
|
} => format!(""),
|
|
BasicBlockType::Normal {
|
|
next_block_addr: Some(next_block_addr),
|
|
..
|
|
} => format!("0x{:x}", next_block_addr),
|
|
BasicBlockType::Normal {
|
|
next_block_addr: None,
|
|
..
|
|
} => format!(""),
|
|
BasicBlockType::Exit => format!(""),
|
|
BasicBlockType::AbsoluteEip => format!(""),
|
|
}
|
|
);
|
|
}
|
|
|
|
let graph = control_flow::make_graph(&basic_blocks);
|
|
let mut structure = control_flow::loopify(&graph);
|
|
|
|
if print {
|
|
dbg_log!("before blockify:");
|
|
for group in &structure {
|
|
dbg_log!("=> Group");
|
|
group.print(0);
|
|
}
|
|
}
|
|
|
|
control_flow::blockify(&mut structure, &graph);
|
|
|
|
if cfg!(debug_assertions) {
|
|
control_flow::assert_invariants(&structure);
|
|
}
|
|
|
|
if print {
|
|
dbg_log!("after blockify:");
|
|
for group in &structure {
|
|
dbg_log!("=> Group");
|
|
group.print(0);
|
|
}
|
|
}
|
|
|
|
if ctx.wasm_table_index_free_list.is_empty() {
|
|
dbg_log!("wasm_table_index_free_list empty, clearing cache");
|
|
|
|
// When no free slots are available, delete all cached modules. We could increase the
|
|
// size of the table, but this way the initial size acts as an upper bound for the
|
|
// number of wasm modules that we generate, which we want anyway to avoid getting our
|
|
// tab killed by browsers due to memory constraints.
|
|
jit_clear_cache(ctx);
|
|
|
|
profiler::stat_increment(stat::INVALIDATE_ALL_MODULES_NO_FREE_WASM_INDICES);
|
|
|
|
dbg_log!(
|
|
"after jit_clear_cache: {} free",
|
|
ctx.wasm_table_index_free_list.len(),
|
|
);
|
|
|
|
// This assertion can fail if all entries are pending (not possible unless
|
|
// WASM_TABLE_SIZE is set very low)
|
|
dbg_assert!(!ctx.wasm_table_index_free_list.is_empty());
|
|
}
|
|
|
|
// allocate an index in the wasm table
|
|
let wasm_table_index = ctx
|
|
.wasm_table_index_free_list
|
|
.pop()
|
|
.expect("allocate wasm table index");
|
|
dbg_assert!(wasm_table_index != WasmTableIndex(0));
|
|
|
|
dbg_assert!(!pages.is_empty());
|
|
dbg_assert!(pages.len() <= MAX_PAGES);
|
|
|
|
let basic_block_by_addr: HashMap<u32, BasicBlock> =
|
|
basic_blocks.into_iter().map(|b| (b.addr, b)).collect();
|
|
|
|
let entries = jit_generate_module(
|
|
structure,
|
|
&basic_block_by_addr,
|
|
cpu,
|
|
&mut ctx.wasm_builder,
|
|
wasm_table_index,
|
|
state_flags,
|
|
);
|
|
dbg_assert!(!entries.is_empty());
|
|
|
|
let mut page_info = HashMap::new();
|
|
for &(addr, state) in &entries {
|
|
let code = page_info
|
|
.entry(Page::page_of(addr))
|
|
.or_insert_with(|| PageInfo {
|
|
wasm_table_index,
|
|
state_flags,
|
|
entry_points: Vec::new(),
|
|
hidden_wasm_table_indices: Vec::new(),
|
|
});
|
|
code.entry_points.push((addr as u16 & 0xFFF, state));
|
|
}
|
|
|
|
profiler::stat_increment_by(
|
|
stat::COMPILE_WASM_TOTAL_BYTES,
|
|
ctx.wasm_builder.get_output_len() as u64,
|
|
);
|
|
profiler::stat_increment_by(stat::COMPILE_PAGE, pages.len() as u64);
|
|
|
|
for &p in &pages {
|
|
ctx.entry_points
|
|
.entry(p)
|
|
.or_insert_with(|| (0, HashSet::new()));
|
|
}
|
|
|
|
cpu::tlb_set_has_code_multiple(&pages, true);
|
|
|
|
dbg_assert!(ctx.compiling.is_none());
|
|
ctx.compiling = Some((
|
|
wasm_table_index,
|
|
CompilingPageState::Compiling { pages: page_info },
|
|
));
|
|
|
|
let phys_addr = page.to_address();
|
|
|
|
// will call codegen_finalize_finished asynchronously when finished
|
|
codegen_finalize(
|
|
wasm_table_index,
|
|
phys_addr,
|
|
state_flags,
|
|
ctx.wasm_builder.get_output_ptr() as u32,
|
|
ctx.wasm_builder.get_output_len(),
|
|
);
|
|
|
|
check_jit_state_invariants(ctx);
|
|
}
|
|
|
|
#[no_mangle]
|
|
pub fn codegen_finalize_finished(
|
|
wasm_table_index: WasmTableIndex,
|
|
phys_addr: u32,
|
|
state_flags: CachedStateFlags,
|
|
) {
|
|
let ctx = get_jit_state();
|
|
|
|
dbg_assert!(wasm_table_index != WasmTableIndex(0));
|
|
|
|
dbg_log!(
|
|
"Finished compiling for page at {:x}",
|
|
Page::page_of(phys_addr).to_address()
|
|
);
|
|
|
|
let pages = match mem::replace(&mut ctx.compiling, None) {
|
|
None => {
|
|
dbg_assert!(false);
|
|
return;
|
|
},
|
|
Some((in_progress_wasm_table_index, CompilingPageState::CompilingWritten)) => {
|
|
dbg_assert!(wasm_table_index == in_progress_wasm_table_index);
|
|
|
|
profiler::stat_increment(stat::INVALIDATE_MODULE_WRITTEN_WHILE_COMPILED);
|
|
free_wasm_table_index(ctx, wasm_table_index);
|
|
check_jit_state_invariants(ctx);
|
|
return;
|
|
},
|
|
Some((in_progress_wasm_table_index, CompilingPageState::Compiling { pages })) => {
|
|
dbg_assert!(wasm_table_index == in_progress_wasm_table_index);
|
|
dbg_assert!(!pages.is_empty());
|
|
pages
|
|
},
|
|
};
|
|
|
|
for i in 0..unsafe { cpu::valid_tlb_entries_count } {
|
|
let page = unsafe { cpu::valid_tlb_entries[i as usize] };
|
|
let entry = unsafe { cpu::tlb_data[page as usize] };
|
|
if 0 != entry {
|
|
let tlb_physical_page = Page::of_u32(
|
|
(entry as u32 >> 12 ^ page as u32) - (unsafe { memory::mem8 } as u32 >> 12),
|
|
);
|
|
if let Some(info) = pages.get(&tlb_physical_page) {
|
|
set_tlb_code(
|
|
Page::of_u32(page as u32),
|
|
wasm_table_index,
|
|
&info.entry_points,
|
|
state_flags,
|
|
);
|
|
}
|
|
}
|
|
}
|
|
|
|
let mut check_for_unused_wasm_table_index = HashSet::new();
|
|
|
|
for (page, mut info) in pages {
|
|
if let Some(old_entry) = ctx.pages.remove(&page) {
|
|
info.hidden_wasm_table_indices
|
|
.extend(old_entry.hidden_wasm_table_indices);
|
|
info.hidden_wasm_table_indices
|
|
.push(old_entry.wasm_table_index);
|
|
check_for_unused_wasm_table_index.insert(old_entry.wasm_table_index);
|
|
}
|
|
ctx.pages.insert(page, info);
|
|
}
|
|
|
|
let unused: Vec<&WasmTableIndex> = check_for_unused_wasm_table_index
|
|
.iter()
|
|
.filter(|&&i| ctx.pages.values().all(|page| page.wasm_table_index != i))
|
|
.collect();
|
|
|
|
for &index in unused {
|
|
for p in ctx.pages.values_mut() {
|
|
p.hidden_wasm_table_indices.retain(|&w| w != index);
|
|
}
|
|
|
|
dbg_log!("unused after overwrite {}", index.to_u16());
|
|
profiler::stat_increment(stat::INVALIDATE_MODULE_UNUSED_AFTER_OVERWRITE);
|
|
free_wasm_table_index(ctx, index);
|
|
}
|
|
|
|
check_jit_state_invariants(ctx);
|
|
}
|
|
|
|
pub fn update_tlb_code(virt_page: Page, phys_page: Page) {
|
|
let ctx = get_jit_state();
|
|
|
|
match ctx.pages.get(&phys_page) {
|
|
Some(PageInfo {
|
|
wasm_table_index,
|
|
entry_points,
|
|
state_flags,
|
|
hidden_wasm_table_indices: _,
|
|
}) => set_tlb_code(virt_page, *wasm_table_index, entry_points, *state_flags),
|
|
None => cpu::clear_tlb_code(phys_page.to_u32() as i32),
|
|
};
|
|
}
|
|
|
|
pub fn set_tlb_code(
|
|
virt_page: Page,
|
|
wasm_table_index: WasmTableIndex,
|
|
entries: &Vec<(u16, u16)>,
|
|
state_flags: CachedStateFlags,
|
|
) {
|
|
let c = match unsafe { cpu::tlb_code[virt_page.to_u32() as usize] } {
|
|
None => {
|
|
let state_table = [u16::MAX; 0x1000];
|
|
unsafe {
|
|
let mut c = NonNull::new_unchecked(Box::into_raw(Box::new(cpu::Code {
|
|
wasm_table_index,
|
|
state_flags,
|
|
state_table,
|
|
})));
|
|
cpu::tlb_code[virt_page.to_u32() as usize] = Some(c);
|
|
c.as_mut()
|
|
}
|
|
},
|
|
Some(mut c) => unsafe {
|
|
let c = c.as_mut();
|
|
c.state_table.fill(u16::MAX);
|
|
c.state_flags = state_flags;
|
|
c.wasm_table_index = wasm_table_index;
|
|
c
|
|
},
|
|
};
|
|
|
|
for &(addr, state) in entries {
|
|
dbg_assert!(state != u16::MAX);
|
|
c.state_table[addr as usize] = state;
|
|
}
|
|
}
|
|
|
|
fn jit_generate_module(
|
|
structure: Vec<WasmStructure>,
|
|
basic_blocks: &HashMap<u32, BasicBlock>,
|
|
mut cpu: CpuContext,
|
|
builder: &mut WasmBuilder,
|
|
wasm_table_index: WasmTableIndex,
|
|
state_flags: CachedStateFlags,
|
|
) -> Vec<(u32, u16)> {
|
|
builder.reset();
|
|
|
|
let mut register_locals = (0..8)
|
|
.map(|i| {
|
|
builder.load_fixed_i32(global_pointers::get_reg32_offset(i));
|
|
builder.set_new_local()
|
|
})
|
|
.collect();
|
|
|
|
builder.const_i32(0);
|
|
let instruction_counter = builder.set_new_local();
|
|
|
|
let exit_label = builder.block_void();
|
|
let exit_with_fault_label = builder.block_void();
|
|
let main_loop_label = builder.loop_void();
|
|
if JIT_USE_LOOP_SAFETY {
|
|
builder.get_local(&instruction_counter);
|
|
builder.const_i32(cpu::LOOP_COUNTER);
|
|
builder.geu_i32();
|
|
if cfg!(feature = "profiler") {
|
|
builder.if_void();
|
|
codegen::gen_debug_track_jit_exit(builder, 0);
|
|
builder.br(exit_label);
|
|
builder.block_end();
|
|
}
|
|
else {
|
|
builder.br_if(exit_label);
|
|
}
|
|
}
|
|
let brtable_default = builder.block_void();
|
|
|
|
let ctx = &mut JitContext {
|
|
cpu: &mut cpu,
|
|
builder,
|
|
register_locals: &mut register_locals,
|
|
start_of_current_instruction: 0,
|
|
exit_with_fault_label,
|
|
exit_label,
|
|
current_instruction: Instruction::Other,
|
|
previous_instruction: Instruction::Other,
|
|
instruction_counter,
|
|
};
|
|
|
|
let entry_blocks = {
|
|
let mut nodes = &structure;
|
|
let result;
|
|
loop {
|
|
match &nodes[0] {
|
|
WasmStructure::Dispatcher(e) => {
|
|
result = e.clone();
|
|
break;
|
|
},
|
|
WasmStructure::Loop { .. } => {
|
|
dbg_assert!(false);
|
|
},
|
|
WasmStructure::BasicBlock(_) => {
|
|
dbg_assert!(false);
|
|
},
|
|
// Note: We could use these blocks as entry points, which will yield
|
|
// more entries for free, but it requires adding those to the dispatcher
|
|
// It's to be investigated if this yields a performance improvement
|
|
// See also the comment at the bottom of this function when creating entry
|
|
// points
|
|
WasmStructure::Block(children) => {
|
|
nodes = children;
|
|
},
|
|
}
|
|
}
|
|
result
|
|
};
|
|
|
|
let mut index_for_addr = HashMap::new();
|
|
for (i, &addr) in entry_blocks.iter().enumerate() {
|
|
index_for_addr.insert(addr, i as i32);
|
|
}
|
|
for b in basic_blocks.values() {
|
|
if !index_for_addr.contains_key(&b.addr) {
|
|
let i = index_for_addr.len();
|
|
index_for_addr.insert(b.addr, i as i32);
|
|
}
|
|
}
|
|
|
|
let mut label_for_addr: HashMap<u32, (Label, Option<i32>)> = HashMap::new();
|
|
|
|
enum Work {
|
|
WasmStructure(WasmStructure),
|
|
BlockEnd {
|
|
label: Label,
|
|
targets: Vec<u32>,
|
|
olds: HashMap<u32, (Label, Option<i32>)>,
|
|
},
|
|
LoopEnd {
|
|
label: Label,
|
|
entries: Vec<u32>,
|
|
olds: HashMap<u32, (Label, Option<i32>)>,
|
|
},
|
|
}
|
|
let mut work: VecDeque<Work> = structure
|
|
.into_iter()
|
|
.map(|x| Work::WasmStructure(x))
|
|
.collect();
|
|
|
|
while let Some(block) = work.pop_front() {
|
|
let next_addr: Option<Vec<u32>> = work.iter().find_map(|x| match x {
|
|
Work::WasmStructure(l) => Some(l.head().collect()),
|
|
_ => None,
|
|
});
|
|
let target_block = &ctx.builder.arg_local_initial_state.unsafe_clone();
|
|
|
|
match block {
|
|
Work::WasmStructure(WasmStructure::BasicBlock(addr)) => {
|
|
let block = basic_blocks.get(&addr).unwrap();
|
|
jit_generate_basic_block(ctx, block);
|
|
|
|
if block.has_sti {
|
|
match block.ty {
|
|
BasicBlockType::ConditionalJump {
|
|
condition,
|
|
jump_offset,
|
|
jump_offset_is_32,
|
|
..
|
|
} => {
|
|
codegen::gen_set_eip_low_bits(
|
|
ctx.builder,
|
|
block.end_addr as i32 & 0xFFF,
|
|
);
|
|
codegen::gen_condition_fn(ctx, condition);
|
|
ctx.builder.if_void();
|
|
if jump_offset_is_32 {
|
|
codegen::gen_relative_jump(ctx.builder, jump_offset);
|
|
}
|
|
else {
|
|
codegen::gen_jmp_rel16(ctx.builder, jump_offset as u16);
|
|
}
|
|
ctx.builder.block_end();
|
|
},
|
|
BasicBlockType::Normal {
|
|
jump_offset,
|
|
jump_offset_is_32,
|
|
..
|
|
} => {
|
|
if jump_offset_is_32 {
|
|
codegen::gen_set_eip_low_bits_and_jump_rel32(
|
|
ctx.builder,
|
|
block.end_addr as i32 & 0xFFF,
|
|
jump_offset,
|
|
);
|
|
}
|
|
else {
|
|
codegen::gen_set_eip_low_bits(
|
|
ctx.builder,
|
|
block.end_addr as i32 & 0xFFF,
|
|
);
|
|
codegen::gen_jmp_rel16(ctx.builder, jump_offset as u16);
|
|
}
|
|
},
|
|
BasicBlockType::Exit => {},
|
|
BasicBlockType::AbsoluteEip => {},
|
|
};
|
|
codegen::gen_debug_track_jit_exit(ctx.builder, block.last_instruction_addr);
|
|
codegen::gen_move_registers_from_locals_to_memory(ctx);
|
|
codegen::gen_fn0_const(ctx.builder, "handle_irqs");
|
|
codegen::gen_update_instruction_counter(ctx);
|
|
ctx.builder.return_();
|
|
continue;
|
|
}
|
|
|
|
match &block.ty {
|
|
BasicBlockType::Exit => {
|
|
// Exit this function
|
|
codegen::gen_debug_track_jit_exit(ctx.builder, block.last_instruction_addr);
|
|
codegen::gen_profiler_stat_increment(ctx.builder, stat::DIRECT_EXIT);
|
|
ctx.builder.br(ctx.exit_label);
|
|
},
|
|
BasicBlockType::AbsoluteEip => {
|
|
// Check if we can stay in this module, if not exit
|
|
codegen::gen_get_eip(ctx.builder);
|
|
ctx.builder.const_i32(wasm_table_index.to_u16() as i32);
|
|
ctx.builder.const_i32(state_flags.to_u32() as i32);
|
|
ctx.builder.call_fn3_ret("jit_find_cache_entry_in_page");
|
|
ctx.builder.tee_local(target_block);
|
|
ctx.builder.const_i32(0);
|
|
ctx.builder.ge_i32();
|
|
// TODO: Could make this unconditional by including exit_label in the main br_table
|
|
ctx.builder.br_if(main_loop_label);
|
|
|
|
codegen::gen_debug_track_jit_exit(ctx.builder, block.last_instruction_addr);
|
|
ctx.builder.br(ctx.exit_label);
|
|
},
|
|
&BasicBlockType::Normal {
|
|
next_block_addr: None,
|
|
jump_offset,
|
|
jump_offset_is_32,
|
|
} => {
|
|
if jump_offset_is_32 {
|
|
codegen::gen_set_eip_low_bits_and_jump_rel32(
|
|
ctx.builder,
|
|
block.end_addr as i32 & 0xFFF,
|
|
jump_offset,
|
|
);
|
|
}
|
|
else {
|
|
codegen::gen_set_eip_low_bits(
|
|
ctx.builder,
|
|
block.end_addr as i32 & 0xFFF,
|
|
);
|
|
codegen::gen_jmp_rel16(ctx.builder, jump_offset as u16);
|
|
}
|
|
|
|
codegen::gen_debug_track_jit_exit(ctx.builder, block.last_instruction_addr);
|
|
codegen::gen_profiler_stat_increment(ctx.builder, stat::DIRECT_EXIT);
|
|
ctx.builder.br(ctx.exit_label);
|
|
},
|
|
&BasicBlockType::Normal {
|
|
next_block_addr: Some(next_block_addr),
|
|
jump_offset,
|
|
jump_offset_is_32,
|
|
} => {
|
|
// Unconditional jump to next basic block
|
|
// - All instructions that don't change eip
|
|
// - Unconditional jumps
|
|
|
|
if Page::page_of(next_block_addr) != Page::page_of(block.addr) {
|
|
if jump_offset_is_32 {
|
|
codegen::gen_set_eip_low_bits_and_jump_rel32(
|
|
ctx.builder,
|
|
block.end_addr as i32 & 0xFFF,
|
|
jump_offset,
|
|
);
|
|
}
|
|
else {
|
|
codegen::gen_set_eip_low_bits(
|
|
ctx.builder,
|
|
block.end_addr as i32 & 0xFFF,
|
|
);
|
|
codegen::gen_jmp_rel16(ctx.builder, jump_offset as u16);
|
|
}
|
|
|
|
codegen::gen_profiler_stat_increment(
|
|
ctx.builder,
|
|
stat::NORMAL_PAGE_CHANGE,
|
|
);
|
|
|
|
codegen::gen_page_switch_check(
|
|
ctx,
|
|
next_block_addr,
|
|
block.last_instruction_addr,
|
|
);
|
|
|
|
#[cfg(debug_assertions)]
|
|
codegen::gen_fn2_const(
|
|
ctx.builder,
|
|
"check_page_switch",
|
|
block.addr,
|
|
next_block_addr,
|
|
);
|
|
}
|
|
|
|
if next_addr
|
|
.as_ref()
|
|
.map_or(false, |n| n.contains(&next_block_addr))
|
|
{
|
|
// Blocks are consecutive
|
|
if next_addr.unwrap().len() > 1 {
|
|
let target_index = *index_for_addr.get(&next_block_addr).unwrap();
|
|
if cfg!(feature = "profiler") {
|
|
ctx.builder.const_i32(target_index);
|
|
ctx.builder.call_fn1("debug_set_dispatcher_target");
|
|
}
|
|
ctx.builder.const_i32(target_index);
|
|
ctx.builder.set_local(target_block);
|
|
codegen::gen_profiler_stat_increment(
|
|
ctx.builder,
|
|
stat::NORMAL_FALLTHRU_WITH_TARGET_BLOCK,
|
|
);
|
|
}
|
|
else {
|
|
codegen::gen_profiler_stat_increment(
|
|
ctx.builder,
|
|
stat::NORMAL_FALLTHRU,
|
|
);
|
|
}
|
|
}
|
|
else {
|
|
let &(br, target_index) = label_for_addr.get(&next_block_addr).unwrap();
|
|
if let Some(target_index) = target_index {
|
|
if cfg!(feature = "profiler") {
|
|
ctx.builder.const_i32(target_index);
|
|
ctx.builder.call_fn1("debug_set_dispatcher_target");
|
|
}
|
|
ctx.builder.const_i32(target_index);
|
|
ctx.builder.set_local(target_block);
|
|
codegen::gen_profiler_stat_increment(
|
|
ctx.builder,
|
|
stat::NORMAL_BRANCH_WITH_TARGET_BLOCK,
|
|
);
|
|
}
|
|
else {
|
|
codegen::gen_profiler_stat_increment(
|
|
ctx.builder,
|
|
stat::NORMAL_BRANCH,
|
|
);
|
|
}
|
|
ctx.builder.br(br);
|
|
}
|
|
},
|
|
&BasicBlockType::ConditionalJump {
|
|
next_block_addr,
|
|
next_block_branch_taken_addr,
|
|
condition,
|
|
jump_offset,
|
|
jump_offset_is_32,
|
|
} => {
|
|
// Conditional jump to next basic block
|
|
// - jnz, jc, loop, jcxz, etc.
|
|
|
|
// Generate:
|
|
// (1) condition()
|
|
// (2) br_if()
|
|
// (3) br()
|
|
// Except:
|
|
// If we need to update eip in case (2), it's replaced by if { update_eip(); br() }
|
|
// If case (3) can fall through to the next basic block, the branch is eliminated
|
|
// Dispatcher target writes can be generated in either case
|
|
// Condition may be inverted if it helps generate a fallthrough instead of the second branch
|
|
|
|
codegen::gen_profiler_stat_increment(ctx.builder, stat::CONDITIONAL_JUMP);
|
|
|
|
#[derive(PartialEq)]
|
|
enum Case {
|
|
BranchTaken,
|
|
BranchNotTaken,
|
|
}
|
|
|
|
let mut handle_case = |case: Case, is_first| {
|
|
// first case generates condition and *has* to branch away,
|
|
// second case branches unconditionally or falls through
|
|
|
|
if is_first {
|
|
if case == Case::BranchNotTaken {
|
|
codegen::gen_condition_fn_negated(ctx, condition);
|
|
}
|
|
else {
|
|
codegen::gen_condition_fn(ctx, condition);
|
|
}
|
|
}
|
|
|
|
let next_block_addr = if case == Case::BranchTaken {
|
|
next_block_branch_taken_addr
|
|
}
|
|
else {
|
|
next_block_addr
|
|
};
|
|
|
|
if let Some(next_block_addr) = next_block_addr {
|
|
if Page::page_of(next_block_addr) != Page::page_of(block.addr) {
|
|
dbg_assert!(case == Case::BranchTaken); // currently not possible in other case
|
|
if is_first {
|
|
ctx.builder.if_i32();
|
|
}
|
|
if jump_offset_is_32 {
|
|
codegen::gen_set_eip_low_bits_and_jump_rel32(
|
|
ctx.builder,
|
|
block.end_addr as i32 & 0xFFF,
|
|
jump_offset,
|
|
);
|
|
}
|
|
else {
|
|
codegen::gen_set_eip_low_bits(
|
|
ctx.builder,
|
|
block.end_addr as i32 & 0xFFF,
|
|
);
|
|
codegen::gen_jmp_rel16(ctx.builder, jump_offset as u16);
|
|
}
|
|
|
|
codegen::gen_profiler_stat_increment(
|
|
ctx.builder,
|
|
stat::CONDITIONAL_JUMP_PAGE_CHANGE,
|
|
);
|
|
codegen::gen_page_switch_check(
|
|
ctx,
|
|
next_block_addr,
|
|
block.last_instruction_addr,
|
|
);
|
|
|
|
#[cfg(debug_assertions)]
|
|
codegen::gen_fn2_const(
|
|
ctx.builder,
|
|
"check_page_switch",
|
|
block.addr,
|
|
next_block_addr,
|
|
);
|
|
|
|
if is_first {
|
|
ctx.builder.const_i32(1);
|
|
ctx.builder.else_();
|
|
ctx.builder.const_i32(0);
|
|
ctx.builder.block_end();
|
|
}
|
|
}
|
|
|
|
if next_addr
|
|
.as_ref()
|
|
.map_or(false, |n| n.contains(&next_block_addr))
|
|
{
|
|
// blocks are consecutive
|
|
|
|
// fallthrough, has to be second
|
|
dbg_assert!(!is_first);
|
|
|
|
if next_addr.as_ref().unwrap().len() > 1 {
|
|
let target_index =
|
|
*index_for_addr.get(&next_block_addr).unwrap();
|
|
if cfg!(feature = "profiler") {
|
|
ctx.builder.const_i32(target_index);
|
|
ctx.builder.call_fn1("debug_set_dispatcher_target");
|
|
}
|
|
ctx.builder.const_i32(target_index);
|
|
ctx.builder.set_local(target_block);
|
|
codegen::gen_profiler_stat_increment(
|
|
ctx.builder,
|
|
stat::CONDITIONAL_JUMP_FALLTHRU_WITH_TARGET_BLOCK,
|
|
);
|
|
}
|
|
else {
|
|
codegen::gen_profiler_stat_increment(
|
|
ctx.builder,
|
|
stat::CONDITIONAL_JUMP_FALLTHRU,
|
|
);
|
|
}
|
|
}
|
|
else {
|
|
let &(br, target_index) =
|
|
label_for_addr.get(&next_block_addr).unwrap();
|
|
if let Some(target_index) = target_index {
|
|
if cfg!(feature = "profiler") {
|
|
// Note: Currently called unconditionally, even if the
|
|
// br_if below doesn't branch
|
|
ctx.builder.const_i32(target_index);
|
|
ctx.builder.call_fn1("debug_set_dispatcher_target");
|
|
}
|
|
ctx.builder.const_i32(target_index);
|
|
ctx.builder.set_local(target_block);
|
|
}
|
|
|
|
if is_first {
|
|
if cfg!(feature = "profiler") {
|
|
ctx.builder.if_void();
|
|
codegen::gen_profiler_stat_increment(
|
|
ctx.builder,
|
|
if target_index.is_some() {
|
|
stat::CONDITIONAL_JUMP_BRANCH_WITH_TARGET_BLOCK
|
|
}
|
|
else {
|
|
stat::CONDITIONAL_JUMP_BRANCH
|
|
},
|
|
);
|
|
ctx.builder.br(br);
|
|
ctx.builder.block_end();
|
|
}
|
|
else {
|
|
ctx.builder.br_if(br);
|
|
}
|
|
}
|
|
else {
|
|
codegen::gen_profiler_stat_increment(
|
|
ctx.builder,
|
|
if target_index.is_some() {
|
|
stat::CONDITIONAL_JUMP_BRANCH_WITH_TARGET_BLOCK
|
|
}
|
|
else {
|
|
stat::CONDITIONAL_JUMP_BRANCH
|
|
},
|
|
);
|
|
ctx.builder.br(br);
|
|
}
|
|
}
|
|
}
|
|
else {
|
|
// target is outside of this module, update eip and exit
|
|
if is_first {
|
|
ctx.builder.if_void();
|
|
}
|
|
|
|
if case == Case::BranchTaken {
|
|
if jump_offset_is_32 {
|
|
codegen::gen_set_eip_low_bits_and_jump_rel32(
|
|
ctx.builder,
|
|
block.end_addr as i32 & 0xFFF,
|
|
jump_offset,
|
|
);
|
|
}
|
|
else {
|
|
codegen::gen_set_eip_low_bits(
|
|
ctx.builder,
|
|
block.end_addr as i32 & 0xFFF,
|
|
);
|
|
codegen::gen_jmp_rel16(ctx.builder, jump_offset as u16);
|
|
}
|
|
}
|
|
else {
|
|
codegen::gen_set_eip_low_bits(
|
|
ctx.builder,
|
|
block.end_addr as i32 & 0xFFF,
|
|
);
|
|
}
|
|
|
|
codegen::gen_debug_track_jit_exit(
|
|
ctx.builder,
|
|
block.last_instruction_addr,
|
|
);
|
|
codegen::gen_profiler_stat_increment(
|
|
ctx.builder,
|
|
stat::CONDITIONAL_JUMP_EXIT,
|
|
);
|
|
ctx.builder.br(ctx.exit_label);
|
|
|
|
if is_first {
|
|
ctx.builder.block_end();
|
|
}
|
|
}
|
|
};
|
|
|
|
let branch_taken_is_fallthrough = next_block_branch_taken_addr
|
|
.map_or(false, |addr| {
|
|
next_addr.as_ref().map_or(false, |n| n.contains(&addr))
|
|
});
|
|
let branch_not_taken_is_fallthrough = next_block_addr
|
|
.map_or(false, |addr| {
|
|
next_addr.as_ref().map_or(false, |n| n.contains(&addr))
|
|
});
|
|
|
|
if branch_not_taken_is_fallthrough && branch_taken_is_fallthrough {
|
|
let next_block_addr = next_block_addr.unwrap();
|
|
let next_block_branch_taken_addr =
|
|
next_block_branch_taken_addr.unwrap();
|
|
|
|
dbg_log!(
|
|
"Conditional control flow: fallthrough in both cases, page_switch={} next_is_multi={}",
|
|
Page::page_of(next_block_branch_taken_addr)
|
|
!= Page::page_of(block.addr),
|
|
next_addr.as_ref().unwrap().len() > 1,
|
|
);
|
|
|
|
dbg_assert!(
|
|
Page::page_of(next_block_addr) == Page::page_of(block.addr)
|
|
); // currently not possible
|
|
|
|
if Page::page_of(next_block_branch_taken_addr)
|
|
!= Page::page_of(block.addr)
|
|
{
|
|
codegen::gen_condition_fn(ctx, condition);
|
|
ctx.builder.if_void();
|
|
|
|
if jump_offset_is_32 {
|
|
codegen::gen_set_eip_low_bits_and_jump_rel32(
|
|
ctx.builder,
|
|
block.end_addr as i32 & 0xFFF,
|
|
jump_offset,
|
|
);
|
|
}
|
|
else {
|
|
codegen::gen_set_eip_low_bits(
|
|
ctx.builder,
|
|
block.end_addr as i32 & 0xFFF,
|
|
);
|
|
codegen::gen_jmp_rel16(ctx.builder, jump_offset as u16);
|
|
}
|
|
|
|
codegen::gen_profiler_stat_increment(
|
|
ctx.builder,
|
|
stat::CONDITIONAL_JUMP_PAGE_CHANGE,
|
|
);
|
|
codegen::gen_page_switch_check(
|
|
ctx,
|
|
next_block_branch_taken_addr,
|
|
block.last_instruction_addr,
|
|
);
|
|
|
|
#[cfg(debug_assertions)]
|
|
codegen::gen_fn2_const(
|
|
ctx.builder,
|
|
"check_page_switch",
|
|
block.addr,
|
|
next_block_branch_taken_addr,
|
|
);
|
|
|
|
dbg_assert!(next_addr.unwrap().len() > 1);
|
|
|
|
let target_index_taken =
|
|
*index_for_addr.get(&next_block_branch_taken_addr).unwrap();
|
|
let target_index_not_taken =
|
|
*index_for_addr.get(&next_block_addr).unwrap();
|
|
|
|
ctx.builder.const_i32(target_index_taken);
|
|
ctx.builder.set_local(target_block);
|
|
|
|
ctx.builder.else_();
|
|
ctx.builder.const_i32(target_index_not_taken);
|
|
ctx.builder.set_local(target_block);
|
|
|
|
ctx.builder.block_end();
|
|
}
|
|
else if next_addr.unwrap().len() > 1 {
|
|
let target_index_taken =
|
|
*index_for_addr.get(&next_block_branch_taken_addr).unwrap();
|
|
let target_index_not_taken =
|
|
*index_for_addr.get(&next_block_addr).unwrap();
|
|
|
|
codegen::gen_condition_fn(ctx, condition);
|
|
ctx.builder.if_i32();
|
|
ctx.builder.const_i32(target_index_taken);
|
|
ctx.builder.else_();
|
|
ctx.builder.const_i32(target_index_not_taken);
|
|
ctx.builder.block_end();
|
|
ctx.builder.set_local(target_block);
|
|
}
|
|
}
|
|
else if branch_taken_is_fallthrough {
|
|
handle_case(Case::BranchNotTaken, true);
|
|
handle_case(Case::BranchTaken, false);
|
|
}
|
|
else {
|
|
handle_case(Case::BranchTaken, true);
|
|
handle_case(Case::BranchNotTaken, false);
|
|
}
|
|
},
|
|
}
|
|
},
|
|
Work::WasmStructure(WasmStructure::Dispatcher(entries)) => {
|
|
profiler::stat_increment(stat::COMPILE_DISPATCHER);
|
|
|
|
if cfg!(feature = "profiler") {
|
|
ctx.builder.get_local(target_block);
|
|
ctx.builder.const_i32(index_for_addr.len() as i32);
|
|
ctx.builder.call_fn2("check_dispatcher_target");
|
|
}
|
|
|
|
if entries.len() > BRTABLE_CUTOFF {
|
|
// generate a brtable
|
|
codegen::gen_profiler_stat_increment(ctx.builder, stat::DISPATCHER_LARGE);
|
|
let mut cases = Vec::new();
|
|
for &addr in &entries {
|
|
let &(label, target_index) = label_for_addr.get(&addr).unwrap();
|
|
let &index = index_for_addr.get(&addr).unwrap();
|
|
dbg_assert!(target_index.is_none() || target_index == Some(index));
|
|
while index as usize >= cases.len() {
|
|
cases.push(brtable_default);
|
|
}
|
|
cases[index as usize] = label;
|
|
}
|
|
ctx.builder.get_local(target_block);
|
|
ctx.builder.brtable(brtable_default, &mut cases.iter());
|
|
}
|
|
else {
|
|
// generate a if target == block.addr then br block.label ...
|
|
codegen::gen_profiler_stat_increment(ctx.builder, stat::DISPATCHER_SMALL);
|
|
let nexts: HashSet<u32> = next_addr
|
|
.as_ref()
|
|
.map_or(HashSet::new(), |nexts| nexts.iter().copied().collect());
|
|
for &addr in &entries {
|
|
if nexts.contains(&addr) {
|
|
continue;
|
|
}
|
|
let index = *index_for_addr.get(&addr).unwrap();
|
|
let &(label, _) = label_for_addr.get(&addr).unwrap();
|
|
ctx.builder.get_local(target_block);
|
|
ctx.builder.const_i32(index);
|
|
ctx.builder.eq_i32();
|
|
ctx.builder.br_if(label);
|
|
}
|
|
}
|
|
},
|
|
Work::WasmStructure(WasmStructure::Loop(children)) => {
|
|
profiler::stat_increment(stat::COMPILE_WASM_LOOP);
|
|
|
|
let entries: Vec<u32> = children[0].head().collect();
|
|
let label = ctx.builder.loop_void();
|
|
codegen::gen_profiler_stat_increment(ctx.builder, stat::LOOP);
|
|
|
|
if entries.len() == 1 {
|
|
let addr = entries[0];
|
|
codegen::gen_set_eip_low_bits(ctx.builder, addr as i32 & 0xFFF);
|
|
profiler::stat_increment(stat::COMPILE_WITH_LOOP_SAFETY);
|
|
codegen::gen_profiler_stat_increment(ctx.builder, stat::LOOP_SAFETY);
|
|
if JIT_USE_LOOP_SAFETY {
|
|
ctx.builder.get_local(&ctx.instruction_counter);
|
|
ctx.builder.const_i32(cpu::LOOP_COUNTER);
|
|
ctx.builder.geu_i32();
|
|
if cfg!(feature = "profiler") {
|
|
ctx.builder.if_void();
|
|
codegen::gen_debug_track_jit_exit(ctx.builder, addr);
|
|
ctx.builder.br(exit_label);
|
|
ctx.builder.block_end();
|
|
}
|
|
else {
|
|
ctx.builder.br_if(exit_label);
|
|
}
|
|
}
|
|
}
|
|
|
|
let mut olds = HashMap::new();
|
|
for &target in entries.iter() {
|
|
let index = if entries.len() == 1 {
|
|
None
|
|
}
|
|
else {
|
|
Some(*index_for_addr.get(&target).unwrap())
|
|
};
|
|
let old = label_for_addr.insert(target, (label, index));
|
|
if let Some(old) = old {
|
|
olds.insert(target, old);
|
|
}
|
|
}
|
|
|
|
work.push_front(Work::LoopEnd {
|
|
label,
|
|
entries,
|
|
olds,
|
|
});
|
|
for c in children.into_iter().rev() {
|
|
work.push_front(Work::WasmStructure(c));
|
|
}
|
|
},
|
|
Work::LoopEnd {
|
|
label,
|
|
entries,
|
|
olds,
|
|
} => {
|
|
for target in entries {
|
|
let old = label_for_addr.remove(&target);
|
|
dbg_assert!(old.map(|(l, _)| l) == Some(label));
|
|
}
|
|
for (target, old) in olds {
|
|
let old = label_for_addr.insert(target, old);
|
|
dbg_assert!(old.is_none());
|
|
}
|
|
|
|
ctx.builder.block_end();
|
|
},
|
|
Work::WasmStructure(WasmStructure::Block(children)) => {
|
|
profiler::stat_increment(stat::COMPILE_WASM_BLOCK);
|
|
|
|
let targets = next_addr.clone().unwrap();
|
|
let label = ctx.builder.block_void();
|
|
let mut olds = HashMap::new();
|
|
for &target in targets.iter() {
|
|
let index = if targets.len() == 1 {
|
|
None
|
|
}
|
|
else {
|
|
Some(*index_for_addr.get(&target).unwrap())
|
|
};
|
|
let old = label_for_addr.insert(target, (label, index));
|
|
if let Some(old) = old {
|
|
olds.insert(target, old);
|
|
}
|
|
}
|
|
|
|
work.push_front(Work::BlockEnd {
|
|
label,
|
|
targets,
|
|
olds,
|
|
});
|
|
for c in children.into_iter().rev() {
|
|
work.push_front(Work::WasmStructure(c));
|
|
}
|
|
},
|
|
Work::BlockEnd {
|
|
label,
|
|
targets,
|
|
olds,
|
|
} => {
|
|
for target in targets {
|
|
let old = label_for_addr.remove(&target);
|
|
dbg_assert!(old.map(|(l, _)| l) == Some(label));
|
|
}
|
|
for (target, old) in olds {
|
|
let old = label_for_addr.insert(target, old);
|
|
dbg_assert!(old.is_none());
|
|
}
|
|
|
|
ctx.builder.block_end();
|
|
},
|
|
}
|
|
}
|
|
|
|
dbg_assert!(label_for_addr.is_empty());
|
|
|
|
{
|
|
ctx.builder.block_end(); // default case for the brtable
|
|
ctx.builder.unreachable();
|
|
}
|
|
{
|
|
ctx.builder.block_end(); // main loop
|
|
}
|
|
{
|
|
// exit-with-fault case
|
|
ctx.builder.block_end();
|
|
codegen::gen_move_registers_from_locals_to_memory(ctx);
|
|
codegen::gen_fn0_const(ctx.builder, "trigger_fault_end_jit");
|
|
codegen::gen_update_instruction_counter(ctx);
|
|
ctx.builder.return_();
|
|
}
|
|
{
|
|
// exit
|
|
ctx.builder.block_end();
|
|
codegen::gen_move_registers_from_locals_to_memory(ctx);
|
|
codegen::gen_update_instruction_counter(ctx);
|
|
}
|
|
|
|
for local in ctx.register_locals.drain(..) {
|
|
ctx.builder.free_local(local);
|
|
}
|
|
ctx.builder
|
|
.free_local(ctx.instruction_counter.unsafe_clone());
|
|
|
|
ctx.builder.finish();
|
|
|
|
let entries = Vec::from_iter(entry_blocks.iter().map(|addr| {
|
|
let block = basic_blocks.get(&addr).unwrap();
|
|
let index = *index_for_addr.get(&addr).unwrap();
|
|
|
|
profiler::stat_increment(stat::COMPILE_ENTRY_POINT);
|
|
|
|
dbg_assert!(block.addr < block.end_addr);
|
|
// Note: We also insert blocks that weren't originally marked as entries here
|
|
// This doesn't have any downside, besides making the hash table slightly larger
|
|
|
|
let initial_state = index.safe_to_u16();
|
|
(block.addr, initial_state)
|
|
}));
|
|
|
|
for b in basic_blocks.values() {
|
|
if b.is_entry_block {
|
|
dbg_assert!(entries.iter().find(|(addr, _)| *addr == b.addr).is_some());
|
|
}
|
|
}
|
|
|
|
return entries;
|
|
}
|
|
|
|
fn jit_generate_basic_block(ctx: &mut JitContext, block: &BasicBlock) {
|
|
let needs_eip_updated = match block.ty {
|
|
BasicBlockType::Exit => true,
|
|
_ => false,
|
|
};
|
|
|
|
profiler::stat_increment(stat::COMPILE_BASIC_BLOCK);
|
|
|
|
let start_addr = block.addr;
|
|
let last_instruction_addr = block.last_instruction_addr;
|
|
let stop_addr = block.end_addr;
|
|
|
|
// First iteration of do-while assumes the caller confirms this condition
|
|
dbg_assert!(!is_near_end_of_page(start_addr));
|
|
|
|
if cfg!(feature = "profiler") {
|
|
ctx.builder.const_i32(start_addr as i32);
|
|
ctx.builder.call_fn1("enter_basic_block");
|
|
}
|
|
|
|
ctx.builder.get_local(&ctx.instruction_counter);
|
|
ctx.builder.const_i32(block.number_of_instructions as i32);
|
|
ctx.builder.add_i32();
|
|
ctx.builder.set_local(&ctx.instruction_counter);
|
|
|
|
ctx.cpu.eip = start_addr;
|
|
ctx.current_instruction = Instruction::Other;
|
|
ctx.previous_instruction = Instruction::Other;
|
|
|
|
loop {
|
|
let mut instruction = 0;
|
|
if cfg!(feature = "profiler") {
|
|
instruction = memory::read32s(ctx.cpu.eip) as u32;
|
|
opstats::gen_opstats(ctx.builder, instruction);
|
|
opstats::record_opstat_compiled(instruction);
|
|
}
|
|
|
|
if ctx.cpu.eip == last_instruction_addr {
|
|
// Before the last instruction:
|
|
// - Set eip to *after* the instruction
|
|
// - Set previous_eip to *before* the instruction
|
|
if needs_eip_updated {
|
|
codegen::gen_set_previous_eip_offset_from_eip_with_low_bits(
|
|
ctx.builder,
|
|
last_instruction_addr as i32 & 0xFFF,
|
|
);
|
|
codegen::gen_set_eip_low_bits(ctx.builder, stop_addr as i32 & 0xFFF);
|
|
}
|
|
}
|
|
|
|
let wasm_length_before = ctx.builder.instruction_body_length();
|
|
|
|
ctx.start_of_current_instruction = ctx.cpu.eip;
|
|
let start_eip = ctx.cpu.eip;
|
|
let mut instruction_flags = 0;
|
|
jit_instructions::jit_instruction(ctx, &mut instruction_flags);
|
|
let end_eip = ctx.cpu.eip;
|
|
|
|
let instruction_length = end_eip - start_eip;
|
|
let was_block_boundary = instruction_flags & JIT_INSTR_BLOCK_BOUNDARY_FLAG != 0;
|
|
|
|
let wasm_length = ctx.builder.instruction_body_length() - wasm_length_before;
|
|
opstats::record_opstat_size_wasm(instruction, wasm_length as u64);
|
|
|
|
dbg_assert!((end_eip == stop_addr) == (start_eip == last_instruction_addr));
|
|
dbg_assert!(instruction_length < MAX_INSTRUCTION_LENGTH);
|
|
|
|
let end_addr = ctx.cpu.eip;
|
|
|
|
if end_addr == stop_addr {
|
|
// no page was crossed
|
|
dbg_assert!(Page::page_of(end_addr) == Page::page_of(start_addr));
|
|
break;
|
|
}
|
|
|
|
if was_block_boundary || is_near_end_of_page(end_addr) || end_addr > stop_addr {
|
|
dbg_log!(
|
|
"Overlapping basic blocks start={:x} expected_end={:x} end={:x} was_block_boundary={} near_end_of_page={}",
|
|
start_addr,
|
|
stop_addr,
|
|
end_addr,
|
|
was_block_boundary,
|
|
is_near_end_of_page(end_addr)
|
|
);
|
|
dbg_assert!(false);
|
|
break;
|
|
}
|
|
|
|
ctx.previous_instruction = mem::replace(&mut ctx.current_instruction, Instruction::Other);
|
|
}
|
|
}
|
|
|
|
pub fn jit_increase_hotness_and_maybe_compile(
|
|
virt_address: i32,
|
|
phys_address: u32,
|
|
cs_offset: u32,
|
|
state_flags: CachedStateFlags,
|
|
heat: u32,
|
|
) {
|
|
let ctx = get_jit_state();
|
|
let page = Page::page_of(phys_address);
|
|
let (hotness, entry_points) = ctx.entry_points.entry(page).or_insert_with(|| {
|
|
cpu::tlb_set_has_code(page, true);
|
|
profiler::stat_increment(stat::RUN_INTERPRETED_NEW_PAGE);
|
|
(0, HashSet::new())
|
|
});
|
|
|
|
if !is_near_end_of_page(phys_address) {
|
|
entry_points.insert(phys_address as u16 & 0xFFF);
|
|
}
|
|
|
|
*hotness += heat;
|
|
if *hotness >= JIT_THRESHOLD {
|
|
if ctx.compiling.is_some() {
|
|
return;
|
|
}
|
|
// only try generating if we're in the correct address space
|
|
if cpu::translate_address_read_no_side_effects(virt_address) == Ok(phys_address) {
|
|
*hotness = 0;
|
|
jit_analyze_and_generate(ctx, virt_address, phys_address, cs_offset, state_flags)
|
|
}
|
|
else {
|
|
profiler::stat_increment(stat::COMPILE_WRONG_ADDRESS_SPACE);
|
|
}
|
|
};
|
|
}
|
|
|
|
fn free_wasm_table_index(ctx: &mut JitState, wasm_table_index: WasmTableIndex) {
|
|
if CHECK_JIT_STATE_INVARIANTS {
|
|
dbg_assert!(!ctx.wasm_table_index_free_list.contains(&wasm_table_index));
|
|
|
|
match &ctx.compiling {
|
|
Some((wasm_table_index_compiling, _)) => {
|
|
dbg_assert!(
|
|
*wasm_table_index_compiling != wasm_table_index,
|
|
"Attempt to free wasm table index that is currently being compiled"
|
|
);
|
|
},
|
|
_ => {},
|
|
}
|
|
|
|
dbg_assert!(
|
|
!ctx.pages
|
|
.values()
|
|
.any(|info| info.wasm_table_index == wasm_table_index)
|
|
);
|
|
|
|
dbg_assert!(
|
|
!ctx.pages
|
|
.values()
|
|
.any(|info| info.hidden_wasm_table_indices.contains(&wasm_table_index))
|
|
);
|
|
|
|
for i in 0..unsafe { cpu::valid_tlb_entries_count } {
|
|
let page = unsafe { cpu::valid_tlb_entries[i as usize] };
|
|
unsafe {
|
|
match cpu::tlb_code[page as usize] {
|
|
None => {},
|
|
Some(c) => {
|
|
let c = c.as_ref();
|
|
dbg_assert!(c.wasm_table_index != wasm_table_index);
|
|
},
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
ctx.wasm_table_index_free_list.push(wasm_table_index);
|
|
|
|
// It is not strictly necessary to clear the function, but it will fail more predictably if we
|
|
// accidentally use the function and may garbage collect unused modules earlier
|
|
jit_clear_func(wasm_table_index);
|
|
}
|
|
|
|
/// Register a write in this page: Delete all present code
|
|
pub fn jit_dirty_page(ctx: &mut JitState, page: Page) {
|
|
let mut did_have_code = false;
|
|
|
|
if let Some(PageInfo {
|
|
wasm_table_index,
|
|
hidden_wasm_table_indices,
|
|
state_flags: _,
|
|
entry_points: _,
|
|
}) = ctx.pages.remove(&page)
|
|
{
|
|
profiler::stat_increment(stat::INVALIDATE_PAGE_HAD_CODE);
|
|
did_have_code = true;
|
|
|
|
free(ctx, wasm_table_index);
|
|
for wasm_table_index in hidden_wasm_table_indices {
|
|
free(ctx, wasm_table_index);
|
|
}
|
|
|
|
fn free(ctx: &mut JitState, wasm_table_index: WasmTableIndex) {
|
|
for i in 0..unsafe { cpu::valid_tlb_entries_count } {
|
|
let page = unsafe { cpu::valid_tlb_entries[i as usize] };
|
|
let entry = unsafe { cpu::tlb_data[page as usize] };
|
|
if 0 != entry {
|
|
let tlb_physical_page = Page::of_u32(
|
|
(entry as u32 >> 12 ^ page as u32) - (unsafe { memory::mem8 } as u32 >> 12),
|
|
);
|
|
match unsafe { cpu::tlb_code[page as usize] } {
|
|
None => {},
|
|
Some(c) => unsafe {
|
|
let w = c.as_ref().wasm_table_index;
|
|
if wasm_table_index == w {
|
|
drop(Box::from_raw(c.as_ptr()));
|
|
cpu::tlb_code[page as usize] = None;
|
|
if !ctx.entry_points.contains_key(&tlb_physical_page) {
|
|
cpu::tlb_data[page as usize] &= !cpu::TLB_HAS_CODE; // XXX
|
|
}
|
|
}
|
|
},
|
|
}
|
|
}
|
|
}
|
|
|
|
ctx.pages.retain(
|
|
|
|
|
_,
|
|
&mut PageInfo {
|
|
wasm_table_index: w,
|
|
..
|
|
},
|
|
| w != wasm_table_index,
|
|
);
|
|
|
|
for info in ctx.pages.values_mut() {
|
|
info.hidden_wasm_table_indices
|
|
.retain(|&w| w != wasm_table_index)
|
|
}
|
|
|
|
free_wasm_table_index(ctx, wasm_table_index);
|
|
}
|
|
}
|
|
|
|
match ctx.entry_points.remove(&page) {
|
|
None => {},
|
|
Some(_) => {
|
|
profiler::stat_increment(stat::INVALIDATE_PAGE_HAD_ENTRY_POINTS);
|
|
did_have_code = true;
|
|
|
|
match &ctx.compiling {
|
|
Some((index, CompilingPageState::Compiling { pages })) => {
|
|
if pages.contains_key(&page) {
|
|
ctx.compiling = Some((*index, CompilingPageState::CompilingWritten));
|
|
}
|
|
},
|
|
_ => {},
|
|
}
|
|
},
|
|
}
|
|
|
|
match &ctx.compiling {
|
|
Some((_, CompilingPageState::Compiling { pages })) => {
|
|
dbg_assert!(!pages.contains_key(&page));
|
|
},
|
|
_ => {},
|
|
}
|
|
|
|
check_jit_state_invariants(ctx);
|
|
|
|
dbg_assert!(!jit_page_has_code_ctx(ctx, page));
|
|
|
|
if did_have_code {
|
|
cpu::tlb_set_has_code(page, false);
|
|
}
|
|
|
|
if !did_have_code {
|
|
profiler::stat_increment(stat::DIRTY_PAGE_DID_NOT_HAVE_CODE);
|
|
}
|
|
}
|
|
|
|
#[no_mangle]
|
|
pub fn jit_dirty_cache(start_addr: u32, end_addr: u32) {
|
|
dbg_assert!(start_addr < end_addr);
|
|
|
|
let start_page = Page::page_of(start_addr);
|
|
let end_page = Page::page_of(end_addr - 1);
|
|
|
|
for page in start_page.to_u32()..end_page.to_u32() + 1 {
|
|
jit_dirty_page(get_jit_state(), Page::page_of(page << 12));
|
|
}
|
|
}
|
|
|
|
/// dirty pages in the range of start_addr and end_addr, which must span at most two pages
|
|
pub fn jit_dirty_cache_small(start_addr: u32, end_addr: u32) {
|
|
dbg_assert!(start_addr < end_addr);
|
|
|
|
let start_page = Page::page_of(start_addr);
|
|
let end_page = Page::page_of(end_addr - 1);
|
|
|
|
let ctx = get_jit_state();
|
|
jit_dirty_page(ctx, start_page);
|
|
|
|
// Note: This can't happen when paging is enabled, as writes across
|
|
// boundaries are split up on two pages
|
|
if start_page != end_page {
|
|
dbg_assert!(start_page.to_u32() + 1 == end_page.to_u32());
|
|
jit_dirty_page(ctx, end_page);
|
|
}
|
|
}
|
|
|
|
#[no_mangle]
|
|
pub fn jit_clear_cache_js() { jit_clear_cache(get_jit_state()) }
|
|
|
|
pub fn jit_clear_cache(ctx: &mut JitState) {
|
|
let mut pages_with_code = HashSet::new();
|
|
|
|
for &p in ctx.entry_points.keys() {
|
|
pages_with_code.insert(p);
|
|
}
|
|
for &p in ctx.pages.keys() {
|
|
pages_with_code.insert(p);
|
|
}
|
|
|
|
for page in pages_with_code {
|
|
jit_dirty_page(ctx, page);
|
|
}
|
|
}
|
|
|
|
pub fn jit_page_has_code(page: Page) -> bool { jit_page_has_code_ctx(get_jit_state(), page) }
|
|
|
|
pub fn jit_page_has_code_ctx(ctx: &mut JitState, page: Page) -> bool {
|
|
ctx.pages.contains_key(&page) || ctx.entry_points.contains_key(&page)
|
|
}
|
|
|
|
#[no_mangle]
|
|
pub fn jit_get_wasm_table_index_free_list_count() -> u32 {
|
|
if cfg!(feature = "profiler") {
|
|
get_jit_state().wasm_table_index_free_list.len() as u32
|
|
}
|
|
else {
|
|
0
|
|
}
|
|
}
|
|
#[no_mangle]
|
|
pub fn jit_get_cache_size() -> u32 {
|
|
if cfg!(feature = "profiler") {
|
|
get_jit_state()
|
|
.pages
|
|
.values()
|
|
.map(|p| p.entry_points.len() as u32)
|
|
.sum()
|
|
}
|
|
else {
|
|
0
|
|
}
|
|
}
|
|
|
|
#[cfg(feature = "profiler")]
|
|
pub fn check_missed_entry_points(phys_address: u32, state_flags: CachedStateFlags) {
|
|
let ctx = get_jit_state();
|
|
|
|
if let Some(infos) = ctx.pages.get(&Page::page_of(phys_address)) {
|
|
if infos.state_flags != state_flags {
|
|
return;
|
|
}
|
|
|
|
let last_jump_type = unsafe { cpu::debug_last_jump.name() };
|
|
let last_jump_addr = unsafe { cpu::debug_last_jump.phys_address() }.unwrap_or(0);
|
|
let last_jump_opcode =
|
|
if last_jump_addr != 0 { memory::read32s(last_jump_addr) } else { 0 };
|
|
|
|
let opcode = memory::read32s(phys_address);
|
|
dbg_log!(
|
|
"Compiled exists, but no entry point, \
|
|
phys_addr={:x} opcode={:02x} {:02x} {:02x} {:02x}. \
|
|
Last jump at {:x} ({}) opcode={:02x} {:02x} {:02x} {:02x}",
|
|
phys_address,
|
|
opcode & 0xFF,
|
|
opcode >> 8 & 0xFF,
|
|
opcode >> 16 & 0xFF,
|
|
opcode >> 16 & 0xFF,
|
|
last_jump_addr,
|
|
last_jump_type,
|
|
last_jump_opcode & 0xFF,
|
|
last_jump_opcode >> 8 & 0xFF,
|
|
last_jump_opcode >> 16 & 0xFF,
|
|
last_jump_opcode >> 16 & 0xFF,
|
|
);
|
|
}
|
|
}
|
|
|
|
#[no_mangle]
|
|
#[cfg(feature = "profiler")]
|
|
pub fn debug_set_dispatcher_target(_target_index: i32) {
|
|
//dbg_log!("About to call dispatcher target_index={}", target_index);
|
|
}
|
|
|
|
#[no_mangle]
|
|
#[cfg(feature = "profiler")]
|
|
pub fn check_dispatcher_target(target_index: i32, max: i32) {
|
|
//dbg_log!("Dispatcher called target={}", target_index);
|
|
dbg_assert!(target_index >= 0);
|
|
dbg_assert!(target_index < max);
|
|
}
|
|
|
|
#[no_mangle]
|
|
#[cfg(feature = "profiler")]
|
|
pub fn enter_basic_block(phys_eip: u32) {
|
|
let eip =
|
|
unsafe { cpu::translate_address_read(*global_pointers::instruction_pointer).unwrap() };
|
|
if Page::page_of(eip) != Page::page_of(phys_eip) {
|
|
dbg_log!(
|
|
"enter basic block failed block=0x{:x} actual eip=0x{:x}",
|
|
phys_eip,
|
|
eip
|
|
);
|
|
panic!();
|
|
}
|
|
}
|
|
|
|
#[no_mangle]
|
|
#[cfg(feature = "profiler")]
|
|
pub fn get_config(index: u32) -> u32 {
|
|
match index {
|
|
0 => MAX_PAGES as u32,
|
|
1 => JIT_USE_LOOP_SAFETY as u32,
|
|
2 => MAX_EXTRA_BASIC_BLOCKS as u32,
|
|
_ => 0,
|
|
}
|
|
}
|