编译+链接自定义操作系统与Cygwin gcc:无法识别的仿真模式:elf_i386

时间:2018-03-15 09:17:36

标签: c assembly x86 kernel osdev

我有一个用汇编(boot.s)编写的bootloader和一个用c(kernel.c)编写的内核。

我还有一些其他文件,例如:linker.ldgrub.cfg,但我不知道如何使用它们......

我的问题:

如果我跑:

gcc -g -m32 -c -ffreestanding -o kernel.o kernel.c -lgcc
ld -melf_i386 -Tlinker.ld -nostdlib --nmagic -o kernel.elf kernel.o
objcopy -O binary kernel.elf kernel.bin

我收到错误:ld: Unrecognized emulation mode: elf_i386

PS:我正在使用Windows 10 Pro 32Bit并且还安装了VirtualBox(如果这有帮助)gcc即时使用cygwin。

kernel.c

#include <stdbool.h>
#include <stddef.h>
#include <stdint.h>

static const uint8_t COLOR_BLACK = 0;
static const uint8_t COLOR_BLUE = 1;
static const uint8_t COLOR_GREEN = 2;
static const uint8_t COLOR_CYAN = 3;
static const uint8_t COLOR_RED = 4;
static const uint8_t COLOR_MAGENTA = 5;
static const uint8_t COLOR_BROWN = 6;
static const uint8_t COLOR_LIGHT_GREY = 7;
static const uint8_t COLOR_DARK_GREY = 8;
static const uint8_t COLOR_LIGHT_BLUE = 9;
static const uint8_t COLOR_LIGHT_GREEN = 10;
static const uint8_t COLOR_LIGHT_CYAN = 11;
static const uint8_t COLOR_LIGHT_RED = 12;
static const uint8_t COLOR_LIGHT_MAGENTA = 13;
static const uint8_t COLOR_LIGHT_BROWN = 14;
static const uint8_t COLOR_WHITE = 15;

uint8_t make_color(uint8_t fg, uint8_t bg)
{
    return fg | bg << 4;
}

uint16_t make_vgaentry(char c, uint8_t color)
{
    uint16_t c16 = c;
    uint16_t color16 = color;
    return c16 | color16 << 8;
}

size_t strlen(const char* str)
{
    size_t ret = 0;
    while ( str[ret] != 0 )
        ret++;
    return ret;
}

static const size_t VGA_WIDTH = 80;
static const size_t VGA_HEIGHT = 24;

size_t terminal_row;
size_t terminal_column;
uint8_t terminal_color;
uint16_t* terminal_buffer;

void terminal_initialize()
{
    terminal_row = 0;
    terminal_column = 0;
    terminal_color = make_color(COLOR_LIGHT_GREY, COLOR_BLACK);
    terminal_buffer = (uint16_t*) 0xB8000;
    for ( size_t y = 0; y < VGA_HEIGHT; y++ )
        for ( size_t x = 0; x < VGA_WIDTH; x++ )
        {
            const size_t index = y * VGA_WIDTH + x;
            terminal_buffer[index] = make_vgaentry(' ', terminal_color);
        }
}

void terminal_setcolor(uint8_t color)
{
    terminal_color = color;
}

void terminal_putentryat(char c, uint8_t color, size_t x, size_t y)
{
    const size_t index = y * VGA_WIDTH + x;
    terminal_buffer[index] = make_vgaentry(c, color);
}

void terminal_putchar(char c)
{
    terminal_putentryat(c, terminal_color, terminal_column, terminal_row);
    if ( ++terminal_column == VGA_WIDTH )
    {
        terminal_column = 0;
        if ( ++terminal_row == VGA_HEIGHT )
        {
            terminal_row = 0;
        }
    }
}

void terminal_writestring(const char* data)
{
    size_t datalen = strlen(data);
    for ( size_t i = 0; i < datalen; i++ )
        terminal_putchar(data[i]);
}

void kmain()
{
    terminal_initialize();
    terminal_writestring("Starting mOS...\n\n");
    terminal_writestring("mOS Version alpha1 - Created by milan44\n");
}

boot.s

.set ALIGN,    1<<0
.set MEMINFO,  1<<1
.set FLAGS,    ALIGN | MEMINFO
.set MAGIC,    0x1BADB002
.set CHECKSUM, -(MAGIC + FLAGS)

.section .multiboot
.align 4
.long MAGIC
.long FLAGS
.long CHECKSUM

.section .bootstrap_stack
stack_bottom:
.skip 16384
stack_top:

.section .text
.global _start
_start:
    movl $stack_top, %esp

    call kmain

    cli
hang:
    hlt
    jmp hang

linker.ld

ENTRY(_start)

SECTIONS
{
    . = 1M;

    .text BLOCK(4K) : ALIGN(4K)
    {
        *(.multiboot)
        *(.text)
    }

    .rodata BLOCK(4K) : ALIGN(4K)
    {
        *(.rodata)
    }

    .data BLOCK(4K) : ALIGN(4K)
    {
        *(.data)
    }

    .bss BLOCK(4K) : ALIGN(4K)
    {
        *(COMMON)
        *(.bss)
        *(.bootstrap_stack)
    }
}

1 个答案:

答案 0 :(得分:2)

使用交叉编译器工具链

