Split from assembly-cheat.

This commit is contained in:
Ciro Santilli
2015-09-06 16:17:39 +02:00
commit c1211ace36
21 changed files with 757 additions and 0 deletions

4
.gitignore vendored Normal file
View File

@@ -0,0 +1,4 @@
*.bin
*.img
*.iso
*.o

31
Makefile Normal file
View File

@@ -0,0 +1,31 @@
.POSIX:
-include params.makefile
BITS ?= 32
IN_EXT ?= .S
LD ?= ld
MYAS ?= as
OBJ_EXT ?= .o
OUT_EXT ?= .img
RUN ?= bios_hello_world
INS := $(wildcard *$(IN_EXT))
OUTS := $(patsubst %$(IN_EXT),%$(OUT_EXT),$(INS))
.PRECIOUS: %$(OBJ_EXT)
.PHONY: all clean run
all: $(OUTS)
%$(OUT_EXT): %$(OBJ_EXT)
$(LD) --oformat binary -o '$@' '$<' -T a.ld #-Ttext 0x7C00
%$(OBJ_EXT): %$(IN_EXT)
$(MYAS) -o '$@' '$<'
clean:
rm -f *$(OBJ_EXT) *$(OUT_EXT)
run: all
qemu-system-i386 '$(RUN)$(OUT_EXT)'

87
README.md Normal file
View File

@@ -0,0 +1,87 @@
# x86 Bare Metal Examples
Hello world examples of programs without an OS. A.K.A. bare bones.
1. Examples
1. [min-printf](min-printf/)
1. [min](min.S)
1. [bios_one_char](bios_one_char.S)
1. [bios_hello_world](bios_hello_world.S)
1. TODO not working
1. [hello-world-multiboot-c/](hello-world-multiboot-c/)
1. [hajji-hello-world/](hajji-hello-world/)
1. [BIOS](bios.md)
1. [Multiboot](multiboot.md)
## Getting started
sudo apt-get install build-essential qemu
make run
make run RUN=min
make run RUN=bios_one_char
Tested on Ubuntu 14.04. TODO: get working on real hardware:
sudo dd if=bios_hello_world.img of=/dev/sdX
into an USB did not work.
More assembly info at: <https://github.com/cirosantilli/assembly-cheat>
## Formats
When we create a regular Linux program, we generate an ELF file, which is read by the OS.
Without an OS, we can use the following formats:
- boot sector. TODO where is it specified, if at all?
- multiboot. Defined by GRUB. More boilerplate, but much more convenient.
## Gotchas
- bytes 511 and 512 of the boot sector must be `0x55aa` or else the BIOS will refuse to load
- BIOS loads the program into memory at the address `0x7C00`.
We must tell that magic number to the linker somehow, either with a linker script, `-tText=-Ttext 0x7C00` or NASM `org 0x7c00`.
This will only matter when you access a memory address, because of relocation.
If you don't know what relocation is, first read this: <http://stackoverflow.com/questions/12122446/how-does-c-linking-work-in-practice/30507725#30507725>
When we link a normal program with an OS, the linker tells where it wants the OS to place it in virtual memory.
But for the boot sector, the BIOS puts the program into memory. So we must tell that to the linker somehow. Otherwise it cannot know what addresses to use for instructions.
- x86 processors start in 16-bit mode.
## IO
You cannot use any libraries, so how to do IO? Some ways that this can be done:
- BIOS functions: <http://wiki.osdev.org/BIOS>. Not well standardized like it's successor UEFI.
- <https://en.wikipedia.org/wiki/VGA-compatible_text_mode>
- VBE <https://en.wikipedia.org/wiki/VESA_BIOS_Extensions>
## Bibliography
- <http://stackoverflow.com/questions/22054578/run-a-program-without-an-operating-system>
- <https://github.com/programble/bare-metal-tetris> tested on Ubuntu 14.04. Just works.
Has Multiboot and El Torito. Uses custom linker script.
Almost entirely in C `-nostdlib`, with very few inline `asm` commands, and a small assembly entry point. So a good tutorial in how to do the bridge.
- osdev.org is a major source for this.
- <http://wiki.osdev.org/C%2B%2B_Bare_Bones>
- <http://wiki.osdev.org/Text_UI>
- <http://wiki.osdev.org/GUI>
- <https://github.com/scanlime/metalkit> A more automated / general bare metal compilation system. Untested, but looks promising.
The following did not work on my machine out of the box:
- <https://github.com/apparentlymart/ToyOS>
- <https://github.com/rde1024/toyos>

9
a.ld Normal file
View File

