Skip to content

Commit d375656

Browse files
committed
Rename the JIT
1 parent 00768e4 commit d375656

38 files changed

Lines changed: 435 additions & 449 deletions

.github/workflows/ci.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,7 @@ jobs:
2929
run: deps/clone-linux.sh
3030
- name: Build
3131
run: |
32-
meson build -Dengine=jit -Dkernel=${{matrix.kernel}}
32+
meson build -Dengine=asbestos -Dkernel=${{matrix.kernel}}
3333
ninja -C build
3434
env:
3535
CC: ${{matrix.cc}}

README.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -59,9 +59,9 @@ Available channels:
5959
- `verbose`: Debug logs that don't fit into another category.
6060
- Grep for `DEFAULT_CHANNEL` to see if more log channels have been added since this list was updated.
6161

62-
# A note on the JIT
62+
# A note on the interpreter
6363

64-
Possibly the most interesting thing I wrote as part of iSH is the JIT. It's not actually a JIT since it doesn't target machine code. Instead it generates an array of pointers to functions called gadgets, and each gadget ends with a tailcall to the next function; like the threaded code technique used by some Forth interpreters. The result is a speedup of roughly 3-5x compared to pure emulation.
64+
Possibly the most interesting thing I wrote as part of iSH is the interpreter. It's not quite a JIT since it doesn't target machine code. Instead it generates an array of pointers to functions called gadgets, and each gadget ends with a tailcall to the next function; like the threaded code technique used by some Forth interpreters. The result is a speedup of roughly 3-5x compared to emulation using a simpler switch dispatch.
6565

