我不推荐 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 处,它们不会发生冲突。您需要在调用 GCC 之前使用 CLD 清除方向标志 (DF),因为这是一项要求。我的General Bootloader Tips 记录了其中许多问题。在我的另一个Stackoverflow answer 中可以找到一个更复杂的引导加载程序,它确定内核(stage2)的大小并从磁盘读取适当数量的扇区。
我们需要一个链接器脚本来在内存中正确布局并确保一开始的指令跳转到真正的C入口点kmain。我们还需要正确地将 BSS 部分归零,因为 GCC 期望这样做。链接描述文件用于确定 BSS 部分的开始和结束。函数zero_bss 将该内存清除为0x00。
Makefile 可以稍微清理一下,以便将来更容易添加代码。我已经修改了代码,以便在 src 目录中构建目标文件。这简化了制作过程。
当引入实模式代码支持并将支持添加到 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
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 中写入字符串
使用更复杂的GCC extended inline assembly 的代码版本如下所示。这个答案并不是要讨论 GCC 的扩展内联汇编用法,但是网上有 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 $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"Writing a bootloader in C" 在 Google 上的热门搜索之一是代码项目教程。它的评价很高,并且曾一度获得月度最高的文章。 不幸的是,就像许多涉及内联汇编的教程一样,它们教了很多坏习惯并弄错了。他们很幸运能够让他们的代码与他们使用的编译器一起工作。许多人试图利用这些坏主意用 GCC 编写实模式内核,结果惨遭失败。我选择了 Code Project 教程,因为它是过去 Stackoverflow 上许多问题的基础。像许多其他教程一样,它真的完全不可信。一个例外是文章Real mode in C with gcc : writing a bootloader。
我提供了第二个代码示例作为最小完整可验证示例,以显示正确的 GCC 内联汇编打印字符和打印字符串的样子。很少有文章展示如何使用 GCC 正确执行此操作。第二个示例显示了在 C 函数中编写汇编代码与使用低级内联汇编编写 C 函数之间的区别,以用于所需的事情,例如 BIOS 调用等。如果你要使用 GCC 来包装整个汇编代码函数,那么在汇编中编写函数开始时会更容易且问题更少。这违背了使用 C 的目的。