无法从引导加载程序调用实模式C函数(NASM + GCC工具链) [英] Cannot call real mode C function from bootloader (NASM + GCC toolchain)

查看:132
本文介绍了无法从引导加载程序调用实模式C函数(NASM + GCC工具链)的处理方法,对大家解决问题具有一定的参考价值,需要的朋友们下面随着小编来一起学习吧!

问题描述

我正在尝试编写自己的OS内核,并且在使引导加载程序与(不久将成为)我的内核(用C语言编写)之间的链接正常工作时遇到了一些问题.

I am attempting to write my own OS kernel, and have been having some issues getting the linking to work properly between my bootloader and (what will soon be) my kernel (written in C).

我有以下代码...

src/bootloader.asm

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

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

最后是Makefile

and, finally... the 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.但是,在打印字符串之后什么也没发生.

The bootloader is pretty simple at the moment. It just types out "Booting..." and (attempts to) load kmain. However, nothing happens after the string is printed.

当调用kmain时,我仍处于实模式,因此我不希望失败是因为无法从我的嵌入式程序集访问BIOS中断.如果我错了,请纠正我.

I am still in real-mode when kmain gets called so I don't expect the failure is because of lack of access to BIOS interrupts from my inline assembly. Correct me if I'm wrong.

推荐答案

我不建议将GCC用于16位代码.一个GCC替代方案可能是单独的 IA16-GCC项目,该项目正在进行中,尚在试验中

I don't recommend GCC for 16-bit code. A GCC alternative may be the separate IA16-GCC project which is a work in progress and is experimental.

由于需要内联汇编,因此很难使GCC发出适当的实模式代码.如果您希望避免细微的错误,尤其是在启用优化的情况下,GCC的内联汇编很难实现.可以编写这样的代码,但强烈建议.

It is hard to get GCC to emit proper real-mode code because of the need for inline assembly. GCC's inline assembly is difficult to get right if you wish to avoid subtle bugs especially when optimizations are enabled. It is possible to write such code but I strongly advise against it.

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

You don't have a linker script so your compiled C code was placed after the bootloader signature. The BIOS only reads one sector into memory. Your jmp kmain ends up jumping to memory where the kernel would have been had it actually been loaded into memory, but it wasn't loaded so it fails to work as expected. You need to add code to call BIOS Int 13/AH=2 to read additional disk sectors starting from Cylinder, Head, Sector (CHS) = (0,0,2) which is the sector right after the bootloader.

您的引导程序未正确设置段寄存器.因为您使用的是GCC,所以期望CS = DS = ES = SS.由于我们需要将数据加载到内存中,因此需要将堆栈放置在安全的地方.内核将被加载到0x0000:0x7e00,因此我们可以将堆栈放置在引导加载程序下方的0x0000:0x7c00处,它们不会冲突.您需要先使用 CLD 清除方向标记(DF),然后再调用GCC要求.我的常规Bootloader提示.在我的其他 Stackoverflow中,可以找到更复杂的引导程序,该程序可以确定内核的大小(stage2)并从磁盘读取适当数量的扇区.答案.

Your bootloader doesn't properly set up the segment registers. Because you are using GCC, it expects CS=DS=ES=SS. Since we need to load data into memory we need to put the stack somewhere safe. The kernel will be loaded to 0x0000:0x7e00, so we can place the stack below the bootloader at 0x0000:0x7c00 where they won't conflict. You need to clear the direction flag (DF) with CLD before calling GCC as it is a requirement. Many of these issues are captured in my General Bootloader Tips. 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 Stackoverflow answer.

我们需要一个链接描述文件,以正确地将内容布置在内存中,并确保指令从一开始就跳转到实际的 C 入口点kmain.我们还需要适当地将BSS部分归零,因为GCC希望这样做.链接描述文件用于确定BSS节的开始和结束.函数zero_bss将该内存清除为0x00.

We need a linker script to properly lay things out in memory and ensure the instruction(s) at the very beginning jumps to the real C entry point kmain. We also need to properly zero out the BSS section because GCC expects that. The linker script is used to determine the beginning and the end of the BSS section. The function zero_bss clears that memory to 0x00.

