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 🙂

Make your own OS in 30 days – day2: Assembly Language

Day 2 changes the previous one into x86 assembly code as below. That will go into the boot sector and the other sectors are created by the author’s image edit tool.

; hello-os
; TAB=4

ORG		0x7c00			; Where the program is loaded
						; If ORG is available, $ will be the address of the current code
						; 0x00007c00 - 0x00007dff : boot sector should be loaded here

; For standard FAT12 floppy disk
; --- boot sector ---

JMP		entry
DB		0x90
DB		"HELLOIPL"		; Boot sector name (8bytes)
DW		512			; Sector size (must be 512)
DB		1			; Cluster size (must be 1)
DW		1			; Where FAT begins (usually 1)
DB		2			; Number of FATs (must be 2)
DW		224			; Root directory size (usually 224 entries)
DW		2880			; Drive size (must be 2880 sectors)
DB		0xf0			; Media type(must be 0xf0)
DW		9			; Length of FAT area(must be 9 sectors)
DW		18			; Sectors per track(must be 18)
DW		2			; Number of heads (must be 2)
DD		0			; Must be 0 since there is no partition
DD		2880			; Drive size again
DB		0,0,0x29		; Not sure
DD		0xffffffff		; Probably volume serial number
DB		"HELLO-OS   "		; Name of the disc (11 bytes)
DB		"FAT12   "		; Name of the format (8 bytes)
RESB	18				; Reserve 18 bytes

; program body

entry:
MOV		AX,0			; init registers
MOV		SS,AX
MOV		SP,0x7c00
MOV		DS,AX
MOV		ES,AX

MOV		SI,msg
putloop:
MOV		AL,[SI]
ADD		SI,1
CMP		AL,0
JE		fin
MOV		AH,0x0e			; Function to display a character
MOV		BX,15			; Color code
INT		0x10			; Video BIOS call (INT 0x10), see http://en.wikipedia.org/wiki/BIOS_interrupt_call
JMP		putloop
fin:
HLT					; Halt CPU when something happens
JMP		fin			; Infinite loop

msg:
DB		0x0a, 0x0a		; LF x 2
DB		"hello, koide"
DB		0x0a			; LF
DB		0

RESB	0x7dfe-$			; Pad 0x00 until 0x7dfe

DB		0x55, 0xaa

We are making it 32bit on Day 3.

Make your own OS in 30 days – day1: How PC starts up

Make your own OS in 30 daysI bought a very nice book Meke your own OS in 30 days.

Someone created a Mac version here.

When you type below in helloos0.img and boot from there,

    1 0000000: eb4e 9048 454c 4c4f 4950 4c00 0201 0100  .N.HELLOIPL.....                                                                                              
    2 0000010: 02e0 0040 0bf0 0900 1200 0200 0000 0000  ...@............
    3 0000020: 400b 0000 0000 29ff ffff ff48 454c 4c4f  @.....)....HELLO
    4 0000030: 2d4f 5320 2020 4641 5431 3220 2020 0000  -OS   FAT12   ..
    5 0000040: 0000 0000 0000 0000 0000 0000 0000 0000  ................
    6 0000050: b800 008e d0bc 007c 8ed8 8ec0 be74 7c8a  .......|.....t|.
    7 0000060: 0483 c601 3c00 7409 b40e bb0f 00cd 10eb  ....
...

It starts up and shows “hello, world”.

OS_day01_1

The image was created by compiling the following nas file. The has file is a source fails for nask which was created by the author by copying a popular assembler nasm.

; hello-os
; TAB=4

; For standard FAT12 floppy disk

; --- boot sector ---

DB		0xeb, 0x4e, 0x90
DB		"HELLOIPL"		; Boot sector name (8bytes)
DW		512			; Sector size (must be 512)
DB		1			; Cluster size (must be 1)
DW		1			; Where FAT begins (usually 1)
DB		2			; Number of FATs (must be 2)
DW		224			; Root directory size (usually 224 entries)
DW		2880			; Drive size (must be 2880 sectors)
DB		0xf0			; Media type(must be 0xf0)
DW		9			; Length of FAT area(must be 9 sectors)
DW		18			; Sectors per track(must be 18)
DW		2			; Number of heads (must be 2)
DD		0			; Must be 0 since there is no partition
DD		2880			; Drive size again
DB		0,0,0x29		; Not sure
DD		0xffffffff		; Probably volume serial number
DB		"HELLO-OS   "		; Name of the disc (11 bytes)
DB		"FAT12   "		; Name of the format (8 bytes)
RESB	18				; Reserve 18 bytes

; Program body

DB		0xb8, 0x00, 0x00, 0x8e, 0xd0, 0xbc, 0x00, 0x7c
DB		0x8e, 0xd8, 0x8e, 0xc0, 0xbe, 0x74, 0x7c, 0x8a
DB		0x04, 0x83, 0xc6, 0x01, 0x3c, 0x00, 0x74, 0x09
DB		0xb4, 0x0e, 0xbb, 0x0f, 0x00, 0xcd, 0x10, 0xeb
DB		0xee, 0xf4, 0xeb, 0xfd

; Messages

DB		0x0a, 0x0a		; LF x 2
DB		"hello, koide"
DB		0x0a			; LF
DB		0

RESB	0x1fe-$				; Pad 0x00 until 0x001fe

DB		0x55, 0xaa

; --- end of boot sector ---
; --- 2nd sector and later ---

DB		0xf0, 0xff, 0xff, 0x00, 0x00, 0x00, 0x00, 0x00
RESB	4600
DB		0xf0, 0xff, 0xff, 0x00, 0x00, 0x00, 0x00, 0x00
RESB	1469432
; --- end of the image

When PC starts up, it reads the boot sector of the disk. If the word at 0x1fe is ’55aa’, it assumes it’s a bootable device and continue. According to the author, it’s decided by the PC designer and he doesn’t know why it’s 55aa.

We are changing the DB,DW,DD into more meaningful instructions on day 2.

GCC Inline Assembly

From here, here and here

#include <stdio.h>

int func(int a, int b); 
void cpuid(int info, int *eax, int *ebx, int *ecx, int *edx);

int func(int a, int b)
{
	int out = 0;

	__asm__(
		"movl %1, %%eax;"
		"addl %2, %%eax;"
		"movl %%eax, %0;"
		:"=r"(out)      /* output */
		:"r"(a),"r"(b)▸ /* input */
		:"eax"▸ 		/* clobbered register */
	);
	return out;
}

void cpuid(int info, int *eax, int *ebx, int *ecx, int *edx)
{
	*eax = info;
	__asm__(
		"movl %%ebx, %%edi;"	/* 32bit PIC: don't clobber ebx */
		"cpuid;"
		"movl %%ebx, %%esi;"
		"movl %%edi, %%ebx;"
		:"+a"(*eax),"=S"(*ebx),"=c"(*ecx),"=d"(*edx)
		:
		:"edi"
	);
}


int main(int args, char* argv[])
{
	int eax, ebx, ecx, edx;
	printf("ret=%d\n", func(3,5));
	cpuid(1, &eax, &ebx, &ecx, &edx);
	printf("eax:0x%08x, ebx:0x%08x, ecx:0x%08x, edx:0x%08x\n", eax, ebx, ecx, edx);
	printf("stepping:        0x%x\n", eax & 0xF);
	printf("model:           0x%x\n", (eax>>4) & 0xF);
	printf("family:          0x%x\n", (eax>>8) & 0xF);
	printf("processor type:  0x%x\n", (eax>>12) & 0x3);
	printf("extended model:  0x%x\n", (eax>>16) & 0xF);
	printf("extended family: 0x%x\n", (eax>>20) & 0xFF);
	return 0;
}

Result:

ret=8
eax:0x000206a7, ebx:0x00100800, ecx:0x1fbae3ff, edx:0xbfebfbff
stepping:        0x7
model:           0xa
family:          0x6
processor type:  0x0
extended model:  0x2
extended family: 0x0

Import Lookup Table

Came back to Windows world and played with windbg.

The following code showed f1096 as a function pointer of ‘add’, but the final address was f13c0. Visual Studio compiler creates function address table ILT which makes relative jump to the actual functions.

