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

n7taea2i  于 2023-10-19  发布在  其他
关注(0)|答案(1)|浏览(84)

我试图写我自己的操作系统内核,并一直有一些问题,使链接正常工作之间的引导装载程序和(什么很快将是)我的内核(用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 $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);
}

最后生成文件

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

bootloader目前非常简单。它只是键入“Booting...”并(尝试)加载kmain。但是,在打印字符串之后什么也没有发生。
kmain被调用时,我仍然处于实模式,所以我不认为失败是因为无法从内联程序集访问BIOS中断。如果我说错了请纠正我。

mum43rcc

mum43rcc1#

我不推荐GCC用于16位代码。GCC的替代方案可能是单独的IA16-GCC project,这是一个正在进行的工作,是实验性的。
因为需要内联汇编,所以很难让GCC发出正确的实模式代码。如果您希望避免细微的错误,特别是在启用优化时,GCC的内联汇编很难正确。写这样的代码是可能的,但我强烈建议不要这样做
您没有链接器脚本,因此编译后的 C 代码被放置在引导加载程序签名之后。BIOS只将一个扇区读入内存。您的jmp kmain最终会跳转到内核实际加载到内存中时应该在的内存中,但它没有加载,因此无法按预期工作。您需要添加代码来调用BIOS Int 13/AH=2来读取从Cylinder,Head,Sector(CHS)=(0,0,2)开始的其他磁盘扇区,这是 Bootstrap 之后的扇区。
您的引导加载程序没有正确设置段寄存器。因为你使用的是GCC,所以它需要CS=DS=ES=SS。因为我们需要将数据加载到内存中,所以我们需要将堆栈放在安全的地方。内核将被加载到0x 0000:0x 7 e00,所以我们可以将堆栈放在引导加载程序0x 0000:0x 7 c 00下面,这样它们就不会冲突。在调用GCC之前,您需要使用CLD清除方向标志(DF),因为这是必需的。其中许多问题都在我的通用引导加载程序提示中得到了解决。在我的另一个Stackoverflow answer中可以找到一个更复杂的 Bootstrap ,它确定内核的大小(stage 2)并从磁盘读取适当数量的扇区。
我们需要一个链接器脚本来正确地在内存中布局,并确保指令在一开始就跳转到真实的 C 入口点kmain。我们还需要正确地将BSS部分归零,因为GCC希望如此。链接器脚本用于确定BSS部分的开始和结束。函数zero_bss将该内存清零为0x 00。
Makefile可以稍微清理一下,以便将来更容易添加代码。我已经修改了代码,所以目标文件被构建在src目录中。这简化了制作过程。
当实模式代码支持被引入并添加到GNU汇编程序中时,它在GCC中通过使用asm (".code16gcc");启用。一段时间以来,GCC一直支持-m16选项,它可以做同样的事情。使用-m16,您不需要将.code16gcc指令添加到所有文件的顶部。
我还没有修改将a打印到屏幕的内联程序集。只是因为我没有修改它,并不意味着它没有问题。由于寄存器被破坏,编译器没有被告知,这可能会导致奇怪的错误,特别是当优化打开时。这个答案的第二部分展示了一种使用BIOS将字符和字符串打印到控制台的机制。
我推荐编译器选项-Os -mregparm=3 -fomit-frame-pointer来优化空间。

Makefile

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 $0, %dl;"
        "inc %dh;"
        "movb $2, %ah;"
        "movb $0, %bh;"
        "int $0x10;"
        "movb $'a', %al;"
        "movb $10, %ah;"
        "movw $1, %cx;"
        "int $0x10;"
    );

    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正确使用Inline Assembly写字符串

下面是使用更复杂的GCC extended inline assembly的代码版本。这个答案并不意味着要讨论GCC的扩展内联汇编用法,但在线上有关于它的information。应该注意的是,有很多不好的建议,文档,教程和充满问题的示例代码是由可能对主题没有正确理解的人编写的。”你被警告!**1

Makefile

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 $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

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 Google对"Writing a bootloader in C"的热门搜索之一是Code Project教程。它的评价很高,并在一个点上获得了每月最高的文章。**不幸的是,就像许多涉及内联汇编的教程一样,它们教了很多坏习惯和错误的东西。他们很幸运,他们的代码可以与他们使用的编译器一起工作。许多人试图使用这些糟糕的想法来编写实模式内核与GCC和失败的悲惨。我之所以选择Code Project教程,是因为它是过去Stackoverflow上许多问题的基础。像许多其他教程一样,它真的不能被信任。一个例外是文章Real mode in C with gcc : writing a bootloader

我已经提供了第二个代码示例作为最小的完整可验证示例,以显示正确的GCC内联程序集打印字符和字符串的样子。很少有文章展示如何使用GCC正确地做到这一点。第二个例子展示了在 C 函数内部编写汇编代码和编写带有低级内联汇编的 C 函数之间的区别,这些汇编代码用于BIOS调用等所需的事情。如果你打算使用 GCC 来 Package 整个汇编代码函数,那么在开始时用汇编编写函数会更容易,问题也更少。这违背了使用 C 的目的。

相关问题