In this article will guide you through creating printing function in Stage 2 of a bootloader, specifically in VGA text mode (Mode 3), using C
.
As we know that by default BIOS put us in VGA text mode (Mode 3)
which operates with a 80x25
character display. Each character on the screen is represented by two bytes in memory: First byte for the ASCII character and Second one for the attribute (color). The memory for VGA text mode starts at address 0xB8000
. As we are in protected mode, we don't have access to BIOS interrupts. We will directly write to the video memory.
Layout of Video Memory
1 Address Range: Video memory for text mode typically starts at the address 0xB8000
and spans 4,000 bytes (80 columns x 25 rows x 2 bytes per character).
2 Character Representation: Each character on the screen is represented by 2 bytes:
Byte1
: The first byte is the ASCII code of the character.Byte 2
: The second byte is the attribute byte, which defines the foreground and background colors.
-: Visual Representation :-
Address Content
0xB8000 ASCII of character 1 in row 1, column 1
0xB8001 Attribute of character 1 in row 1, column 1
0xB8002 ASCII of character 2 in row 1, column 2
0xB8003 Attribute of character 2 in row 1, column 2
...
0xB8FA0 ASCII of character 80 in row 25, column 80
0xB8FA1 Attribute of character 80 in row 25, column 80
Memory Address | Screen Position
-------------------------------------
0xB8000 (ASCII) | Row 1, Column 1
0xB8001 (Attr) |
0xB8002 (ASCII) | Row 1, Column 2
0xB8003 (Attr) |
... | ...
0xB80A0 (ASCII) | Row 2, Column 1
0xB80A1 (Attr) |
... | ...
0xB8FA0 (ASCII) | Row 25, Column 80
0xB8FA1 (Attr) |
Attribute Byte
The attribute byte determines the colors of the text and background. It's an 8-bit value:
- Bits 0-3: Foreground color (text color)
- Bits 4-6: Background color
- Bit 7: Blink attribute (1 = blinking, 0 = not blinking)
Foreground Color (Bits 0-3):
- 0000: Black
- 0001: Blue
- 0010: Green
- 0011: Cyan
- 0100: Red
- 0101: Magenta
- 0110: Brown
- 0111: Light Gray
- 1000: Dark Gray
- 1001: Light Blue
- 1010: Light Green
- 1011: Light Cyan
- 1100: Light Red
- 1101: Light Magenta
- 1110: Yellow
- 1111: White
Background Color (Bits 4-6):
- 000: Black
- 001: Blue
- 010: Green
- 011: Cyan
- 100: Red
- 101: Magenta
- 110: Brown
- 111: Light Gray
Example:
- Attribute byte
0x1F
:- Foreground: White (
1111
) - Background: Blue (
001
)
- Foreground: White (
-: Code :-
Color Enumerations
Let's first make the enumerations for the foreground and background colors, which help make our code more readable and maintainable.
// Foreground colors
enum VGA_ForegroundColor {
FG_BLACK = 0x0,
FG_BLUE = 0x1,
FG_GREEN = 0x2,
FG_CYAN = 0x3,
FG_RED = 0x4,
FG_MAGENTA = 0x5,
FG_BROWN = 0x6,
FG_LIGHT_GRAY = 0x7,
FG_DARK_GRAY = 0x8,
FG_LIGHT_BLUE = 0x9,
FG_LIGHT_GREEN = 0xA,
FG_LIGHT_CYAN = 0xB,
FG_LIGHT_RED = 0xC,
FG_LIGHT_MAGENTA= 0xD,
FG_YELLOW = 0xE,
FG_WHITE = 0xF
};
// Background colors
enum VGA_BackgroundColor {
BG_BLACK = 0x0 << 4,
BG_BLUE = 0x1 << 4,
BG_GREEN = 0x2 << 4,
BG_CYAN = 0x3 << 4,
BG_RED = 0x4 << 4,
BG_MAGENTA = 0x5 << 4,
BG_BROWN = 0x6 << 4,
BG_LIGHT_GRAY = 0x7 << 4
};
// Combine foreground and background colors
#define VGA_COLOR(fg, bg) ((fg) | (bg))
Constants
Declare some constants which we will use a lot of times in our code, such that in change at one place reflects at every place it is used.
#define SCREEN_WIDTH 80 // Mode 3 Screen Width | Column
#define SCREEN_HEIGHT 25 // Mode 3 Screen Height | Row
#define VIDEO_MEMORY 0xB8000
Print Char to Screen at Particular Position
To print a character at a specific position, we need to calculate the offset in video memory based on the row and column.
- Calculate Offset:
- Offset = (row * 80 + column)
80
represents the number of columns per row.
- Offset = (row * 80 + column)
- Access Video Memory:
- Start from the base address
0xB8000
. - Add the calculated offset to get the memory location where the character and its attribute are stored.
- Start from the base address
unsigned short *videoMem = (unsigned short*) VIDEO_MEMORY;
unsigned char attribute = VGA_COLOR(FG_WHITE, BG_BLACK);
void print_char_at(char c, int row, int col) {
// Get Offset by row and column
int offset = row * SCREEN_WIDTH + col;
videoMem[offset] = (attribute << 8) | c;
}
// Usage:-
// Row = 0
//Column = 0
print_char_at('J', 0, 0);
Visualization Example:
Let's visualize printing the character 'A' at position (10, 20)
with white text on a blue background.
1 Calculate Offset:
- Row = 10, Column = 20
- Offset = (10 * 80 + 20)
- Offset = 800 + 20
- Offset = 820 (unsigned short) = 820*2 = 1640 (unsigned char)
2 Memory Address:
- Video Memory Base:
0xB8000
- Memory Address =
0xB8000 + 820
- Memory Address =
0xB8640
- Memory Address =
3 Data Representation (unsigned short):
- ASCII Code ('A') =
0x41
(decimal 65). - Attribute (FG_WHITE | BG_BLUE) =
0x1F
(assuming white text on blue background).
4 Video Memory Layout:
Memory Address | Content (unsigned short)
-----------------------------------------------
0xB8640 | 0x1F41
In memory:
- The lower byte (
0x41
) represents the ASCII character ('A'). - The higher byte (
0x1F
) represents the attribute byte (FG_WHITE | BG_BLUE).
Printing String
int row = 0;
int column = 0;
void boot_print(char *str) {
while (*str) {
if (*str == '\n') {
// Move to the next line and reset column
row++;
column = 0;
} else {
// Print character at current position
print_char_at(*str, row, column);
column++;
if (column >= SCREEN_WIDTH) {
// Move to the next line if the end of the current line is reached
column = 0;
row++;
}
}
// Scroll the screen if necessary
if (row >= SCREEN_HEIGHT) {
// Either scroll or do wraparound
boot_scroll();
row = SCREEN_HEIGHT - 1;
}
str++;
}
}
Scroll Window
We need to handle the case where the text reaches the bottom of the screen.
When the cursor reaches the end of the screen, then we have two solutions of printing the next character.
- Wraparound: Start printing from the top of the screen again.
- Scrolling: Shift all rows up by one, clear the last row, and continue printing on the last row.
We will make use of Scrolling
screen. In the scrolling option, the screen scrolls up by one row when the bottom is reached, making space for new text at the bottom:
void boot_scroll() {
// Scroll up by copying each line up one row
for (int i = 0; i < SCREEN_HEIGHT - 1; ++i) {
for (int j = 0; j < SCREEN_WIDTH; ++j) {
videoMem[i * SCREEN_WIDTH + j] = videoMem[(i + 1) * SCREEN_WIDTH + j];
}
}
// Clear the last line
for (int j = 0; j < SCREEN_WIDTH; ++j) {
videoMem[(SCREEN_HEIGHT - 1) * SCREEN_WIDTH + j] = (attribute << 8) | ' ';
}
}
- Scroll Up: Copies each row of the screen to the row above it.
- Clear Last Line: Clears the last line after scrolling by filling it with spaces.
Clear Screen
void clear_screen() {
unsigned short blank = (attribute << 8) | ' ';
for (int row = 0; row < SCREEN_HEIGHT; ++row) {
for (int column = 0; column < SCREEN_WIDTH; ++column) {
videoMem[row * SCREEN_WIDTH + column] = blank;
}
}
// Reset global row and column to 0.
row = 0;
column = 0;
}
Print Hexadecimal
void boot_print_hex(unsigned int value) {
char hex_chars[] = "0123456789ABCDEF";
char hex_str[9]; // 8 hex digits + null terminator
hex_str[8] = '\0'; // Null terminator
for (int i = 7; i >= 0; --i) {
hex_str[i] = hex_chars[value & 0xF];
value >>= 4;
}
boot_print(hex_str);
}
Step-by-Step Breakdown
- Initialize Hex Characters: The string
hex_chars
contains all possible hexadecimal characters (0-9 and A-F).char hex_chars[] = “0123456789ABCDEF”;
- Prepare Hex String:
hex_str
is an array of 9 characters, enough to hold 8 hex digits and a null terminator.char hex_str[9];
- Null Terminator: The last element of
hex_str
is set to\0
to ensure it is a valid C string.hex_str[8] = ‘\0’;
// Null terminator
- Convert to Hex: A loop iterates over each nibble (4 bits) of the input integer, starting from the least significant nibble. For each iteration:
- The current value of
value
is bitwise ANDed with0xF
to isolate the last 4 bits (one hex digit). - The corresponding hex character is placed in the
hex_str
array. value
is right-shifted by 4 bits to process the next nibble.
- The current value of
- Print the Hex String: The
scrolling_print
function is called to print thehex_str
to the screen.
Visualization:
1. Initialize Hex Characters
char hex_chars[] = "0123456789ABCDEF";
2. Prepare Hex String
char hex_str[9];
hex_str[8] = '\0'; // Null terminator
3. Convert to Hex
Let's say the input value
is 0x1234ABCD
.
- Initialize
hex_str
to contain only the null terminator.
hex_str = "00000000\0"
- Start the loop to process each nibble from least significant to most significant:
First Iteration (i = 7):
- Isolate the last nibble:
value & 0xF
→0xD
(13 in decimal). hex_chars[13]
→D
- Update
hex_str[7]
toD
. - Right shift
value
:0x1234ABCD >> 4
→0x01234ABC
hex_str = "0000000D\0"
Second Iteration (i = 6):
- Isolate the last nibble:
value & 0xF
→0xC
(12 in decimal). hex_chars[12]
→C
- Update
hex_str[6]
toC
. - Right shift
value
:0x01234ABC >> 4
→0x001234AB
hex_str = "000000CD\0"
Third Iteration (i = 5):
- Isolate the last nibble:
value & 0xF
→0xB
(11 in decimal). hex_chars[11]
→B
- Update
hex_str[5]
toB
. - Right shift
value
:0x001234AB >> 4
→0x0001234A
hex_str = "00000BCD\0"
Fourth Iteration (i = 4):
- Isolate the last nibble:
value & 0xF
→0xA
(10 in decimal). hex_chars[10]
→A
- Update
hex_str[4]
toA
. - Right shift
value
:0x0001234A >> 4
→0x00001234
hex_str = "0000ABCD\0"
Fifth Iteration (i = 3):
- Isolate the last nibble:
value & 0xF
→0x4
. hex_chars[4]
→4
- Update
hex_str[3]
to4
. - Right shift
value
:0x00001234 >> 4
→0x00000123
hex_str = "0004ABCD\0"
Sixth Iteration (i = 2):
- Isolate the last nibble:
value & 0xF
→0x3
. hex_chars[3]
→3
- Update
hex_str[2]
to3
. - Right shift
value
:0x00000123 >> 4
→0x00000012
hex_str = "0034ABCD\0"
Seventh Iteration (i = 1):
- Isolate the last nibble:
value & 0xF
→0x2
. hex_chars[2]
→2
- Update
hex_str[1]
to2
. - Right shift
value
:0x00000012 >> 4
→0x00000001
hex_str = "0234ABCD\0"
Eighth Iteration (i = 0):
- Isolate the last nibble:
value & 0xF
→0x1
. hex_chars[1]
→1
- Update
hex_str[0]
to1
. - Right shift
value
:0x00000001 >> 4
→0x00000000
hex_str = "1234ABCD\0"