CppGeneral.cpp:

#include "stdafx.h"

int add(int a, int b);

typedef int (*PTR_ADD)(int, int);

int add(int a, int b){
    return a+b;
}

int _tmain(int argc, _TCHAR* argv[])
{
    PTR_ADD pAdd = add;

    _tprintf(_T("%d\n"), add(3, 4));
    _tprintf(_T("%d, address=%x\n"), pAdd(3, 4), pAdd);

    getchar();
    return 0;
}

Output:

7
7, address = f1096

Debug log:

0:001> uf CppGeneral!wmain
CppGeneral!wmain [c:\users\sokoide\projects\general\cppgeneral\cppgeneral.cpp @ 15]:
   15 000f1a70 55              push    ebp
   15 000f1a71 8bec            mov     ebp,esp
   15 000f1a73 81eccc000000    sub     esp,0CCh
   15 000f1a79 53              push    ebx
   15 000f1a7a 56              push    esi
   15 000f1a7b 57              push    edi
   15 000f1a7c 8dbd34ffffff    lea     edi,[ebp-0CCh]
   15 000f1a82 b933000000      mov     ecx,33h
   15 000f1a87 b8cccccccc      mov     eax,0CCCCCCCCh
   15 000f1a8c f3ab            rep stos dword ptr es:[edi]
   16 000f1a8e c745f896100f00  mov     dword ptr [ebp-8],offset CppGeneral!ILT+145(?addYAHHHZ) (000f1096)
   18 000f1a95 6a04            push    4
   18 000f1a97 6a03            push    3
   18 000f1a99 e8f8f5ffff      call    CppGeneral!ILT+145(?addYAHHHZ) (000f1096)
   18 000f1a9e 83c408          add     esp,8
   18 000f1aa1 8bf4            mov     esi,esp
   18 000f1aa3 50              push    eax
   18 000f1aa4 683c570f00      push    offset CppGeneral!`string' (000f573c)
   18 000f1aa9 ff15d4820f00    call    dword ptr [CppGeneral!_imp__wprintf (000f82d4)]
   18 000f1aaf 83c408          add     esp,8
   18 000f1ab2 3bf4            cmp     esi,esp
   18 000f1ab4 e887f6ffff      call    CppGeneral!ILT+315(__RTC_CheckEsp) (000f1140)
   19 000f1ab9 8bf4            mov     esi,esp
   19 000f1abb 8b45f8          mov     eax,dword ptr [ebp-8]
   19 000f1abe 50              push    eax
   19 000f1abf 8bfc            mov     edi,esp
   19 000f1ac1 6a04            push    4
   19 000f1ac3 6a03            push    3
   19 000f1ac5 ff55f8          call    dword ptr [ebp-8]
   19 000f1ac8 83c408          add     esp,8
   19 000f1acb 3bfc            cmp     edi,esp
   19 000f1acd e86ef6ffff      call    CppGeneral!ILT+315(__RTC_CheckEsp) (000f1140)
   19 000f1ad2 50              push    eax
   19 000f1ad3 68e05a0f00      push    offset CppGeneral!`string' (000f5ae0)
   19 000f1ad8 ff15d4820f00    call    dword ptr [CppGeneral!_imp__wprintf (000f82d4)]
   19 000f1ade 83c40c          add     esp,0Ch
   19 000f1ae1 3bf4            cmp     esi,esp
   19 000f1ae3 e858f6ffff      call    CppGeneral!ILT+315(__RTC_CheckEsp) (000f1140)
   21 000f1ae8 8bf4            mov     esi,esp
   21 000f1aea ff15d8820f00    call    dword ptr [CppGeneral!_imp__getchar (000f82d8)]
   21 000f1af0 3bf4            cmp     esi,esp
   21 000f1af2 e849f6ffff      call    CppGeneral!ILT+315(__RTC_CheckEsp) (000f1140)
   22 000f1af7 33c0            xor     eax,eax
   23 000f1af9 5f              pop     edi
   23 000f1afa 5e              pop     esi
   23 000f1afb 5b              pop     ebx
   23 000f1afc 81c4cc000000    add     esp,0CCh
   23 000f1b02 3bec            cmp     ebp,esp
   23 000f1b04 e837f6ffff      call    CppGeneral!ILT+315(__RTC_CheckEsp) (000f1140)
   23 000f1b09 8be5            mov     esp,ebp
   23 000f1b0b 5d              pop     ebp
   23 000f1b0c c3              ret


