在不使用C库的情况下在0xb8000处显示文本视频内存

Pan*_*der 4 assembly kernel real-mode osdev x86-16

我一直在用C编写内核.我一直在使用GCC交叉编译器,在Windows系统上编写并以16位实模式为目标.我没有可用于编写内核的C库.我已经开始使用一些代码来假设将字符直接打印到屏幕上.这是一个函数来自kernel.c:

int main()
{
  char *src = (char *)0xB8000000L;
  *src = 'M';
  src += 2;
  *src = 'D';
  return 0;
}
Run Code Online (Sandbox Code Playgroud)

我使用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
Run Code Online (Sandbox Code Playgroud)

Stack Overflow用户Michael Petch解决了我的链接器问题,但评论代码本身是不正确的.他发表了这样的评论:

除链接器问题外,您是否尝试将旧的TurboC/MSVC 16位代码转换为GCC?我发现(char*)0xB8000000L可疑.如果它是一个真正的16位C编译器,它可能是好的,如果它是(char far*)0xB8000000L.GCC不是一个真正的16位C编译器,并没有旧式远指针的概念.所以,即使你得到这个代码进行编译,这可能不会做你认为它做的,我假设从-m16选项与GCC你正在尝试创建一个实模式16位内核(而不是保护模式一个) )?

我一直在尝试printf在C中为我自己的操作系统实现自己的类似功能.我上面提供的代码只是我理解的一小部分.我在程序集中创建了一个bootloader(8086).

迈克尔是对的吗?如果是这样,我该如何解决这个问题并直接写入视频内存0xb8000

Mic*_*tch 13

如果您打算使用GCC,-m16那么它会自动假设您将在80386+上运行.我不能强调这一点,使用GCC创建16位代码充满了陷阱.你选择将内核放在内存中的0x10000会变得更糟.0x10000的不能表示为16位偏移,这可能导致GCC发出的代码可能无法正常工作,特别是如果你想打开与优化-O1,-O2,-O3等等.甚至访问全局变量可能导致的问题!

高度推荐(几乎需要避免大多数麻烦):如果将内核及其数据放在第一个64kb的内存中,可能会遇到更少的问题.存储器地址0x00520的原点位于BIOS数据区域和较低存储器的保留区域之上.

预先警告:具有目标实模式的GCC将根据您的使用风险使用.你也可能失去理智.将处理器置于具有平坦存储器模型的 32位保护模式(从0延伸到0xffffffff),其中CS = DS = ESGCC的理想选择-m16


尽管您的系统可能处于该模式,但此代码假设您不处于虚幻模式.

GCC假设CS = DS = ES,并且内存模型是平坦的.改变ES通常不是一个好主意.如果您保存ES,可以使用ES,并且可以在干预C代码的情况下恢复所有ES.由于GCC需要80386,我们可以使用其他段寄存器之一:FSGS.在这个例子中,我们将使用FS.

另一个先决条件是您了解实模式分段.我假设你已经创建了一个bootloader.物理内存地址的计算是:

Physical memory address = (segment << 4) + offset
Run Code Online (Sandbox Code Playgroud)

文本模式(彩色)视频内存位于物理地址0xb8000.该内存的基础可以表示为一个段:偏移对0xb800:0x0000,因为:

(0xb800 << 4) + 0x0000 = 0xb8000
Run Code Online (Sandbox Code Playgroud)

可见屏幕上的每个单元格都是WORD(16位).WORD的高8位是属性,下面是链接中详细说明的字符.调色板在此Wiki页面中描述.

如果我们使用FS作为我们的段,我们可以将其设置为0xb800并使用它引用视频内存.由于您的代码最终可能会将FS用于各种事物,因此我们将使用一些内联汇编程序代码保存它,在视频内存上工作,并将FS恢复到以前的状态.

由于我使用内联汇编程序,您可能希望查看Peter Corde 关于该主题的有用链接列表.

将上述内容考虑在内的代码,并提供了一种连续更新屏幕的机制,通过我们设置为0xb800 的FS段寄存器来定义属性.

代码比您可能喜欢的多,但我想展示的不仅仅是输出单个字符.代码注释可以帮助您继续前进.

#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);
}
Run Code Online (Sandbox Code Playgroud)

Windows上GCC的链接器脚本

在Windows下kernel.bin使用GCC时,您可能会比您预期的更大.这是因为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 = .;
    }
}
Run Code Online (Sandbox Code Playgroud)

此脚本设置为ORG 0x520(不是0x10000).如前所述,强烈建议不要像使用16位GCC生成的代码那样使用0x10000的原点.命名链接器脚本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
Run Code Online (Sandbox Code Playgroud)

您必须修改引导加载程序以从内核0x520开始将内核扇区读入内存.

使用简单的引导加载程序和使用提供的代码/链接器脚本构建的内核,这是Bochs在运行时显示的内容:

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);
Run Code Online (Sandbox Code Playgroud)

使用此objdump命令可以看到生成的代码:

objdump -Dx kernel.pe --no-show-raw-insn -mi8086 -Mintel
Run Code Online (Sandbox Code Playgroud)

我的编译器(使用-O3优化)的Intel语法输出如下:

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
Run Code Online (Sandbox Code Playgroud)

这行恢复FSC代码:

 set_fs(oldfs);
Run Code Online (Sandbox Code Playgroud)

稍后通过此说明:

 571:   mov    fs,si                   ; Restore original value previously saved in SI
Run Code Online (Sandbox Code Playgroud)

我用注释注释了反汇编,以显示每个WORD值是如何在视频显示内存中更新的.很多行的C代码,但输出很简单.

  • 请注意,随着OS /内核的增长(并且您开始想要执行异步IO,网络,USB设备插入/移除等操作),您会发现BIOS功能对于除视频模式切换以外的所有内容都无用通过尝试保持BIOS可用(不接触PIC,IO APIC,本地APIC,HPET等)最终是不可克服的. (4认同)
  • @PantherCoder:是的,非常如此,然后_GCC_的代码应该按预期工作,而不会跳过尽可能多的箍.缺点是您将无法直接在内核中使用BIOS中断,但如果需要,您可以从保护模式切换到实模式; 做BIOS中断; 然后切换回保护模式,如果你想(这不是首选的方式,但它会工作). (3认同)