Displaying text video memory at 0xb8000 without using the C library
Asked Answered
G

1

5

I have been writing kernel in C. I've been using the GCC cross-compiler, writing on a Windows system and targeting 16bit Real Mode. I don't have the C library available to write the kernel. I have started with some code which is suppose to print a character directly to the screen. Here is a function from kernel.c:

int main()
{
  char *src = (char *)0xB8000000L;
  *src = 'M';
  src += 2;
  *src = 'D';
  return 0;
}

I compiled my code using GCC with the parameter -m16 to generate code that will run in real mode. I use these commands to generate my kernel.bin:

gcc -ffreestanding -c -m16 kernel.c -o kernel.o
ld -Ttext 0x10000 -o kernel.pe kernel.o
objcopy -O binary kernel.pe kernel.bin

Stack Overflow user Michael Petch solved my linker problem but commented the code itself is incorrect. He made this comment:

Besides the linker problem, are you trying to convert old TurboC/MSVC 16-bit code to GCC? I find (char *)0xB8000000L suspicious. If it was a true 16-bit C compiler it might be okay if it was (char far *)0xB8000000L. GCC is not a true 16-bit C compiler and doesn't have a notion of old style far pointers. So even if you get this code to compile this may not do what you think it does, I'm assuming from the -m16 option with GCC you are trying to create a real-mode 16-bit kernel (rather than a protected mode one)?

I have been trying to implement my own printf like function in C for my own operating system. The code I provided above is only a small extract of what I understand. I have created a bootloader in assembly(8086).

Is Michael correct? If so, how can I resolve this problem and write directly to the video memory at 0xb8000?

Genitals answered 20/5, 2016 at 19:45 Comment(3)
Look here. You probably need to use colors (check make_vgaentry in the link).Polypody
The basic idea is to boot into protected mode so you can then use a 32 bit compiler without segmentation. If you don't want to bother with a loader yourself, just use multiboot.Ockham
don't use gcc for 16-bit code. Looking for 16-bit x86 compilerChanterelle
G
14

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:

Bochs output


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.

Glacis answered 21/5, 2016 at 16:44 Comment(4)
So, Is it better if I enter protected mode and write a kernel in 32bit?Genitals
@PantherCoder : Yes, very much so, then GCC's code should work as expected without jumping through as many hoops. The downside is that you won't be able to use BIOS interrupts in the kernel directly, although if need be you can switch out of protected mode to real mode; do the BIOS interrupt; then switch back to protected mode if you wanted to (It isn't the preferred way, but it will work).Glacis
@PantherCoder If you want to write a 16-bit kernel in C I'd suggest a 16-bit C compiler. Not sure if Bruce's C compiler has a version for Windows or not. It would work. It doesn't have the bells and whistles of GCC, and it doesn't optimize code nearly as good, but you may have fewer hassles. An old version of MSVC 1.52c and a copy an MS-DOS 16-bit linker would probably work, but I don't know off the top of my head what would be needed to get that to work properly.Glacis
Note that as the OS/kernel grows (and you start wanting to do things like asynchronous IO, networking, USB device insertion/removal, etc) you'll find that the BIOS functions are useless for everything except video mode switches and the limitations caused by trying to keep the BIOS usable (not touching PIC, IO APIC, local APIC, HPET, etc) end up being insurmountable.Philosophize

© 2022 - 2024 — McMap. All rights reserved.