0:001> X CppGeneral!a*
000f7144 CppGeneral!argv = 0x003a1480
000f713c CppGeneral!argc = 0n1
000f13c0 CppGeneral!add (int, int)             // <-- add is at f13c0
000f2880 CppGeneral!atexit (<function> *)


0:001> u f1096
CppGeneral!ILT+145(?addYAHHHZ):
000f1096 e925030000      jmp     CppGeneral!add (000f13c0)   // <-- f1096 is calling jmp to f13c0


(It seems ILT is stored here)
0:001> u f1004 f1100
CppGeneral!_enc$textbss$end <PERF> (CppGeneral+0x11004):
000f1004 cc              int     3
CppGeneral!ILT+0(__setdefaultprecision):
000f1005 e936160000      jmp     CppGeneral!_setdefaultprecision (000f2640)
CppGeneral!ILT+5(_wmain):
000f100a e9610a0000      jmp     CppGeneral!wmain (000f1a70)
...
000f1096 e925030000      jmp     CppGeneral!add (000f13c0)
CppGeneral!ILT+150(__exit):
000f109b e938190000      jmp     CppGeneral!exit (000f29d8)
...


(add starts at f13c0)
0:001> uf CppGeneral!add
CppGeneral!add [c:\users\sokoide\projects\general\cppgeneral\cppgeneral.cpp @ 10]:
   10 000f13c0 55              push    ebp
   10 000f13c1 8bec            mov     ebp,esp
   10 000f13c3 81ecc0000000    sub     esp,0C0h
   10 000f13c9 53              push    ebx
   10 000f13ca 56              push    esi
   10 000f13cb 57              push    edi
   10 000f13cc 8dbd40ffffff    lea     edi,[ebp-0C0h]
   10 000f13d2 b930000000      mov     ecx,30h
   10 000f13d7 b8cccccccc      mov     eax,0CCCCCCCCh
   10 000f13dc f3ab            rep stos dword ptr es:[edi]
   11 000f13de 8b4508          mov     eax,dword ptr [ebp+8]
   11 000f13e1 03450c          add     eax,dword ptr [ebp+0Ch]
   12 000f13e4 5f              pop     edi
   12 000f13e5 5e              pop     esi
   12 000f13e6 5b              pop     ebx
   12 000f13e7 8be5            mov     esp,ebp
   12 000f13e9 5d              pop     ebp
   12 000f13ea c3              ret

16/32bit hello world

Continuation of the MASM book. Built 16/32bit exe.

hello16.asm

.286
.model small

end_process macro ret_value
            mov   al, ret_value
            mov   ah, 4ch
            int   21h
            endm

display     macro string
            mov   dx, offset string
            mov   ah, 09h
            int   21h
            endm

; AL <- keycode
read_kbd    macro
            mov   ah, 08h
            int   21h
            endm

.data
MSG         db 'Hello 16bit world.', 0dh, 0ah, '$'
MSG2        db '(hit any key).', 0dh, 0ah, '$'
MSGKEY      db '" " typed.', 0dh, 0ah, '$'

.code
START:
            mov   ax, _DATA
            mov   ds, ax

            display     MSG   ; Display message
            display     MSG2  ; Display message
            read_kbd          ; Wait for input

            mov [MSGKEY+1], AL
            mov BX, offset MSGKEY
            
            display     MSGKEY   ; Display
            end_process 0     ; exit


end START ; end

hello32.asm

; for pentium
.586
.model flat, stdcall
; ExitProcess != exitprocess with the option
option casemap:none

NULL            EQU 0
MB_OK           EQU 0
NUM             EQU 1

wsprintfA       proto c :dword, :dword, :dword, :dword, :dword
MessageBoxA     proto :dword, :dword, :dword, :dword
ExitProcess     proto :dword

