无法从引导加载程序(NASM + GCC 工具链)调用实模式 C 函数

mdx*_*x97 5 x86 assembly gcc osdev bootloader

我正在尝试编写自己的操作系统内核,但在引导加载程序和(即将成为)我的内核(用 C 编写的)之间正确连接时遇到了一些问题。

我有以下代码...

src/bootloader.asm

; Allows our code to be run in real mode.
BITS 16
extern kmain

section .text
global _start
_start:
        jmp Start

; Moves the cursor to row dl, col dh.
MoveCursor:
    mov ah, 2
    mov bh, 0
    int 10h
    ret

; Prints the character in al to the screen.
PrintChar:
    mov ah, 10
    mov bh, 0
    mov cx, 1
    int 10h
    ret

; Set cursor position to 0, 0.
ResetCursor:
    mov dh, 0
    mov dl, 0
    call MoveCursor
    ret

Start:
        call ResetCursor

; Clears the screen before we print the boot message.
; QEMU has a bunch of crap on the screen when booting.
Clear:
        mov al, ' '
        call PrintChar

        inc dl
        call MoveCursor

        cmp dl, 80
        jne Clear

        mov dl, 0
        inc dh
        call MoveCursor

        cmp dh, 25
        jne Clear

; Begin printing the boot message. 
Msg:    call ResetCursor
        mov si, BootMessage

NextChar:
        lodsb
        call PrintChar

        inc dl
        call MoveCursor

        cmp si, End
        jne NextChar 

call kmain

BootMessage: db "Booting..."
End:

; Zerofill up to 510 bytes
times 510 - ($ - $$)  db 0

; Boot Sector signature
dw 0AA55h
Run Code Online (Sandbox Code Playgroud)

源代码/god.c

asm(".code16gcc");

// JASOS kernel entry point.
void kmain()
{
    asm(     "movb $0, %dl;"
             "inc %dh;"
             "movb $2, %ah;"
             "movb $0, %bh;"
             "int $0x10;"
             "movb $'a', %al;"
             "movb $10, %ah;"
             "movw $1, %cx;"
             "int $0x10;"   );

    while (1);
}
Run Code Online (Sandbox Code Playgroud)

最后...... Makefile

bootloader: src/bootloader.asm
    nasm -f elf32 src/bootloader.asm -o build/bootloader.o

god: src/god.c
    i686-elf-gcc -c src/god.c -o build/god.o -ffreestanding

os: bootloader god
    i686-elf-ld -Ttext=0x7c00 --oformat binary build/bootloader.o build/god.o -o bin/jasos.bin
Run Code Online (Sandbox Code Playgroud)

引导加载程序目前非常简单。它只是输入“正在启动...”并(尝试)加载 kmain。但是,打印字符串后没有任何反应。

我在kmain被调用时仍处于实模式,所以我不认为失败是因为无法从我的内联程序集访问 BIOS 中断。如果我错了纠正我。

Mic*_*tch 7

我不推荐 GCC 用于 16 位代码。GCC 替代方案可能是单独的IA16-GCC 项目,该项目正在进行中并处于试验阶段。

由于需要内联汇编,很难让 GCC 发出正确的实模式代码。如果您希望避免细微的错误,尤其是在启用优化时,GCC 的内联汇编很难正确执行。可以编写这样的代码,但我强烈建议不要这样做

您没有链接器脚本,因此您编译的C代码放在引导加载程序签名之后。BIOS 只将一个扇区读入内存。您jmp kmain最终会跳转到内核实际上已加载到内存中的内存,但它没有加载,因此无法按预期工作。您需要添加代码来调用 BIOSInt 13/AH=2以读取从 Cylinder, Head, Sector (CHS) = (0,0,2) 开始的附加磁盘扇区,该扇区是引导加载程序之后的扇区。

