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

Assembly 在不使用C库的情况下以0xb8000显示文本视频内存,assembly,kernel,x86-16,osdev,real-mode,Assembly,Kernel,X86 16,Osdev,Real Mode,我一直在用C编写内核。我一直在使用GCC交叉编译器,在Windows系统上编写,目标是16位实模式。我没有可用的C库来编写内核。我从一些代码开始,假设它可以直接将一个字符打印到屏幕上。下面是kernel.c中的一个函数: int main() { char *src = (char *)0xB8000000L; *src = 'M'; src += 2; *src = 'D'; return 0; } 我使用带有参数-m16的GCC编译代码,以生成将在实模式下运行的代码。我

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

int main()
{
  char *src = (char *)0xB8000000L;
  *src = 'M';
  src += 2;
  *src = 'D';
  return 0;
}
我使用带有参数
-m16
的GCC编译代码,以生成将在实模式下运行的代码。我使用这些命令生成我的
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
堆栈溢出用户解决了我的问题,但注释代码本身不正确。他说,

除了链接器问题,您是否正在尝试将旧的TurboC/MSVC 16位代码转换为GCC?我发现(字符*)0xB800000L可疑。如果它是一个真正的16位C编译器,那么如果它是(char far*)0xB800000L,就可以了。GCC不是一个真正的16位C编译器,也没有旧式远指针的概念。因此,即使您让这段代码进行编译,它也可能不会像您认为的那样,我假设使用GCC的-m16选项,您正在尝试创建一个实模式16位内核(而不是保护模式内核)

我一直在尝试为自己的操作系统在C中实现自己的
printf
类函数。我上面提供的代码只是我理解的一小部分。我在汇编(8086)中创建了一个引导加载程序


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

如果您打算将GCC与
-m16
一起使用,则自动假定您将在80386+上运行。我怎么强调都不过分,使用GCC创建16位代码充满了陷阱。如果您选择将内核放在内存中0x10000,情况会变得更糟。0x10000不能表示为16位偏移量,这可能会导致GCC发出可能无法工作的代码,特别是如果您希望使用
-O1
-O2
-O3
等启用优化,那么即使访问全局变量也可能会导致问题

强烈推荐(几乎是避免大多数麻烦所必需的):如果将内核及其数据放在前64kb内存中,问题可能会少一些。内存地址0x00520处的原点正好位于BIOS数据区和较低内存的保留区上方

预先警告:带有
-m16
瞄准真实模式的GCC将在您的风险中使用。你也可能失去理智。将处理器置于32位保护模式(从0扩展到0xFFFFFF),其中CS=DS=ES是GCC的理想选择


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

GCC假设CS=DS=ES,并且内存模型是平面的。一般来说,改变习惯不是一个好主意。如果保存ES、执行工作并将其全部还原,而不需要中间插入C代码,则可以使用ES。由于GCC需要80386,我们可以使用其他段寄存器之一:FS和GS。在本例中,我们将使用FS

另一个先决条件是你要理解。我想你会的,因为你已经创建了一个引导加载程序。物理内存地址的计算为:

Physical memory address = (segment << 4) + offset
此脚本设置为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开始将内核扇区读入内存

通过一个简单的引导加载程序和使用提供的代码/链接器脚本构建的内核,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
我的编译器的英特尔语法输出如下(使用
-O3
优化):

按照以下说明:

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

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

Look。您可能需要使用颜色(在链接中选中
make_vgaentry
)。基本思想是引导到保护模式,这样您就可以使用32位编译器而无需分段。如果你不想自己为加载程序而烦恼,只需使用multiboot。。那么,如果我进入受保护模式并以32位编写内核是否更好?@PantherCoder:是的,非常好,那么GCC的代码应该能像预期的那样工作,而不会跳出那么多的障碍。缺点是您不能直接在内核中使用BIOS中断,尽管如果需要,您可以从保护模式切换到实模式;BIOS是否中断;然后,如果您想切换回保护模式(这不是首选方式,但它会工作)。@PantherCoder如果您想用C编写16位内核,我建议使用16位C编译器。不确定是否有适用于Windows的版本。这会奏效的。它没有GCC的功能,它对代码的优化也没有GCC的那么好,但是您可能会遇到更少的麻烦。一个旧版本的MSVC 1.52c和一个MS-DOS 16位链接器的副本可能会工作,但我不知道需要什么才能让它正常工作。请注意,随着操作系统/内核的增长(并且你开始想要做异步IO、联网、USB设备插入/删除等事情)您会发现BIOS功能除了视频模式开关之外对任何东西都没有用处,而试图保持BIOS可用性(不接触PIC、IO APIC、本地APIC、HPET等)所造成的限制最终无法克服。
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 -Dx kernel.pe --no-show-raw-insn -mi8086 -Mintel
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
 set_fs(oldfs);
 571:   mov    fs,si                   ; Restore original value previously saved in SI