Make your own OS in 30 days – day17/18: Idle task and console

Day16’s task_a was sleeping until interrupted, and other task_b0,b1,b2 were running. If we don’t have bx tasks, it needs a different logic.
It introduces an idle task to keep the design simple by always having a task than the main task_a.

void task_idle(void)
{
	for (;;) {
		io_hlt();
	}
}

To allow all tasks to get keyboard input, the task manager defines FIFO queue per task. Then added a console which accepts ‘mem’, ‘cls’ and ‘dir’ commands.
‘dir’ checks 8.3 file name and size.

struct FILEINFO *finfo = (struct FILEINFO *) (ADR_DISKIMG + 0x002600);

if (strcmp(cmdline, "mem") == 0) {
	sprintf(s, "total   %dMB", memtotal / (1024 * 1024));
	putfonts8_asc_sht(sheet, 8, cursor_y, COL8_FFFFFF, COL8_000000, s, 30);
	cursor_y = cons_newline(cursor_y, sheet);
	sprintf(s, "free %dKB", memman_total(memman) / 1024);
	putfonts8_asc_sht(sheet, 8, cursor_y, COL8_FFFFFF, COL8_000000, s, 30);
	cursor_y = cons_newline(cursor_y, sheet);
	cursor_y = cons_newline(cursor_y, sheet);
} else if (strcmp(cmdline, "cls") == 0) {
	for (y = 28; y < 28 + 128; y++) {
		for (x = 8; x < 8 + 240; x++) {
			sheet->buf[x + y * sheet->bxsize] = COL8_000000;
		}
	}
	sheet_refresh(sheet, 8, 28, 8 + 240, 28 + 128);
	cursor_y = 28;
} else if (strcmp(cmdline, "dir") == 0) {
	for (x = 0; x < 224; x++) {
		if (finfo[x].name[0] == 0x00) {
			break;
		}
		if (finfo[x].name[0] != 0xe5) {
			if ((finfo[x].type & 0x18) == 0) {
				sprintf(s, "filename.ext   %7d", finfo[x].size);
				for (y = 0; y < 8; y++) {
					s[y] = finfo[x].name[y];
				}
				s[ 9] = finfo[x].ext[0];
				s[10] = finfo[x].ext[1];
				s[11] = finfo[x].ext[2];
				putfonts8_asc_sht(sheet, 8, cursor_y, COL8_FFFFFF, COL8_000000, s, 30);
				cursor_y = cons_newline(cursor_y, sheet);
			}
		}
	}
	cursor_y = cons_newline(cursor_y, sheet);
}

day17_18

Make your own OS in 30 days – day15/16: Multitasking

First, changed from Q ver 0.9 to qemu 2.2.1 to resolve VESA issue before day15/16. I needed to make qemu foreground after running it.

Makefie)
default :
	#./Q.app/Contents/MacOS/i386-softmmu.app/Contents/MacOS/i386-softmmu -L . -m 32 -localtime -std-vga -fda fdimage0.bin
	/usr/local/Cellar/qemu/2.2.1/bin/qemu-system-i386 -fda fdimage0.bin -L . -m 32 -localtime -vga std -smp cpus=2 &
	sleep 1
	osascript -e "tell application \"/usr/local/Cellar/qemu/2.2.1/bin/qemu-system-i386\" to activate"

Day15/16 is about multitasking. You can do far-jump to TSS; Task Status Segment, CPU doesn’t make a regular jump but change contexts. If you want to create and jump to a new task, the procedure is as follows.
1. Register TSS in GDT
2. Allocate a stack for the new task
3. Far jump to the TSS

#define TASK_GDT0▸▸ 3 // first task segment number
for (i = 0; i < MAX_TASKS; i++) {
	taskctl->tasks0[i].flags = 0;
	taskctl->tasks0[i].sel = (TASK_GDT0 + i) * 8;
	set_segmdesc(gdt + TASK_GDT0 + i, 103, (int) &taskctl->tasks0[i].tss, AR_TSS32);
}

task_a = task_init(memman);
fifo.task = task_a;
task_b = task_alloc();
task_b->tss.esp = memman_alloc_4k(memman, 64 * 1024) + 64 * 1024 - 8;
task_b->tss.eip = (int) &task_b_main;
task_b->tss.es = 1 * 8;
task_b->tss.cs = 2 * 8;
task_b->tss.ss = 1 * 8;
task_b->tss.ds = 1 * 8;
task_b->tss.fs = 1 * 8;
task_b->tss.gs = 1 * 8;
*((int *) (task_b->tss.esp + 4)) = (int) sht_back;

task_run(task_b);

Then it switches tasks using yesterday’s timer interrupt handler every 20ms.
Task_a (main task) is basically sleeping (not getting CPU time at all) until interrupted to use more CPU time for task_b. Task_a should not be sleeping when it’s interrupted. The OS resolves it by awaken task_a when it gets interrupting in FIFO handler via key/mouse/other interrupts.

It only uses a single core through the book. I found how to enable other CPU cores and message each other at University of San Francisco’s class “Advanced Microcomputer Programming”-> lesson 5 -> pmhello.s via StackOverflow.

day15_16

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.

VBEMODE	EQU		0x107
;	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		ES,AX
		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		CX,VBEMODE
		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		[SCRNX],AX
		MOV		AX,[ES:DI+0x14]
		MOV		[SCRNY],AX
		MOV		EAX,[ES:DI+0x28]
		MOV		[VRAM],EAX
		JMP		keystatus

scrn320:
		MOV		AL,0x13			; VGA, 320x200x8bit color
		MOV		AH,0x00
		INT		0x10
		MOV		BYTE [VMODE],8		; Memo screen mode
		MOV		WORD [SCRNX],320
		MOV		WORD [SCRNY],200
		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.

day14

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
    return;
}

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);
	timerctl.count++;
	if (timerctl.next > timerctl.count) {
		return;
	}
	for (i = 0; i < timerctl.using; i++) {
		if (timerctl.timers[i]->timeout > timerctl.count) {
			break;
		}
		// 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.next = timerctl.timers[0]->timeout;
	} else {
		timerctl.next = 0xffffffff;
	}
	return;
}

day13

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;
	struct FREEINFO free[MEMMAN_FREES];
};


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
memman_init(memman);
// '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.
day9

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);
	return;
}


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);
	return;
}

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

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

day7_8

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

_asm_inthandler21:
		PUSH	ES
		PUSH	DS
		PUSHAD
		MOV		EAX,ESP
		PUSH	EAX
		MOV		AX,SS
		MOV		DS,AX
		MOV		ES,AX
		CALL	_inthandler21
		POP		EAX
		POPAD
		POP		DS
		POP		ES
		IRETD
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 (;;) {
    io_hlt();
  }
}

Day 07 will get key codes and mouse data.

Keyboard Interrupt

Make your own OS in 30 days – day5: Characters and Mouse Cursor

VRAM address, x/y size were hard coded in Day 4 example.

vram = (char *) 0xa0000;
xsize = 320;
ysize = 200;

Day 5 reads it from 0x0ff4 set by asmhead.nas via BOOTINFO struct as below.
asmhead.nas:

; BOOT_INFO
CYLS    EQU             0x0ff0 
LEDS    EQU             0x0ff1
VMODE   EQU             0x0ff2 
SCRNX   EQU             0x0ff4 
SCRNY   EQU             0x0ff6 
VRAM    EQU             0x0ff8 
...
MOV		BYTE [VMODE],8
MOV		WORD [SCRNX],320
MOV		WORD [SCRNY],200
MOV		DWORD [VRAM],0x000a0000

bootpack.c:

struct BOOTINFO {
 char cyls, leds, vmode, reserve;
 short scrnx, scrny;
 char *vram;
};

