Отображение текстовой видеопамяти на 0xb8000 без использования библиотеки C

Я пишу ядро ​​на C. Я использую кросс-компилятор GCC, пишу в системе Windows и ориентирован на 16-битный реальный режим. У меня нет библиотеки C, доступной для написания ядра. Я начал с некоторого кода, который должен печатать символ прямо на экране. Вот функция от kernel.c:

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

Я скомпилировал свой код, используя GCC с параметром -m16 генерировать код, который будет работать в реальном режиме. Я использую эти команды для генерации моего 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

Пользователь переполнения стека Michael Petch решил мою проблему с компоновщиком, но прокомментировал сам код неверно. Он сделал этот комментарий:

Помимо проблемы компоновщика, вы пытаетесь конвертировать старый 16-битный код TurboC/MSVC в GCC? Я нахожу (char *)0xB8000000L подозрительным. Если бы это был настоящий 16-битный компилятор C, все было бы хорошо, если бы он был (char far *)0xB8000000L. GCC не является настоящим 16-битным компилятором C и не имеет представления о дальних указателях старого стиля. Таким образом, даже если вы получите этот код для компиляции, он может не работать так, как вы думаете, я предполагаю, что с опцией -m16 в GCC вы пытаетесь создать 16-битное ядро ​​реального режима (а не защищенного режима).)?

Я пытался реализовать свой собственный printf как функция в C для моей собственной операционной системы. Код, который я предоставил выше, является лишь небольшим фрагментом того, что я понимаю. Я создал загрузчик в сборке (8086).

Майкл прав? Если это так, как я могу решить эту проблему и записать непосредственно в видеопамять на 0xb8000?

1 ответ

Решение

Если вы намереваетесь использовать GCC с -m16 тогда автоматически предполагается, что вы будете работать на 80386+. Я не могу подчеркнуть это достаточно, использование GCC для создания 16-битного кода чревато подводными камнями. Это усугубляется тем, что ваше ядро ​​помещает в память 0x10000. 0x10000 нельзя представить как 16-битное смещение, и это может привести к тому, что GCC будет выдавать код, который может не работать, особенно если вы когда-нибудь захотите включить оптимизацию с помощью -O1, -O2, -O3 и т.д. Даже доступ к глобальным переменным может вызвать проблемы!

НАСТОЯТЕЛЬНО РЕКОМЕНДУЕМ (почти требуется, чтобы избежать большинства неприятностей): у вас может быть меньше проблем, если вы поместите свое ядро ​​и его данные в первые 64 КБ памяти. Источник по адресу памяти 0x00520 находится чуть выше области данных BIOS и зарезервированной области нижней памяти.

Будьте предупреждены: GCC с -m16 Нацеленность на реальный режим - ИСПОЛЬЗОВАТЬ НА СВОЙ РИСК. Вы также можете потерять рассудок. Перевод процессора в 32-битный защищенный режим с моделью плоской памяти (с расширением от 0 до 0xffffffff), где CS=DS=ES идеально подходит для GCC


Этот код предполагает, что вы не находитесь в нереальном режиме, хотя ваша система, скорее всего, находится в этом режиме.

GCC предполагает, что CS=DS=ES и что модель памяти плоская. Как правило, менять ES не очень хорошая идея. Можно использовать ES, если вы сохраните его, сделаете работу и восстановите все это без промежуточного кода на C. Поскольку GCC требует 80386, мы можем использовать один из других регистров сегмента: FS и GS. В этом примере мы будем использовать FS.

Еще одно предварительное условие - это понимание сегментации реального режима. Я предполагаю, что вы делаете, так как вы создали загрузчик. Расчет для адреса физической памяти:

Physical memory address = (segment << 4) + offset

Видеопамять в текстовом режиме (цветная) находится по физическому адресу 0xb8000. Основание этой памяти может быть представлено в виде пары сегмент: смещение 0xb800:0x0000, поскольку:

(0xb800 << 4) + 0x0000 = 0xb8000

Каждая ячейка на видимом экране - это слово (16-битное). Верхние 8 битов WORD являются атрибутом, а нижние - символом, указанным в ссылке. Цветовая палитра описана на этой вики-странице.

Если мы используем FS в качестве нашего сегмента, мы можем установить его на 0xb800 и ссылаться на него в видеопамяти. Поскольку ваш код может в конечном итоге использовать FS для разных целей, мы сохраним его, используя некоторый встроенный код на ассемблере, поработаем с видеопамятью и восстановим FS до прежнего уровня.

Поскольку я использую встроенный ассемблер, вы можете посмотреть список полезных ссылок Питера Корда на эту тему.

Код, который учитывает вышеизложенное и предоставляет механизм для обновления экрана подряд, col с атрибутом через регистр сегмента FS, который мы установили в 0xb800.

Существует больше кода, чем вам, возможно, понравилось, но я хотел показать больше, чем вывод одного символа. Комментарии к коду могут помочь вам в этом.

#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);
}

Скриптер компоновщика для GCC на Windows

Ваш kernel.bin может стать больше, чем вы ожидаете при использовании GCC под Windows. Это из-за правил выравнивания по умолчанию, которые использует GCC. Следующий скрипт компоновщика может помочь уменьшить размер:

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 = .;
    }
}

Этот сценарий имеет значение ORG 0x520 (не 0x10000). Как упоминалось ранее, настоятельно рекомендуется не использовать источник 0x10000, как вы работали с 16-битным кодом, сгенерированным GCC. Назовите скрипт компоновщика linker.ld и затем вы можете использовать эти команды для ассемблера и связать ядро:

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

Вы должны будете изменить свой загрузчик для чтения секторов ядра в память, начиная с адреса 0x520.

С простым загрузчиком и этим ядром, построенным с использованием предоставленного скрипта code / linker, Bochs показывает, когда он выполняется:

Выход Бохса


Посмотрите на некоторые из сгенерированного кода

Первые несколько строк функции main сохраните текущий регистр FS, установите FS в сегмент видео 0xb800 и распечатайте 3 символа:

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);

Сгенерированный код можно увидеть с помощью этого objdump команда:

objdump -Dx kernel.pe --no-show-raw-insn -mi8086 -Mintel

Синтаксический вывод Intel выглядит следующим образом с моим компилятором (используя -O3 Оптимизации):

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

Эта строка кода C, которая восстановила FS:

 set_fs(oldfs);

С этими инструкциями позже:

 571:   mov    fs,si                   ; Restore original value previously saved in SI

Я аннотировал разборку комментариями, чтобы показать, как каждое из значений WORD обновлялось в памяти видеодисплея. Много строк кода на C, но вывод очень прост.

Другие вопросы по тегам