2

I am writing a simple hello world bootloader in C with inline assembly using this article. Nothing fancy, no kernel loading and other advanced topics. Just a plain old "hello world" message.

Here are my files:

boot.c

/* generate 16-bit code */
__asm__(".code16\n");
/* jump boot code entry */
__asm__("jmpl $0x0000, $main\n");

/* user defined function to print series of characters terminated by null 
character */
void printString(const char* pStr) {
    while (*pStr) {
      __asm__ __volatile__ (
           "int $0x10" : : "a"(0x0e00 | *pStr), "b"(0x0007)
      );
      ++pStr;
    }
}

void main() {
    /* calling the printString function passing string as an argument */
    printString("Hello, world!");
}

boot.ld

ENTRY(main);
SECTIONS
{
    . = 0x7C00;
    .text : AT(0x7C00)
    {
        *(.text);
    }
    .sig : AT(0x7DFE)
    {
        SHORT(0xaa55);
    }
}

I then ran the following commands: (different from the first article; adapted from another StackOverflow article as the commands in the first article won't work for me)

gcc -std=c99 -c -g -Os -march=i686 -m32 -ffreestanding -Wall -Werror boot.c -o boot.o

ld -static -T boot.ld -m elf_i386 -nostdlib --nmagic -o boot.elf boot.o

The first line compiles successfully, but I get errors upon executing the second line:

ld: warning: cannot find entry symbol main; defaulting to 0000000000007c00

boot.o:boot.c:(.text+0x2): undefined reference to 'main'

boot.o: In function 'main':

C:(...)/boot.c:16: undefined reference to '__main'

C:(...)/boot.c:16:(.text.startup+0xe): relocation truncated to fit: DISP16 against undefined symbol '__main'

What's wrong? I use Windows 10 x64 with the gcc compiler that comes with Dev-C++.

Michael Petch
  • 46,082
  • 8
  • 107
  • 198
  • I didn't read that linked CodeProject article, but…why bother? I mean, what's the advantage of writing the bootloader in C if you're going to do it with inline assembly? Inline assembly is a *lot* harder to write correctly than regular assembly, which means more time-consuming and more possibility for bugs. Not to mention, GCC isn't designed to emit 16-bit code, so what you get is very inefficient. It would be much easier to just write the code in assembly and assemble it using something like NASM, or even the Gnu assembler (GAS). – Cody Gray - on strike Jun 25 '17 at 07:43
  • As for your actual question, I would guess that you probably need a forward declaration of `main`, so that the assembler can see the symbol *before* you try to use it. But I'm not actually certain how this works, or if that's the only thing that might be wrong here. I've never tried doing this in C. – Cody Gray - on strike Jun 25 '17 at 07:44
  • I already made a bootloader in pure assembly and I agree, it's _way_ easier. I decided to kick it up a notch and do it in C so I can transition smoothly when I get to write kernels. Aside from that, we're going to be required to write bootloaders anytime in the near future in both assembly _and_ C, so I'm gonna have to do it in C anyway. – Celesti Aurus Jun 25 '17 at 07:47
  • That isn't really kicking it up a notch. I glanced at the article, and read the part about writing it in C, and he is literally just writing in assembly using the inline assembler. That's nonsense. When you get ready to write a kernel, you'll write the kernel in C, but you still write the bootloader in assembly. You only get 512 bytes to write the bootloader anyway, so it's not that hard to just write it in assembler. The bootloader (written in assembly) loads the kernel. The second stages can be written in C, if you prefer. I'm sure that's what the assignment intends for you to do. – Cody Gray - on strike Jun 25 '17 at 07:49
  • I just read the requirements again. It explicitly told us to write the bootloader in both assembly code AND "C++ or C (plus some assembly if needed)". – Celesti Aurus Jun 25 '17 at 07:55
  • Well I guess one problem is that you aren't using a cross compiler so you are at the mercy of the host environment. you seem to be using a native GCC for Windows that mangles the names by placing an `_` in front. What happens if you change `__asm__("jmpl $0x0000, $main\n");` to `__asm__("jmpl $0x0000, $_main\n");` and in `boot.ld` change `ENTRY(main);` to `ENTRY(_main);` – Michael Petch Jun 25 '17 at 09:05
  • `__asm__(".code16\n");` should probably be `__asm__(".code16gcc\n");` – Michael Petch Jun 25 '17 at 09:06
  • Somewhere along the line there is a question on SO I responded to that pretty much points out that this Code Project article has many issues with it and has some incorrect inline assembly that may not work with optimizations turned on. – Michael Petch Jun 25 '17 at 09:14
  • Must be [this one](https://stackoverflow.com/questions/32931063/write-a-simple-bootloader-helloworld-error-function-print-string), @Michael. That's the only one I find with my Google-fu. – Cody Gray - on strike Jun 25 '17 at 10:52
  • @CodyGray Yeah, after you posted that question I realized the conversation I had regarding how poor that particular Code Project tutorial was had occurred in the OSDev IRC char.Although someone tried to put an effort into writing the tutorial it is rife with bad advice and the code won't work on a wide variety of hardware and may fail unexpectedly with optimizations on. – Michael Petch Jun 25 '17 at 16:42
  • After changing the `void main()` to `void _main()`, `__asm__("jmpl $0x0000, $main\n");` to `__asm__("jmpl $0x0000, $__main\n");`, and `ENTRY(main)` to `ENTRY(__main);`, I finally got rid of the errors. However, now the problem is that when tested on hardware (a thumb drive and a CD), I get a "Missing operating system" error. Upon closer inspection with a hex editor, I find that the boot signature 0xAA55 wasn't written to the .bin file **at all**, even when explicitly stated by the .ld script. What happened? – Celesti Aurus Jun 26 '17 at 05:04
  • @Michael Also, the resulting .bin file wasn't 512 bytes long like it should be. Something's messed up. – Celesti Aurus Jun 26 '17 at 05:23
  • @CelestiAurus : You don't show us how you convert boot.elf to a binary file. I ope you aren't putting boot.elf directly onto your USB media. USB also poses other issues. I'd work with getting it working under an x86 emulator (QEMU/Bochs etc) as a floppy first. There are other issues to potentially overcome with USB media that isn't an issue with floppies. – Michael Petch Jun 26 '17 at 05:55
  • To convert boot.elf to something like boot.bin you'd need to use something like `objcopy` to do the conversion. – Michael Petch Jun 26 '17 at 05:55
  • @Michael I used `objcopy -O binary boot.elf boot.bin` then used dd (for USB drives) and MagicISO (for CDs) to put the bin file into the hardware. The .bin file lacks the 0xAA55 signature. – Celesti Aurus Jun 26 '17 at 06:00
  • Are you sure you are using `ld -static -T boot.ld -m elf_i386 -nostdlib --nmagic -o boot.elf boot.o` ? With a Windows native compiler I would likely expect `-mi386pe` ? – Michael Petch Jun 26 '17 at 22:32
  • I used `ld -static -Tboot.ld -nostdlib --nmagic -mi386pe -o boot.elf boot.o`. Still no luck. – Celesti Aurus Jun 27 '17 at 02:35

1 Answers1

2

I'd suggest an i686-elf cross compiler rather than using a native windows compiler and tool chain. I think part of your problem is peculiarities related to the Windows i386pe format.

The .sig section is likely not being written at all since that unknown section probably isn't marked allocatable data. The result of that is the signature isn't written to the final binary file. It is also possible the virtual memory address (VMA) is not being set in boot.ld so it may not advance the boot signature into the last 2 bytes of the 512 byte sector. As well with the Windows format read only data will be placed in sections starting with .rdata. You'll want to make sure those are included after the data section and before the boot signature. Failure to do this will default the linker script into placing unprocessed input sections at the end beyond the boot signature.

Assuming you have made the changes as you mentioned in the comments about the extra underscores your files may work this way:

boot.ld:

ENTRY(__main);

SECTIONS
{
    . = 0x7C00;
    .text : AT(0x7C00)
    {
        *(.text);
    }
    .data :
    {
        *(.data);
        *(.rdata*);
    }
    .sig 0x7DFE : AT(0x7DFE) SUBALIGN(0)
    {
        SHORT(0xaa55);
    }
}

The commands to compile/link and adjust the .sig section to be a regular readonly allocated data section would look like:

gcc.exe -std=c99 -c -g -Os -march=i686 -m32 -ffreestanding -Wall -Werror boot.c -o boot.o
ld.exe -mi386pe -static -T boot.ld -nostdlib --nmagic -o boot.elf boot.o

# This adjusts the .sig section attributes and updates boot.elf 
objcopy --set-section-flags .sig=alloc,contents,load,data,readonly boot.elf boot.elf
# Convert to binary
objcopy -O binary boot.elf boot.bin

Other Observations

Your use of __asm__(".code16\n"); will not generate usable code for a bootloader. You'll want to use the experimental pseudo 16-bit code generation that forces the assembler to modify instructions to be compatible with 32-bit code but encoded to be usable in 16-bit real mode. You can do this by using __asm__(".code16gcc\n"); at the top of each C/C++ files.


This tutorial has some bad advice. The global level basic assembly statement that does the JMP to main may be relocated to somewhere other than the beginning of the bootloader (some optimization levels may cause this). The startup code doesn't set ES, DS, CS to 0x0000, nor does it set the SS:SP stack segment and pointer. This can cause problems.


If trying to run from a USB drive on real hardware you may find you'll need a Boot Parameter Block. This Stackoverflow Answer I wrote discusses this issue and a possible work around under Real Hardware / USB / Laptop Issues


Note: The only useful code that GCC currently generates is 32-bit code that can run in 16-bit real mode. This means that you can't expect this code to run on a processor earlier than a 386 like the 80186/80286/8086 etc.

My general recommendation is to not create bootloaders with GCC unless you know what you are really doing and understand all the nuances involved. Writing it in assembly is probably a much better idea.

If you want a C/C++ compiler that generates true 16-bit code you may wish to look at OpenWatcom

Michael Petch
  • 46,082
  • 8
  • 107
  • 198