Build Your Own OS #3

Niroshan Pushparaj
9 min readAug 6, 2021

interact with hardware

Hello everyone in the previous article we saw how connect C code with assembly language. In this article we will able to control hardware and printing some texts in our OS. End of this article we can control serial port also.

Interacting with the Hardware

There are two different ways to interact with the hardware, memory-mapped I/O and I/O ports. If the hardware uses memory-mapped I/O then you can write to a specific memory address and the hardware will be updated with the new data. Frame-buffer is memory mapped I/O method.

If the hardware uses I/O ports then the assembly code instructions out and in must be used to communicate with the hardware. The instruction out takes two parameters: the address of the I/O port and the data to send. The instruction in takes a single parameter, the address of the I/O port, and returns data from the hardware. One can think of I/O ports as communicating with hardware the same way as you communicate with a server using sockets. The cursor (the blinking rectangle) of the framebuffer is one example of hardware controlled via I/O ports on a PC.

The Frame buffer

The framebuffer is a hardware device that is capable of displaying a buffer of memory on the screen. The framebuffer has 80 columns and 25 rows, and the row and column indices start at 0 (so rows are labelled 0–24). The first cell corresponds to row zero, column zero on the console. Using an ASCII table, one can see that A corresponds to 65 or 0x41. Therefore, to write the character A with a green foreground (2) and dark grey background (8) at place (0,0). The second cell then corresponds to row zero, column one and its address is therefore:

0x000B8000 + 16 = 0x000B8010

Writing to the framebuffer can also be done in C by treating the address 0x000B8000 as a char pointer, char *fb = (char *) 0x000B8000. Then, writing A at place (0,0) with green foreground and dark grey background becomes:

fb[0] = 'A';
fb[1] = 0x28;

make some header files then define some functions.


#ifndef INCLUDE_IO_H
#define INCLUDE_IO_H
/** outb:
* Sends the given data to the given I/O port. Defined in io.s
* @param port The I/O port to send the data to
* @param data The data to send to the I/O port
void outb(unsigned short port, unsigned char data);
/* in file io.h *//** inb:
* Read a byte from an I/O port.
* @param port The address of the I/O port
* @return The read byte
unsigned char inb(unsigned short port);
#endif /* INCLUDE_IO_H */

Moving the curser

Moving the cursor of the framebuffer is done via two different I/O ports. The cursor’s position is determined with a 16 bits integer: 0 means row zero, column zero; 1 means row zero, column one; 80 means row one, column zero and so on. Since the position is 16 bits large, and the out assembly code instruction argument is 8 bits, the position must be sent in two turns, first 8 bits then the next 8 bits. The framebuffer has two I/O ports, one for accepting the data, and one for describing the data being received. Port 0x3D4 is the port that describes the data and port 0x3D5 is for the data itself. To set the cursor at row one, column zero (position 80 = 0x00500 )


