If you are intent on using GCC with -m16
then it's automatically assumed that you will be running on an 80386+. I can't stress this enough, using GCC to create 16-bit code is fraught with pitfalls. It's made worse by your choice to put your kernel at 0x10000 in memory. 0x10000 can't be represented as a 16-bit offset and this can cause GCC to emit code that may not work, especially if you ever want to turn on optimizations with -O1
, -O2
, -O3
etc. Even accessing global variables may cause issues!
HIGHLY RECOMMEND (almost required to avoid most hassles): You may have fewer issues if you put your kernel and its data in the first 64kb of memory. An origin at memory address 0x00520 is just above the BIOS data area and reserved area of lower memory.
Be forewarned: GCC with -m16
targeting real mode is USE AT YOUR ON RISK. You may lose your sanity as well. Putting the processor into 32-bit protected mode with a flat memory model (extending from 0 to 0xffffffff) where CS=DS=ES is ideal for GCC
This code makes the assumption that you are not in unreal mode, although your system likely is in that mode.
GCC assumes that CS=DS=ES, and that the memory model is flat. It's generally not a good idea to change ES. It's possible to use ES if you save it, do work, and restore it all without intervening C code in between. Since GCC requires 80386, we can use one of the other segment registers: FS and GS. In this example we'll use FS.
One other prerequisite is that you understand Real Mode Segmentation. I assume you do since you have created a bootloader. The calculation for physical memory address is:
Physical memory address = (segment << 4) + offset
Text mode (color) video memory is at physical address 0xb8000. The base of that memory can be represented as a segment:offset pair of 0xb800:0x0000 since:
(0xb800 << 4) + 0x0000 = 0xb8000
Each cell on the visible screen is a WORD (16-bit). The upper 8 bits of the WORD are an attribute and the lower are the character as detailed at the link. The color palette is described in this Wiki page.
If we use FS as our segment, we can set it to 0xb800 and reference the video memory with it. Since your code may eventually use FS for a variety of things, we'll save it using some inline assembler code, do work on the video memory, and restore FS to what it was previously.
Since I am using inline assembler you may wish to look at Peter Corde's list of useful links on the subject.
The code that takes the above into account, and provides a mechanism for updating the screen at a row, col with an attribute via the FS segment register that we set to 0xb800.
There is more code than you may have liked but I wanted to show more than outputting a single character. The code comments may help you get on your way.
#include <stdint.h>
/* use regparm(3) to use convention where first three
* integer sized parameters are passed in registers (EAX, EDX, ECX) rather
* than the stack. regparm(0) is default CDECL stack based
* parameter passing. regparm(3) is generally faster overall, compared
* to passing all parameters on the stack. Internally, the Linux kernel
* uses this convention to reduce stack overhead when functions
* are called across different kernel modules.
*/
#define fastcall __attribute__((regparm(3)))
#define asmlinkage __attribute__((regparm(0)))
/* Global functions that will be exported */
extern fastcall void dispchar(uint16_t celldata, uint16_t offset);
extern fastcall void dispstring(const char *outstring, uint8_t attr,
uint16_t offset);
extern fastcall void dispchar_nofsupd(uint16_t celldata, uint16_t offset);
extern fastcall void dispstring_nofsupd(const char *outstring, uint8_t attr,
uint16_t offset);
extern fastcall uint32_t getset_fs(uint32_t segment);
extern fastcall void set_fs(uint32_t segment);
extern fastcall uint32_t set_videomode_fs(void);
static inline uint16_t
tm_rowcol_to_vidoffset(uint16_t row, uint16_t col, uint16_t numcols);
static inline uint16_t
tm_charattr_to_celldata(uint8_t ochar, uint8_t attr);
/*----------------------------------------------------------*/
#define COLSPERROW 80
#define ROW 3
#define COL 40
#define RED_ON_BLACK 4 /* attribute= Red character on black background */
#define MAGENTA_ON_BLACK 5 /* attribute= Magenta character on black background */
/* Color text mode memory segment */
#define VIDEO_SEG 0xb800
/* Place main before all other code */
int
_main()
{
/* Set FS to video mode segment and save previous value of FS */
uint32_t oldfs = set_videomode_fs();
dispchar_nofsupd(tm_charattr_to_celldata('A', RED_ON_BLACK),
tm_rowcol_to_vidoffset(ROW, COL, COLSPERROW));
dispchar_nofsupd(tm_charattr_to_celldata('B', RED_ON_BLACK),
tm_rowcol_to_vidoffset(ROW, COL + 1, COLSPERROW));
dispchar_nofsupd(tm_charattr_to_celldata(' ', RED_ON_BLACK),
tm_rowcol_to_vidoffset(ROW, COL + 2, COLSPERROW));
dispstring_nofsupd("Hello World", RED_ON_BLACK,
tm_rowcol_to_vidoffset(ROW, COL + 3, COLSPERROW));
/* Restore FS to original value when finished doing video mode work */
set_fs(oldfs);
/* Display Hello World using version dispstring
* that saves/restores FS automatically */
dispstring("Hello World", MAGENTA_ON_BLACK,
tm_rowcol_to_vidoffset(ROW+1, COL + 3, COLSPERROW));
return 0;
}
/* Convert Text Mode(TM) row, col, numcols
* to a video offset. numcols is the number of columns
* per row. Return value is a BYTE offset (not WORD)
*/
static inline uint16_t
tm_rowcol_to_vidoffset(uint16_t row, uint16_t col, uint16_t numcols)
{
return ((row * numcols + col) * 2);
}
static inline uint16_t
tm_charattr_to_celldata(uint8_t ochar, uint8_t attr)
{
return (uint16_t) (attr << 8) | (uint8_t) ochar;
}
/* Display character with FS change */
fastcall void
dispchar(uint16_t celldata, uint16_t offset)
{
uint32_t oldfs = set_videomode_fs();
dispchar_nofsupd(celldata, offset);
set_fs(oldfs);
}
/* Display character with no FS change */
fastcall void
dispchar_nofsupd(uint16_t celldata, uint16_t offset)
{
__asm__ ("movw %w[wordval], %%fs:%[memloc]\n\t"
:
:[wordval]"ri"(celldata),
[memloc] "m"(*(uint32_t *)(uint32_t)offset)
:"memory");
}
/* Set FS segment and return previous value */
fastcall uint32_t
getset_fs(uint32_t segment)
{
uint32_t origfs;
__asm__ __volatile__("mov %%fs, %w[origfs]\n\t"
"mov %w[segment], %%fs\n\t"
:[origfs] "=&rm"(origfs)
:[segment] "rm"(segment));
return origfs;
}
/* Set FS segment */
fastcall void
set_fs(uint32_t segment)
{
__asm__("mov %w[segment], %%fs\n\t"
:
:[segment]"rm"(segment));
}
/* Set FS to video mode segment 0xb800 */
fastcall uint32_t
set_videomode_fs(void)
{
return getset_fs(VIDEO_SEG);
}
/* Display string with FS change */
fastcall void
dispstring(const char *outstring, uint8_t attr, uint16_t offset)
{
uint32_t oldfs = set_videomode_fs();
dispstring_nofsupd(outstring, attr, offset);
set_fs(oldfs);
}
/* Display string with FS change */
fastcall void
dispstring_nofsupd(const char *outstring, uint8_t attr, uint16_t offset)
{
const char *curchar = outstring;
int i = 0;
for (; *curchar; curchar++, i++)
dispchar_nofsupd(tm_charattr_to_celldata(*curchar, attr),
offset + i * 2);
}
Linker script for GCC on Windows
Your kernel.bin
may become larger than you expect when using GCC under windows. This is because of the default alignment rules that GCC is using. The following linker script may help reduce the size:
ENTRY(__main);
OUTPUT(i386pe);
SECTIONS
{
__kernelbase = 0x520;
. = __kernelbase;
.text : SUBALIGN(4) {
*(.text.st);
*(.text);
}
.data :
SUBALIGN(4) {
__data_start = .;
*(.rdata*);
*(.data);
__data_end = .;
__bss_start = .;
*(COMMON);
*(.bss);
__bss_end = .;
}
}
This script is set to an ORG of 0x520 (not 0x10000). As mentioned previously it's highly recommended to not use an origin of 0x10000 as you have been with 16-bit GCC generated code. Name the linker script linker.ld
and then you can use these commands to assembler and link the kernel:
gcc -ffreestanding -c -m16 kernel.c -o kernel.o -O3
ld -o kernel.pe kernel.o -Tlinker.ld
objcopy -O binary kernel.pe kernel.bin
You would have to modify your bootloader to read kernel sectors into memory starting at address 0x520.
With a simple bootloader and this kernel built using the provided code/linker script, this is what Bochs shows when it's run:
A Look at some of the Generated Code
The first few lines of function main
save the current FS register, set FS to the video segment 0xb800 and prints out 3 characters:
int
_main()
{
/* Set FS to video mode segment and save previous value of FS */
uint32_t oldfs = set_videomode_fs();
dispchar_nofsupd(tm_charattr_to_celldata('A', RED_ON_BLACK),
tm_rowcol_to_vidoffset(ROW, COL, COLSPERROW));
dispchar_nofsupd(tm_charattr_to_celldata('B', RED_ON_BLACK),
tm_rowcol_to_vidoffset(ROW, COL + 1, COLSPERROW));
dispchar_nofsupd(tm_charattr_to_celldata(' ', RED_ON_BLACK),
tm_rowcol_to_vidoffset(ROW, COL + 2, COLSPERROW));
dispstring_nofsupd("Hello World", RED_ON_BLACK,
tm_rowcol_to_vidoffset(ROW, COL + 3, COLSPERROW));
[code that prints strings has been snipped for brevity]
set_fs(oldfs);
The code generated can be seen by using this objdump
command:
objdump -Dx kernel.pe --no-show-raw-insn -mi8086 -Mintel
The Intel syntax output is as follows with my compiler (using -O3
optimizations):
00000520 <__main>:
520: push esi ; Save register contents
522: mov eax,0xb800
528: push ebx ; Save register contents
52a: mov si,fs ; Save old FS to SI
52d: mov fs,ax ; Update FS with 0xb800 (segment of video)
52f: mov WORD PTR fs:0x230,0x441 ; 0x441 = Red on black Letter 'A'
; Write to offset 0x230 ((80*3+40)*2) row=3,col=40
536: mov WORD PTR fs:0x232,0x442 ; 0x442 = Red on black Letter 'B'
; Write to offset 0x232 ((80*3+41)*2) row=3,col=41
53d: mov WORD PTR fs:0x234,0x420 ; 0x420 = Red on black space char
; Write to offset 0x234 ((80*3+42)*2) row=3,col=42
This line of C code that restored FS:
set_fs(oldfs);
With this instructions later on:
571: mov fs,si ; Restore original value previously saved in SI
I've annotated the disassembly with comments to show how each of the WORD values were updated in video display memory. A lot of lines of C code, but the output is very simple.
make_vgaentry
in the link). – Polypody