Files
x86-bare-metal-examples/common.h

753 lines
15 KiB
C
Raw Normal View History

2015-09-20 15:51:38 +02:00
/*
Using macros for now instead of functions because it simplifies the linker script.
2015-09-20 15:51:38 +02:00
But the downsides are severe:
2015-10-18 14:56:12 +02:00
- no symbols to help debugging
- impossible to step over method calls: you have to step into everything
- larger output, supposing I can get linker gc for unused functions working,
see --gc-section, which is for now uncertain.
If I can get this working, I'll definitely move to function calls.
The problem is that if I don't, every image will need a stage 2 loader.
That is not too serious though, it could be added to BEGIN.
## Conventions
2015-10-28 18:36:31 +01:00
Every "function-like macro" should maintain GP register state
(flags currently not maintained).
%sp cannot be used to pass most arguments.
We don't care about setting %bp.
2015-09-20 15:51:38 +02:00
*/
/*
I really want this for the local labels.
The major downside is that every register passed as argument requires `<>`:
http://stackoverflow.com/questions/19776992/gas-altmacro-macro-with-a-percent-sign-in-a-default-parameter-fails-with-oper/
*/
2015-09-20 15:51:38 +02:00
.altmacro
/* Helpers */
/* Push registers ax, bx, cx and dx. Lightweight `pusha`. */
.macro PUSH_ADX
push %ax
push %bx
push %cx
push %dx
.endm
/*
Pop registers dx, cx, bx, ax. Inverse order from PUSH_ADX,
so this cancels that one.
*/
.macro POP_DAX
pop %dx
pop %cx
pop %bx
pop %ax
.endm
2015-10-21 21:41:07 +02:00
.macro PUSH_EADX
push %eax
push %ebx
push %ecx
push %edx
.endm
.macro POP_EDAX
pop %edx
pop %ecx
pop %ebx
pop %eax
.endm
/*
Convert the low nibble of a r8 reg to ASCII of 8-bit in-place.
reg: r8 to be converted
Output: stored in reg itself. Letters are uppercase.
*/
.macro HEX_NIBBLE reg
LOCAL letter, end
cmp $10, \reg
jae letter
add $'0, \reg
jmp end
letter:
/* 0x37 == 'A' - 10 */
add $0x37, \reg
end:
.endm
/*
Convert a byte to hex ASCII value.
c: r/m8 byte to be converted
Output: two ASCII characters, is stored in `ah:al`
http://stackoverflow.com/questions/3853730/printing-hexadecimal-digits-with-assembly
*/
.macro HEX c
mov \c, %al
mov \c, %ah
shr $4, %al
HEX_NIBBLE <%al>
and $0x0F, %ah
HEX_NIBBLE <%ah>
.endm
/* Structural. */
/*
Setup a sane initial state.
Should be the first thing in every file.
2015-10-30 19:54:39 +01:00
Discussion of what is needed exactly: http://stackoverflow.com/a/32509555/895245
2015-11-06 22:05:47 +01:00
*/
2015-10-18 14:56:12 +02:00
.macro BEGIN
.code16
cli
/* Set %cs to 0. TODO Is that really needed? */
2015-10-18 14:56:12 +02:00
ljmp $0, $1f
1:
xor %ax, %ax
/* We must zero %ds for any data access. */
2015-10-18 14:56:12 +02:00
mov %ax, %ds
/* TODO is it really need to clear all those segment registers, e.g. for BIOS calls? */
2015-10-18 14:56:12 +02:00
mov %ax, %es
mov %ax, %fs
mov %ax, %gs
/*
TODO What to move into BP and SP?
http://stackoverflow.com/questions/10598802/which-value-should-be-used-for-sp-for-booting-process
*/
2015-10-30 19:54:39 +01:00
mov %ax, %bp
/* Automatically disables interrupts until the end of the next instruction. */
2015-10-18 14:56:12 +02:00
mov %ax, %ss
/* We should set SP because BIOS calls may depend on that. TODO confirm. */
2015-09-20 20:37:12 +02:00
mov %bp, %sp
2015-10-18 14:56:12 +02:00
.endm
2015-09-20 10:59:36 +02:00
/*
Load stage2 from disk to memory, and jump to it.
To be used when the program does not fit in the 512 bytes.
Sample usage:
STAGE2
Stage 2 code here.
*/
2015-10-18 14:56:12 +02:00
.macro STAGE2
/* Defined in the linker script. */
mov $__stage2_nsectors, %al
mov $0x02, %ah
2015-10-18 14:56:12 +02:00
mov $1f, %bx
mov $0x0002, %cx
mov $0x0080, %dx
2015-10-18 14:56:12 +02:00
int $0x13
jmp 1f
.section .stage2
1:
2015-10-18 14:56:12 +02:00
.endm
/*
Enter protected mode.
Use the simplest GDT possible.
*/
.macro PROTECTED_MODE
/* Must come before they are used. */
.equ CODE_SEG, 8
.equ DATA_SEG, gdt_data - gdt_start
2015-10-18 14:56:12 +02:00
/* Tell the processor where our Global Descriptor Table is in memory. */
lgdt gdt_descriptor
/*
Set PE (Protection Enable) bit in CR0 (Control Register 0),
effectively entering protected mode.
*/
2015-10-18 14:56:12 +02:00
mov %cr0, %eax
orl $0x1, %eax
mov %eax, %cr0
ljmp $CODE_SEG, $protected_mode
/*
Our GDT contains:
- a null entry to fill the unusable entry 0:
http://stackoverflow.com/questions/33198282/why-have-the-first-segment-descriptor-of-the-global-descriptor-table-contain-onl
- a code and data. Both are necessary, because:
- it is impossible to write to the code segment
- it is impossible execute the data segment
Both start at 0 and span the entire memory,
allowing us to access anything without problems.
2015-10-21 21:41:07 +02:00
A real OS might have 2 extra segments: user data and code.
2015-10-27 09:04:46 +01:00
This is the case for the Linux kernel.
2015-10-21 21:41:07 +02:00
This is better than modifying the privilege bit of the GDT
as we'd have to reload it several times, losing cache.
*/
2015-10-18 14:56:12 +02:00
gdt_start:
gdt_null:
.long 0x0
.long 0x0
gdt_code:
.word 0xffff
.word 0x0
.byte 0x0
.byte 0b10011010
.byte 0b11001111
.byte 0x0
gdt_data:
.word 0xffff
.word 0x0
.byte 0x0
.byte 0b10010010
.byte 0b11001111
.byte 0x0
gdt_end:
gdt_descriptor:
.word gdt_end - gdt_start
.long gdt_start
vga_current_line:
.long 0
.code32
2015-10-18 14:56:12 +02:00
protected_mode:
/*
Setup the other segments.
Those movs are mandatory because they update the descriptor cache:
http://wiki.osdev.org/Descriptor_Cache
*/
2015-10-18 14:56:12 +02:00
mov $DATA_SEG, %ax
mov %ax, %ds
mov %ax, %es
mov %ax, %fs
mov %ax, %gs
mov %ax, %ss
/*
TODO detect the last memory address available properly.
It depends on how much RAM we have.
*/
mov $0X7000, %ebp
2015-10-18 14:56:12 +02:00
mov %ebp, %esp
.endm
2015-10-28 18:36:31 +01:00
/*
Setup a single page directory, which give us 2^10 * 2^12 == 4MiB
of identity memory starting at address 0.
The currently executing code is inside that range, or else we'd jump somewhere and die.
*/
.equ page_directory, __end_align_4k
.equ page_table, __end_align_4k + 0x1000
.macro SETUP_PAGING_4M
LOCAL page_setup_start page_setup_end
PUSH_EADX
/* Page directory steup. */
/* Set the top 20 address bits. */
mov $page_table, %eax
/* Zero out the 4 low flag bits of the second byte (top 20 are address). */
and $0xF000, %ax
mov %eax, page_directory
/* Set flags for the first byte. */
mov $0b00100111, %al
mov %al, page_directory
/* Page table setup. */
mov $0, %eax
mov $page_table, %ebx
page_setup_start:
cmp $0x400, %eax
je page_setup_end
/* Top 20 address bits. */
mov %eax, %edx
shl $12, %edx
/*
Set flag bits 0-7. We only set to 1:
- bit 0: Page present
- bit 1: Page is writable.
Might work without this as the permission also depends on CR0.WP.
*/
mov $0b00000011, %dl
/* Zero flag bits 8-11 */
and $0xF0, %dh
mov %edx, (%ebx)
inc %eax
add $4, %ebx
jmp page_setup_start
page_setup_end:
POP_EDAX
.endm
/*
Turn paging on.
Registers are not saved because memory will be all messed up.
*/
.macro PAGING_ON
/* Tell the CPU where the page directory is. */
mov $page_directory, %eax
mov %eax, %cr3
/* Turn paging on. */
mov %cr0, %eax
or $0x80000000, %eax
mov %eax, %cr0
.endm
/* Turn paging off. */
.macro PAGING_OFF
mov %cr0, %eax
and $0x7FFFFFFF, %eax
mov %eax, %cr0
.endm
2015-10-27 09:04:46 +01:00
/* IDT */
.macro IDT_START
idt_start:
.endm
.macro IDT_END
idt_end:
/* Exact same structure as gdt_descriptor. */
idt_descriptor:
.word idt_end - idt_start
.long idt_start
.endm
.macro IDT_ENTRY
2015-10-28 18:36:31 +01:00
/*
Low handler address.
2015-10-27 09:04:46 +01:00
It is impossible to write:
.word (handler & 0x0000FFFF)
as we would like:
http://stackoverflow.com/questions/18495765/invalid-operands-for-binary-and
because this address has to be split up into two.
So this must be done at runtime.
Why this design choice from Intel?! Weird.
*/
.word 0
/* Segment selector: byte offset into the GDT. */
.word CODE_SEG
/* Reserved 0. */
.byte 0
/*
Flags. Format:
- 1 bit: present. If 0 and this happens, triple fault.
- 2 bits: ring level we will be called from.
- 5 bits: fixed to 0xE.
*/
.byte 0x8E
/* High word of base. */
.word 0
.endm
2015-10-28 18:36:31 +01:00
/* Skip n IDT entries, usually to set the Nth one next. */
.macro IDT_SKIP n=1
.skip n * 8
.endm
2015-10-27 09:04:46 +01:00
/*
- index: r/m/imm32 Index of the entry to setup.
- handler: r/m/imm32 Address of the handler function.
*/
.macro IDT_SETUP_ENTRY index, handler
2015-10-28 18:36:31 +01:00
push %eax
push %edx
2015-10-27 09:04:46 +01:00
mov \index, %eax
2015-10-28 18:36:31 +01:00
mov \handler, %edx
mov %dx, idt_start(,%eax, 8)
shr $16, %edx
mov %dx, (idt_start + 6)(,%eax, 8)
pop %edx
pop %eax
2015-10-27 09:04:46 +01:00
.endm
/* Shamelessly copied from James Molloy's tutorial. */
.macro ISR_NOERRCODE i
isr\()\i:
cli
push $0
push $\i
jmp interrupt_handler
.endm
.macro ISR_ERRCODE i
isr\()\i:
cli
push $\i
jmp interrupt_handler
.endm
/*
Entries and handlers.
48 = 32 processor built-ins + 16 PIC interrupts.
*/
.macro IDT_48_ENTRIES
IDT_START
.rept 48
IDT_ENTRY
.endr
IDT_END
.irp i, 0, 1, 2, 3, 4, 5, 6, 7
ISR_NOERRCODE \i
.endr
ISR_ERRCODE 8
ISR_NOERRCODE 9
.irp i, 10, 11, 12, 13, 14
ISR_ERRCODE \i
.endr
.irp i, 15, 16, 17, 18, 19, \
20, 21, 22, 23, 24, 25, 26, 27, 28, 29, \
30, 31, 32, 33, 34, 35, 36, 37, 38, 39, \
40, 41, 42, 43, 44, 45, 46, 47, 48
ISR_NOERRCODE \i
.endr
.endm
.macro IDT_SETUP_48_ISRS
.irp i, 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, \
10, 11, 12, 13, 14, 15, 16, 17, 18, 19, \
20, 21, 22, 23, 24, 25, 26, 27, 28, 29, \
20, 21, 22, 23, 24, 25, 26, 27, 28, 29, \
30, 31, 32, 33, 34, 35, 36, 37, 38, 39, \
40, 41, 42, 43, 44, 45, 46, 47, 48
IDT_SETUP_ENTRY $\i, $isr\()\i
.endr
lidt idt_descriptor
.endm
/* BIOS */
.macro CURSOR_POSITION x=$0, y=$0
PUSH_ADX
mov $0x02, %ah
mov $0x00, %bh
mov \x, %dh
mov \y, %dl
2015-09-20 10:59:36 +02:00
int $0x10
POP_DAX
.endm
2015-09-20 10:59:36 +02:00
/* Clear the screen, move to position 0, 0. */
2015-10-18 14:56:12 +02:00
.macro CLEAR
PUSH_ADX
2015-10-18 14:56:12 +02:00
mov $0x0600, %ax
mov $0x7, %bh
mov $0x0, %cx
mov $0x184f, %dx
int $0x10
CURSOR_POSITION
POP_DAX
2015-10-18 14:56:12 +02:00
.endm
2015-09-20 10:59:36 +02:00
/*
Print a 8 bit ASCII value at current cursor position.
2015-09-20 15:51:38 +02:00
- c r/m/imm8 ASCII value to be printed.
2015-09-20 15:51:38 +02:00
Usage:
PUTC $'a
2015-09-20 15:51:38 +02:00
prints `'a'` to the screen.
2015-09-24 10:17:28 +02:00
*/
.macro PUTC c=$0x20
push %ax
2015-09-24 10:17:28 +02:00
mov \c, %al
mov $0x0E, %ah
int $0x10
pop %ax
2015-09-24 10:17:28 +02:00
.endm
/*
Print a byte as two hexadecimal digits.
- reg: 1 byte register.
2015-09-24 10:17:28 +02:00
*/
.macro PRINT_HEX reg=<%al>
push %ax
HEX <\reg>
PUTC <%al>
PUTC <%ah>
pop %ax
2015-09-24 10:17:28 +02:00
.endm
/*
Print a 16-bit number
2015-10-06 12:27:24 +02:00
- in: r/m/imm16
*/
.macro PRINT_WORD_HEX in=<%ax>
push %ax
mov \in, %ax
PRINT_HEX <%ah>
PRINT_HEX <%al>
pop %ax
2015-10-20 21:04:49 +02:00
.endm
2015-09-29 23:55:54 +02:00
.macro PRINT_NEWLINE
PUTC $'\n
PUTC $'\r
.endm
2015-09-29 23:55:54 +02:00
2015-09-20 15:51:38 +02:00
/*
Print a null terminated string.
Use as:
PRINT_STRING $s
2015-09-20 15:51:38 +02:00
hlt
s:
.asciz "string"
*/
.macro PRINT_STRING s
LOCAL end, loop
2015-09-20 15:51:38 +02:00
mov s, %si
mov $0x0e, %ah
cld
2015-09-20 15:51:38 +02:00
loop:
lodsb
or %al, %al
2015-09-24 10:17:28 +02:00
jz end
2015-09-20 10:59:36 +02:00
int $0x10
2015-09-20 15:51:38 +02:00
jmp loop
2015-09-24 10:17:28 +02:00
end:
2015-09-20 15:51:38 +02:00
.endm
2015-10-06 12:27:24 +02:00
/*
Dump memory.
- s: starting address
- n: number of bytes to dump
*/
.macro PRINT_BYTES s, n=$16
LOCAL end, loop, no_newline
PUSH_ADX
push %di
mov s, %si
mov \n, %cx
mov $0, %di
cld
loop:
cmp $0, %cx
je end
dec %cx
lodsb
PRINT_HEX
PUTC
/* Print a newline for every 8 bytes. */
mov $0, %dx
mov %di, %ax
mov $8, %bx
div %bx
cmp $7, %dx
jne no_newline
PRINT_NEWLINE
no_newline:
inc %di
jmp loop
end:
pop %di
POP_DAX
.endm
/* VGA */
2015-10-18 14:56:12 +02:00
/*
Print a NULL terminated string to position 0 in VGA.
s: 32-bit register or memory containing the address of the string to print.
Clobbers: none.
Uses and updates vga_current_line to decide the current line.
Loops around the to the top.
2015-10-18 14:56:12 +02:00
*/
.macro VGA_PRINT_STRING s
2015-10-18 14:56:12 +02:00
LOCAL loop, end
2015-10-21 21:41:07 +02:00
PUSH_EADX
mov \s, %ecx
mov vga_current_line, %eax
mov $0, %edx
/* Number of horizontal lines. */
mov $25, %ebx
div %ebx
mov %edx, %eax
/* 160 == 80 * 2 == line width * bytes per character on screen */
mov $160, %edx
mul %edx
/* 0xb8000 == magic video memory address which shows on the screen. */
lea 0xb8000(%eax), %edx
2015-10-18 14:56:12 +02:00
/* White on black. */
mov $0x0f, %ah
loop:
mov (%ecx), %al
cmp $0, %al
je end
mov %ax, (%edx)
add $1, %ecx
add $2, %edx
jmp loop
end:
incl vga_current_line
2015-10-21 21:41:07 +02:00
POP_EDAX
.endm
2015-10-20 21:00:24 +02:00
/*
2015-10-28 18:36:31 +01:00
Print a 32-bit r/m/immm in hex.
2015-10-20 21:00:24 +02:00
Sample usage:
mov $12345678, %eax
2015-10-28 18:36:31 +01:00
VGA_PRINT_HEX_4 <%eax>
2015-10-20 21:00:24 +02:00
Expected output on screen:
12345678
*/
2015-10-28 18:36:31 +01:00
.macro VGA_PRINT_HEX_4 reg=<%eax>
LOCAL loop
2015-10-21 21:41:07 +02:00
PUSH_EADX
/* Null terminator. */
mov \reg, %ecx
/* Write ASCII representation to memory. */
push $0
mov $2, %ebx
loop:
HEX <%cl>
mov %ax, %dx
shl $16, %edx
HEX <%ch>
mov %ax, %dx
push %edx
shr $16, %ecx
dec %ebx
cmp $0, %ebx
jne loop
/* Print it. */
mov %esp, %edx
VGA_PRINT_STRING <%edx>
/* Restore the stack. We have pushed 3 * 4 bytes. */
add $12, %esp
2015-10-21 21:41:07 +02:00
POP_EDAX
2015-10-18 14:56:12 +02:00
.endm
2015-10-28 18:36:31 +01:00
/*
Dump memory.
- s: starting address
- n: number of bytes to dump
TODO implement
*/
.macro VGA_PRINT_BYTES s, n=$16
LOCAL end, loop, no_newline
PUSH_ADX
push %edi
mov s, %esi
mov \n, %ecx
mov $0, %edi
cld
loop:
cmp $0, %ecx
je end
dec %ecx
lodsb
PRINT_HEX
PUTC
/* Print a newline for every 8 bytes. */
mov $0, %edx
mov %di, %eax
mov $8, %ebx
div %ebx
cmp $7, %edx
jne no_newline
/*VGA_PRINT_NEWLINE*/
no_newline:
inc %edi
jmp loop
end:
pop %di
POP_DAX
.endm
2015-10-30 15:24:06 +01:00
/* IO ports. */
.macro OUTB value, port
push %ax
mov \value, %al
out %al, \port
pop %ax
.endm
#define PORT_PIC_MASTER_CMD $0x20
#define PORT_PIC_MASTER_DATA $0x21
#define PORT_PIT_CHANNEL0 $0x40
#define PORT_PIT_MODE $0x43
2015-10-30 15:24:06 +01:00
#define PORT_PIC_SLAVE_CMD $0xA0
#define PORT_PIC_SLAVE_DATA $0xA1
/* PIC */
2015-10-30 15:24:06 +01:00
#define PIC_CMD_RESET $0x20
#define PIC_ICR_ADDRESS $0xFEE00300
/* EOI End Of Interrupt: PIC it will not fire again unless we reset it. */
.macro PIC_EOI
OUTB PIC_CMD_RESET, PORT_PIC_MASTER_CMD
.endm
.macro REMAP_PIC_32
/*
Remap the PIC interrupts to start at 32.
TODO understand.
*/
OUTB $0x11, PORT_PIC_MASTER_CMD
OUTB $0x11, PORT_PIC_SLAVE_CMD
OUTB $0x20, PORT_PIC_MASTER_DATA
OUTB $0x28, PORT_PIC_SLAVE_DATA
OUTB $0x04, PORT_PIC_MASTER_DATA
OUTB $0x02, PORT_PIC_SLAVE_DATA
OUTB $0x01, PORT_PIC_MASTER_DATA
OUTB $0x01, PORT_PIC_SLAVE_DATA
OUTB $0x00, PORT_PIC_MASTER_DATA
OUTB $0x00, PORT_PIC_SLAVE_DATA
.endm
/* PIT */
/*
Set the minimum possible PIT frequency.
This is a human friendly frequency: you can see individual events,
but you don't have to wait much for each one.
*/
.macro PIT_SET_MIN_FREQ
mov $0xFF, %al
out %al, PORT_PIT_CHANNEL0
out %al, PORT_PIT_CHANNEL0
.endm
/* IVT */
#define IVT_PIT 8
2015-11-09 14:29:15 +01:00
#define IVT_HANDLER_SIZE 4
#define IVT_CODE_OFFSET 2
/* Setup interrupt handler 8: this is where the PIC maps IRQ 0 to. */
.macro IVT_PIT_SETUP
2015-11-09 14:29:15 +01:00
movw $handler, IVT_PIT * IVT_HANDLER_SIZE
mov %cs, IVT_PIT * IVT_HANDLER_SIZE + IVT_CODE_OFFSET
.endm