#ifndef INCLUDE_FB_H
#define INCLUDE_FB_H
#include “io.h”/* The I/O ports */
#define FB_COMMAND_PORT 0x3D4
#define FB_DATA_PORT 0x3D5
/* The I/O port commands */
#define FB_GREEN 2
#define FB_DARK_GREY 8
char *fb = (char *) 0x00B8000;/** fb_write_cell:
* Writes a character with the given foreground and background to position i
* in the framebuffer.
* @param i The location in the framebuffer
* @param c The character
* @param fg The foreground color
* @param bg The background color
void fb_write_cell(unsigned int i, char c, unsigned char fg, unsigned char bg)
fb[i*2] = c;
fb[i*2 + 1] = ((fg & 0x0F) << 4) | (bg & 0x0F);
/** fb_move_cursor:
* Moves the cursor of the framebuffer to the given position
* @param pos The new position of the cursor
void fb_move_cursor(unsigned short pos)
outb(FB_DATA_PORT, ((pos >> 8) & 0x00FF));
outb(FB_DATA_PORT, pos & 0x00FF);
int write(char *buf, unsigned int len)
unsigned int i = 0;
for ( i = 0; i < len; i++)
fb_write_cell(i, buf[i], FB_GREEN, FB_DARK_GREY);
return 0;

The Serial Ports

The serial port is an interface for communicating between hardware devices and although it is available on almost all motherboards, it is seldom exposed to the user in the form of a DE-9 connector nowadays. The serial port is easy to use, and, more importantly, it can be used as a logging utility in Bochs. If a computer has support for a serial port, then it usually has support for multiple serial ports, but we will only make use of one of the ports. This is because we will only use the serial ports for logging. Furthermore, we will only use the serial ports for output, not input. The serial ports are completely controlled via I/O ports.

Configuring the Serial Port

The first data that need to be sent to the serial port is configuration data. In order for two hardware devices to be able to talk to each other they must agree upon a couple of things. These things include:

  • The speed used for sending data (bit or baud rate)
  • If any error checking should be used for the data (parity bit, stop bits)
  • The number of bits that represent a unit of data (data bits)

Configuring the Line

Configuring the line means to configure how data is being sent over the line. The serial port has an I/O port, the line command port, that is used for configuration.

First the speed for sending data will be set. The serial port has an internal clock that runs at 115200 Hz. Setting the speed means sending a divisor to the serial port, for example sending 2 results in a speed of 115200 / 2 = 57600 Hz.

The divisor is a 16 bit number but we can only send 8 bits at a time. We must therefore send an instruction telling the serial port to first expect the highest 8 bits, then the lowest 8 bits. This is done by sending 0x80 to the line command port.


#include “io.h” /* io.h is implement in the section “Moving the cursor” *//* The I/O port com1: enabled=1, mode=file, dev=com1.outs */
/* All the I/O ports are calculated relative to the data port. This is because
* all serial ports (COM1, COM2, COM3, COM4) have their ports in the same
* order, but they start at different values.
#define SERIAL_COM1_BASE 0x3F8 /* COM1 base port */#define SERIAL_DATA_PORT(base) (base)
#define SERIAL_FIFO_COMMAND_PORT(base) (base + 2)
#define SERIAL_LINE_COMMAND_PORT(base) (base + 3)
#define SERIAL_MODEM_COMMAND_PORT(base) (base + 4)
#define SERIAL_LINE_STATUS_PORT(base) (base + 5)
/* The I/O port commands *//* SERIAL_LINE_ENABLE_DLAB:
* Tells the serial port to expect first the highest 8 bits on the data port,
* then the lowest 8 bits will follow
void serial_configure_baud_rate(unsigned short com, unsigned short divisor) {
/* Tell the serial port to first expect the highest 8 bits, then the lowest
* 8 bits. This is done by sending 0x80 to the line command port
outb(SERIAL_DATA_PORT(com), (divisor >> 8) & 0x00FF);
outb(SERIAL_DATA_PORT(com), divisor & 0x00FF);
void serial_configure_line(unsigned short com) {
/* Bit: | 7 | 6 | 5 4 3 | 2 | 1 0 |
* Content: | d | b | prty | s | dl |
* Value: | 0 | 0 | 0 0 0 | 0 | 1 1 | = 0x03
* data length of 8 bits, one stop bit, no parity bit, break control
* disabled and DLAB disabled
outb(SERIAL_LINE_COMMAND_PORT(com), 0x03);
void serial_configure_fifo_buffer(unsigned short com) {
/* Bit: | 7 6 | 5 | 4 | 3 | 2 | 1 | 0 |
* Content: | lvl | bs | r | dma | clt | clr | e |
* Value: | 1 1 | 0 | 0 | 0 | 1 | 1 | 1 | = 0xC7
void serial_configure_modem(unsigned short com) {
/* Bit: | 7 | 6 | 5 | 4 | 3 | 2 | 1 | 0 |
* Content: | r | r | af | lb | ao2 | ao1 | rts | dtr |
* Value: | 0 | 0 | 0 | 0 | 0 | 0 | 1 | 1 | = 0x03
int serial_is_transmit_fifo_empty(unsigned short com) {
/* 0x20 = 0010 0000 */
return inb(SERIAL_LINE_STATUS_PORT(com)) & 0x20;
void serial_configure(unsigned short port, unsigned short baudRate) {
serial_configure_baud_rate(port, baudRate);
void serial_write_byte(unsigned short port, char byteData) {
outb(port, byteData);
int serial_write(unsigned short com, char *buf, unsigned int len) {
unsigned int indexToBuffer = 0;
while (indexToBuffer < len) {
if (serial_is_transmit_fifo_empty(com)) {
serial_write_byte(com, buf[indexToBuffer]);
return 0;

make a assembly file io.s

global outb ; make the label outb visible outside this file; outb — send a byte to an I/O port
; stack: [esp + 8] the data byte
; [esp + 4] the I/O port
; [esp ] return address
mov al, [esp + 8] ; move the data to be sent into the al register
mov dx, [esp + 4] ; move the address of the I/O port into the dx register
out dx, al ; send the data to the I/O port
ret ; return to the calling function
global inb; inb — returns a byte from the given I/O port
; stack: [esp + 4] The address of the I/O port
; [esp ] The return address
mov dx, [esp + 4] ; move the address of the I/O port to the dx register
in al, dx ; read a byte from the I/O port and store it in the al register
ret ; return the read byte

write a C code for printing letters kmain.c

#include “io.h”
#include “fb.h”
#include “serial_port.h”
void run(){
char c[] = “Welcome”;
write(c, 9);
serial_write(SERIAL_COM1_BASE, c, 7);

Call that C code into loader file

global loader ; the entry symbol for ELFMAGIC_NUMBER equ 0x1BADB002 ; define the magic number constant
FLAGS equ 0x0 ; multiboot flags
CHECKSUM equ -MAGIC_NUMBER ; calculate the checksum
; (magic number + checksum + flags should equal 0)
KERNEL_STACK_SIZE equ 4096 ; size of stack in bytes
section .bss
align 4 ; align at 4 bytes
kernel_stack: ; label points to beginning of memory
resb KERNEL_STACK_SIZE ; reserve stack for the kernel
section .text: ; start of the text (code) section
align 4 ; the code must be 4 byte aligned
dd MAGIC_NUMBER ; write the magic number to the machine code,
dd FLAGS ; the flags,
dd CHECKSUM ; and the checksum
loader: ; the loader label (defined as entry point in linker script)
mov eax, 0xCAFEBABE ; place the number 0xCAFEBABE in the register eax
mov esp, kernel_stack + KERNEL_STACK_SIZE ; point esp to the start of the
; stack (end of memory area)
; Calling C code
extern run
call run
jmp .loop ; loop forever

to convert io.s file into object file we should make some changes in our make file

OBJECTS = loader.o kmain.o io.o
CC = gcc
CFLAGS = -m32 -nostdlib -nostdinc -fno-builtin -fno-stack-protector \
-nostartfiles -nodefaultlibs -Wall -Wextra -Werror -c
LDFLAGS = -T link.ld -m elf_i386
AS = nasm
ASFLAGS = -f elf
all: kernel.elfkernel.elf: $(OBJECTS)
ld $(LDFLAGS) $(OBJECTS) -o kernel.elf
baseOS.iso: kernel.elf
cp kernel.elf iso/boot/kernel.elf
genisoimage -R \
-b boot/grub/stage2_eltorito \
-no-emul-boot \
-boot-load-size 4 \
-A os \
-input-charset utf8 \
-quiet \
-boot-info-table \
-o baseOS.iso \
run: baseOS.iso
bochs -f bochsrc.txt -q
%.o: %.c
$(CC) $(CFLAGS) $< -o $@
%.o: %.s
$(AS) $(ASFLAGS) $< -o $@
rm -rf *.o kernel.elf baseOS.iso

Make some changes in your bochs configure file for get serialoutput.

megs: 32
display_library: sdl
romimage: file=/usr/share/bochs/BIOS-bochs-latest
vgaromimage: file=/usr/share/bochs/VGABIOS-lgpl-latest
ata0-master: type=cdrom, path=baseOS.iso, status=inserted
boot: cdrom
log: bochslog.txt
clock: sync=realtime, time0=local
cpu: count=1, ips=1000000
com1: enabled=1, mode=file, dev=com1.out

after creating above files you can run your OS. Now ypu have successfully displayed “Welcome “ in your kernel.

Hope you have get some some knowledge about printing in display. Keep in touch with comming articles for more knowledge about OS making.

Thank you!!!