Improve waterbox "lazystates"

Cores that used the .invisible section to store data were saving it; this was a regression from before, so PCFX states should be back down to the previous release size, or perhaps a bit smaller.

Add the ability to dirty track libco cothreads, as used in the bsnes core.  This saves a lot of space in those states and they're now quite competitive in size.
This commit is contained in:
nattthebear 2020-06-04 12:13:06 -04:00
parent c4586545fa
commit 7792eb2e80
10 changed files with 231 additions and 138 deletions

Binary file not shown.

View File

@ -21,7 +21,7 @@ namespace BizHawk.BizInvoke
void Deactivate();
/// <summary>
/// Change protection on [start, start + size), guaranteed to be page aligned and in the committed area.
/// Will only be called when active.
/// Will only be called when active. Will not be called with RW_Invisible, which is a front end artifact.
/// </summary>
void Protect(ulong start, ulong size, MemoryBlock.Protection prot);
/// <summary>
@ -38,7 +38,11 @@ namespace BizHawk.BizInvoke
/// automatically changing to RW and setting DidChange
/// </summary>
/// <param name="dest">Caller-owned array that the PAL will overwrite with page data</param>
void GetWriteStatus(WriteDetectionStatus[] dest);
/// <param name="pagedata">
/// Caller-owned array that should indicate which areas were set to RW_Stack.
/// Will not be modified by callee. Some implementations need this to get all of the correct information in dest.
/// </param>
void GetWriteStatus(WriteDetectionStatus[] dest, MemoryBlock.Protection[] pagedata);
/// <summary>
/// Sets the current write detection status on each page in the block. Pages marked with CanChange
/// that are also committed and set to R, will not trigger a segmentation violation on write; instead

View File

@ -121,7 +121,7 @@ namespace BizHawk.BizInvoke
{
EnsureActive();
if (_sealed)
_pal.GetWriteStatus(_dirtydata);
_pal.GetWriteStatus(_dirtydata, _pageData);
_pal.Deactivate();
Active = false;
}
@ -144,7 +144,7 @@ namespace BizHawk.BizInvoke
if (length == 0)
return;
if (_sealed)
_pal.GetWriteStatus(_dirtydata);
_pal.GetWriteStatus(_dirtydata, _pageData);
// Note: asking for prot.none on memory that was not previously committed, commits it
@ -166,7 +166,8 @@ namespace BizHawk.BizInvoke
for (int i = pstart; i <= pend; i++)
{
_pageData[i] = prot;
if (prot == Protection.RW)
// inform the low level code what addresses might fault on it
if (prot == Protection.RW || prot == Protection.RW_Stack)
_dirtydata[i] |= WriteDetectionStatus.CanChange;
else
_dirtydata[i] &= ~WriteDetectionStatus.CanChange;
@ -192,12 +193,35 @@ namespace BizHawk.BizInvoke
ulong zstart = GetStartAddr(ps);
ulong zend = GetStartAddr(i + 1);
var prot = _pageData[i];
if (_sealed && prot == Protection.RW)
// adjust frontend notion of prot to the PAL layer's expectation
if (prot == Protection.RW_Stack)
{
if (!_sealed)
{
// don't activate this protection yet
prot = Protection.RW;
}
else
{
var didChange = (_dirtydata[i] & WriteDetectionStatus.DidChange) != 0;
if (didChange)
// don't needlessly retrigger
prot = Protection.RW;
}
}
else if (prot == Protection.RW_Invisible)
{
// this never matters to the backend
prot = Protection.RW;
}
else if (_sealed && prot == Protection.RW)
{
var didChange = (_dirtydata[i] & WriteDetectionStatus.DidChange) != 0;
if (!didChange)
// set to trigger if we have not before
prot = Protection.R;
}
_pal.Protect(zstart, zend - zstart, prot);
ps = i + 1;
}
@ -228,7 +252,7 @@ namespace BizHawk.BizInvoke
{
EnsureActive();
EnsureSealed();
_pal.GetWriteStatus(_dirtydata);
_pal.GetWriteStatus(_dirtydata, _pageData);
w.Write(MAGIC);
w.Write(Start);
@ -244,7 +268,7 @@ namespace BizHawk.BizInvoke
ulong endAddr = Start + CommittedSize;
for (p = 0, addr = Start; addr < endAddr; p++, addr += 4096)
{
if ((_dirtydata[p] & WriteDetectionStatus.DidChange) != 0)
if (_pageData[p] != Protection.RW_Invisible && (_dirtydata[p] & WriteDetectionStatus.DidChange) != 0)
{
// TODO: It's slow to toggle individual pages like this.
// Maybe that's OK because None is not used much?
@ -256,13 +280,43 @@ namespace BizHawk.BizInvoke
_pal.Protect(addr, 4096, Protection.None);
}
}
// Console.WriteLine($"{Start:x16}");
// var voom = _pageData.Take((int)(CommittedSize >> 12)).Select(p =>
// {
// switch (p)
// {
// case Protection.None: return ' ';
// case Protection.R: return 'R';
// case Protection.RW: return 'W';
// case Protection.RX: return 'X';
// case Protection.RW_Invisible: return '!';
// case Protection.RW_Stack: return '+';
// default: return '?';
// }
// }).Select((c, i) => new { c, i }).GroupBy(a => a.i / 60);
// var zoom = _dirtydata.Take((int)(CommittedSize >> 12)).Select(p =>
// {
// switch (p)
// {
// case WriteDetectionStatus.CanChange: return '.';
// case WriteDetectionStatus.CanChange | WriteDetectionStatus.DidChange: return '*';
// case 0: return ' ';
// case WriteDetectionStatus.DidChange: return '!';
// default: return '?';
// }
// }).Select((c, i) => new { c, i }).GroupBy(a => a.i / 60);
// foreach (var l in voom.Zip(zoom, (a, b) => new { a, b }))
// {
// Console.WriteLine("____" + new string(l.a.Select(a => a.c).ToArray()));
// Console.WriteLine("____" + new string(l.b.Select(a => a.c).ToArray()));
// }
}
public void LoadState(BinaryReader r)
{
EnsureActive();
EnsureSealed();
_pal.GetWriteStatus(_dirtydata);
_pal.GetWriteStatus(_dirtydata, _pageData);
if (r.ReadUInt64() != MAGIC || r.ReadUInt64() != Start || r.ReadUInt64() != Size)
throw new InvalidOperationException("Savestate internal mismatch");
@ -297,8 +351,9 @@ namespace BizHawk.BizInvoke
{
var dirty = (_dirtydata[p] & WriteDetectionStatus.DidChange) != 0;
var newDirty = (newDirtyData[p] & WriteDetectionStatus.DidChange) != 0;
var inState = newPageData[p] != Protection.RW_Invisible && newDirty;
if (dirty || newDirty)
if (dirty || inState)
{
// must write out changed data
@ -306,7 +361,10 @@ namespace BizHawk.BizInvoke
if (_pageData[p] != Protection.RW)
_pal.Protect(addr, 4096, Protection.RW);
if (newDirty)
// NB: There are some weird behaviors possible if a block transitions to or from RW_Invisible,
// but nothing really "broken" (as far as I know); just reflecting the fact that you cannot track it.
if (inState)
{
// changed data comes from the savestate
r.Read(buff, 0, 4096);
@ -350,7 +408,21 @@ namespace BizHawk.BizInvoke
public static MemoryBlock Create(ulong size) => Create(0, size);
/// <summary>Memory protection constant</summary>
public enum Protection : byte { None, R, RW, RX }
public enum Protection : byte
{
None,
R,
RW,
RX,
/// <summary>
/// This area should not be tracked for changes, and should not be saved.
/// </summary>
RW_Invisible,
/// <summary>
/// This area may be used as a stack and should use a change detection that will work on stacks.
/// (In windows, this is an inferior detection that triggers on reads. In Linux, this flag has no effect.)
/// </summary>
RW_Stack,
}
}
}

View File

@ -87,7 +87,7 @@ namespace BizHawk.BizInvoke
throw new InvalidOperationException($"{nameof(mprotect)}() returned {errorCode}!");
}
public void GetWriteStatus(WriteDetectionStatus[] dest)
public void GetWriteStatus(WriteDetectionStatus[] dest, Protection[] pagedata)
{
throw new NotImplementedException();
}

View File

@ -87,6 +87,11 @@ namespace BizHawk.BizInvoke
case Protection.R: p = Kernel32.MemoryProtection.READONLY; break;
case Protection.RW: p = Kernel32.MemoryProtection.READWRITE; break;
case Protection.RX: p = Kernel32.MemoryProtection.EXECUTE_READ; break;
// VEH can't work when the stack is not writable, because VEH is delivered on that selfsame stack. The kernel
// simply declines to return to user mode on a first chance exception if the stack is not writable.
// So we use guard pages instead, which are much worse because reads set them off as well, but it doesn't matter
// in this case.
case Protection.RW_Stack: p = Kernel32.MemoryProtection.READWRITE | Kernel32.MemoryProtection.GUARD_Modifierflag; break;
default: throw new ArgumentOutOfRangeException(nameof(prot));
}
return p;
@ -107,12 +112,41 @@ namespace BizHawk.BizInvoke
}
}
public void GetWriteStatus(WriteDetectionStatus[] dest)
public void GetWriteStatus(WriteDetectionStatus[] dest, Protection[] pagedata)
{
var p = (IntPtr)WinGuard.ExamineTripGuard(Z.UU(_start), Z.UU(_size));
if (p == IntPtr.Zero)
throw new InvalidOperationException($"{nameof(WinGuard.ExamineTripGuard)}() returned NULL!");
Marshal.Copy(p, (byte[])(object)dest, 0, dest.Length);
// guard pages do not trigger VEH when they are actually being used as a stack and there are other
// free pages below them, so virtualquery to get that information out
Kernel32.MEMORY_BASIC_INFORMATION mbi;
for (int i = 0; i < pagedata.Length;)
{
if (pagedata[i] == Protection.RW_Stack)
{
var q = ((ulong)i << WaterboxUtils.PageShift) + _start;
Kernel32.VirtualQuery(Z.UU(q), &mbi, Z.SU(sizeof(Kernel32.MEMORY_BASIC_INFORMATION)));
var pstart = (int)(((ulong)mbi.BaseAddress - _start) >> WaterboxUtils.PageShift);
var pend = pstart + (int)((ulong)mbi.RegionSize >> WaterboxUtils.PageShift);
if (pstart != i)
throw new Exception("Huh?");
if ((mbi.Protect & Kernel32.MemoryProtection.GUARD_Modifierflag) == 0)
{
// tripped!
for (int j = pstart; j < pend; j++)
dest[j] |= WriteDetectionStatus.DidChange;
}
i = pend;
}
else
{
i++;
}
}
}
public void SetWriteStatus(WriteDetectionStatus[] src)
@ -212,6 +246,34 @@ namespace BizHawk.BizInvoke
}
public static readonly IntPtr INVALID_HANDLE_VALUE = Z.US(0xffffffffffffffff);
[StructLayout(LayoutKind.Sequential)]
public struct MEMORY_BASIC_INFORMATION
{
public IntPtr BaseAddress;
public IntPtr AllocationBase;
public MemoryProtection AllocationProtect;
public UIntPtr RegionSize;
public StateEnum State;
public MemoryProtection Protect;
public TypeEnum Type;
}
public enum StateEnum : uint
{
MEM_COMMIT = 0x1000,
MEM_FREE = 0x10000,
MEM_RESERVE = 0x2000
}
public enum TypeEnum : uint
{
MEM_IMAGE = 0x1000000,
MEM_MAPPED = 0x40000,
MEM_PRIVATE = 0x20000
}
[DllImport("kernel32.dll")]
public static extern UIntPtr VirtualQuery(UIntPtr lpAddress, MEMORY_BASIC_INFORMATION* lpBuffer, UIntPtr dwLength);
}
private static unsafe class WinGuard

View File

@ -44,9 +44,8 @@ namespace BizHawk.Emulation.Cores.Waterbox
/// </summary>
private ulong _postSealWriteStart;
/// <summary>
/// Where the saveable program data begins
/// Where executable data ends
/// </summary>
private ulong _saveStart;
private ulong _execEnd;
public ElfLoader(string moduleName, byte[] fileData, ulong assumedStart, bool skipCoreConsistencyCheck, bool skipMemoryConsistencyCheck)
@ -125,7 +124,6 @@ namespace BizHawk.Emulation.Cores.Waterbox
_writeStart = WaterboxUtils.AlignDown(writable.Min(s => s.LoadAddress));
_postSealWriteStart = WaterboxUtils.AlignDown(postSealWritable.Min(s => s.LoadAddress));
_saveStart = WaterboxUtils.AlignDown(saveable.Min(s => s.LoadAddress));
_execEnd = WaterboxUtils.AlignUp(executable.Max(s => s.LoadAddress + s.Size));
// validate; this may require linkscript cooperation
@ -133,13 +131,6 @@ namespace BizHawk.Emulation.Cores.Waterbox
// in the wrong place (because the linkscript doesn't know "eventually readonly")
if (_execEnd > _writeStart)
throw new InvalidOperationException($"ElfLoader: Executable data to {_execEnd:X16} overlaps writable data from {_writeStart}");
var actuallySaved = allocated.Where(a => a.LoadAddress + a.Size > _saveStart);
var oopsSaved = actuallySaved.Except(saveable);
foreach (var s in oopsSaved)
{
Console.WriteLine($"ElfLoader: Section {s.Name} will be saved, but that was not expected");
}
}
PrintSections();
@ -159,12 +150,11 @@ namespace BizHawk.Emulation.Cores.Waterbox
Console.WriteLine($"Mounted `{ModuleName}` @{Memory.Start:x16}");
foreach (var s in _elf.Sections.OrderBy(s => s.LoadAddress))
{
Console.WriteLine(" @{0:x16} {1}{2}{3}{4} `{5}` {6} bytes",
Console.WriteLine(" @{0:x16} {1}{2}{3} `{4}` {5} bytes",
s.LoadAddress,
(s.Flags & SectionFlags.Allocatable) != 0 ? "R" : " ",
(s.Flags & SectionFlags.Writable) != 0 ? "W" : " ",
(s.Flags & SectionFlags.Executable) != 0 ? "X" : " ",
s.LoadAddress + s.Size > _saveStart ? "V" : " ",
s.Name,
s.Size);
}
@ -172,8 +162,9 @@ namespace BizHawk.Emulation.Cores.Waterbox
private void PrintTopSavableSymbols()
{
// TODO: With change detection, this isn't so great anymore.
// It could look at actual intersections with what we saved, but that would mean going into MemoryBlock
var tops = _allSymbols
.Where(s => s.Value + s.Size > _saveStart)
.OrderByDescending(s => s.Size)
.Where(s => s.Size >= 20 * 1024)
.Take(30)
@ -182,7 +173,7 @@ namespace BizHawk.Emulation.Cores.Waterbox
if (tops.Count > 0)
{
Console.WriteLine("Top savestate symbols:");
Console.WriteLine("Top potential savestate symbols:");
foreach (var text in tops)
{
Console.WriteLine(text);
@ -206,7 +197,7 @@ namespace BizHawk.Emulation.Cores.Waterbox
}
/// <summary>
/// Set normal (post-seal) memory protections
/// Set memory protections, pre or post seal
/// </summary>
private void Protect()
{
@ -215,6 +206,20 @@ namespace BizHawk.Emulation.Cores.Waterbox
Memory.Protect(Memory.Start, _execEnd - Memory.Start, MemoryBlock.Protection.RX);
Memory.Protect(_execEnd, writeStart - _execEnd, MemoryBlock.Protection.R);
Memory.Protect(writeStart, Memory.EndExclusive - writeStart, MemoryBlock.Protection.RW);
if (_invisible != null)
{
if (!WaterboxUtils.Aligned(_invisible.LoadAddress))
throw new InvalidOperationException("Invisible section start not aligned! Check linker script");
var end = WaterboxUtils.AlignUp(_invisible.LoadAddress + _invisible.Size);
if (_sectionsByName.Values.Any(s => s != _invisible && s.LoadAddress < end && s.LoadAddress >= _invisible.LoadAddress))
{
throw new InvalidOperationException("Invisible section end not aligned, or not padded! Check linker script");
}
Memory.Protect(
_invisible.LoadAddress,
_invisible.Size,
MemoryBlock.Protection.RW_Invisible);
}
}
// connect all of the .wbxsyscall stuff

View File

@ -170,8 +170,19 @@ namespace BizHawk.Emulation.Cores.Waterbox
private bool EnsureMapped(int startPage, int pageCount)
{
for (int i = startPage; i < startPage + pageCount; i++)
{
if (_pages[i] == FREE)
return false;
}
return true;
}
private bool EnsureMappedNonStack(int startPage, int pageCount)
{
for (int i = startPage; i < startPage + pageCount; i++)
{
if (_pages[i] == FREE || _pages[i] == MemoryBlock.Protection.RW_Stack)
return false;
}
return true;
}
@ -197,7 +208,7 @@ namespace BizHawk.Emulation.Cores.Waterbox
var oldStartPage = GetPage(start);
var oldNumPages = WaterboxUtils.PagesNeeded(oldSize);
if (!EnsureMapped(oldStartPage, oldNumPages))
if (!EnsureMappedNonStack(oldStartPage, oldNumPages))
return 0;
var oldProt = _pages[oldStartPage];
@ -252,11 +263,7 @@ namespace BizHawk.Emulation.Cores.Waterbox
public bool Unmap(ulong start, ulong size)
{
return Protect(start, size, FREE);
}
public bool Protect(ulong start, ulong size, MemoryBlock.Protection prot)
{
// TODO: eliminate copy+pasta between unmap and protect
if (start < Memory.Start || start + size > Memory.EndExclusive || size == 0)
return false;
@ -265,6 +272,21 @@ namespace BizHawk.Emulation.Cores.Waterbox
if (!EnsureMapped(startPage, numPages))
return false;
ProtectInternal(startPage, numPages, FREE, true);
return true;
}
public bool Protect(ulong start, ulong size, MemoryBlock.Protection prot)
{
// TODO: eliminate copy+pasta between unmap and protect
if (start < Memory.Start || start + size > Memory.EndExclusive || size == 0)
return false;
var startPage = GetPage(start);
var numPages = WaterboxUtils.PagesNeeded(size);
if (!EnsureMappedNonStack(startPage, numPages))
return false;
ProtectInternal(startPage, numPages, prot, true);
return true;
}

View File

@ -117,7 +117,7 @@ namespace BizHawk.Emulation.Cores.Waterbox
case PROT_WRITE:
return Z.SS(MAP_FAILED); // write only????
case PROT_READ | PROT_WRITE:
mprot = MemoryBlock.Protection.RW;
mprot = (flags & MAP_STACK) != 0 ? MemoryBlock.Protection.RW_Stack : MemoryBlock.Protection.RW;
break;
case PROT_READ:
mprot = MemoryBlock.Protection.R;

View File

@ -305,17 +305,6 @@ namespace BizHawk.Emulation.Cores.Waterbox
}
_module.SealImportsAndTakeXorSnapshot();
_mmapheap?.Memory.Seal();
// MemoryBlock's lazy write detection can't work on the current stack, because the VEH handler runs in the stack
// so it has to already be writable. This isn't a problem for normal stacks, which are outside the cycle, but
// co stacks have to be fully probed before they're used as stacks.
IntPtr co_probe;
if ((co_probe = _module.GetProcAddrOrZero("co_probe")) != IntPtr.Zero)
{
Console.WriteLine("Calling co_probe().");
CallingConventionAdapters.Waterbox.GetDelegateForFunctionPointer<Action>(co_probe)();
}
}
}
@ -362,7 +351,7 @@ namespace BizHawk.Emulation.Cores.Waterbox
}
private const ulong MAGIC = 0x736b776162727477;
private const ulong WATERBOXSTATEVERSION = 1;
private const ulong WATERBOXSTATEVERSION = 2;
public void SaveStateBinary(BinaryWriter bw)
{

View File

@ -17,18 +17,12 @@
// and include guard space between the stack and the storage
typedef struct {
// used by coswap.s
// used by coswap.s, has to be at the beginning of the struct
uint64_t jmp_buf[32];
// lowest pointer in the stack; when we go below here, we'll hit the fake guard pages and overflow
uint64_t* stack_bottom;
// pointer just off the top (starting) end of the stack
uint64_t* stack_top;
// total size that went to mmap
uint64_t mmap_size;
uint64_t padding[477];
// will be protected to unreadable, unwritable
uint64_t guard[0x3000 / sizeof(uint64_t)];
uint64_t stack[0];
// points to the lowest address in the stack
void* stack;
// length of the stack that we allocated in bytes
uint64_t stack_size;
} cothread_impl;
// the cothread that represents the real host thread we started from
@ -36,78 +30,39 @@ static cothread_impl co_host_buffer;
// what cothread are we in right now
static cothread_impl* co_active_handle;
// a list of all real cothreads (does not include co_host_buffer)
#define MAX_THREADS 64
static cothread_impl* allthreads[MAX_THREADS];
static cothread_impl* alloc_thread(uint64_t size)
{
cothread_impl** dest = NULL;
for (int i = 0; i < MAX_THREADS; i++)
{
if (!allthreads[i])
{
dest = &allthreads[i];
break;
}
}
if (!dest)
cothread_impl* co = calloc(1, sizeof(*co));
if (!co)
return NULL;
// align up to 4k
size = (size + 4095) & ~4095;
uint64_t alloc_size = sizeof(cothread_impl) + size;
cothread_impl* co = mmap(NULL, alloc_size,
PROT_READ | PROT_WRITE, MAP_ANONYMOUS, -1, 0);
if (co == (cothread_impl*)(-1))
co->stack = mmap(NULL, size,
PROT_READ | PROT_WRITE, MAP_ANONYMOUS | MAP_STACK, -1, 0);
if (co->stack == (void*)(-1))
{
free(co);
return NULL;
co->stack_bottom = &co->stack[0];
co->stack_top = &co->stack[size / sizeof(uint64_t)];
co->mmap_size = alloc_size;
for (int i = 0; i < 0x3000 / sizeof(uint64_t); i++)
co->guard[i] = 0xdeadbeeffeedface;
if (mprotect(&co->guard[0], sizeof(co->guard), PROT_NONE) != 0)
abort();
*dest = co;
}
co->stack_size = size;
return co;
}
static void free_thread(cothread_impl* co)
{
cothread_impl** src = NULL;
for (int i = 0; i < MAX_THREADS; i++)
{
if (allthreads[i] == co)
{
src = &allthreads[i];
break;
}
}
if (!src)
if (munmap(co->stack, co->stack_size) != 0)
abort();
if (mprotect(&co->guard[0], sizeof(co->guard), PROT_READ | PROT_WRITE) != 0)
abort();
uint64_t alloc_size = co->mmap_size;
memset(co, 0, alloc_size);
if (munmap(co, alloc_size) != 0)
abort();
*src = NULL;
free(co);
}
extern void co_swap(cothread_t, cothread_t);
extern void co_swap(cothread_impl*, cothread_impl*);
static void crash(void)
{
assert(0); /* called only if cothread_t entrypoint returns */
__asm__("int3"); // called only if cothread_t entrypoint returns
}
ECL_EXPORT void co_clean(void)
@ -115,23 +70,6 @@ ECL_EXPORT void co_clean(void)
memset(&co_host_buffer, 0, sizeof(co_host_buffer));
}
ECL_EXPORT void co_probe(void)
{
// VEH is delivered on the same thread that experienced the exception.
// That means the stack has to be writable already, so the waterbox host's fault detector won't work
// when we're on a readonly cothread stack. So we conservatively probe this entire stack before switching to it.
for (int i = 0; i < MAX_THREADS; i++)
{
if (allthreads[i])
{
uint64_t volatile* p = (uint64_t volatile*)allthreads[i]->stack_bottom;
uint64_t* pend = allthreads[i]->stack_top;
for (; p < pend; p++)
*p = *p;
}
}
}
cothread_t co_active(void)
{
if (!co_active_handle)
@ -147,10 +85,10 @@ cothread_t co_create(unsigned int sz, void (*entrypoint)(void))
if ((co = alloc_thread(sz)))
{
uint64_t* p = co->stack_top; // seek to top of stack
uint64_t* p = (uint64_t*)((char*)co->stack + co->stack_size); // seek to top of stack
*--p = (uint64_t)crash; // crash if entrypoint returns
*--p = (uint64_t)entrypoint; // start of function */
*(uint64_t*)co = (uint64_t)p; // stack pointer
*--p = (uint64_t)entrypoint; // start of function
co->jmp_buf[0] = (uint64_t)p; // stack pointer
}
return co;
@ -166,6 +104,8 @@ static uint64_t hostend;
void co_switch(cothread_t handle)
{
cothread_impl* co = handle;
uint64_t start;
uint64_t end;
if (co_active_handle == &co_host_buffer)
@ -187,13 +127,12 @@ void co_switch(cothread_t handle)
else
{
// migrating onto cothread; compute its extents we allocated them
cothread_impl* co = handle;
start = (uintptr_t)co->stack_bottom;
end = (uintptr_t)co->stack_top;
start = (uintptr_t)co->stack;
end = start + co->stack_size;
}
__asm__("movq %0, %%gs:0x08":: "r"(end));
__asm__("movq %0, %%gs:0x10":: "r"(start));
register cothread_t co_previous_handle = co_active_handle;
co_swap(co_active_handle = handle, co_previous_handle);
register cothread_impl* co_previous_handle = co_active_handle;
co_swap(co_active_handle = co, co_previous_handle);
}