With AT&T syntax you can put a label at the start of your bootloader and then use something like this:
.global _start
.text
.code16
_start:
jmp .
.space 510-(.-_start)
.word 0xaa55
Period .
is the current location counter relative to the beginning of the current section. The difference between period .
and _start
is an absolute value so should work in this expression.
You can use GCC (that will invoke LD) to assemble this to a bootloader with a command like:
gcc -Wl,--oformat=binary -Wl,-Ttext=0x7c00 -Wl,--build-id=none
-nostartfiles -nostdlib -m32 -o boot.bin boot.s
The option -Wl,--oformat=binary
passes this option to the linker which will force it to output to a flat binary file. -Wl,-Ttext=0x7c00
will pass this option to the linker that will effectively set the origin point to 0x07c00. -Wl,--build-id=none
tell the linker not to use the build id that GCC may generate. 0x7c00 is the offset the code is expected to be loaded at. Since we can't use a standard library or C runtime we exclude them with -nostartfiles -nostdlib
You won't be able to use this method if you intend to link multiple files together. In that case you will need to leave the boot signature out of the code and let the linker take care of it with a specially crafted linker script. The method above will work if you contain your bootloader to a single assembly file.
I have some general bootloader tips for writing bootloader code. One big issue people usually have is not setting the segment registers up. If you use an origin point of 0x7c00 then you need to make sure at a minimum that the DS register us set to 0. That will be important if you write code that uses memory operands that reference a label within your code.
When assembling with GNU assembler ensure that you set the proper instruction encoding you want. .code16
will make the assembler assume the target processor is running in 16-bit mode. .code32
for 32-bit encoding, .code64
assumes 64-bit encoding. The default for as
is generally never .code16
.
Bootloader with Multiple Object Files
As I mentioned above using multiple object files to create your bootloader presents challenges that can't be overcome with assembly directives. In order to do this you can create a special linker script that sets the Origin point to 0x7c00 and lets the linker place the boot signature in the output file. Using this method you don't need to do any padding, the linker will do it for you. A basic linker script that deals with traditional sections like .text
, .data
, .rodata
is shown below. You may never use some of the section, but I added them as an example:
File bootloader.ld
OUTPUT_FORMAT("elf32-i386");
ENTRY(_start);
SECTIONS
{
. = 0x7C00;
/* Code section, .text.bootentry code before other code */
.text : SUBALIGN(0) {
*(.text.bootentry);
*(.text)
}
/* Read only data section with no alignment */
.rodata : SUBALIGN(0) {
*(.rodata)
}
/* Data section with no alignment */
.data : SUBALIGN(0) {
*(.data)
}
/* Boot signature at 510th byte from 0x7c00 */
.sig : AT(0x7DFE) {
SHORT(0xaa55);
}
/DISCARD/ : {
*(.eh_frame);
*(.comment);
*(.note*);
}
}
File boot.s
containing main entry point of bootloader:
# Section .text.bootentry is always placed before all other code and data
# in the linker script. If using multiple object files only specify
# one .text.bootentry as that will be the code that will start executing
# at 0x7c00
.section .text.bootentry
.code16
.global _start
_start:
# Initialize the segments especially DS and set the stack to grow down from
# start of bootloader at _start. SS:SP=0x0000:0x7c00
xor %ax, %ax
mov %ax, %ds
mov %ax, %ss
mov $_start, %sp
cld # Set direction flag forward for string instructions
mov $0x20, %al # 1st param: Attribute black on green
xor %cx, %cx # 2nd param: Screen cell index to write to. (0, 0) = upper left
mov $boot_msg, %dx # 3rd param: String pointer
call print_str
# Infinite loop to end bootloader
cli
.endloop:
hlt
jmp .endloop
.section .rodata
boot_msg: .asciz "My bootloader is running"
File aux.s
with a simple function to display a string directly to screen:
.global print_str # Make this available to other modules
.section .text
.code16
# print_str (uint8_t attribute, char *str, uint16_t cellindex)
#
# Print a NUL terminated string directly to video memory at specified screen cell
# using a specified attribute (foreground/background)
#
# Calling convention:
# Watcom
# Inputs:
# AL = Attribute of characters to print
# CX = Pointer to NUL terminated string to print
# DX = Screen cell index to start printing at (cells are 2 bytes wide)
# Clobbers:
# AX, ES
# Returns:
# Nothing
print_str:
push %di
push %si
mov $0xb800, %di # Segment b800 = text video memory
mov %di, %es
mov %cx, %di # DI = screen cell index (0 = upper left corner)
mov %dx, %si # SI = pointer to string (2nd parameter)
mov %al, %ah # AH = attribute (3rd parameter)
jmp .testchar
# Print each character until NUL terminator found
.nextchar:
stosw # Store current attrib(AH) and char(AL) to screen
# Advances DI by 2. Each text mode cell is 2 bytes
.testchar:
lodsb # Load current char from string into AL(advances SI by 1)
test %al, %al
jne .nextchar # If we haven't reach NUL terminator display character
# and advance to the next one
pop %si
pop %di
ret
To build this bootloader to a file called boot.bin
we could do something like:
as --32 aux.s -o aux.o
as --32 boot.s -o boot.o
ld -melf_i386 --oformat=binary -Tlink.ld -nostartfiles -nostdlib
aux.o boot.o -o boot.bin
The special .text.bootentry
is placed as the first code by the linker script. This section should only be defined in one object file as it will be the code that appears right at the beginning of the bootloader at 0x7c00. The linker script adjusts the VMA (origin) to 0x7dfe and writes the boot signature(0xaa55). 0x7dfe is 2 bytes below the end of the first 512 bytes. We no longer do any padding in the assembly code nor do we emit the boot signature there.
When run this sample bootloader should print a string to the upper left of the display with black on a green background.