void HariMain(void)
{
 struct BOOTINFO *binfo = (struct BOOTINFO *) 0x0ff0;                                                                                              
...
 init_screen8(binfo->vram, binfo->scrnx, binfo->scrny);

Then it creates ASCII bitmap fonts and a mouse cursor from obj file built from text files like this.

char 0x31
........
....*...
...**...
..*.*...
....*...
....*...
....*...
....*...
....*...
....*...
....*...
....*...
....*...
..*****.
........
........

And use it like this. Finally we can get variable values on the screen for debugging. Since we don’t have debug output or more fancy port to get debug info, this is useful during OS creation time.

void putfont8(char *vram, int xsize, int x, int y, char c, char *font)
{
	int i;
	char *p, d /* data */;
	for (i = 0; i < 16; i++) {
		p = vram + (y + i) * xsize + x;
		d = font[i];
		if ((d & 0x80) != 0) { p[0] = c; }
		if ((d & 0x40) != 0) { p[1] = c; }
		if ((d & 0x20) != 0) { p[2] = c; }
		if ((d & 0x10) != 0) { p[3] = c; }
		if ((d & 0x08) != 0) { p[4] = c; }
		if ((d & 0x04) != 0) { p[5] = c; }
		if ((d & 0x02) != 0) { p[6] = c; }
		if ((d & 0x01) != 0) { p[7] = c; }
	}
	return;
}

void putfonts8_asc(char *vram, int xsize, int x, int y, char c, unsigned char *s)
{
	extern char hankaku[4096]; // <-- stored in obj file as _hankaku
	for (; *s != 0x00; s++) {
		putfont8(vram, xsize, x, y, c, hankaku + *s * 16);
		x += 8;
	}
	return;
}

Then it explains GDT; Global Descriptor Table, IDT; Interrupt Descriptor Table, Segmentation Interrupt or etc to control devices such as a mouse/keyboard/NIC.

Screen Shot 2014-10-13 at 22.22.37

Make your own OS in 30 days – day4: VRAM

Day 4 initialize pallet and write directly into VRAM in C.
To set pallet,

  1. disable interrupts (call CLI = CLear Interrupt flag to reset it)
  2. write pallet number int 0x03c8
  3. write R, G, B into 0x03c9 in the order
  4. restore the interrupt flag

Since the interrupt flag is the 9th bit of the eflags, if you restore it, you don’t need to call STI.

void set_palette(int start, int end, unsigned char *rgb)
{
	int i, eflags;
	eflags = io_load_eflags();	/* save the current eflags */
	io_cli(); 			/* disable interrupts */
	io_out8(0x03c8, start);		/* send data to the device 3c8 */
	for (i = start; i <= end; i++) {
		io_out8(0x03c9, rgb[0] / 4);  /* send data to the device 3c9 */
		io_out8(0x03c9, rgb[1] / 4);
		io_out8(0x03c9, rgb[2] / 4);
		rgb += 3;
	}
	io_store_eflags(eflags);	/* reset eflags */
	return;
}

io_load_eflags, io_store, flags, _io_cli and io_out8 are written as below.

_io_cli:
		CLI
		RET

_io_out8:
		MOV		EDX,[ESP+4]		; port
		MOV		AL,[ESP+8]		; data
		OUT		DX,AL
		RET

_io_load_eflags:
		PUSHFD		; PUSH EFLAGS
		POP		EAX
		RET

_io_store_eflags:
		MOV		EAX,[ESP+4]
		PUSH	EAX
		POPFD		; POP EFLAGS
		RET

Now we are ready to write in VRAM (starts at 0xa000).

{
...
    vram = (char *) 0xa0000;
    xsize = 320;
    ysize = 200;
    boxfill8(vram, xsize, COL8_008484,  0,         0,          xsize -  1, ysize - 29);
...
}

void boxfill8(unsigned char *vram, int xsize, unsigned char c, int x0, int y0, int x1, int y1)
{
    int x, y;
        for (y = y0; y <= y1; y++) {
            for (x = x0; x <= x1; x++)
                vram[y * xsize + x] = c;
    }
    return;
}

When you draw several rectangles, it'll be like this.
OS_day04_1

Day 5 will draw characters and the mouse cursor which doesn't move, and learn segmentation and interrupt.

Make your own OS in 30 days – day3: 32bit mode and OS in C

We only wrote 16bit assembly language on day 1 and 2.
Day 3 started with Floppy Disk architecture by illustrating cylinder, header, sector and drive, then how to read from there.

MOV		AX,0x0820 		; loads it at 0x8200 (ES x 16 + BX)
MOV		ES,AX
MOV		CH,0			; cylinder 0
MOV		DH,0			; head 0
MOV		CL,2			; sector 2

MOV		AH,0x02			; AH=0x02 : read disk
MOV		AL,1			; read 1 sector
MOV		BX,0
MOV		DL,0x00			; A drive
INT		0x13			; Disk BIOS call
JC		error

Int 13H is described here.
It explains segment registers, how to retry when error (if the carry flag is set), read 18 sectors, and read 18 sectors x 2 heads x 10 cylinders into memory.

When you compile this very simple OS code

fin:
    HLT
    JMP		fin

It’ll be compiled to the 3 byte code.

f4 eb fd

Then when you make a disk image file, it’ll be placed at 0x4200.

0004200: f4eb fd00 0000 0000 0000 0000 0000 0000
0004210: 0000 0000 0000 0000 0000 0000 0000 0000
0004220: 0000 0000 0000 0000 0000 0000 0000 0000
0004230: 0000 0000 0000 0000 0000 0000 0000 0000

Since the bootstrap loads the first byte of sector 2 at 0x8200 (first byte of sector 1 is at 0x8000), the 3byte OS code at 0x4200 will be loaded at 0x8000 + 0x4200 = 0xc200.

So, “ORG 0xc200” is added to the OS code,

    ORG 0xc200

fin:
    HLT
    JMP		fin

then it adds “JMP 0xc200” at the end of the IPL. Then the IPS will JMP to the OS (HLT code) after loading the sectors into memory.

Just doing HLT is not interesting. The book changes the OS to this which fills the screen in black.

CYLS	EQU		0x0ff0			; set by the bootsector
LEDS	EQU		0x0ff1
VMODE	EQU		0x0ff2			; color bitness
SCRNX	EQU		0x0ff4			; X resolution
SCRNY	EQU		0x0ff6			; Y resolution
VRAM	EQU		0x0ff8			; VRAM

	ORG		0xc200
	MOV		AL,0x13			; VGA,3 20x200x8bit color
	MOV		AH,0x00
	INT		0x10
	MOV		BYTE [VMODE],8	; screen mode
	MOV		WORD [SCRNX],320
	MOV		WORD [SCRNY],200
	MOV		DWORD [VRAM],0x000a0000

; Get Keyboard's LED state from BIOS
	MOV		AH,0x02
	INT		0x16 			; keyboard BIOS
	MOV		[LEDS],AL

fin:
	HLT
	JMP		fin

We are getting into 32bit mode & C language from here. Now we have 4 source files.
ipl10.nas: bootstrap code (x86 16bit assembly)
asmhead.nas: OS loader called by bootstrap (x86 32bit assembly, it’s moving into 32bit mode and loads OS code written in C. Not explained as of Day 3.)
bootpack.c: OS code (C)
nasfunc.nas: functions called by boot pack.c (x86 32bit assembly)

bootpack.c:

void io_hlt(void);

void HariMain(void)
{
fin:
	io_hlt(); /* calls _io_hlt in naskfunc.nas */
	goto fin;

}

nasfunc.nas:

; naskfunc
; TAB=4

[FORMAT "WCOFF"]				; create an object file
[BITS 32]						; 32bit mode


[FILE "naskfunc.nas"]			; source file name
		GLOBAL	_io_hlt			; function name included in this file

[SECTION .text]		; text section

_io_hlt:	; void io_hlt(void);
		HLT
		RET

When you boot it, ipl10 loads the image into memory and JPM to 0xC200 (asmhead.nas) fills in the screen in black and calls bootpack.c which halts.

OS_day03_1

Day 4 will do more in C 🙂

1 2 3