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

Cannot call real mode C function from bootloader (NASM + GCC toolchain)

我正在尝试编写我自己的 OS 内核,但在我的引导加载程序和(即将成为)我的内核(用 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

src/god.c

asm(".code16gcc");

// JASOS kernel entry point.
void kmain()
{
    asm(     "movb [=12=], %dl;"
             "inc %dh;"
             "movb , %ah;"
             "movb [=12=], %bh;"
             "int [=12=]x10;"
             "movb $'a', %al;"
             "movb , %ah;"
             "movw , %cx;"
             "int [=12=]x10;"   );

    while (1);
}

最后... 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

引导加载程序目前非常简单。它只是键入 "Booting..." 并(尝试)加载 kmain。但是,打印字符串后没有任何反应。

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

我不推荐 GCC 用于 16 位代码。 GCC 替代方案可能是单独的 IA16-GCC project,这是一项正在进行的工作并且是实验性的。

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

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

您的引导加载程序没有正确设置段寄存器。因为您使用的是 GCC,所以它需要 CS=DS=ES=SS。由于我们需要将数据加载到内存中,因此我们需要将堆栈放在安全的地方。内核将被加载到 0x0000:0x7e00,因此我们可以将堆栈放在引导加载程序下方的 0x0000:0x7c00 处,它们不会发生冲突。您需要使用 CLD before calling GCC as it is a requirement. Many of these issues are captured in my . A more complex bootloader that determines the size of the kernel (stage2) and reads the appropriate number of sectors from disk can be found in my other .

清除方向标志 (DF)

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

Makefile 可以稍微清理一下,以便将来更容易添加代码。我修改了代码,以便在 src 目录中构建目标文件。这简化了制作过程。

当引入实模式代码支持并将支持添加到 GNU 汇编程序时,使用 asm (".code16gcc"); 在 GCC 中启用了它。很长一段时间以来,GCC 已经支持做同样事情的 -m16 选项。使用 -m16,您无需将 .code16gcc 指令添加到所有文件的顶部。

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

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

生成文件:

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

link.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);
    }
}

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 [=12=], %dl;"
        "inc %dh;"
        "movb , %ah;"
        "movb [=12=], %bh;"
        "int [=12=]x10;"
        "movb $'a', %al;"
        "movb , %ah;"
        "movw , %cx;"
        "int [=12=]x10;"
    );

    return;
}

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

创建了一个名为 build/disk.img 的 1.44MiB 软盘映像。在 QEMU 中它可以是 运行,命令如下:

qemu-system-i386 -fda build/disk.img

预期输出应类似于:


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

一个版本的代码,用的比较复杂GCC extended inline assembly is presented below. This answer is not meant to be a discussion on GCC's extended inline assembly usage, but there is information网上约一下吧。应该注意的是,有很多错误的建议、文档、教程和充满问题的示例代码 是由对主题没有正确理解的人编写的。 您已被警告!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

link.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);
    }
}

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

include/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

include/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 [=19=]x10\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

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

链接描述文件和引导加载程序与本答案中提供的第一个版本相比没有任何修改。

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


脚注:

  • 1"Writing a bootloader in C" is a Code Project tutorial. It is highly rated, and got top monthly article at one point. Unfortunately like many tutorials that involve inline assembly, they teach a lot of bad habits and get things wrong. They were lucky to have their code work with the compiler they did use. Many people attempt to use those bad ideas to write real-mode kernels with GCC and fail miserably. I single the Code Project tutorial out because it has been the basis for many questions on Whosebug in the past. Like many other tutorials it really can't be trusted at all. One exception is the article Real mode in C with gcc : writing a bootloader.

    的热门 Google 之一

    我提供了第二个代码示例作为最小的完整可验证示例,以显示正确的 GCC 内联汇编看起来像打印字符和打印字符串。很少有文章展示如何使用 GCC 正确执行此操作。第二个示例显示了在 C 函数中编写汇编代码与编写具有低级内联汇编的 C 函数之间的区别,用于 BIOS 调用等所需的事情等。如果您打算使用 GCC 来包装整个汇编代码函数,那么开始时在汇编中编写函数会更容易,问题也更少。这违背了使用 C.

  • 的目的