您的引导加载程序没有正确设置段寄存器。因为您使用的是 GCC,所以它需要 CS=DS=ES=SS。由于我们需要将数据加载到内存中,因此我们需要将堆栈放在安全的地方。内核将加载到 0x0000:0x7e00,因此我们可以将堆栈放在引导加载程序下方的 0x0000:0x7c00 处,它们不会发生冲突。您需要CLD在调用 GCC 之前清除方向标志 (DF),因为这是一项要求。这些问题中的许多问题都在我的“一般引导加载程序提示”中捕获。可以在我的其他Stackoverflow 回答中找到一个更复杂的引导加载程序,它确定内核 (stage2) 的大小并从磁盘读取适当数量的扇区。

我们需要一个链接描述文件来正确地在内存中进行布局,并确保最开始的指令跳转到真正的C入口点kmain。我们还需要正确地将 BSS 部分清零,因为 GCC 期望这样做。链接描述文件用于确定 BSS 部分的开始和结束。该函数zero_bss将该内存清除为 0x00。

Makefile可以清理一下,以便在未来添加代码更容易。我修改了代码,以便在src目录中构建目标文件。这简化了 make 处理。

当引入实模式代码支持并将支持添加到 GNU 汇编器时,它在 GCC 中通过使用asm (".code16gcc");. 很长一段时间以来,GCC 都支持-m16做同样事情的选项。随着-m16你不需要到添加.code16gcc指令的所有文件的顶部。

我没有修改打印a到屏幕上的内联程序集。仅仅因为我没有修改它,并不意味着它没有问题。由于寄存器被破坏并且编译器没有被告知它可能导致奇怪的错误,尤其是在优化时。此答案的第二部分显示了一种机制,该机制使用 BIOS 通过适当的内联汇编将字符和字符串打印到控制台。

我推荐使用编译器选项-Os -mregparm=3 -fomit-frame-pointer来优化空间。

生成文件

CROSSPRE=i686-elf-
CC=$(CROSSPRE)gcc
LD=$(CROSSPRE)ld
OBJCOPY=$(CROSSPRE)objcopy
DD=dd
NASM=nasm

DIR_SRC=src
DIR_BIN=bin
DIR_BUILD=build

KERNEL_NAME=jasos
KERNEL_BIN=$(DIR_BIN)/$(KERNEL_NAME).bin
KERNEL_ELF=$(DIR_BIN)/$(KERNEL_NAME).elf
BOOTLOADER_BIN=$(DIR_BIN)/bootloader.bin
BOOTLOADER_ASM=$(DIR_SRC)/bootloader.asm
DISK_IMG=$(DIR_BUILD)/disk.img

CFLAGS=-g -fno-PIE -static -std=gnu99 -m16 -Os -mregparm=3 \
    -fomit-frame-pointer -nostdlib -ffreestanding -Wall -Wextra
LDFLAGS=-melf_i386

# List all object files here
OBJS=$(DIR_SRC)/god.o

.PHONY: all clean

all: $(DISK_IMG)

$(BOOTLOADER_BIN): $(BOOTLOADER_ASM)
        $(NASM) -f bin $< -o $@

%.o: %.c
        $(CC) -c $(CFLAGS) $< -o $@

$(KERNEL_ELF): $(OBJS)
        $(LD) $(LDFLAGS) -Tlink.ld $^ -o $@

$(KERNEL_BIN): $(KERNEL_ELF)
        $(OBJCOPY) -O binary $< $@

$(DISK_IMG): $(KERNEL_BIN) $(BOOTLOADER_BIN)
        $(DD) if=/dev/zero of=$@ bs=1024 count=1440
        $(DD) if=$(BOOTLOADER_BIN) of=$@ conv=notrunc
        $(DD) if=$(KERNEL_BIN) of=$@ conv=notrunc seek=1

