NOTE This code won't work at the application level. It directly addresses memory at 0xB8000 which will result in a segfault at the application level. This code must be part of an OS or an application in a not-so-secure operating system like DOS, where applications can directly address any part of memory.

This is some simple code which facilitates the printing of strings, without needing to use an interrupt like int 0x10. This gives you the foundations so that you can better customise aspects of how printing is done, so that you can, say, make special control characters or speed it up by reading and writing 32 or 64 bits at a time. I've pulled this code up from the remnants of the OS project that I ditched.

This code is simple enough (and has enough comments) that it should explain itself, but I'll highlight some quick points about how VGA text memory is organised at 0xB8000. For each of the 2000 characters on a 80x25 text screen, there are two bytes; the first byte is for the ASCII value of the character, and the second byte is the colour code for the character. This snippet uses the classic white-on-black as a hard-coded value for all character colours

This means that for each row on the screen (80 columns), there are 160 bytes allocated. Hence when we encounter a carriage return in the string, we simply add 160 to the offset in VGA memory for the next byte to be printed.

The LF handler is what confuses most people. Here's a numerical example:

  • The current VGA pointer is 400.
  • We divide it by 160, resulting in an answer of 2 (remember that div keeps the remainder in a separate register from the answer)
  • We multiply 160 by 2 (the answer to our division), giving 320 as the next place to write a byte

So all of a sudden, we've got a memory pointer that points to the start of the current line. Makes sense?

To use this, just point to a null-terminated string in DS:SI, then call this function.

Edited 3 Years Ago by Assembly Guy: Formatting in code - tabs

kernel_print:
	mov	ax, 0xB800
	mov	es, ax
	mov	di, [position]		; Load DI with offset for next character to print
.loop:
	lodsb				; Load next byte from the string into AL
	cmp	al, 0			; Test for end-of-string null-termination byte
	jz	.quit			; Stop printing if we're at the end of string
	
	cmp	al, 8d			; Is the character a backspace?
	jz	.bs			; If so, jump to special handler
	cmp	al, 13d			; Is the byte a CR?
	jz	.cr			; if so, jump to special handler
	cmp	al, 10d			; Is the byte an LF?
	jz	.lf			; Jump to special handler if it is
	
	stosb				; Store the byte in VGA memory.
	mov	al, 0x07		; 0x07 = grey on black text colour
	stosb
	jmp	.loop
.cr:
	add	di, 160d		; Add 160 to the byte offset, moving it down one line
	jmp	.loop
.lf:
	mov	ax, di			; Move our current position/pointer into AX
	xor	dx, dx
	mov	di, 160d		; Load DI with 160 (# of bytes per line)
	div	di			; AX = AX / DI  and  DX = remainder
	mul	di			; AX = AX * DI
	mov	di, ax			; AX has been divided and had the remainder truncated
	jmp	.loop						

.quit:
	mov	[position], di		; Move the new cursor position into a safe memory location
	ret				; Return

position dw 0

mov al, 0x07 ; 0x07 = grey on black text colour
stosb

Why not load ah with 7 outside of the loop, and then do a stosw inside the loop?

Edited 3 Years Ago by mathematician

Good pointing that out - I adapted this code from some old kernel code I'd written years ago, and the context in which it was being used meand that it couldn't be optimised that way. That aside, I agree, that would certainly make the algorithm more efficient.

Edited 3 Years Ago by Assembly Guy

The article starter has earned a lot of community kudos, and such articles offer a bounty for quality replies.