.data

TITLE0          DB 'Assembler Test', 0
MESSAGE         DB 'Hello 32bit world!', 0
BUFFER          DB 64 DUP(0)

.code
WinMainCRTStartup   proc
    mov eax, NUM
    mov ecx, 10

MYLOOP:
    mov ebx, eax
    add eax, ebx
    dec ecx
    jnz MYLOOP

    ; offset -> address of the variable
    invoke MessageBoxA, NULL, offset MESSAGE, offset TITLE0, MB_OK
    invoke ExitProcess, 0

    ret
WinMainCRTStartup   endp
end

makefile

HELLO16 = hello16.exe
HELLO32 = hello32.exe

HELLO16OBJS = hello16.obj
HELLO32OBJS = hello32.obj

LDFLAGS = /LIBPATH:"C:\Program Files\Microsoft SDKs\Windows\v7.0A\Lib" /LIBPATH:"C:\Program Files\Microsoft Visual Studio 10.0\VC\lib"

hello16 : $(HELLO16OBJS)
	link16 $(HELLO16OBJS),$(HELLO16);

hello32 : $(HELLO32OBJS)
	link /NOLOGO /SUBSYSTEM:WINDOWS $(HELLO32OBJS) $(LDFLAGS) kernel32.lib user32.lib /OUT:$(HELLO32)
	
clean:
	del /f *.obj *.o *.exe *.lst

.SUFFIXES: .a16 .asm .obj

.a16.obj: 
	ml /c /Fl $<

.asm.obj: 
	ml /c /coff /Cp /nologo /Fl $< /Zi

MS-DOS COM

Read an old MASM book and build the first MS-DOS COM file. You need to donwnload 16bit linker to compile, and need exe2bin to convert from exe to com.

It sets AH/DL for Console input and does INT 21H to call it. Loops if no input (AL=0H).
CPU usage of NTVDM.exe (16bit host) was 25% on the 4CPU machine for the first 5-6 seconds (because it’s looping), then went down to 0%. Why?

AH=06H , DL=0FFH -> Console input (will get the char code in AL)
INT 21H -> DOS system call (no echo, no wait)

    MOV AH,06H
    MOV DL,0FFH
    INT 21H

Similarly, it sets AH=09H, DL= & does INT 21H to print it later. Then, AH=4CH means ‘process exit’ and AL=00H is the return code.

comtest.a16

.286

ASSUME CS:CODE,DS:CODE
CODE SEGMENT
    ORG 100H
START:
    MOV BX,0
NOINPUT:
    MOV AH,06H
    MOV DL,0FFH
    INT 21H
    JNZ PRINT
    INC BX
    CMP BX,5
    JGE START
    JMP NOINPUT
 PRINT:
    SHL BX,1
    MOV DX,TABLE[BX]
    MOV AH,09H
    INT 21H
    
    MOV AH,4CH
    MOV AL,00H
    INT 21H

FAIR    DB  'FAIR',0DH,0AH,'$'
CLOUDY  DB  'CLOUDY',0DH,0AH,'$'
RAINY   DB  'RAINY',0DH,0AH,'$'
RLATER  DB  'RAINY LATER CLOUDY',0DH,0AH,'$'
CLATER  DB  'CLOUDY LATER RAINY',0DH,0AH,'$'

TABLE   DW  OFFSET FAIR, OFFSET CLOUDY, OFFSET RAINY
        DW  OFFSET RLATER, OFFSET CLATER

 CODE ENDS
    END START

comtest.mak

COMTEST = comtest.exe
COMTESTCOM = comtest.com

COMTESTOBJS = comtest.obj

all : comtest

comtest : $(COMTESTOBJS)
	link16 $(COMTESTOBJS),$(COMTEST);
	exe2bin $(COMTEST) $(COMTESTCOM)

clean:
	del /f *.obj *.o *.exe *.lst

.SUFFIXES: .a16 .asm .obj

.a16.obj: 
	ml /c /Fl $<

.asm.obj: 
	ml /c /coff /Cp /nologo /Fl $< /Zi
1 2 3