clean:
        rm -f $(DIR_BIN)/*
        rm -f $(DIR_BUILD)/*
        rm -f $(DIR_SRC)/*.o
Run Code Online (Sandbox Code Playgroud)

链接.ld

OUTPUT_FORMAT("elf32-i386");
ENTRY(kmain);
SECTIONS
{
    . = 0x7E00;

    .text.main : SUBALIGN(0) {
        *(.text.bootstrap);
        *(.text.*);
    }

    .data.main : SUBALIGN(4) {
        *(.data);
        *(.rodata*);
    }

    .bss : SUBALIGN(4) {
        __bss_start = .;
        *(.COMMON);
        *(.bss)
    }
    . = ALIGN(4);
    __bss_end = .;

    __bss_sizel = ((__bss_end)-(__bss_start))>>2;
    __bss_sizeb = ((__bss_end)-(__bss_start));

    /DISCARD/ : {
        *(.eh_frame);
        *(.comment);
    }
}
Run Code Online (Sandbox Code Playgroud)

src/god.c :

#include <stdint.h>

/* The linker script ensures .text.bootstrap code appears first.
 * The code simply jumps to our real entrypoint kmain */

asm (".pushsection .text.bootstrap\n\t"
     "jmp kmain\n\t"
     ".popsection");

extern uintptr_t __bss_start[];
extern uintptr_t __bss_end[];

/* Zero the BSS section */
static inline void zero_bss()
{
    uint32_t *memloc = __bss_start;

    while (memloc < __bss_end)
        *memloc++ = 0;
}

/* JASOS kernel C entrypoint */
void kmain()
{
    /* We need to zero out the BSS section */
    zero_bss();

    asm (
        "movb $0, %dl;"
        "inc %dh;"
        "movb $2, %ah;"
        "movb $0, %bh;"
        "int $0x10;"
        "movb $'a', %al;"
        "movb $10, %ah;"
        "movw $1, %cx;"
        "int $0x10;"
    );

    return;
}
Run Code Online (Sandbox Code Playgroud)

src/bootloader.asm

; Allows our code to be run in real mode.
BITS 16
ORG 0x7c00

_start:
    xor ax, ax                 ; DS=ES=0
    mov ds, ax
    mov es, ax
    mov ss, ax                 ; SS:SP=0x0000:0x7c00
    mov sp, 0x7c00
    cld                        ; Direction flag = 0 (forward movement)
                               ; Needed by code generated by GCC

    ; Read 17 sectors starting from CHS=(0,0,2) to 0x0000:0x7e00
    ; 17 * 512 = 8704 bytes (good enough to start with)
    mov bx, 0x7e00             ; ES:BX (0x0000:0x7e00) is memory right after bootloader
    mov ax, 2<<8 | 17          ; AH=2 Disk Read, AL=17 sectors to read
    mov cx, 0<<8 | 2           ; CH=Cylinder=0, CL=Sector=2
    mov dh, 0                  ; DH=Head=0
    int 0x13                   ; Do BIOS disk read

    jmp 0x0000:Start           ; Jump to start set CS=0

; Moves the cursor to row dl, col dh.
MoveCursor:
    mov ah, 2
    mov bh, 0
    int 10h
    ret

; Prints the character in al to the screen.
PrintChar:
    mov ah, 10
    mov bh, 0
    mov cx, 1
    int 10h
    ret

; Set cursor position to 0, 0.
ResetCursor:
    mov dh, 0
    mov dl, 0
    call MoveCursor
    ret

Start:

    call ResetCursor

; Clears the screen before we print the boot message.
; QEMU has a bunch of crap on the screen when booting.
Clear:
    mov al, ' '
    call PrintChar

    inc dl
    call MoveCursor

    cmp dl, 80
    jne Clear

    mov dl, 0
    inc dh
    call MoveCursor

    cmp dh, 25
    jne Clear

; Begin printing the boot message.
Msg:
    call ResetCursor
    mov si, BootMessage

NextChar:
    lodsb
    call PrintChar

    inc dl
    call MoveCursor

    cmp si, End
    jne NextChar

    call dword 0x7e00          ; Because GCC generates code with stack
                               ; related calls that are 32-bits wide we
                               ; need to specify `DWORD`. If we don't, when
                               ; kmain does a `RET` it won't properly return
                               ; to the code below.

    ; Infinite ending loop when kmain returns
    cli
.endloop:
    hlt
    jmp .endloop

BootMessage: db "Booting..."
End:

; Zerofill up to 510 bytes
times 510 - ($ - $$)  db 0

; Boot Sector signature
dw 0AA55h
Run Code Online (Sandbox Code Playgroud)

