Last active
July 1, 2019 20:41
-
-
Save cfsamson/294d1e8f22dff36303036aca1f5b4c29 to your computer and use it in GitHub Desktop.
Minimum code for proving the context switch assebly on Windows
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
STACK_SIZE = 1024 * 1024 * 8 | |
PUSH_POP_REG_COUNT = 7 | |
class FiberPoc | |
@stack : Array(UInt8) | |
property rsp : UInt8* = Pointer(UInt8).null | |
property f = Proc(Void).new { } | |
def initialize | |
@stack = Array.new(STACK_SIZE, 0_u8) # OK, as long as we don't push/pop -> reallocate | |
end | |
def stack | |
@stack | |
end | |
def run | |
@f.call | |
end | |
def makecontext(proc : Proc) | |
@f = proc | |
# we pop 7 registers before we return | |
s_ptr = @stack.to_unsafe | |
s_start = s_ptr + (STACK_SIZE - 32) | |
# Our entry | |
@rsp = (s_start - 7*8) | |
s_start_ptr = s_start.as(UInt64*) | |
s_start_ptr.value = ->(f : FiberPoc){f.run}.pointer.address | |
# First parameter | |
first_param = (s_start - 8).as(UInt64*) | |
first_param.value = self.as(Void*).address | |
p "fiber_address: #{self.as(Void*)}" | |
end | |
end | |
class Scheduler | |
@fibers = Array(FiberPoc).new(3) | |
@current = 0 | |
@running = 1 | |
def initialize | |
@fibers = Array(FiberPoc).new | |
(0..2).each do |i| | |
@fibers << FiberPoc.new | |
end | |
# base thread | |
@fibers[0].makecontext(->{}) | |
end | |
def spawn(fiber_no : UInt64, f : Proc) | |
p "spawn" | |
@running += 1 | |
@fibers[fiber_no].makecontext(f) | |
end | |
def run | |
self.yield_control | |
end | |
def yield_control | |
p "yield running: #{@running}, current: #{@current}" | |
if @running == 2 | |
# do nothing | |
elsif @running == 3 | |
if @current == 0 | |
p "old: 0, new: 1, from: #{@fibers[0].rsp}, to: #{@fibers[1].rsp}" | |
@current = 1 | |
Scheduler.swapcontext(@fibers[0].rsp, @fibers[1].rsp) | |
elsif @current == 1 | |
p "old: 1, new: 2, from: #{@fibers[1].rsp}, to: #{@fibers[2].rsp}" | |
@current = 2 | |
Scheduler.swapcontext(@fibers[1].rsp, @fibers[2].rsp) | |
elsif @current == 2 | |
p "old: 2, new: 1, from: #{@fibers[2].rsp}, to: #{@fibers[1].rsp}" | |
@current = 1 | |
Scheduler.swapcontext(@fibers[2].rsp, @fibers[1].rsp) | |
end | |
end | |
end | |
def ret | |
p "ret" | |
@running -= 1 | |
if @running == 1 | |
p "Done. Exiting." | |
Process.exit(0) | |
end | |
self.yield_control | |
end | |
@[NoInline] | |
@[Naked] | |
def self.swapcontext(current : UInt8*, to : UInt8*) : Nil | |
asm(" | |
pushq %rdi | |
pushq %rbx | |
pushq %rbp | |
pushq %r12 | |
pushq %r13 | |
pushq %r14 | |
pushq %r15 | |
movq %rsp, ($0) | |
movq $1, %rsp | |
popq %r15 | |
popq %r14 | |
popq %r13 | |
popq %r12 | |
popq %rbp | |
popq %rbx | |
popq %rdi" | |
:: "r"(current), "r"(to)) | |
end | |
end | |
fun test | |
puts "Hello" | |
end | |
class Executor | |
@scheduler = Scheduler.new | |
def main | |
# hello1 = ->{ | |
# (0..10).each do |i| | |
# puts("Fiber1: #{i}") | |
# @scheduler.yield_control | |
# end | |
# @scheduler.ret | |
# } | |
# hello2 = ->{ | |
# (0..10).each do |i| | |
# puts("Fiber2: #{i}") | |
# @scheduler.yield_control | |
# end | |
# @scheduler.ret | |
# } | |
hello1 = ->{ | |
puts("Fiber2: HELLO") | |
@scheduler.yield_control | |
puts("Fiber2: HELLO2") | |
@scheduler.ret | |
} | |
hello2 = ->{ | |
puts("Fiber2: HELLO") | |
@scheduler.yield_control | |
puts("Fiber2: HELLO2") | |
@scheduler.ret | |
} | |
@scheduler.spawn(1, hello1) | |
@scheduler.spawn(2, hello2) | |
@scheduler.run | |
end | |
end | |
executor = Executor.new | |
executor.main |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
STACK_SIZE = 100 | |
class FiberPoc | |
@stack : Array(UInt8) | |
def initialize | |
@stack = Array.new(STACK_SIZE, 0_u8) | |
end | |
def stack | |
@stack | |
end | |
def makecontext (f : Proc) | |
p f | |
s_ptr = @stack.to_unsafe | |
s_start = s_ptr + (STACK_SIZE - 32) | |
s_start_ptr = s_start.as(UInt64*) | |
s_start_ptr.value = ->start(Void*, Void*).pointer.as(UInt64*).value | |
# # ===== UNNCOMMENT FOR DEBUGGING ===== | |
# p_ptr = ->start(Void*, Void*).pointer.as(UInt64*).address | |
# p_ptr = Pointer(Void).new(p_ptr) | |
# proc = Proc(Void*, Void*, Void).new(p_ptr, Pointer(Void).null) | |
# proc.call(f.pointer, f.closure_data) | |
# p @stack | |
end | |
end | |
fun start(p_ptr : Void*, p_closure : Void*) | |
fn = Proc(Void).new(p_ptr, p_closure) | |
fn.call | |
end | |
class Scheduler | |
@fibers : Array(FiberPoc) | |
@current = 0 | |
@running = 0 | |
def initialize | |
@fibers = Array(FiberPoc).new | |
(0..1).each do |i| | |
@fibers << FiberPoc.new | |
end | |
end | |
def spawn (fiber_no : UInt64, f : Proc) | |
@running += 1 | |
@fibers[fiber_no].makecontext(f) | |
end | |
def yield_control | |
new_fiber : UInt64 | |
old_fiber : UInt64 | |
if @running == 2 | |
if @current = 0 | |
#switch(0, 1) | |
p "old: 0, new: 1" | |
else | |
p "new: 0, old: 1" | |
end | |
end | |
end | |
def ret | |
@running -= 1 | |
if @running == 0 | |
p "Done. Exiting." | |
Process.exit(0) | |
end | |
self.yield_control | |
end | |
end | |
fun switch(old : Void*, new : Void*) | |
p "Switching" | |
end | |
class Executor | |
@scheduler = Scheduler.new | |
def main | |
hello1 = ->{ | |
(0..10).each do |i| | |
puts("Fiber1: {}", i) | |
@scheduler.yield_control | |
end | |
@scheduler.ret | |
} | |
hello2 = ->{ | |
(0..10).each do |i| | |
puts("Fiber2: {}", i) | |
@scheduler.yield_control | |
end | |
@scheduler.ret | |
} | |
@scheduler.spawn(0, hello1) | |
@scheduler.spawn(0, hello2) | |
end | |
end | |
executor = Executor.new | |
executor.main() |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
STACK_SIZE = 1024 * 1024 * 8 | |
# give the OS some time to kill the process if it's bad | |
SLEEP_INTERVAL = 0.5 | |
# Defined here as well just for easier editing. The two things that are interesting to play with is the `swapcontext` | |
# and the `makecontext` functions | |
class Scheduler | |
@[NoInline] | |
@[Naked] | |
def self.swapcontext(current : Pointer(Pointer(Void)), to : Pointer(Void)) : Nil | |
{% if flag?(:win32) %} | |
asm(" | |
pushq %rcx | |
pushq %gs: 0x10 | |
pushq %gs:0x08 | |
pushq %rdi | |
pushq %rbx | |
pushq %rbp | |
pushq %rsi | |
pushq %r12 | |
pushq %r13 | |
pushq %r14 | |
pushq %r15 | |
movq %rsp, ($0) | |
movq ($1), %rsp | |
popq %r15 | |
popq %r14 | |
popq %r13 | |
popq %r12 | |
popq %rsi | |
popq %rbp | |
popq %rbx | |
popq %rdi | |
popq %gs:0x08 | |
popq %gs:010 | |
popq %rcx | |
" | |
: | |
: "r"(current), "r"(to) | |
: | |
: "volatile", "alignstack" | |
) | |
{% else %} | |
asm(" | |
pushq %rdi | |
pushq %rbx | |
pushq %rbp | |
pushq %r12 | |
pushq %r13 | |
pushq %r14 | |
pushq %r15 | |
leaq -0x08(%rsp), %rsp # testing 16 byte align, just remove, remember to change @rsp = (s_start - 7*8) | |
movq %rsp, ($0) | |
movq $1, %rsp | |
leaq 0x08(%rsp), %rsp # testing 16 byte align, just remove, remember to change @rsp = (s_start - 7*8) | |
popq %r15 | |
popq %r14 | |
popq %r13 | |
popq %r12 | |
popq %rbp | |
popq %rbx | |
popq %rdi | |
" | |
:: "r"(current), "r"(to)) | |
{% end %} | |
end | |
end | |
class FiberPoc | |
{% if flag?(:win32) %} | |
def makecontext(proc : Proc) | |
@f = proc | |
s_ptr = @stack.to_unsafe | |
s_start = s_ptr + (STACK_SIZE - 32) # note that this is different from the original crystal impl (uses offset of 16 IRC) | |
puts s_start.address | |
# Our entry | |
# 8 registers + 2 qwords for NT_TIB + 1 parameter | |
@rsp = (s_start - 11*8).as(Void*) | |
s_start_ptr = s_start.as(UInt64*) | |
s_start_ptr.value = ->(f : FiberPoc) { f.run }.pointer.address | |
# https://en.wikipedia.org/wiki/Win32_Thread_Information_Block | |
# stack end - "stack limit" - low address | |
stack_limit = (s_start - 24).as(UInt64*) | |
stack_limit.value = s_ptr.address | |
# stack top - "stack base" - high address | |
stack_base = (s_start - 16).as(UInt64*) | |
stack_base.value = (s_ptr + STACK_SIZE).address | |
# First parameter | |
first_param = (s_start - 8).as(UInt64*) | |
first_param.value = self.as(Void*).address | |
end | |
{% else %} | |
def makecontext(proc : Proc) | |
@f = proc | |
s_ptr = @stack.to_unsafe | |
s_start = s_ptr + (STACK_SIZE - 32) # note that this is different from the original crystal impl (uses offset of 16 IRC) | |
# Our entry | |
# we pop 7 registers before we return | |
@rsp = (s_start - 8*8).as(Void*) | |
s_start_ptr = s_start.as(UInt64*) | |
s_start_ptr.value = ->(f : FiberPoc) { f.run }.pointer.address | |
# First parameter | |
first_param = (s_start - 8).as(UInt64*) | |
first_param.value = self.as(Void*).address | |
end | |
{% end %} | |
property stack : Array(UInt8) | |
property rsp : Void* = Pointer(Void).null | |
property f = Proc(Void).new { } | |
def initialize | |
@stack = Array.new(STACK_SIZE, 0_u8) # OK, as long as we don't push/pop -> reallocate | |
end | |
def run | |
@f.call | |
end | |
end | |
class Scheduler | |
@fibers = Array(FiberPoc).new(3) | |
@current = 0 | |
@running = 1 | |
@finished : Int32 = 0 # just a hack since we only have two executing fibers, this is the one that finished first | |
def initialize | |
@fibers = Array(FiberPoc).new | |
(0..2).each do |i| | |
@fibers << FiberPoc.new | |
end | |
# base thread | |
@fibers[0].makecontext(->{}) | |
end | |
def spawn(fiber_no : UInt64, f : Proc) | |
puts "Queuing fiber #{fiber_no}" | |
@running += 1 | |
@fibers[fiber_no].makecontext(f) | |
end | |
def run | |
self.yield_control | |
end | |
def yield_control | |
# if we only have two threads, one is finished so we only swap if it just finished to continoue on the last | |
if @running == 2 | |
if @current == @finished | |
next_ctx = @finished == 2 ? 1 : 2 | |
current = @fibers[next_ctx] | |
@current = next_ctx | |
Scheduler.swapcontext(pointerof(current.@rsp), current.@rsp) | |
end | |
elsif @running == 3 | |
if @current == 0 | |
current = @fibers[0] | |
# p "old: 0, new: 1, from: #{@fibers[0].rsp}, to: #{@fibers[1].rsp}" | |
@current = 1 | |
Scheduler.swapcontext(pointerof(current.@rsp), @fibers[1].@rsp) | |
elsif @current == 1 | |
current = @fibers[1] | |
# p "old: 1, new: 2, from: #{@fibers[1].rsp}, to: #{@fibers[2].rsp}" | |
@current = 2 | |
Scheduler.swapcontext(pointerof(current.@rsp), @fibers[2].@rsp) | |
elsif @current == 2 | |
# p "old: 2, new: 1, from: #{@fibers[2].rsp}, to: #{@fibers[1].rsp}" | |
@current = 1 | |
current = @fibers[2] | |
Scheduler.swapcontext(pointerof(current.@rsp), @fibers[1].@rsp) | |
end | |
end | |
end | |
def ret | |
puts "ret" | |
@finished = @current | |
@running -= 1 | |
if @running == 1 | |
puts "Done. Exiting." | |
Process.exit(0) | |
end | |
self.yield_control | |
end | |
end | |
class Executor | |
@scheduler = Scheduler.new | |
def main | |
hello1 = ->{ | |
(0..5).each do |i| | |
puts("Fiber1: #{i}") | |
sleep SLEEP_INTERVAL | |
@scheduler.yield_control | |
end | |
@scheduler.ret | |
} | |
hello2 = ->{ | |
(0..10).each do |i| | |
puts("Fiber2: #{i}") | |
sleep SLEEP_INTERVAL | |
@scheduler.yield_control | |
end | |
@scheduler.ret | |
} | |
@scheduler.spawn(1, hello1) | |
@scheduler.spawn(2, hello2) | |
@scheduler.run | |
end | |
end | |
executor = Executor.new | |
executor.main |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
STACK_SIZE = 1024 * 1024 * 8 | |
class FiberPoc | |
@stack : Array(UInt8) | |
property rsp : UInt64 = 0 | |
def initialize | |
@stack = Array.new(STACK_SIZE, 0_u8) | |
end | |
def stack | |
@stack | |
end | |
def makecontext(f : Proc) | |
p f | |
s_ptr = @stack.to_unsafe | |
s_start = s_ptr + (STACK_SIZE - 32) | |
p s_start | |
@rsp = (s_start - 8*8).address # we pop 8 registers before we return | |
# Our entry | |
s_start_ptr = s_start.as(UInt64*) | |
s_start_ptr.value = ->start(Void*, Void*).pointer.as(UInt64*).address | |
# First parameter | |
first_param = (s_start - 8).as(UInt64*) | |
first_param.value = f.pointer.address | |
# Second parameter | |
second_param = (s_start - 16).as(UInt64*) | |
second_param.value = f.closure_data.address | |
p f.pointer | |
p f.closure_data | |
# # ===== UNNCOMMENT FOR DEBUGGING ===== | |
# p_ptr = ->start(Void*, Void*).pointer.as(UInt64*).address | |
# p_ptr = Pointer(Void).new(p_ptr) | |
# proc = Proc(Void*, Void*, Void).new(p_ptr, Pointer(Void).null) | |
# proc.call(f.pointer, f.closure_data) | |
# p @stack | |
end | |
end | |
fun start(p_ptr : Void*, p_closure : Void*) | |
p p_ptr | |
p p_closure | |
fn = Proc(Void).new(p_ptr, p_closure) | |
fn.call | |
puts "hello from start" | |
end | |
class Scheduler | |
@fibers = Array(FiberPoc).new(3) | |
@current = 0 | |
@running = 1 | |
def initialize | |
@fibers = Array(FiberPoc).new | |
(0..2).each do |i| | |
@fibers << FiberPoc.new | |
end | |
@fibers[0].makecontext(->{}); | |
end | |
def spawn(fiber_no : UInt64, f : Proc) | |
p "spawn" | |
@running += 1 | |
p @running | |
@fibers[fiber_no].makecontext(f) | |
end | |
def run | |
self.yield_control | |
end | |
def yield_control | |
p "yield running: #{@running}, current: #{@current}" | |
if @running == 2 | |
# do nothing | |
elsif @running == 3 | |
if @current == 0 | |
p "old: 0, new: 1, from: #{@fibers[0].rsp}, to: #{@fibers[1].rsp}" | |
@current = 1 | |
Scheduler.swapcontext(@fibers[0].rsp, @fibers[1].rsp) | |
elsif @current == 1 | |
p "old: 1, new: 2, from: #{@fibers[1].rsp}, to: #{@fibers[2].rsp}" | |
@current = 2 | |
Scheduler.swapcontext(@fibers[1].rsp, @fibers[2].rsp) | |
elsif @current == 2 | |
p "old: 2, new: 1, from: #{@fibers[2].rsp}, to: #{@fibers[1].rsp}" | |
@current = 1 | |
Scheduler.swapcontext(@fibers[2].rsp, @fibers[1].rsp) | |
end | |
end | |
end | |
def ret | |
p "ret" | |
@running -= 1 | |
if @running == 1 | |
p "Done. Exiting." | |
Process.exit(0) | |
end | |
self.yield_control | |
end | |
@[NoInline] | |
@[Naked] | |
def self.swapcontext(current : UInt64, to : UInt64) : Nil | |
# # ===== UNNCOMMENT FOR DEBUGGING ===== | |
# p_ptr = Pointer(Void*).new(to) | |
# p p_ptr | |
# p_ptr = p_ptr.value | |
# proc = Proc(Void*, Void*, Void).new(p_ptr, Pointer(Void).null) | |
# proc.call(Pointer(Void).null, Pointer(Void).null) | |
# asm(" | |
# movq $0, %rsp | |
# " | |
# :: "r"(to)) | |
asm(" | |
# Needed to run the closure | |
pushq %rdi | |
pushq %rsi | |
# Registers | |
pushq %rbx | |
pushq %rbp | |
pushq %r12 | |
pushq %r13 | |
pushq %r14 | |
pushq %r15 | |
movq %rsp, $0 | |
movq $1, %rsp | |
# Registers | |
popq %r15 | |
popq %r14 | |
popq %r13 | |
popq %r12 | |
popq %rbp | |
popq %rbx | |
# Needed to run the closure | |
popq %rsi | |
popq %rdi | |
" | |
: | |
: "r"(current), "r"(to) | |
: | |
: "volatile" | |
) | |
end | |
end | |
class Executor | |
@scheduler = Scheduler.new | |
def main | |
hello1 = ->{ | |
(0..10).each do |i| | |
puts("Fiber1: #{i}") | |
@scheduler.yield_control | |
end | |
@scheduler.ret | |
} | |
hello2 = ->{ | |
(0..10).each do |i| | |
puts("Fiber2: #{i}") | |
@scheduler.yield_control | |
end | |
@scheduler.ret | |
} | |
@scheduler.spawn(1, hello1) | |
@scheduler.spawn(2, hello2) | |
@scheduler.run | |
end | |
end | |
executor = Executor.new | |
executor.main |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment