GRUB supports ELF32 and flat binaries. Your header, although implicitly says that you are providing the ELF binary.
Using flat binary files with multi-boot
If you want to tell the Multiboot (GRUB) bootloader that you are using a flat binary, you must set bit 16 to 1:
MULTIBOOT_AOUT_KLUDGE equ 1 << 16 ;FLAGS[16] indicates to GRUB we are not ;an ELF executable and the fields ;header address,load address,load end address, ;bss end address, and entry address will be ;available in our Multiboot header
It is not as simple as simply specifying this flag. You must provide the full Multiboot header, which provides the Multiboot bootloader with information for loading our binary into memory. When using the ELF format, this information is in the ELF header, which precedes our code, so it does not have to be explicitly provided. The Multiboot header is described in detail in the GRUB documentation .
When using NASM with -f bin it is important to note that we need to specify the starting point for our code. Multi-boot loaders load our kernel at physical address 0x100000 . We must indicate in our assembler file that our point of origin is 0x100000 , so that in our final flat binary image we create our own offsets, etc.
This is an example, stripped and modified from one of my own projects, which provides a simple title. The _Main call _Main configured as a C call in the example, but you do not need to do this. I usually call a function that takes a couple of parameters on the stack (using the C calling convention).
[BITS 32] [global _start] [ORG 0x100000] ;If using '-f bin' we need to specify the ;origin point for our code with ORG directive ;multiboot loaders load us at physical ;address 0x100000 MULTIBOOT_AOUT_KLUDGE equ 1 << 16 ;FLAGS[16] indicates to GRUB we are not ;an ELF executable and the fields ;header address, load address, load end address; ;bss end address and entry address will be available ;in Multiboot header MULTIBOOT_ALIGN equ 1<<0 ; align loaded modules on page boundaries MULTIBOOT_MEMINFO equ 1<<1 ; provide memory map MULTIBOOT_HEADER_MAGIC equ 0x1BADB002 ;magic number GRUB searches for in the first 8k ;of the kernel file GRUB is told to load MULTIBOOT_HEADER_FLAGS equ MULTIBOOT_AOUT_KLUDGE|MULTIBOOT_ALIGN|MULTIBOOT_MEMINFO CHECKSUM equ -(MULTIBOOT_HEADER_MAGIC + MULTIBOOT_HEADER_FLAGS) KERNEL_STACK equ 0x00200000 ; Stack starts at the 2mb address & grows down _start: xor eax, eax ;Clear eax and ebx in the event xor ebx, ebx ;we are not loaded by GRUB. jmp multiboot_entry ;Jump over the multiboot header align 4 ;Multiboot header must be 32 ;bits aligned to avoid error 13 multiboot_header: dd MULTIBOOT_HEADER_MAGIC ;magic number dd MULTIBOOT_HEADER_FLAGS ;flags dd CHECKSUM ;checksum dd multiboot_header ;header address dd _start ;load address of code entry point ;in our case _start dd 00 ;load end address : not necessary dd 00 ;bss end address : not necessary dd multiboot_entry ;entry address GRUB will start at multiboot_entry: mov esp, KERNEL_STACK ;Setup the stack push 0 ;Reset EFLAGS popf push eax ;2nd argument is magic number push ebx ;1st argument multiboot info pointer call _Main ;Call _Main add esp, 8 ;Cleanup 8 bytes pushed as arguments cli endloop: hlt jmp endloop _Main: ret ; Do nothing
The Multiboot bootloader (GRUB) typically loads into the first 8k of your file (whether it be an ELF or a flat binary file), looking for the Multiboot header on a 32-bit boundary. If the 16 FLAG bit of the Multiboot header is transparent, it is assumed that you are providing an ELF image. He then analyzes the ELF header to obtain the information needed to load the kernel file into memory. If bit 16 is set, the full Multiboot header is required so that the bootloader has information to read your kernel into memory, perform initialization, and then call it into your kernel.
Then you would put your init.s into flat binary with something like:
nasm -f bin -o init.bin init.s
Using ELF with Multiboot
To associate Jester's comments with the original question, you had to boot from ELF and get it to work, but this was not due to one small detail. In your example, you used this to create init.bin:
nasm -f elf32 -o init.bin init.s
When using -f elf32 NASM generates object files (they are not executable) that must be associated (for example, with LD) to create the final ELF executable (ELF32). This would probably work if you assembled and associated processes with something like:
nasm -f elf32 init.s -o init.o ld -Ttext=0x100000 -melf_i386 -o init.bin init.o
Note that when using -f elf32 you must remove the ORG directive from init.s. The ORG directive applies only when using -f bin . Multi-boot loaders will load us at the physical address 0x100000 , so we need to make sure that the assembled and associated code is generated with this starting point. When using -f elf32 we specify an entry point with -Ttext=0x100000 on the linker (LD) command line. Alternatively, the starting point can be set in the script builder.
Using NASM / LD / OBJCOPY to Create Flat Binary Images
You can use NASM / LD / OBJCOPY together to create the final flat binary image, rather than using -f bin with NASM. If you remove the ORG directive from init.s and use these commands, it should generate a flat binary init.bin:
nasm -f elf32 init.s -o init.o ld -Ttext=0x100000 -melf_i386 -o init.elf init.o objcopy -O binary init.elf init.bin
This NASM is reported to generate ELF32 objects. We collect init.s in an ELF object file called init.o. Then we can use the linker (LD) to generate the ELF executable from init.o called init.elf. We use a special program called objcopy to disable all ELF headers and create a flat binary executable file init.bin.
This is much more than just using NASM with the -f bin option to create a flat exec.init.bin. Why bother then? Using the method described above, NASM can generate debugging information that can be used by gdb (the GNU debugger). If you try to use -g (enable debugging) using NASM using -f bin , no debugging information will be generated. You can generate debug information by changing the build sequence as follows:
nasm -g3 -F dwarf -f elf32 init.s -o init.o ld -Ttext=0x100000 -melf_i386 -o init.elf init.o objcopy -O binary init.elf init.bin
init.o will contain debugging information (in a dwarf format) that will be associated with LD in init.elf (which saves debugging information). Flat binaries do not contain debugging information because they are deleted when using objcopy with -O binary . You can use init.elf if you have enabled the remote debugging tool in QEMU and use GDB for debugging. This debugging information in init.elf provides information to the debugger that allows you to perform one step through your code, access variables and labels by name, see the assembler source code, etc.
Besides generating debugging information, there is another reason to use the NASM / LD / OBJCOPY process to generate binary kernel code. LD is very configurable. LD allows a person to create linker scripts that allow you to better customize how things will be laid out in the final binary format. This can be useful for more complex cores, which may contain a mixture of code from different environments (C, Assembler, etc.). For a small game kernel, this might not be necessary, but as the kernel becomes more complex, the benefits of using a script linker will become more apparent.
Remote QEMU Debugging with GDB
If you use the method in the previous section to generate debugging information inside the ELF executable (init.elf), you can run QEMU and have it:
- Boot the QEMU environment and stop the CPU at startup. From the man page:
-S Do not start the CPU at startup (you must enter "c" on the monitor).
- Make QEMU to listen to the remote GDB connection on localhost: 1234. From the man page:
-s Shorthand for -gdb tcp :: 1234, i.e. open gdbserver on TCP port 1234.
Then you just need to run GDB so that it:
- Launches GDB with our ELF executable (init.elf) with debugging symbols and information
- Connects to localhost: 1234, where QEMU is listening
- Sets the debug layout of your choice.
- Sets a breakpoint to stop in our kernel (in this example multiboot_entry)
Here is an example of starting our kernel from the init.iso CD-ROM image and starting GDB to connect to it:
qemu-system-x86_64 -cdrom ./init.iso -S -s & gdb init.elf \ -ex 'target remote localhost:1234' \ -ex 'layout src' \ -ex 'layout regs' \ -ex 'break multiboot_entry' \ -ex 'continue'
You should be able to use GDB in much the same way as debugging a regular program. This assumes that you will not debug a 16-bit program (kernel).
Important considerations
According to Jester, when using multiboot-compatible boot loaders such as GRUB, the CPU is in 32-bit protected mode (not 16-bit real mode). Unlike booting from the BIOS, you cannot use 16-bit code, including most PC-BIOS interrupts. If you need to be in real mode, you will have to manually return to real mode or create a VM86 task (the latter is not trivial).
This is an important consideration, as some of the code you are linked to on MikeOS is 16-bit.