@@ -0,0 +1,9 @@
SECTIONS
{
/* We could also pass the -Ttext 0x7C00 to as instead of doing this. */
. = 0x7c00;
bootsec :
{
*(.text)
}
}

37
bios.md Normal file
View File

@@ -0,0 +1,37 @@
# BIOS
<https://en.wikipedia.org/wiki/BIOS>
<http://wiki.osdev.org/BIOS>
<http://wiki.osdev.org/Real_Mode>
Can only be used in real mode.
## Get BIOS information
## SMBIOS
## dmidecode
<http://stackoverflow.com/questions/20604644/how-to-check-the-bios-version-or-name-in-linux-through-command-prompt>
<https://en.wikipedia.org/wiki/System_Management_BIOS>
Standardized by: <https://en.wikipedia.org/wiki/Distributed_Management_Task_Force>
TODO: how is it obtained at the low level?
## UEFI
<https://en.wikipedia.org/wiki/Unified_Extensible_Firmware_Interface>
<https://github.com/tqh/efi-example>
## SeaBIOS
<http://www.seabios.org/SeaBIOS>
Open source x86 BIOS implementation.
Default BIOS for QEMU and KVM.

16
bios_hello_world.S Normal file
View File

@@ -0,0 +1,16 @@
.code16
cli
mov $msg, %si
mov $0x0e, %ah
loop:
lodsb
or %al, %al
jz halt
int $0x10
jmp loop
halt:
hlt
msg:
.asciz "hello world"
.org 510
.word 0xaa55

10
bios_one_char.S Normal file
View File

@@ -0,0 +1,10 @@
# Print a single `@` character with the BIOS.
# More minimal than the hello world.
.code16
cli
mov $0x40, %al
mov $0x0e, %ah
int $0x10
hlt
.org 510
.word 0xaa55

View File

@@ -0,0 +1,19 @@
RUN = hello.img
INC = biosfunc.S
all: $(INC) $(RUN)
$(RUN): hello.o
ld -N -e start -Ttext 0x7c00 --oformat binary -o $(RUN) hello.o
hello.o: hello.S $(INC)
as -o hello.o hello.S
disassemble: $(RUN)
objdump --disassemble-all --target=binary --architecture=i8086 $(RUN)
run: $(RUN)
qemu-system-i386 -hda $(RUN)
clean:
rm -f *.o a.out $(RUN)

View File

@@ -0,0 +1,12 @@
# Hajji hello world
TODO broken
Originally taken from <http://farid.hajji.name/blog/2010/05/25/hello-world-on-the-bare-metal/>
Not working very well out of the box: output toggles randomly between printing:
- 2 hello worlds
- 4 hello worlds
- the above and then resetting the screen
- rubbish

View File

@@ -0,0 +1,81 @@
/* biosfunc.S -- real-mode BIOS and convenience functions. */
.file "biosfunc.S"
.code16
/*
* The following convenience functions are only available
* in real mode through BIOS:
*
* void clrscr() # clear display
* void curshome() # move cursor home (0:0)
* void puts(%si) # display string
* void putc(%al) # display char
*
* use this libary like this:
* .include biosfunc.S
*/
/* clrscr() -- clear dislay */
clrscr:
/*
* clrscr() clears the video buffer, using a special case in
* the BIOS function "SCROLL UP WINDOW". Note that this
* function is only available in real mode, and that some
* buggy BIOSes destroy the base pointer %bp, so we better
* temporarily save it on the stack.
*/
pushw %bp # BIOS call below *can* destroy %BP
movb $0x06, %ah # BIOS function "SCROLL UP WINDOW"
movb $0x0, %al # nr. of lines to scroll (00=clear window)
movb $0x7, %bh # attr. to fill new lines at bottom
movw $0x0, %cx # CH,CL: row,column upper left corner (00:00)
movw $0x184f, %dx # DH,DL: row,column lower right corner (24:79)
int $0x10 # call BIOS
popw %bp
retw
/* curshome() -- set cursor position to 0:0 */
curshome:
/*
* curshome() moves the cursor to position 0:0 (top:left),
* using the BIOS function "SET CURSOR POSITION". This
* function is only available in real mode.
*/
movb $0x02, %ah # BIOS function "SET CURSOR POSITION"
movb $0x0, %bh # page number 0
movw $0x0, %dx # DH=0 row, DL=0 col
int $0x10 # call BIOS
retw
/* puts(%si) -- display 0-terminated string via putc() */
puts:
/*
* puts() repeatedly loads a byte from the buffer pointed
* to by %si into %al, and displays that byte by calling
* putc(%al), until a \0-byte is encountered. The buffer
* should thus be \0-terminated, like a regular C-string.
*/
lodsb # Load next byte from %si buffer into %al
cmpb $0x0, %al # %al == 0?
je puts1 # Yes: end of string!
callw putc # No: Display current char
jmp puts # Proceed next char
puts1: retw
/* putc(%al) -- output char %al via BIOS call int 10h, func 0Eh */
putc:
/*
* putc(%al) displays the byte %al on the default video
* buffer, using the BIOS function "TELETYPE OUTPUT".
* This function interprets some but not all control
* characters correctly, but it doesn't matter all too
* much in this simple example. This BIOS function is
* only available in real mode.
*/
movw $0x7, %bx # BH: page 0, BL: attribute 7 (normal white)
movb $0xe, %ah # BIOS function "TELETYPE OUTPUT"
int $0x10 # call BIOS
retw

