As @jester suggested you will want to set a Virtual Memory Address(VMA) starting point. The linker you are using on Windows has used an internal linker script that sets a VMA >= 0x10000. So any references to absolute addresses that can't fit in 16-bits will generate a relocation error. You can't fit an address >= 0x10000 into a 16-bit register so the linker aborts with an error similar to:
(.text+0x1f): relocation truncated to fit: R_386_16 against `.text'
The error may be slightly different if you are using a 64-bit tool chain but it will be something like R_???????_16 against '.text'
To fix this you can either create your own linker script or set the base VMA to the appropriate value on the LD command line. I would recommend using -Ttext=0x7c00
and setting DS to 0x0000 in your bootloader.
I have General Bootloader tips that discuss many of the problems writing a bootloader. You shouldn't assume that when your bootloader is running that the segment registers have any particular value. If you use -Ttext=0x7c00
as a VMA (ORG) then you need to set DS to zero. The segment:offset pair 0x0000:0x7c00 = physical address 0x07c00 (0x0000<<4+0x7c00). 0x07c00 is where the legacy BIOS will load your boot sector into memory.
If you ever get a relocation error like:
(.text+0x1f): relocation truncated to fit: R_386_16 against `.text'
You can always use OBJDUMP to show the relocation entries in the object file. In this case since you are writing 16-bit code you'll want to have OBJDUMP dump the code (-D
); decode as 16-bit instructions (-Mi8086
) and output the relocation entries (-r
). The output of objdump -Mi8086 -Dr bootReal.o
would appear similar to this (it may vary based on the linker being used):
00000000 <_start>:
0: eb 1b jmp 1d <_boot>
00000002 <welcome>:
2: 48 dec %ax
3: 65 gs
4: 6c insb (%dx),%es:(%di)
5: 6c insb (%dx),%es:(%di)
6: 6f outsw %ds:(%si),(%dx)
7: 2c 20 sub $0x20,%al
9: 57 push %di
a: 6f outsw %ds:(%si),(%dx)
b: 72 6c jb 79 <_boot+0x5c>
d: 64 0a 0d or %fs:(%di),%cl
...
00000011 <.writeStringIn>:
11: ac lods %ds:(%si),%al
12: 08 c0 or %al,%al
14: 74 06 je 1c <.writeStringOut>
16: b4 0e mov $0xe,%ah
18: cd 10 int $0x10
1a: eb f5 jmp 11 <.writeStringIn>
0000001c <.writeStringOut>:
1c: c3 ret
0000001d <_boot>:
1d: 8d 36 02 00 lea 0x2,%si
1f: R_386_16 .text
21: e8 ed ff call 11 <.writeStringIn>
...
1fc: 00 00 add %al,(%bx,%si)
1fe: 55 push %bp
1ff: aa stos %al,%es:(%di)
In the relocation error .text+0x1f
was referenced as the source of the problem. If you look in the OBJDUMP output there is a relocation entry:
1d: 8d 36 02 00 lea 0x2,%si
1f: R_386_16 .text
This relocation is associated with the instruction above it. Which means essentially that the linker tried to generate an offset for the LEA
instruction but the value couldn't be represented in a 16-bit value. SI is 16-bit register and can't have a value placed in it >= 0x10000.
Problems with the Code
- You want to properly set DS to 0 if using a VMA (ORG) of 0x7c00
- Ensure string instructions like
lodsb
move through strings in the forward direction. Use the CLD
instruction to clear the Direction Flag (DF=0).
- It is usually a good idea to set your own stack pointer SS:SP. This is important if you ever read more data off the disk into memory. You don't know where the BIOS set SS:SP and you don't want to clobber it. A convenient place to set the the stack is just below the bootloader at 0x0000:0x7c00.
- To prevent your bootloader from running semi random code after the last instruction you will want to put the processor in some kind of infinite loop. An easy way would be
jmp .
- LD will generate an executable file in a format that is not a binary file and can't be run as a bootloader. You can have the linker write a binary file with the
--oformat=binary
option.
The revised code could look like:
#generate 16-bit code
.code16
#hint the assembler that here is the executable code located
.text
.globl _start;
#boot code entry
_start:
jmp _boot #jump to boot code
welcome: .asciz "Hello, World\n\r" #here we define the string
.macro mWriteString str #macro which calls a function to print a string
leaw \str, %si
call .writeStringIn
.endm
#function to print the string
.writeStringIn:
lodsb
orb %al, %al
jz .writeStringOut
movb $0x0e, %ah
int $0x10
jmp .writeStringIn
.writeStringOut:
ret
_boot:
xor %ax, %ax
mov %ax, %ds # Initialize the DS segment to zero
mov %ax, %ss # Set the stack pointer below bootloader at 0x0000:0x7c00
mov $0x7c00, %sp
cld # Clear Direction Flag (DF=0) to set forward movement on string
# instructions
mWriteString welcome
jmp . # Enter an infinite loop to stop executing code beyond this point
#move to 510th byte from the start and append boot signature
. = _start + 510
.byte 0x55
.byte 0xaa
To assemble and run you could use something like:
as bootReal.s -o bootReal.o
ld -Ttext=0x7c00 --oformat=binary -o boot.bin bootReal.o
An alternative is to generate an executable with LD and use OBJCOPY to convert the executable to a binary file:
as bootReal.s -o bootReal.o
ld -Ttext=0x7c00 -o file.tmp bootReal.o
objcopy -O binary file.tmp boot.bin
Either way should generate a 512 byte binary file (bootloader) called boot.bin
. If you ran it with QEMU with qemu-system-i386 -fda boot.bin
the output would look similar to:
