Make your own OS in 30 days – day14: High resolution and key input

Day14 calls VESA BIOS extension and switch to high resolution mode if VESA 2.0 or later is available.

;	0x100 :  640 x  400 x 8bit color
;	0x101 :  640 x  480 x 8bit color
;	0x103 :  800 x  600 x 8bit color
;	0x105 : 1024 x  768 x 8bit color
;	0x107 : 1280 x 1024 x 8bit color

VMODE	EQU		0x0ff2			; screen mode

; VBE existence check
		MOV		AX,0x9000
		MOV		DI,0
		MOV		AX,0x4f00
		INT		0x10
		CMP		AX,0x004f
		JNE		scrn320

; VBE version check
		MOV		AX,[ES:DI+4]
		CMP		AX,0x0200
		JB		scrn320			; if (AX < 0x0200) goto scrn320

; Get screen mode
		MOV		AX,0x4f01
		INT		0x10
		CMP		AX,0x004f
		JNE		scrn320

; Check screen mode info
		CMP		BYTE [ES:DI+0x19],8
		JNE		scrn320
		CMP		BYTE [ES:DI+0x1b],4
		JNE		scrn320
		MOV		AX,[ES:DI+0x00]
		AND		AX,0x0080
		JZ		scrn320			; switch to 320x240 mode if above fails

; Switch screen mode
		MOV		BX,VBEMODE+0x4000
		MOV		AX,0x4f02
		INT		0x10
		MOV		BYTE [VMODE],8		; Memo screen mode
		MOV		AX,[ES:DI+0x12]
		MOV		AX,[ES:DI+0x14]
		MOV		EAX,[ES:DI+0x28]
		JMP		keystatus

		MOV		AL,0x13			; VGA, 320x200x8bit color
		MOV		AH,0x00
		INT		0x10
		MOV		BYTE [VMODE],8		; Memo screen mode
		MOV		DWORD [VRAM],0x000a0000

2nd part converts key codes into characters.
I changed from Q to Parallels since VESA didn't work well with Q ver 0.9.0.


Make your own OS in 30 days – day10-13: Layers, Windows and Timer

I skipped day10: Layers and day11: Windows which are generic topics and not Hardware/OS specific. It resolved yesterday’s mouse cursor overlay issue.

First, it sets up an interrupt handler for 0x20 for Timer, then sets up PIT; Programmable Interval Timer to interrupt every 10ms (100Hz).

set_gatedesc(idt + 0x20, (int) asm_inthandler20, 2 * 8, AR_INTGATE32);
#define PIT_CTRL 0x0043
#define PIT_CNT0 0x0040

void init_pit(void) {
    io_out8(PIT_CTRL, 0x34);
    io_out8(PIT_CNT0, 0x9c);
    io_out8(PIT_CNT0, 0x2e);
    ... then init timers here

inthandler20 increments an integer counter and writes into FIFO buffer for Timer when it times out.

void inthandler20(int *esp)
	int i, j;
	io_out8(PIC0_OCW2, 0x60);
	if ( > timerctl.count) {
	for (i = 0; i < timerctl.using; i++) {
		if (timerctl.timers[i]->timeout > timerctl.count) {
		// timeout
		timerctl.timers[i]->flags = TIMER_FLAGS_ALLOC;
		fifo8_put(timerctl.timers[i]->fifo, timerctl.timers[i]->data);
	timerctl.using -= i;
	for (j = 0; j < timerctl.using; j++) {
		timerctl.timers[j] = timerctl.timers[i + j];
	if (timerctl.using > 0) { = timerctl.timers[0]->timeout;
	} else { = 0xffffffff;


Make your own OS in 30 days – day9: Memory management

Day9 manages memory by holding a list of free blocks.

#define MEMMAN_FREES		4090
#define MEMMAN_ADDR			0x003c0000

struct FREEINFO {
	unsigned int addr, size;

struct MEMMAN {
	int frees, maxfrees, lostsize, losts;

unsigned int memman_alloc(struct MEMMAN *man, unsigned int size);
int memman_free(struct MEMMAN *man, unsigned int addr, unsigned int size);

// and use it like this
struct MEMMAN *memman = (struct MEMMAN *) MEMMAN_ADDR;

// get total memory
memtotal = memtest(0x00400000, 0xbfffffff);
// 0 init ... there is no free area at this time
// 'add' the following blocks into free list
memman_free(memman, 0x00400000, memtotal - 0x00400000); // 32MB-0x4000000 = 
// allocate it
char *hoge = (char *)memman_alloc(memman, 4 * 1024 * 1024);

After allocating 4MB on 32MB machine.

Make your own OS in 30 days – day7/8: Mouse interrupt handler

Day 7/8 implements FIFO ring buffer for Keyboard/Mouse handler to handle more than 1 byte code.
The main function wait for interrupt until it gets an interrupt by STI and HLT. When it’s interrupted, it disables interrupts by CLI, get key/mouse data by STI, draw it and wait for the next interrupt.

This version doesn’t have layers and mouse cursor overwrites background as below.

struct FIFO8 mousefifo;

struct FIFO8 {
	unsigned char *buf;
	int p, q, size, free, flags;

void inthandler21(int *esp)
	unsigned char data;
	io_out8(PIC0_OCW2, 0x61);	// IRQ-01 accepted -> PIC0
	data = io_in8(PORT_KEYDAT);
	fifo8_put(&keyfifo, data);

void inthandler2c(int *esp)
	unsigned char data;
	io_out8(PIC1_OCW2, 0x64);	// IRQ-12 accepted -> PIC1
	io_out8(PIC0_OCW2, 0x62);	// IRQ-02 accepted -> PIC0
	data = io_in8(PORT_KEYDAT);
	fifo8_put(&mousefifo, data);

int fifo8_put(struct FIFO8 *fifo, unsigned char data)
	if (fifo->free == 0) {
		fifo->flags |= FLAGS_OVERRUN;
		return -1;
	fifo->buf[fifo->p] = data;
	if (fifo->p == fifo->size) {
		fifo->p = 0;
	return 0;

// loop in the main function
	for (;;) {
		if (fifo8_status(&keyfifo) + fifo8_status(&mousefifo) == 0) {
		} else {
			if (fifo8_status(&keyfifo) != 0) {
				i = fifo8_get(&keyfifo);
				// draw key data
			} else if (fifo8_status(&mousefifo) != 0) {
				i = fifo8_get(&mousefifo);
				// draw mouse data


Make your own OS in 30 days – day6: GDT/IDT and Keyboard/Mouse interrupts

Day6 describes the following 3 which are necessary to receive data from keyboard & mouse.
* GDT; Global Dispatch Table
* IDT; Interrupt Dispatch Table
* PIC; Programmable Interrupt Controller

Here is an int21 handler for keyboard interrupts. It has a similar mouse handler. It only shows the pre-defined message and halts after receiving the first key. It won’t show mouse data since it hasn’t reset interrupt handlers to receive more interrupts (mouse interrupt is not 1 byte).

Since interrupt handler should end with ‘IRETD’ instead of ‘RET’, it’s not fully written in C, but written in asm (_asm_inthandler21) which calls _inthandler21 written in C.

		EXTERN	_inthandler21, _inthandler27, _inthandler2c

		CALL	_inthandler21
void inthandler21(int *esp)
  struct BOOTINFO *binfo = (struct BOOTINFO *)ADR_BOOTINFO;
  boxfill8(binfo->vram, binfo->scrnx, COL8_000000, 0, 0, 32 * 8 - 1, 15);
  putfonts8_asc(binfo->vram, binfo->scrnx, 0, 0, COL8_FFFFFF,
                "INT 21 (IRQ-1) : PS/2 keyboard");
  for (;;) {

Day 07 will get key codes and mouse data.

Keyboard Interrupt

MS-DOS COM on 16bit MS-DOS

I wrote and tested 16bit com on 32bit Win7 before if I remember correctly.
Since I couldn’t run 16bit linker on 64bit Win8 anymore, I installed MS-DOS 6.22 on Parallels. For some reason, EMM386 failed to test 4MB main memory which I think was sufficient. It passed when I assigned 64MB. Hm…
I couldn’t find exe2bin.exe and used exe2com; PDS version of exe2bin.


C:\PROJECTS\FIRST>ml /c first.asm
C:\PROJECTS\FIRST>link first.obj,first.exe,,,null.def
C:\PROJECTS\FIRST>exe2com first.exe


How to get .Net function parameters from crash dump

When debugging a crashed application from a crash dump, you often see that arguments to functions in the call stack are no data. Let’s say you want to get arguments for Hoge.Program.Test(Hoge.Foo, Hoge.Bar).
Since x64 calling convention use rcx/rdx/r8/r9 for the first 4 arguments, it’s not automatically pushed to the stack before the function call. However, sometimes the copy is stored in stack as below.


0:000> !pe
Exception object: 000000e1000068e0
Exception type:   System.Exception
Message:          dummy exception
InnerException:   <none>
StackTrace (generated):
    SP               IP               Function
    000000E17AADEF00 00007FFB516702BC Hoge!Hoge.Program.TestSub()+0x3c
    000000E17AADEF40 00007FFB51670243 Hoge!Hoge.Program.Test(Hoge.Foo, Hoge.Bar)+0x123
    000000E17AADEFB0 00007FFB516700FE Hoge!Hoge.Program.Main(System.String[])+0x6e

StackTraceString: <none>
HResult: 80131500


0:000> !clrstack
OS Thread Id: 0x1bd4 (0)
        Child SP               IP Call Site
000000e17aadee18 00007ffbc0718b9c [HelperMethodFrame: 000000e17aadee18] 
000000e17aadef00 00007ffb516702bc Hoge.Program.TestSub() [c:\Users\sokoide\Projects\Spike\Hoge\Program.cs @ 45]
000000e17aadef40 00007ffb51670243 Hoge.Program.Test(Hoge.Foo, Hoge.Bar) [c:\Users\sokoide\Projects\Spike\Hoge\Program.cs @ 40]
000000e17aadefb0 00007ffb516700fe Hoge.Program.Main(System.String[]) [c:\Users\sokoide\Projects\Spike\Hoge\Program.cs @ 29]
000000e17aadf2d0 00007ffbb0caa7f3 [GCFrame: 000000e17aadf2d0] 

0:000> !clrstack -a
OS Thread Id: 0x1bd4 (0)
        Child SP               IP Call Site
000000e17aadee18 00007ffbc0718b9c [HelperMethodFrame: 000000e17aadee18] 
000000e17aadef00 00007ffb516702bc Hoge.Program.TestSub() [c:\Users\sokoide\Projects\Spike\Hoge\Program.cs @ 45]
        this = <no data>

000000e17aadef40 00007ffb51670243 Hoge.Program.Test(Hoge.Foo, Hoge.Bar) [c:\Users\sokoide\Projects\Spike\Hoge\Program.cs @ 40]
        this = <no data>
        foo = <no data>
        bar = <no data>
        <no data>

000000e17aadefb0 00007ffb516700fe Hoge.Program.Main(System.String[]) [c:\Users\sokoide\Projects\Spike\Hoge\Program.cs @ 29]
        args = <no data>

If you see the JIT compiled code for Hoge.Program.Test(Hoge.Foo, Hoge.Bar), you can confirm that rcx is copied in rbi, and the rbi is pushed into the stack at 00007ffb`51670123.

0:000> !U 00007ffb51670243 
Normal JIT generated code
Hoge.Program.Test(Hoge.Foo, Hoge.Bar)
Begin 00007ffb51670120, size 131

c:\Users\sokoide\Projects\Spike\Hoge\Program.cs @ 34:
00007ffb`51670120 53              push    rbx
00007ffb`51670121 55              push    rbp
00007ffb`51670122 56              push    rsi
00007ffb`51670123 57              push    rdi
00007ffb`51670124 4154            push    r12
00007ffb`51670126 4155            push    r13
00007ffb`51670128 4883ec38        sub     rsp,38h
00007ffb`5167012c 498be8          mov     rbp,r8
00007ffb`5167012f 488bfa          mov     rdi,rdx
00007ffb`51670132 488bf1          mov     rsi,rcx
00007ffb`51670135 33db            xor     ebx,ebx
00007ffb`51670137 bab0000000      mov     edx,0B0h
00007ffb`5167013c b901000000      mov     ecx,1

If you can get stack base address when Test(Hoge.Foo, Hoge.Bar) is called, you can get those copied registers which have the Hoge.Foo/Bar arguments.
Since child-sp when the Test function is called is 000000E17AADEFB0, 000000E17AADEFB0-0x8 is the ret address, and 000000E17AADEFB0-0x10 is the base address as below. Then you can get rcx (this), rdx (1st arg), r8 (2nd arg) stored in rsi(<-rcx), rdi(<-rdx), rbp(<-r8) respectively.

0:000> dq 000000e1`7aadef40 000000e1`7aadefff
[7aadef40 – 7aadef78] = 0x38 … local vars
000000e1`7aadef40  000000e1`00002d38 00000000`00000000
000000e1`7aadef50  000000e1`00005b0c 00000000`00000000
000000e1`7aadef60  000000e1`00000009 000000e1`00002d38
000000e1`7aadef70  000000e1`00002d68 000000e1`7aadf300 (r13)
000000e1`7aadef80  000000e1`7aadf348(r12) 000000e1`00002d50 (rdi)
000000e1`7aadef90  000000e1`00002d38(rsi) 000000e1`7aadf010 (rbp)
000000e1`7aadefa0  000000e1`00002d68(rbx) 00007ffb`516700fe (ret)
000000e1`7aadefb0  000000e1`00002d38(child-sp) 000000e1`00002d50
000000e1`7aadefc0  000000e1`00002d68 000000e1`00000001
000000e1`7aadefd0  000000e1`7aadf2d0 000000e1`7aadf128
000000e1`7aadefe0  000000e1`7aadf1a0 00007ffb`b0caa7f3
000000e1`7aadeff0  000000e1`00002cc8 000000e1`7aadf500

this = rcx)
0:000> !do 000000e1`00002d38
Name:        Hoge.Program
MethodTable: 00007ffb51554038
EEClass:     00007ffb516624a8
Size:        24(0x18) bytes
File:        C:\Users\sokoide\Projects\Spike\Hoge\bin\Release\Hoge.exe

1st arg = rdx)
0:000> !do 000000e1`00002d50
Name:        Hoge.Foo
MethodTable: 00007ffb51554120
EEClass:     00007ffb51662520
Size:        24(0x18) bytes
File:        C:\Users\sokoide\Projects\Spike\Hoge\bin\Release\Hoge.exe
              MT    Field   Offset                 Type VT     Attr            Value Name
00007ffbafad0e08  4000012        8        System.String  0 instance 000000e100002ce8 <Name>k__BackingField

2nd arg = r8)
0:000> !do 000000e1`7aadf010
<Note: this object has an invalid CLASS field>
Invalid object

… looks the pointer doesn’t have the class info. However, we know the type is Hoge.Bar!
0:000> !dumpheap -type Bar
         Address               MT     Size
000000e100002d68 00007ffb51554208       24     

              MT    Count    TotalSize Class Name
00007ffb51554208        1           24 Hoge.Bar
Total 1 objects

then we can get the object detail using !dumpvc and the method table and the object pointer as below.
0:000> !dumpvc 00007ffb51554208 000000e1`7aadf010
Name:        Hoge.Bar
MethodTable: 00007ffb51554208
EEClass:     00007ffb51662598
Size:        24(0x18) bytes
File:        C:\Users\sokoide\Projects\Spike\Hoge\bin\Release\Hoge.exe
              MT    Field   Offset                 Type VT     Attr            Value Name
00007ffbafad0e08  4000013        0        System.String  0 instance 000000e17aadf150 <Name>k__BackingField

Here is the target application which was used to demonstrate it.

class Foo
    public string Name { get; set; }

class Bar
    public string Name { get; set; }

class Program
    static void Main(string[] args)
        Program p = new Program();

        Foo foo = new Foo { Name = "hoge" };
        Bar bar = new Bar { Name = "page" };
        p.Test(foo, bar);

    private void Test(Foo foo, Bar bar)
        for (int i = 0; i < 10; i++)
            Console.WriteLine("Hello {0}", i);

    private void TestSub()
        throw new Exception("dummy exception");


I fixed find_goroutine(goid) using yesterday’s Allg class and finally I’m able to debug goroutines.Thank you Don!
I uploaded entire to my github repo at

(gdb) info goroutine
  1 waiting  fname=runtime.gopark faddr=0x13415 &g=0xc208000120 waitreason="sleep"
  2 waiting  fname=runtime.gopark faddr=0x13415 &g=0xc208000480 waitreason="force gc (idle)"
  3 waiting  fname=runtime.gopark faddr=0x13415 &g=0xc2080005a0 waitreason="GC sweep wait"
  4 waiting  fname=runtime.gopark faddr=0x13415 &g=0xc2080006c0 waitreason="finalizer wait"
  5 syscall  fname=runtime.switchtoM faddr=0x366c0 &g=0xc2080007e0

(gdb) goroutine 1 bt
#0  runtime.gopark (unlockf=0x2eba0 <runtime.parkunlock_c>, lock=0x1576e0 <runtime.timers>, reason="sleep")
    at /Users/sokoide/repo/go/src/runtime/proc.go:131
#1  0x0000000000013488 in runtime.goparkunlock (lock=0x1576e0 <runtime.timers>, reason="sleep") at /Users/sokoide/repo/go/src/runtime/proc.go:136
#2  0x0000000000017de5 in runtime.timeSleep (ns=2000000000) at /Users/sokoide/repo/go/src/runtime/time.go:58
#3  0x00000000000020f8 in main.f2 (a=40, b=2, ~r2=833358237544) at /Users/sokoide/workspace/go/foo/foo.go:24
#4  0x0000000000002551 in main.main () at /Users/sokoide/workspace/go/foo/main.go:22

(gdb) goroutine 4 bt
#0  runtime.gopark (unlockf=0x2eba0 <runtime.parkunlock_c>, lock=0x15e840 <runtime.finlock>, reason="finalizer wait")
    at /Users/sokoide/repo/go/src/runtime/proc.go:131
#1  0x0000000000013488 in runtime.goparkunlock (lock=0x15e840 <runtime.finlock>, reason="finalizer wait")
    at /Users/sokoide/repo/go/src/runtime/proc.go:136
#2  0x000000000000e66a in runtime.runfinq () at /Users/sokoide/repo/go/src/runtime/malloc.go:727
#3  0x00000000000388f1 in runtime.goexit () at /Users/sokoide/repo/go/src/runtime/asm_amd64.s:2232
#4  0x0000000000000000 in ?? ()

Here is the updated find_goroutine(good).

def find_goroutine(goid):
	find_goroutine attempts to find the goroutine identified by goid.
	It returns a touple of gdv.Value's representing the stack pointer
	and program counter pointer for the goroutine.

	@param int goid

	@return tuple (gdb.Value, gdb.Value)
	__allg = Allg()
	while True:
		ptr = __allg.fetch()
		if not ptr:
		if goid == __allg.Goid(ptr):
		    pc = __allg.Pc(ptr)
		    sp = __allg.Sp(ptr)
		    return pc, sp
	return None, None

Debugging GO program with GDB #1

Golang provides a python script to extend gdb to show goroutines and the callstacks here.

However, it doesn’t work as written.
This patch fixed the first problem, but it still fails (at least on OS X and Windows).

I found this article which I think worked in older GO versions, but it fails in go 1.4.2 (latest as of 2015.3).

I changed Allg class’s offset and init function to show a list of goroutines as below.

(gdb) info goroutines
  1 waiting  fname=runtime.gopark faddr=0x13585 &g=0xc208000120 waitreason="sleep"
  2 waiting  fname=runtime.gopark faddr=0x13585 &g=0xc208000480 waitreason="force gc (idle)"
  3 waiting  fname=runtime.gopark faddr=0x13585 &g=0xc2080005a0 waitreason="GC sweep wait"
  4 waiting  fname=runtime.gopark faddr=0x13585 &g=0xc2080006c0 waitreason="finalizer wait"
  5 syscall  fname=runtime.switchtoM faddr=0x36830 &g=0xc208000a20

I’ll need more work to fix ‘go $goroutineid bt’.

(gdb) goroutine 1 bt
Python Exception <class 'gdb.error'> There is no member named status.:
Error occurred in Python command: There is no member named status.

Today’s change.

class Allg:
    __allglen = -1
    __position = 0
    __allg = 0

    __offsets = {
            'status': 120,
            'waitreason': 144,
            'goid': 128,
            'm': 200,
            'sched': 48,
            'sched.pc': 56,
            'sched.sp': 48,
            'stackguard': 16,
            'stackbase': 0,

    def __init__(self):
        # first, fetch the number of active goroutines
        self.__allglen = int(str(gdb.parse_and_eval("&{uint64}'runtime.allglen'")), 16)
        # print("found allglen = {0}".format(self.__allglen))

        # get the next address in the array
        s = "&*{uint64}(&'runtime.allg')"
        self.__allg = int(gdb.parse_and_eval(s))
        # print("found allg = {0}".format(hex(self.__allg)))

    def fetch(self):
        if self.__position >= self.__allglen:
            return None

        s = "&*{uint64}(" + "{0}+{1})".format(self.__allg, self.__position*8)
        p = int(gdb.parse_and_eval(s))
        self.__position += 1
        return p

    def Status(self, a):
        s = "&*{int16}(" + "{0}+{1})".format(a, self.__offsets['status'])
        return int(gdb.parse_and_eval(s))

    def WaitReason(self, a):
        s = "&*{int64}(" + "{0}+{1})".format(a, self.__offsets['waitreason'])
        x = int(gdb.parse_and_eval(s))
        s = "&{int8}" + "{0}".format(x)
        return str(gdb.parse_and_eval(s))

    def Goid(self, a):
        s = "&*{int64}(" + "{0}+{1})".format(a, self.__offsets['goid'])
        return int(gdb.parse_and_eval(s))

    def M(self, a):
        s = "&*{uint64}(" + "{0}+{1})".format(a, self.__offsets['m'])
        return int(gdb.parse_and_eval(s))

    def Pc(self, a):
        s = "&*{uint64}(" + "{0}+{1})".format(a, self.__offsets['sched.pc'])
        return int(gdb.parse_and_eval(s))

    def Sp(self, a):
        s = "&*{uint64}(" + "{0}+{1})".format(a, self.__offsets['sched.sp'])
        return int(gdb.parse_and_eval(s))

    def Stackguard(self, a):
        s = "&*{uint64}(" + "{0}+{1})".format(a, self.__offsets['stackguard'])
        return int(gdb.parse_and_eval(s))

    def Stackbase(self, a):
        s = "&*{uint64}(" + "{0}+{1})".format(a, self.__offsets['stackbase'])
        return int(gdb.parse_and_eval(s))

class GoroutinesCmd(gdb.Command):
	"List all goroutines."
	__allg = None

	def __init__(self):
		gdb.Command.__init__(self, "info goroutines", gdb.COMMAND_STACK, gdb.COMPLETE_NONE)

	def invoke(self, _arg, _from_tty):
		self.__allg = Allg()
		while True:
			ptr = self.__allg.fetch()
			# print("fetched ptr = {0}".format(hex(ptr)))
			if not ptr:

			st = self.__allg.Status(ptr)
			# print("status is {0}".format(st))
			w = self.__allg.WaitReason(ptr)
			# print("waitreason is {0}".format(w))
			#if st == 6:  # 'gdead'
			    #print("skipping over dead goroutine")

			s = ' '
			m = self.__allg.M(ptr)
			if m:
				s = '*'

			# if the status isn't "waiting" then the waitreason doesn' tmatter
			if st != 4:
				w = ''
			w2 = w.split('"')
			if len(w2) > 1:
				w = """waitreason="{0}\"""".format(w2[len(w2) - 2])

			pc = self.__allg.Pc(ptr)
			# print("pc is {0}".format(pc))
                        blk = gdb.block_for_pc(pc)
                        # print("blk is {0}".format(blk))
                        goid = self.__allg.Goid(ptr)
                        a = "fname={0} faddr={1}".format(blk.function, hex(pc))
                        print(s, goid, "{0:8s}".format(sts[st]), a, "&g={0}".format(hex(ptr)), w)

The __offsets in Allg was calculated from go’s runtime zruntime_defs_darwin_amd64.go.

 123 type g struct {
 124 ▸ stack        stack          // 0, +16
 125 ▸ stackguard0  uintptr        // 16
 126 ▸ stackguard1  uintptr        // 24
 127 ▸ _panic       *_panic        // 32
 128 ▸ _defer       *_defer        // 40
 129 ▸ sched        gobuf          // 48, +48
 130 ▸ syscallsp    uintptr        // 96
 131 ▸ syscallpc    uintptr        // 104
 132 ▸ param        unsafe.Pointer // 112
 133 ▸ atomicstatus uint32         // 120, padding +4
 134 ▸ goid         int64          // 128
 135 ▸ waitsince    int64          // 136
 136 ▸ waitreason   string         // 144, +16
 137 ▸ schedlink    *g             // 160
 138 ▸ issystem     bool           // 168
 139 ▸ preempt      bool           // 172
 140 ▸ paniconfault bool           //176
 141 ▸ preemptscan  bool           //180
 142 ▸ gcworkdone   bool           //184
 143 ▸ throwsplit   bool           //188
 144 ▸ raceignore   int8           //192, padding +7
 145 ▸ m            *m             //200
1 2 3 4 5 8