110
hajji-hello-world/hello.S Normal file
View File

@@ -0,0 +1,110 @@
/* hello.S -- Hello, World on bare metal, just after BIOS boot. x86 */
.file "hello.S"
/*
* A couple of constants.
*
* These can't be changed, because they are set by the
* firmware (BIOS).
*/
.set LOAD, 0x7c00 # BIOS loads and jumps here
.set MAGIC, 0xaa55 # Must be at the end of the 512-byte block
.set BLOCKSIZE, 512 # Boot block is BLOCKSIZE bytes long
/*
* The .text section contains the opcodes (code) for our
* program.
*/
.section .text # This is a code (text) section.
.code16 # Boot code runs in 16-bit real mode
.globl start # Entry point is public, for the linker.
start:
/*
* The processor starts in real mode and executes the first
* instruction at address $0xFFFF:FFF0. System designers
* usually map BIOS at this address, so the CPU starts running
* BIOS code. The BIOS initializes RAM and other components.
* Then, it loads $BLOCKSIZE bytes from the first boot device
* in RAM, starting at address $0x0:$LOAD.
*
* If that block finishes with the $MAGIC sequence 0x55, 0xaa
* (it is reversed, because IA-32 arch is little endian), BIOS
* considers this block a valid boot block, and jumps right here.
*/
/*
* Initialize segment descriptors %ds, %es, and %ss to 0x0.
* %cs:%ip is already set by the BIOS to 0x0:$LOAD.
*/
xorw %ax, %ax
movw %ax, %es
movw %ax, %ds
movw %ax, %ss
/*
* Initialize the stack.
*
* Since the stack on x86 grows towards *lower* addresses,
* we anchor it at $LOAD. Note that we don't collide with
* the code because the stack will always remain below
* (i.e. less than) $LOAD and grows downwards from there.
*/
movw $LOAD, %sp
/*
* This is the "main" program:
*
* Clear screen, move cursor to the top:left,
* and display a friendly greetings.
*/
callw clrscr # clear screen
callw curshome # move cursor home - top:left
callw greeting # display a greeting string
/*
* That's all, folks!
*
* We could run a tight loop here, but it's better to halt
* the processor. When run on bare metal, a halted processor
* consumes less power (especially useful if ran on battery).
* When run under an emulator, the emulator doesn't consume
* further CPU cycles.
*/
hlt
/* greeting() -- display a little message. */
greeting:
/*
* greeting dislays the string located at label msg,
* using the convenience function puts() defined below.
* We pass the *address* of that string (thus $msg instead
* of msg) in the %si register.
*/
movw $msg, %si
callw puts
retw
/*
* Finally, include the BIOS convenience functions used above.
*/
.include "biosfunc.S" # BIOS convenience functions.
.file "hello.S"
/* msg: the string buffer to be displayed. */
msg:
.asciz "Hello, World!\r\n" # must be \0-terminated!
/*
* The boot block MUST end with a MAGIC sequence.
*
* The BIOS checks this, and would refuse to boot unless
* MAGIC is there. The last two bytes of the BLOCKSIZE
* long block must contain the magic sequence 0x55, 0xaa.
* We move the assembler pointer .org there, and emit the
* word MAGIC. Note that MAGIC is set to 0xaa55, and not
* 0x55aa, because the IA-32 platform is little endian.
*/
.org BLOCKSIZE - 2
.word MAGIC

View File

@@ -0,0 +1,20 @@
.POSIX:
BIN := isodir/boot/main.bin
MAIN := main.iso
.PHONY: clean run
$(MAIN):
as -32 boot.S -o boot.o
gcc -c kernel.c -ffreestanding -m32 -o kernel.o -std=gnu99
gcc -ffreestanding -m32 -nostdlib -o '$(BIN)' -T linker.ld boot.o kernel.o -lgcc
grub-mkrescue -o main.iso isodir
clean:
rm -f *.o '$(BIN)' '$(MAIN)'
run: $(MAIN)
qemu-system-i386 -cdrom '$(MAIN)'
# Would also work.
#qemu-system-i386 -kernel myos.bin