一个 1.44MiB 的软盘映像build/disk.img被创建。它可以在 QEMU 中使用如下命令运行:

qemu-system-i386 -fda build/disk.img
Run Code Online (Sandbox Code Playgroud)

预期输出应类似于:

在此处输入图片说明


正确使用内联汇编使用 BIOS 写入字符串

下面介绍了使用更复杂的GCC 扩展内联汇编的代码版本。这个答案并不是要讨论 GCC 的扩展内联汇编用法,但网上有关于它的信息。应该注意的是,有很多糟糕的建议、文档、教程和示例代码,其中充满了问题,这些问题可能是由对主题没有正确理解的人编写的。你被警告了!1

生成文件

CROSSPRE=i686-elf-
CC=$(CROSSPRE)gcc
LD=$(CROSSPRE)ld
OBJCOPY=$(CROSSPRE)objcopy
DD=dd
NASM=nasm

DIR_SRC=src
DIR_BIN=bin
DIR_BUILD=build

KERNEL_NAME=jasos
KERNEL_BIN=$(DIR_BIN)/$(KERNEL_NAME).bin
KERNEL_ELF=$(DIR_BIN)/$(KERNEL_NAME).elf
BOOTLOADER_BIN=$(DIR_BIN)/bootloader.bin
BOOTLOADER_ASM=$(DIR_SRC)/bootloader.asm
DISK_IMG=$(DIR_BUILD)/disk.img

CFLAGS=-g -fno-PIE -static -std=gnu99 -m16 -Os -mregparm=3 \
    -fomit-frame-pointer -nostdlib -ffreestanding -Wall -Wextra
LDFLAGS=-melf_i386

# List all object files here
OBJS=$(DIR_SRC)/god.o $(DIR_SRC)/biostty.o

.PHONY: all clean

all: $(DISK_IMG)

$(BOOTLOADER_BIN): $(BOOTLOADER_ASM)
        $(NASM) -f bin $< -o $@

%.o: %.c
        $(CC) -c $(CFLAGS) $< -o $@

$(KERNEL_ELF): $(OBJS)
        $(LD) $(LDFLAGS) -Tlink.ld $^ -o $@

$(KERNEL_BIN): $(KERNEL_ELF)
        $(OBJCOPY) -O binary $< $@

$(DISK_IMG): $(KERNEL_BIN) $(BOOTLOADER_BIN)
        $(DD) if=/dev/zero of=$@ bs=1024 count=1440
        $(DD) if=$(BOOTLOADER_BIN) of=$@ conv=notrunc
        $(DD) if=$(KERNEL_BIN) of=$@ conv=notrunc seek=1

clean:
        rm -f $(DIR_BIN)/*
        rm -f $(DIR_BUILD)/*
        rm -f $(DIR_SRC)/*.o
Run Code Online (Sandbox Code Playgroud)

链接.ld

OUTPUT_FORMAT("elf32-i386");
ENTRY(kmain);
SECTIONS
{
    . = 0x7E00;

    .text.main : SUBALIGN(0) {
        *(.text.bootstrap);
        *(.text.*);
    }

    .data.main : SUBALIGN(4) {
        *(.data);
        *(.rodata*);
    }

    .bss : SUBALIGN(4) {
        __bss_start = .;
        *(.COMMON);
        *(.bss)
    }
    . = ALIGN(4);
    __bss_end = .;

    __bss_sizel = ((__bss_end)-(__bss_start))>>2;
    __bss_sizeb = ((__bss_end)-(__bss_start));

    /DISCARD/ : {
        *(.eh_frame);
        *(.comment);
    }
}
Run Code Online (Sandbox Code Playgroud)

src/biostty.c :

#include <stdint.h>
#include "../include/biostty.h"

void fastcall
writetty_str (const char *str)
{
    writetty_str_i (str);
}

void fastcall
writetty_char (const uint8_t outchar)
{
    writetty_char_i (outchar);
}
Run Code Online (Sandbox Code Playgroud)

包括/x86helper.h

#ifndef X86HELPER_H
#define X86HELPER_H

#include <stdint.h>

#define STR_TEMP(x) #x
#define STR(x) STR_TEMP(x)

#define TRUE 1
#define FALSE 0
#define NULL (void *)0

/* regparam(3) is a calling convention that passes first
   three parameters via registers instead of on stack.
   1st param = EAX, 2nd param = EDX, 3rd param = ECX */