可以对Makefile进行一些清理,以使将来添加代码更加容易.我已经修改了代码,以便在src目录中构建目标文件.这样可以简化制作过程.

The Makefile could be cleaned up a bit to make adding code easier in the future. I've amended the code so the object files get built in the src directory. This simplifies the make processing.

当引入实模式代码支持并将支持添加到GNU汇编程序时,它通过使用asm (".code16gcc");在GCC中启用.现在,GCC已经支持了执行相同功能的-m16选项.使用-m16,您无需在所有文件的顶部添加.code16gcc指令.

When the real-mode code support was introduced and support added to GNU assembler it was enabled in GCC by using asm (".code16gcc");. For quite some time now GCC has supported the -m16 option that does the same thing. With -m16 you don't need to add the .code16gcc directive to the top of all the files.

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

I haven't modified your inline assembly that prints a to the screen. Just because I didn't modify it, doesn't mean that it doesn't have problems. Since registers are clobbered and the compiler isn't told of that it can lead to strange bugs especially when optimizations are on. The second part of this answer shows a mechanism to use the BIOS to print characters and strings to the console with proper inline assembly.

我建议使用编译器选项-Os -mregparm=3 -fomit-frame-pointer来优化空间.

I recommend the compiler options -Os -mregparm=3 -fomit-frame-pointer to optimize for space.

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中运行:

A 1.44MiB floppy disk image called build/disk.img is created. It can be run in QEMU with a command like:

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

预期输出应类似于:

使用更复杂的 GCC扩展内联汇编的代码版本如下所示.这个答案不是要讨论GCC扩展的内联汇编用法,而是有

A version of the code that uses more complex 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 online about it. It should be noted that there is a lot of bad advice, documentation, tutorials, and sample code fraught with problems written by people who may not have had a proper understanding of the subject. You have been warned!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;
}

此答案中给出的第一个版本未更改链接描述文件和引导加载程序.

The linker script and bootloader are unmodified from the first version presented in this answer.

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

When run in QEMU the output should look similar to:

  • 1 是Code Project教程.它获得了很高的评价,并且一次获得了月度最高的文章. 不幸的是,就像许多涉及内联汇编的教程一样,它们教导了许多不良习惯并弄错了事.他们很幸运能够将代码与他们使用的编译器一起使用.许多人试图用这些坏主意用GCC编写实模式内核,但不幸失败了.我之所以选择Code Project教程,是因为它是过去Stackoverflow上许多问题的基础.像许多其他教程一样,它根本是不可信任的.一个例外是文章 带有gcc的C实模式:编写引导加载程序 .

  • 1One of the top Google hits for "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 Stackoverflow 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.

我提供了第二个代码示例作为最小的完整可验证示例",以显示GCC内联汇编看起来像打印一个字符和打印一个字符串的样子.很少有文章显示如何使用GCC正确执行此操作.第二个示例显示了在 C 函数内部编写汇编代码与编写低级内联汇编以编写BIOS调用等必需内容的 C 函数之间的区别.使用 GCC 来包装整个汇编代码函数,那么从一开始就以汇编语言编写函数就容易多了,问题也就更少了.这违反了使用 C 的目的.

I have provided the second code sample as a Minimal Complete Verifiable Example to show what proper GCC inline assembly looks like to print a character and to print a string. There are very few articles out there that show how to do this properly using GCC. The second example shows the difference between writing assembly code inside a C function and writing a C function with low level inline assembly for required things like BIOS calls etc. If you are going to use GCC to wrap entire assembly code functions then it is much easier and less problematic to write the functions in assembly to begin with. That defeats the purpose of using C.

这篇关于无法从引导加载程序调用实模式C函数(NASM + GCC工具链)的文章就介绍到这了,希望我们推荐的答案对大家有所帮助,也希望大家多多支持IT屋!

查看全文
登录 关闭
扫码关注1秒登录
发送“验证码”获取 | 15天全站免登陆