25
votes

I'm a teenager who has become very interested in assembly language. I'm trying to write a small operating system in Intel x86 assembler, and I was wondering how to write directly to the screen, as in without relying on the BIOS or any other operating sytems. I was looking through the sources of coreboot, Linux, and Kolibri, among others, in the hopes of finding and understanding some piece of code that does this. I have not yet succeeded in this regard, though I believe I'll take another look at the Linux source code, it being the most understandable to me of the sources I've searched through.

If anybody knows this, or knows where in some piece of source code that I could look at, I would appreciate it if they told me.

Or better yet, if someone knows how to identify what I/O port on an Intel x86 CPU connects to what piece of hardware, that would be appreciated too. The reason I need to ask this, is that in neither the chapter for input/output in the Intel 64 and IA-32 Architectures Software Developer's Manual Volume 1: Basic Architecture, nor in the sections for the IN or OUT instruction in Volume 3, could I find any of this information. And because it has been too arduous to search for the relevant instructions in the sources that I have.

7
Wow, thanks for the great responses, everyone. All of the answers provided look as though they're likely to be helpful.user336462
user336462 :just out of curiosity: what do you think about all of this, almost 10 years later? :)Almir Campos

7 Answers

22
votes

PART 1

For old VGA modes, there's a fixed address to write to the (legacy) display memory area. For text modes this area starts at 0x000B8000. For graphics modes it starts at 0x000A0000.

For high-resolution video modes (e.g. those set by the VESA/VBE interface) this doesn't work because the size of legacy display memory area is limited to 64 KiB and most high-resolution video modes need a lot more space (e.g. 1024 * 768 * 32-bpp = 2.25 MiB). To get around that there's 2 different methods supported by VBE.

The first method is called "bank switching", where only part of the video card's display memory is mapped into the legacy area at any time (and you can change which part is mapped). This can be quite messy - for example, to draw one pixel you might need to calculate which bank the pixel is in, then switch to that bank, then calculate which offset in the bank. To make this worse, for some video modes (e.g. 24-bpp video modes where there's 3 bytes per pixel) only the first part of a pixel's data might be in one bank and the second part of the same pixel's data is in a different bank. The main benefit of this is that it works with real mode addressing, as the legacy display memory area is below 0x00100000.

The second method is called "Linear Framebuffer" (or just "LFB"), where the video card's entire display memory area can be accessed without any messy bank switching. You have to ask the VESA/VBE interface where this area is (and it's typically in the "PCI hole" somewhere between 0xC0000000 and 0xFFF00000). This means you can't access it in real mode, and need to use protected mode or long mode or "unreal mode".

To find the address of a pixel when you're using an LFB mode, you'd do something like "pixel_address = display_memory_address + y * bytes_per_line + x * bytes_per_pixel". The "bytes_per_line" comes from the VESA/VBE interface (and may not be the same as "horizontal_resolution * bytes_per_pixel" because there can be padding between horizontal lines).

For "bank switched" VBE/VESA modes, it becomes something more like:

pixel_offset = y * bytes_per_line + x * bytes_per_pixel;
bank_number = pixel_offset / bank_size;
pixel_starting_address_within_bank = pixel_offset % bank_size;

For some old VGA modes (e.g. the 256-colour "mode 0x13") it's very similar to LFB, except there is no padding between lines and you can do "pixel_address = display_memory_address + (y * horizontal_resolution + x) * bytes_per_pixel". For text modes it's basically the same thing, except 2 bytes determine each character and its attribute - e.g. "char_address = display_memory_address + (y * horizontal_resolution + x) * 2". For other old VGA modes (monochrome/2-colour, 4-colour and 16-colour modes) the video card's memory is arranged completely differently. It's split into "planes" where each plane contains one bit of the pixel, and (for e.g.) to update one pixel in a 16-colour mode you need to write to 4 separate planes. For performance reasons the VGA hardware supports different write modes and different read modes, and it can get complicated (too complicated to describe adequately here).

PART 2

For I/O ports (on 80x86, "PC compatibles"), there's 3 general categories. The first is "de facto standard" legacy devices which use fixed I/O ports. This includes things like the PIC chips, ISA DMA controller, PS/2 controller, PIT chip, serial/parallel ports, etc. Almost anything that describes how to program each of these devices will tell you which I/O ports the device uses.

The next category is legacy/ISA devices, where the I/O ports the devices use is determined by jumpers on the card itself, and there's no sane way to determine which I/O ports they use from software. To get around this the end-user has to tell the OS which I/O ports each device uses. Thankfully this crusty stuff has all become obsolete (although that doesn't necessarily mean that nobody is using it).