View File

@@ -0,0 +1,26 @@
# Hello world multiboot C
Originally from: <http://wiki.osdev.org/Bare_Bones>, should be a reasonable way to start a serious OS.
## TODO get working
sudo aptitude install -y build-essential gcc-multilib qemu xorriso
make run
QEMU opens the boot menu, but if I select `main` it fails with: "no multiboot header found". Things which might be wrong:
- not using cross compiler
- generated `.bin` does not contain the multiboot section. TODO why?
Related questions:
- <http://stackoverflow.com/questions/17539464/grub-2-not-detecting-multiboot-header-in-kernel>
- <http://stackoverflow.com/questions/25517545/error-no-multiboot-header-found>
---
A hello world, with multiboot and a C interface.
The multiboot interface is prepared in GAS assembly.
Generates a bootable disk image by using `grub-mkrescue` on the multiboot binary.

View File

@@ -0,0 +1,77 @@
# Declare constants used for creating a multiboot header.
.set ALIGN, 1<<0 # align loaded modules on page boundaries
.set MEMINFO, 1<<1 # provide memory map
.set FLAGS, ALIGN | MEMINFO # this is the Multiboot 'flag' field
.set MAGIC, 0x1BADB002 # 'magic number' lets bootloader find the header
.set CHECKSUM, -(MAGIC + FLAGS) # checksum of above, to prove we are multiboot
# Declare a header as in the Multiboot Standard. We put this into a special
# section so we can force the header to be in the start of the final program.
# You don't need to understand all these details as it is just magic values that
# is documented in the multiboot standard. The bootloader will search for this
# magic sequence and recognize us as a multiboot kernel.
.section .multiboot
.align 4
.long MAGIC
.long FLAGS
.long CHECKSUM
# Currently the stack pointer register (esp) points at anything and using it may
# cause massive harm. Instead, we'll provide our own stack. We will allocate
# room for a small temporary stack by creating a symbol at the bottom of it,
# then allocating 16384 bytes for it, and finally creating a symbol at the top.
.section .bootstrap_stack, "aw", @nobits
stack_bottom:
.skip 16384 # 16 KiB
stack_top:
# The linker script specifies _start as the entry point to the kernel and the
# bootloader will jump to this position once the kernel has been loaded. It
# doesn't make sense to return from this function as the bootloader is gone.
.section .text
.global _start
.type _start, @function
_start:
# Welcome to kernel mode! We now have sufficient code for the bootloader to
# load and run our operating system. It doesn't do anything interesting yet.
# Perhaps we would like to call printf("Hello, World\n"). You should now
# realize one of the profound truths about kernel mode: There is nothing
# there unless you provide it yourself. There is no printf function. There
# is no <stdio.h> header. If you want a function, you will have to code it
# yourself. And that is one of the best things about kernel development:
# you get to make the entire system yourself. You have absolute and complete
# power over the machine, there are no security restrictions, no safe
# guards, no debugging mechanisms, there is nothing but what you build.
# By now, you are perhaps tired of assembly language. You realize some
# things simply cannot be done in C, such as making the multiboot header in
# the right section and setting up the stack. However, you would like to
# write the operating system in a higher level language, such as C or C++.
# To that end, the next task is preparing the processor for execution of
# such code. C doesn't expect much at this point and we only need to set up
# a stack. Note that the processor is not fully initialized yet and stuff
# such as floating point instructions are not available yet.
# To set up a stack, we simply set the esp register to point to the top of
# our stack (as it grows downwards).
movl $stack_top, %esp
# We are now ready to actually execute C code. We cannot embed that in an
# assembly file, so we'll create a kernel.c file in a moment. In that file,
# we'll create a C entry point called kernel_main and call it here.
call kernel_main
# In case the function returns, we'll want to put the computer into an
# infinite loop. To do that, we use the clear interrupt ('cli') instruction
# to disable interrupts, the halt instruction ('hlt') to stop the CPU until
# the next interrupt arrives, and jumping to the halt instruction if it ever
# continues execution, just to be safe. We will create a local label rather
# than real symbol and jump to there endlessly.
cli
hlt
.Lhang:
jmp .Lhang
# Set the size of the _start symbol to the current location '.' minus its start.
# This is useful when debugging or when you implement call tracing.
.size _start, . - _start

View File

@@ -0,0 +1,3 @@
menuentry "main" {
multiboot /boot/main.bin
}

View File