#define fastcall  __attribute__((regparm(3)))

/* noreturn lets GCC know that a function that it may detect
   won't exit is intentional */
#define noreturn      __attribute__((noreturn))
#define always_inline __attribute__((always_inline))
#define used          __attribute__((used))

/* Define helper x86 function */
static inline void fastcall always_inline x86_hlt(void){
    __asm__ ("hlt\n\t");
}
static inline void fastcall always_inline x86_cli(void){
    __asm__ ("cli\n\t");
}
static inline void fastcall always_inline x86_sti(void){
    __asm__ ("sti\n\t");
}
static inline void fastcall always_inline x86_cld(void){
    __asm__ ("cld\n\t");
}

/* Infinite loop with hlt to end bootloader code */
static inline void noreturn fastcall haltcpu()
{
    while(1){
        x86_hlt();
    }
}

#endif
Run Code Online (Sandbox Code Playgroud)

包括/biostty.h

#ifndef BIOSTTY_H
#define BIOSTTY_H

#include <stdint.h>
#include "../include/x86helper.h"

/* Functions ending with _i are always inlined */

extern fastcall void
writetty_str (const char *str);

extern fastcall void
writetty_char (const uint8_t outchar);

static inline fastcall always_inline void
writetty_char_i (const uint8_t outchar)
{
   __asm__ ("int $0x10\n\t"
            :
            : "a"(((uint16_t)0x0e << 8) | outchar),
              "b"(0x0000));
}

static inline fastcall always_inline void
writetty_str_i (const char *str)
{
    /* write characters until we reach nul terminator in str */
    while (*str)
        writetty_char_i (*str++);
}

#endif
Run Code Online (Sandbox Code Playgroud)

src/god.c :

#include <stdint.h>
#include "../include/biostty.h"

/* The linker script ensures .text.bootstrap code appears first.
 * The code simply jumps to our real entrypoint kmain */

asm (".pushsection .text.bootstrap\n\t"
     "jmp kmain\n\t"
     ".popsection");

extern uintptr_t __bss_start[];
extern uintptr_t __bss_end[];

/* Zero the BSS section */
static inline void zero_bss()
{
    uint32_t *memloc = __bss_start;

    while (memloc < __bss_end)
        *memloc++ = 0;
}

/* JASOS kernel C entrypoint */
void kmain()
{
    /* We need to zero out the BSS section */
    zero_bss();

    writetty_str("\n\rHello, world!\n\r");
    return;
}
Run Code Online (Sandbox Code Playgroud)

链接器脚本和引导加载程序与本答案中提供的第一个版本相比没有修改。

在 QEMU 中运行时,输出应类似于:

在此处输入图片说明


脚注:

  • 1 Google 上最热门的“用 C 编写引导加载程序”之一是代码项目教程。它的评价很高,并一度获得了月度最高的文章。不幸的是,就像许多涉及内联汇编的教程一样,它们教了很多坏习惯并弄错了。他们很幸运能够让他们的代码与他们使用的编译器一起工作。许多人试图用这些坏主意来编写带有 GCC 的实模式内核,但都惨遭失败。我单独列出了 Code Project 教程,因为它是过去 Stackoverflow 上许多问题的基础。像许多其他教程一样,它真的根本不值得信任。一个例外是文章使用 gcc 在 C 中的实模式:编写引导加载程序

    我提供了第二个代码示例作为最小完整可验证示例,以展示正确的 GCC 内联程序集在打印字符和打印字符串时的样子。很少有文章展示如何使用 GCC 正确执行此操作。第二个示例显示编写内部汇编代码之间的差异Ç功能,而写一ç低级别的功能内联汇编像BIOS所需要的东西调用等,如果你要使用GCC来包装整个汇编代码的功能,然后就容易多了开始时在汇编中编写函数的问题较少。这违背了使用C的目的。