6666
Unfortunately, I made the decision to write nearly all of the gadgets in assembly language. This was probably a good decision with regards to performance (though I'll never know for sure), but a horrible decision with regards to readability, maintainability, and my sanity. The amount of bullshit I've had to put up with from the compiler/assembler/linker is insane. It's like there's a demon in there that makes sure my code is sufficiently deformed, and if not, makes up stupid reasons why it shouldn't compile. In order to stay sane while writing this code, I've had to ignore best practices in code structure and naming. You'll find macros and variables with such descriptive names as `ss` and `s` and `a`. Assembler macros nested beyond belief. And to top it off, there are almost no comments.
6767

asbestos/asbestos.c

Lines changed: 289 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,289 @@
1+
#define DEFAULT_CHANNEL instr
2+
#include "debug.h"
3+
#include "asbestos/asbestos.h"
4+
#include "asbestos/gen.h"
5+
#include "asbestos/frame.h"
6+
#include "emu/cpu.h"
7+
#include "emu/memory.h"
8+
#include "emu/interrupt.h"
9+
#include "util/list.h"
10+
11+
extern int current_pid(void);
12+
13+
static void fiber_block_disconnect(struct asbestos *asbestos, struct fiber_block *block);
14+
static void fiber_block_free(struct asbestos *asbestos, struct fiber_block *block);
15+
static void fiber_free_jetsam(struct asbestos *asbestos);
16+
static void fiber_resize_hash(struct asbestos *asbestos, size_t new_size);
17+
18+
struct asbestos *asbestos_new(struct mmu *mmu) {
19+
struct asbestos *asbestos = calloc(1, sizeof(struct asbestos));
20+
asbestos->mmu = mmu;
21+
fiber_resize_hash(asbestos, FIBER_INITIAL_HASH_SIZE);
22+
asbestos->page_hash = calloc(FIBER_PAGE_HASH_SIZE, sizeof(*asbestos->page_hash));
23+
list_init(&asbestos->jetsam);
24+
lock_init(&asbestos->lock);
25+
wrlock_init(&asbestos->jetsam_lock);
26+
return asbestos;
27+
}
28+
29+
void asbestos_free(struct asbestos *asbestos) {
30+
for (size_t i = 0; i < asbestos->hash_size; i++) {
31+
struct fiber_block *block, *tmp;
32+
if (list_null(&asbestos->hash[i]))
33+
continue;
34+
list_for_each_entry_safe(&asbestos->hash[i], block, tmp, chain) {
35+
fiber_block_free(asbestos, block);
36+
}
37+
}
38+
fiber_free_jetsam(asbestos);
39+
free(asbestos->page_hash);
40+
free(asbestos->hash);
41+
free(asbestos);
42+
}
43+
44+
static inline struct list *blocks_list(struct asbestos *asbestos, page_t page, int i) {
45+
// TODO is this a good hash function?
46+
return &asbestos->page_hash[page % FIBER_PAGE_HASH_SIZE].blocks[i];
47+
}
48+
49+
void asbestos_invalidate_range(struct asbestos *absestos, page_t start, page_t end) {
50+
lock(&absestos->lock);
51+
struct fiber_block *block, *tmp;
52+
for (page_t page = start; page < end; page++) {
53+
for (int i = 0; i <= 1; i++) {
54+
struct list *blocks = blocks_list(absestos, page, i);
55+
if (list_null(blocks))
56+
continue;
57+
list_for_each_entry_safe(blocks, block, tmp, page[i]) {
58+
fiber_block_disconnect(absestos, block);
59+
block->is_jetsam = true;
60+
list_add(&absestos->jetsam, &block->jetsam);
61+
}
62+
}
63+
}
64+
unlock(&absestos->lock);
65+
}
66+
67+
void asbestos_invalidate_page(struct asbestos *asbestos, page_t page) {
68+
asbestos_invalidate_range(asbestos, page, page + 1);
69+
}
70+
void asbestos_invalidate_all(struct asbestos *asbestos) {
71+
asbestos_invalidate_range(asbestos, 0, MEM_PAGES);
72+
}
73+
74+
static void fiber_resize_hash(struct asbestos *asbestos, size_t new_size) {
75+
TRACE_(verbose, "%d resizing hash to %lu, using %lu bytes for gadgets\n", current_pid(), new_size, asbestos->mem_used);
76+
struct list *new_hash = calloc(new_size, sizeof(struct list));
77+
for (size_t i = 0; i < asbestos->hash_size; i++) {
78+
if (list_null(&asbestos->hash[i]))
79+
continue;
80+
struct fiber_block *block, *tmp;
81+
list_for_each_entry_safe(&asbestos->hash[i], block, tmp, chain) {
82+
list_remove(&block->chain);
83+
list_init_add(&new_hash[block->addr % new_size], &block->chain);
84+
}
85+
}
86+
free(asbestos->hash);
87+
asbestos->hash = new_hash;
88+
asbestos->hash_size = new_size;
89+
}
90+
91+
static void fiber_insert(struct asbestos *asbestos, struct fiber_block *block) {
92+
asbestos->mem_used += block->used;
93+
asbestos->num_blocks++;
94+
// target an average hash chain length of 1-2
95+
if (asbestos->num_blocks >= asbestos->hash_size * 2)
96+
fiber_resize_hash(asbestos, asbestos->hash_size * 2);
97+
98+
list_init_add(&asbestos->hash[block->addr % asbestos->hash_size], &block->chain);
99+
list_init_add(blocks_list(asbestos, PAGE(block->addr), 0), &block->page[0]);
100+
if (PAGE(block->addr) != PAGE(block->end_addr))
101+
list_init_add(blocks_list(asbestos, PAGE(block->end_addr), 1), &block->page[1]);
102+
}
103+
104+
static struct fiber_block *fiber_lookup(struct asbestos *asbestos, addr_t addr) {
105+
struct list *bucket = &asbestos->hash[addr % asbestos->hash_size];
106+
if (list_null(bucket))
107+
return NULL;
108+
struct fiber_block *block;
109+
list_for_each_entry(bucket, block, chain) {
110+
if (block->addr == addr)
111+
return block;
112+
}
113+
return NULL;
114+
}
115+
116+
static struct fiber_block *fiber_block_compile(addr_t ip, struct tlb *tlb) {
117+
struct gen_state state;
118+
TRACE("%d %08x --- compiling:\n", current_pid(), ip);
119+
gen_start(ip, &state);
120+
while (true) {
121+
if (!gen_step(&state, tlb))
122+
break;
123+
// no block should span more than 2 pages
124+
// guarantee this by limiting total block size to 1 page
125+
// guarantee that by stopping as soon as there's less space left than
126+
// the maximum length of an x86 instruction
127+
// TODO refuse to decode instructions longer than 15 bytes
128+
if (state.ip - ip >= PAGE_SIZE - 15) {
129+
gen_exit(&state);
130+
break;
131+
}
132+
}
133+
gen_end(&state);
134+
assert(state.ip - ip <= PAGE_SIZE);
135+
state.block->used = state.capacity;
136+
return state.block;
137+
}
138+
139+
// Remove all pointers to the block. It can't be freed yet because another
140+
// thread may be executing it.
141+
static void fiber_block_disconnect(struct asbestos *asbestos, struct fiber_block *block) {
142+
if (asbestos != NULL) {
143+
asbestos->mem_used -= block->used;
144+
asbestos->num_blocks--;
145+
}
146+
list_remove(&block->chain);
147+
for (int i = 0; i <= 1; i++) {
148+
list_remove(&block->page[i]);
149+
list_remove_safe(&block->jumps_from_links[i]);
150+
151+
struct fiber_block *prev_block, *tmp;
152+
list_for_each_entry_safe(&block->jumps_from[i], prev_block, tmp, jumps_from_links[i]) {
153+
if (prev_block->jump_ip[i] != NULL)
154+
*prev_block->jump_ip[i] = prev_block->old_jump_ip[i];
155+
list_remove(&prev_block->jumps_from_links[i]);
156+
}
157+
}
158+
}
159+
160+
static void fiber_block_free(struct asbestos *asbestos, struct fiber_block *block) {
161+
fiber_block_disconnect(asbestos, block);
162+
free(block);
163+
}
164+
165+
static void fiber_free_jetsam(struct asbestos *asbestos) {
166+
struct fiber_block *block, *tmp;
167+
list_for_each_entry_safe(&asbestos->jetsam, block, tmp, jetsam) {
168+
list_remove(&block->jetsam);
169+
free(block);
170+
}
171+
}
172+
173+
int fiber_enter(struct fiber_block *block, struct fiber_frame *frame, struct tlb *tlb);
174+
175+
static inline size_t fiber_cache_hash(addr_t ip) {
176+
return (ip ^ (ip >> 12)) % FIBER_CACHE_SIZE;
177+
}
178+
179+
static int cpu_step_to_interrupt(struct cpu_state *cpu, struct tlb *tlb) {
180+
struct asbestos *asbestos = cpu->mmu->asbestos;
181+
read_wrlock(&asbestos->jetsam_lock);
182+
183+
struct fiber_block **cache = calloc(FIBER_CACHE_SIZE, sizeof(*cache));
184+
struct fiber_frame *frame = malloc(sizeof(struct fiber_frame));
185+
memset(frame, 0, sizeof(*frame));
186+
frame->cpu = *cpu;
187+
assert(asbestos->mmu == cpu->mmu);
188+
189+
int interrupt = INT_NONE;
190+
while (interrupt == INT_NONE) {
191+
addr_t ip = frame->cpu.eip;
192+
size_t cache_index = fiber_cache_hash(ip);
193+
struct fiber_block *block = cache[cache_index];
194+
if (block == NULL || block->addr != ip) {
195+
lock(&asbestos->lock);
196+
block = fiber_lookup(asbestos, ip);
197+
if (block == NULL) {
198+
block = fiber_block_compile(ip, tlb);
199+
fiber_insert(asbestos, block);
200+
} else {
201+
TRACE("%d %08x --- missed cache\n", current_pid(), ip);
202+
}
203+
cache[cache_index] = block;
204+
unlock(&asbestos->lock);
205+
}
206+
struct fiber_block *last_block = frame->last_block;
207+
if (last_block != NULL &&
208+
(last_block->jump_ip[0] != NULL ||
209+
last_block->jump_ip[1] != NULL)) {
210+
lock(&asbestos->lock);
211+
// can't mint new pointers to a block that has been marked jetsam
212+
// and is thus assumed to have no pointers left
213+
if (!last_block->is_jetsam && !block->is_jetsam) {
214+
for (int i = 0; i <= 1; i++) {
215+
if (last_block->jump_ip[i] != NULL &&
216+
(*last_block->jump_ip[i] & 0xffffffff) == block->addr) {
217+
*last_block->jump_ip[i] = (unsigned long) block->code;
218+
list_add(&block->jumps_from[i], &last_block->jumps_from_links[i]);
219+
}
220+
}
221+
}
222+
223+
unlock(&asbestos->lock);
224+
}
225+
frame->last_block = block;
226+
227+
// block may be jetsam, but that's ok, because it can't be freed until
228+
// every thread on this asbestos is not executing anything
229+
230+
TRACE("%d %08x --- cycle %ld\n", current_pid(), ip, frame->cpu.cycle);
231+
232+
interrupt = fiber_enter(block, frame, tlb);
233+
if (interrupt == INT_NONE && __atomic_exchange_n(cpu->poked_ptr, false, __ATOMIC_SEQ_CST))
234+
interrupt = INT_TIMER;
235+
if (interrupt == INT_NONE && ++frame->cpu.cycle % (1 << 10) == 0)
236+
interrupt = INT_TIMER;
237+
*cpu = frame->cpu;
238+
}
239+
240+
free(frame);
241+
free(cache);
242+
read_wrunlock(&asbestos->jetsam_lock);
243+
return interrupt;
244+
}
245+
246+
static int cpu_single_step(struct cpu_state *cpu, struct tlb *tlb) {
247+
struct gen_state state;
248+
gen_start(cpu->eip, &state);
249+
gen_step(&state, tlb);
250+
gen_exit(&state);
251+
gen_end(&state);
252+
253+
struct fiber_block *block = state.block;
254+
struct fiber_frame frame = {.cpu = *cpu};
255+
int interrupt = fiber_enter(block, &frame, tlb);
256+
*cpu = frame.cpu;
257+
fiber_block_free(NULL, block);
258+
if (interrupt == INT_NONE)
259+
interrupt = INT_DEBUG;
260+
return interrupt;
261+
}
262+
263+
int cpu_run_to_interrupt(struct cpu_state *cpu, struct tlb *tlb) {
264+
if (cpu->poked_ptr == NULL)
265+
cpu->poked_ptr = &cpu->_poked;
266+
tlb_refresh(tlb, cpu->mmu);
267+
int interrupt = (cpu->tf ? cpu_single_step : cpu_step_to_interrupt)(cpu, tlb);
268+
cpu->trapno = interrupt;
269+
270+
struct asbestos *asbestos = cpu->mmu->asbestos;
271+
lock(&asbestos->lock);
272+
if (!list_empty(&asbestos->jetsam)) {
273+
// write-lock the jetsam_lock to wait until other asbestos threads get
274+
// to this point, so they will all clear out their block pointers
275+
// TODO: use RCU for better performance
276+
unlock(&asbestos->lock);
277+
write_wrlock(&asbestos->jetsam_lock);
278+
lock(&asbestos->lock);
279+
fiber_free_jetsam(asbestos);
280+
write_wrunlock(&asbestos->jetsam_lock);
281+
}
282+
unlock(&asbestos->lock);
283+
284+
return interrupt;
285+
}
286+
287+
void cpu_poke(struct cpu_state *cpu) {
288+
__atomic_store_n(cpu->poked_ptr, true, __ATOMIC_SEQ_CST);
289+
}

jit/jit.h renamed to asbestos/asbestos.h

Lines changed: 18 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -1,24 +1,24 @@
1-
#ifndef JIT_H
2-
#define JIT_H
1+
#ifndef ASBESTOS_H
2+
#define ASBESTOS_H
33
#include "misc.h"
44
#include "emu/mmu.h"
55
#include "util/list.h"
66
#include "util/sync.h"
77

8-
#define JIT_INITIAL_HASH_SIZE (1 << 10)
9-
#define JIT_CACHE_SIZE (1 << 10)
10-
#define JIT_PAGE_HASH_SIZE (1 << 10)
8+
#define FIBER_INITIAL_HASH_SIZE (1 << 10)
9+
#define FIBER_CACHE_SIZE (1 << 10)
10+
#define FIBER_PAGE_HASH_SIZE (1 << 10)
1111

12-
struct jit {
13-
// there is one jit per address space
12+
struct asbestos {
13+
// there is one asbestos per address space
1414
struct mmu *mmu;
1515
size_t mem_used;
1616
size_t num_blocks;
1717

1818
struct list *hash;
1919
size_t hash_size;
2020

21-
// list of jit_blocks that should be freed soon (at the next RCU grace
21+
// list of fiber_blocks that should be freed soon (at the next RCU grace
2222
// period, if we had such a thing)
2323
struct list jetsam;
2424

@@ -33,9 +33,9 @@ struct jit {
3333

3434
// this is roughly the average number of instructions in a basic block according to anonymous sources
3535
// times 4, roughly the average number of gadgets/parameters in an instruction, according to anonymous sources
36-
#define JIT_BLOCK_INITIAL_CAPACITY 16
36+
#define FIBER_BLOCK_INITIAL_CAPACITY 16
3737

38-
struct jit_block {
38+
struct fiber_block {
3939
addr_t addr;
4040
addr_t end_addr;
4141
size_t used;
@@ -60,15 +60,15 @@ struct jit_block {
6060
unsigned long code[];
6161
};
6262

63-
// Create a new jit
64-
struct jit *jit_new(struct mmu *mmu);
65-
void jit_free(struct jit *jit);
63+
// Create a new asbestos
64+
struct asbestos *asbestos_new(struct mmu *mmu);
65+
void asbestos_free(struct asbestos *asbestos);
6666

67-
// Invalidate all jit blocks in pages start (inclusive) to end (exclusive).
68-
// Locks the jit. Should only be called by memory.c in conjunction with
67+
// Invalidate all fiber blocks in pages start (inclusive) to end (exclusive).
68+
// Locks the asbestos. Should only be called by memory.c in conjunction with
6969
// mem_changed.
70-
void jit_invalidate_range(struct jit *jit, page_t start, page_t end);
71-
void jit_invalidate_page(struct jit *jit, page_t page);
72-
void jit_invalidate_all(struct jit *jit);
70+
void asbestos_invalidate_range(struct asbestos *asbestos, page_t start, page_t end);
71+
void asbestos_invalidate_page(struct asbestos *asbestos, page_t page);
72+
void asbestos_invalidate_all(struct asbestos *asbestos);
7373

7474
#endif

asbestos/frame.h

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
#include <stdatomic.h>
2+
#include "emu/cpu.h"
3+
4+
// keep in sync with asm
5+
#define FIBER_RETURN_CACHE_SIZE 4096
6+
#define FIBER_RETURN_CACHE_HASH(x) ((x) & 0xFFF0) >> 4)
7+
8+
struct fiber_frame {
9+
struct cpu_state cpu;
10+
void *bp;
11+
addr_t value_addr;
12+
uint64_t value[2]; // buffer for crosspage crap
13+
struct fiber_block *last_block;
14+
long ret_cache[FIBER_RETURN_CACHE_SIZE]; // a map of ip to pointer-to-call-gadget-arguments
15+
};

0 commit comments

Comments
 (0)