@@ -0,0 +1,113 @@
#if !defined(__cplusplus)
#include <stdbool.h> /* C doesn't have booleans by default. */
#endif
#include <stddef.h>
#include <stdint.h>
/* Check if the compiler thinks we are targeting the wrong operating system. */
#if defined(__linux__)
/* TODO what is this check? It was failing. */
/*#error "You are not using a cross-compiler, you will most certainly run into trouble"*/
#endif
/* This tutorial will only work for the 32-bit ix86 targets. */
#if !defined(__i386__)
#error "This tutorial needs to be compiled with a ix86-elf compiler"
#endif
/* Hardware text mode color constants. */
enum vga_color {
COLOR_BLACK = 0,
COLOR_BLUE = 1,
COLOR_GREEN = 2,
COLOR_CYAN = 3,
COLOR_RED = 4,
COLOR_MAGENTA = 5,
COLOR_BROWN = 6,
COLOR_LIGHT_GREY = 7,
COLOR_DARK_GREY = 8,
COLOR_LIGHT_BLUE = 9,
COLOR_LIGHT_GREEN = 10,
COLOR_LIGHT_CYAN = 11,
COLOR_LIGHT_RED = 12,
COLOR_LIGHT_MAGENTA = 13,
COLOR_LIGHT_BROWN = 14,
COLOR_WHITE = 15,
};
uint8_t make_color(enum vga_color fg, enum vga_color 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 = 25;
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]);
}
#if defined(__cplusplus)
extern "C" /* Use C linkage for kernel_main. */
#endif
void kernel_main() {
/* Initialize terminal interface */
terminal_initialize();
/* Since there is no support for newlines in terminal_putchar
* yet, '\n' will produce some VGA specific character instead.
* This is normal.
*/
terminal_writestring("Hello, kernel World!\n");
}

View File

@@ -0,0 +1,44 @@
/* The bootloader will look at this image and start execution at the symbol
designated as the entry point. */
ENTRY(_start)
/* Tell where the various sections of the object files will be put in the final
kernel image. */
SECTIONS
{
/* Begin putting sections at 1 MiB, a conventional place for kernels to be
loaded at by the bootloader. */
. = 1M;
/* First put the multiboot header, as it is required to be put very early
early in the image or the bootloader won't recognize the file format.
Next we'll put the .text section. */
.text BLOCK(4K) : ALIGN(4K)
{
*(.multiboot)
*(.text)
}
/* Read-only data. */
.rodata BLOCK(4K) : ALIGN(4K)
{
*(.rodata)
}
/* Read-write data (initialized) */
.data BLOCK(4K) : ALIGN(4K)
{
*(.data)
}
/* Read-write data (uninitialized) and stack */
.bss BLOCK(4K) : ALIGN(4K)
{
*(COMMON)
*(.bss)
*(.bootstrap_stack)
}
/* The compiler may produce other sections, by default it will put them in
a segment with the same name. Simply add stuff here as needed. */
}

19
min-printf/Makefile Normal file
View File

@@ -0,0 +1,19 @@
.POSIX:
MAIN := main.img
.PHONY: clean run
$(MAIN):
rm -f $(MAIN)
# hlt instruction.
printf '\364' >> '$(MAIN)'
printf '%509s' >> '$(MAIN)'
# Mandatory magic bytes 511 and 512 of a boot sector.
printf '\125\252' >> '$(MAIN)'
clean:
rm -f '$(MAIN)'
run: $(MAIN)
qemu-system-i386 -hda '$(MAIN)'

3
min-printf/README.md Normal file
View File

@@ -0,0 +1,3 @@
# min-printf
Minimal example that does nothing, just halts immediately, generated with `printf`.

14
min.S Normal file
View File

@@ -0,0 +1,14 @@
/* Minimal example that does nothing, just halts. */
/* Tell GAS to generate 16 bit code. */
.code16
/* Don't listen to interrupts. */
cli
/* Stop the processor. */
hlt
/* Mandatory magic bytes 511 and 512. */
.org 510
.word 0xaa55

22
multiboot.md Normal file
View File

@@ -0,0 +1,22 @@
# Multiboot
<https://en.wikipedia.org/wiki/Multiboot_Specification>
Standard created by GRUB for booting OSes.
Multiboot files are an extension of ELF files with a special header.
Advantages: GRUB does housekeeping magic for you:
- you can store the OS as a regular file inside a filesystem
- your program starts in 32-bit mode already, not 16 bit real mde
Disadvantages:
- more boilerplate
GRUB leaves the application into a well defined starting state.
Implemented by Linux and many NIXes, but not Windows or Mac.
Use `grub-mkrescue` to make a multiboot file into a bootable disk.