The third category is "plug & play", where there's some method of asking the device which I/O ports it uses (and in most cases, changing the I/O ports the device uses). An example of this is PCI, where there's a "PCI configuration space" that tells you lots of information about each PCI device. For this categories, there is no way anyone can determine which devices will be using which I/O ports without doing it at run-time, and changing some BIOS settings can cause any/all of these devices to change I/O ports.

Also note that an Intel CPU is only a CPU. Nothing prevents those CPUs from being used in something that is radically different to a "PC compatible" computer. Intel's CPU manuals will never tell you anything about hardware that exists outside of the CPU itself (including the chipset or devices).

Part 3

Probably the best place to go for more information (that's intended for OS developers/hobbyists) is http://osdev.org/ (their wiki and their forums).

3
votes

To write directly to the screen, you should probably write to the VGA Text Mode area. This is a block of memory which is a buffer for text mode.

The text-mode screen consists of 80x25 characters; each character is 16 bits wide. If the first bit is set the character will blink on-screen. The next 3 bits then detail the background color; the final 4 bits of the first byte are the foreground (or the text character)'s color. The next 8 bits are the value of the character. This is usually code-page 737 or 437, but it could vary from system to system.

Here is a Wikipedia page detailing this buffer, and here is a link to codepage 437

Almost all BIOSes will set the mode to text mode before your system is booted, but some laptop BIOSes will not boot into text mode. If you are not already in text mode, you can set it with int10h very simply:

xor ah, ah
mov al, 0x03
int 0x10

(The above code uses BIOS interrupts, so it has to be run in Real Mode. I suggest putting this in your bootsector.)

Finally, here is a set of routines I wrote for writing strings in protected mode.

unsigned int terminalX;
unsigned int terminalY;
uint8_t terminalColor;
volatile uint16_t *terminalBuffer;

unsigned int strlen(const char* str) {
    int len;
    int i = 0;
    while(str[i] != '\0') {
        len++;
        i++;
    }
    return len;
}

void initTerminal() {
    terminalColor = 0x07;
    terminalBuffer = (uint16_t *)0xB8000;
    terminalX = 0;
    terminalY = 0;

    for(int y = 0; y < 25; y++) {
        for(int x = 0; x < 80; x++) {
            terminalBuffer[y * 80 + x] = (uint16_t)terminalColor << 8 | ' ';
        }
    }
}

void setTerminalColor(uint8_t color) {
    terminalColor = color;
}

void putCharAt(int x, int y, char c) {
    unsigned int index = y * 80 + x;
    if(c == '\r') {
        terminalX = 0;  
    } else if(c == '\n') {
        terminalX = 0;
        terminalY++;
    } else if(c == '\t') {
        terminalX = (terminalX + 8) & ~(7);
    } else {
        terminalBuffer[index] = (uint16_t)terminalColor << 8 | c;
        terminalX++;
        if(terminalX == 80) {
            terminalX = 0;
            terminalY++;
        }
    }
}

void writeString(const char *data) {
    for(int i = 0; data[i] != '\0'; i++) {
        putCharAt(terminalX, terminalY, data[i]);       
    }           
}

You can read up about this on this page.

2
votes

A little beyond my scope but you might want to look into VESA.

1
votes

This is not so simple. While BIOS provides INT 10h to write text to the screen, graphics differs from one adapter to the next. For example, you can find information for VGA http://www.wagemakers.be/english/doc/vga here. Some ancient SVGA adapters http://www.intel-assembler.it/portale/5/assembly-game-programming-encyclopedia/assembly-game-programming-encyclopedia.asp here.

1
votes

For general I/O ports, you have to go through the BIOS, which means interrupts. Many blue moons ago, I used the references from Don Stoner to help writing some real-mode assembly, but I burnt out on it after a few months and forgot most of what I knew.

0
votes

I found a place that provides some good information on the matter: http://www.osdever.net/FreeVGA/home.htm It talks about some details important to writing code that writes directly to the screen. I found it from the link to Don Stoner's site (Thanks for the link, SilverbackNet!). I'm still looking for more more information, so if anyone has any more ideas, I'd appreciate the help very much.

0
votes

I found a book that seems to answer my questions. I realized that such a book could exist after reading about it on the FreeVGA page. Here's the link: Programmer's Guide to the EGA, VGA and Super VGA cards