我强烈建议您构建一个生成 ELF 对象的 C 交叉编译器和工具链。这使您远离主机编译器和链接器的细微差别。默认Cygwin GCC和LD与通用 ELF 编译器和链接器有许多不同之处。 OSDev Wiki包含用于为Cygwin构建交叉编译器的信息。我没有亲自在Cygwin上构建一个交叉编译器,所以不能说这些指令对于那个环境是否准确。

Cygwin生成Windows PE32(32位)和PE32 +(64位)对象。这就是-melf_i386无效的原因。构建 ELF 交叉编译器将允许您使用-melf_i386。在您的情况下,您将需要一个 ELF 交叉编译器,因为多引导加载程序需要 ELF 可执行文件,即Cywgin的 GCC LD 无法生成。

如果您使用的是64位Windows 10,那么您可以在Windows的Linux子系统(WSL)下执行此操作,因为默认情况下Ubuntu的GCC和LD将生成ELF exectuable。

如果您不想使用交叉编译器

尽管推动您使用交叉编译器是正确的方法,但有一种方法可以使它与Cygwin一起使用。

Cygwin GCC(与其他32位Windows编译器一样)将_添加到具有全局可见范围的非静态函数中。这意味着您的kmain实际上是_kmain。修改您的boot.scall _kmain代替call kmain。这适用于您从程序集调用的任何 C 函数。您在 C 代码中访问的程序集文件中提供的任何函数都必须添加_下划线。

Windows程序的一个重要区别是部分名称可能略有不同。 Cygwin中的rodata可以是.rdata*。可能有许多以rdata开头的部分。您必须在链接描述文件中考虑到这一点:

ENTRY(_start)

SECTIONS
{
    . = 1M;

    .text BLOCK(4K) : ALIGN(4K)
    {
        *(.multiboot)
        *(.text)
    }

    .rodata BLOCK(4K) : ALIGN(4K)
    {
        *(.rodata)
        *(.rdata*)    /* IMPORTANT - Windows uses rdata */
    }

    .data BLOCK(4K) : ALIGN(4K)
    {
        *(.data)
    }

    .bss BLOCK(4K) : ALIGN(4K)
    {
        *(COMMON)
        *(.bss)
        *(.bootstrap_stack)
    }
}

这会产生很大的不同,因为如果您没有正确处理rdata部分,那么它们可能会放置在多重引导标头之前,可能会导致GRUB之类的多重引导加载程序无法看到它。所以这种变化非常很重要。

构建可由Multiboot兼容的引导加载程序(或QEMU的-kernel选项)使用的文件的命令不正确。由于LD无法输出 ELF 文件,因此您需要使用OBJCOPY将 PE32 可执行文件转换为32位 ELF 可执行文件。你的OBJCOPY命令做错了。您转换为二进制文件。不幸的是,编写Multiboot标头的方式并不起作用。

汇编和链接代码以及生成可由多重引导加载程序使用的最终kernel.elf文件的命令可能如下所示:

gcc -g -m32 -c -ffreestanding -o kernel.o kernel.c
gcc -g -m32 -c -ffreestanding -o boot.o boot.s
ld -mi386pe -Tlinker.ld -nostdlib --nmagic -o kernel.pe kernel.o boot.o
objcopy -O elf32-i386 kernel.pe kernel.elf

使用Grub和内核制作可引导的ISO / CD

这个程序有点棘手,因为Cygwin没有附带grub-legacy软件包。要在其上创建带有Grub的可引导ISO / CD映像,您需要获取文件stage2_eltorito。您可以从this project下载副本。

您必须运行Cygwin安装程序并安装包genisoimage

在前面的部分中,我们构建了一个名为kernel.elf的文件。现在我们有了构建ISO / CD映像所需的组件。

从您构建kernel.elf的目录中,我们需要创建一系列子目录。这可以通过以下方式完成:

mkdir -p iso/boot/grub

您需要复制stage2_eltorito文件并将其放在iso/boot/grub目录中。您还需要在menu.lst中创建文件iso/boot/grub

iso / boot / grub / menu.lst

default 0
timeout 0

title MyOS
# kernel ELF file.
kernel /boot/kernel.elf

上述过程只需进行一次。这足以用Grub和我们的内核创建一个基本的可引导ISO。

现在构建内核,将文件复制到iso目录并生成ISO / CD映像的过程可以这样完成:

gcc -g -m32 -c -ffreestanding -o kernel.o kernel.c
gcc -g -m32 -c -ffreestanding -o boot.o boot.s
ld -mi386pe -Tlinker.ld -nostdlib --nmagic -o kernel.pe kernel.o boot.o
objcopy -O elf32-i386 kernel.pe kernel.elf
cp kernel.elf iso/boot
genisoimage -R -b boot/grub/stage2_eltorito -no-emul-boot \
    -boot-load-size 4 -boot-info-table -o myos.iso iso

genisoimage命令创建一个名为myos.iso的ISO / CD。您可以使用您喜欢的名称替换myos.iso命令行上的genisoimage,将名称更改为您喜欢的名称。

myos.iso应该可以从大多数硬件,虚拟机和模拟器启动,作为简单的CD映像。使用内核运行时,它应该显示为:

enter image description here

上图是我在QEMU中使用以下命令启动ISO / CD时看到的:

qemu-system-i386 -cdrom myos.iso

如果你在VirtualBox中运行它,你应该会看到类似的内容。