10
votes

Please forgive the somewhat broad question. I'm wondering how to create an Ada toolchain targeting bare-metal x86. I've seen Lucretia's Ada Bare Bones tutorial on osdev.org, which provides some useful information about building a suitable runtime for bare-metal development. This aspect is quite straightforward, but I'm a little unsure about how to build a cross compiler for the platform, or if this is even necessary.

Am I correct in my assumption that creating a "freestanding" binary is done by compiling with the right kind of RTS? If I were to create/utilise a proper freestanding RTS, would it be suitable to use either the out-of-the-box AdaCore or FSF GNAT targeting x86? Any help understanding this would be greatly appreciated.

1
What OS are you developing on? I’d assumed macOS in my answer to your repost of this question on c.l.a, if not, I’ll shut up.Simon Wright
Hi Simon! Sorry for not getting a chance to reply to you on c.l.a. I haven't had a chance to test one aspect of your answer. My host system is x86 Linux!ajxs

1 Answers

10
votes

First of all, please note that I'm note an expert in bare-metal programming, but as this is interesting, I'll give it a try. That being said, I don't think you need a cross compiler. A native platform compiler (e.g. GNAT CE 2019 for Linux x86-64) will just do.

To illustrate this, you might want to recreate the multiboot/hello_world example found here on GitHub in Ada. Here are the steps I took on my Debian machine with GNAT CE 2019 installed to get this working.

First of all I installed some necessary packages (QEMU, NASM and GNU xorriso) and cloned the repository mentioned above:

$ sudo apt-get install qemu nasm xorriso
$ git clone https://github.com/cirosantilli/x86-bare-metal-examples.git

Then, within the repository, I switched to the directory multiboot/hello-world, built the example as-is and executed the resulting image in QEMU to check if everything was setup correctly:

multiboot/hello-world $ make
multiboot/hello-world $ make run

The result was a QEMU window popping up that said hello world in the top-left corner. I proceeded by closing QEMU and run make clean to clean up.

I then removed main.c and replaced it by the Ada translation main.adb:

with System.Storage_Elements;

procedure Main is

   --  Suppress some checks to prevent undefined references during linking to
   --
   --    __gnat_rcheck_CE_Range_Check
   --    __gnat_rcheck_CE_Overflow_Check
   --
   --  These are Ada Runtime functions (see also GNAT's a-except.adb).

   pragma Suppress (Index_Check);
   pragma Suppress (Overflow_Check);


   --  See also:
   --    https://en.wikipedia.org/wiki/VGA-compatible_text_mode
   --    https://en.wikipedia.org/wiki/Color_Graphics_Adapter#Color_palette

   type Color is (BLACK, BRIGHT);

   for Color'Size use 4;
   for Color use (BLACK => 0, BRIGHT => 7);


   type Text_Buffer_Char is
      record
         Ch : Character;
         Fg : Color;
         Bg : Color;
      end record;   

   for Text_Buffer_Char use
      record
         Ch at 0 range 0 .. 7;
         Fg at 1 range 0 .. 3;
         Bg at 1 range 4 .. 7;
      end record;


   type Text_Buffer is
     array (Natural range <>) of Text_Buffer_Char;


   COLS : constant := 80;
   ROWS : constant := 24;   

   subtype Col is Natural range 0 .. COLS - 1;
   subtype Row is Natural range 0 .. ROWS - 1;


   Output : Text_Buffer (0 .. (COLS * ROWS) - 1);
   for Output'Address use System.Storage_Elements.To_Address (16#B8000#);


   --------------
   -- Put_Char --
   --------------

   procedure Put_Char (X : Col; Y : Row; Fg, Bg : Color; Ch : Character) is
   begin
      Output (Y * COLS + X) := (Ch, Fg, Bg);
   end Put_Char;

   ----------------
   -- Put_String --
   ----------------

   procedure Put_String (X : Col; Y : Row; Fg, Bg : Color; S : String) is
      C : Natural := 0;
   begin
      for I in S'Range loop
         Put_Char (X + C, Y, Fg, Bg, S (I));
         C := C + 1;
      end loop;
   end Put_String;

   -----------
   -- Clear --
   -----------

   procedure Clear (Bg : Color) is
   begin
      for X in Col'Range loop
         for Y in Row'Range loop
            Put_Char (X, Y, Bg, Bg, ' ');
         end loop;
      end loop;
   end Clear;


begin

   Clear (BLACK);
   Put_String (0, 0, BRIGHT, BLACK, "Ada says: Hello world!");

   --  Loop forever.
   while (True) loop
      null;
   end loop;

end Main;

Because we're running Ada, I had to change entry.asm and replaced the following lines to make sure that that the entry point of the Ada program instead of the C program was invoked. The entry point of the Ada program emitted by GNAT is _ada_main (see output of objdump -t main.o after compilation):

-- extern main
++ extern _ada_main

[...]

-- call main
++ call _ada_main

In the Makefile I replaced the following lines to properly compile and link the Ada program. Note that I compile to i386 (using the -m32 switch) and request the linker to emit an elf_i386 executable as the processor will not execute 64-bit instructions directly after startup:

-- ld -m elf_i386 -nostdlib -T linker.ld -o '$@' $^
++ ld -m elf_i386 -T linker.ld -o '$@' $^

[...]

-- main.o: main.c
-- <TAB>gcc -c -m32 -std=c99 -ffreestanding -fno-builtin -Os -o '$@' -Wall -Wextra '$<'
++ main.o: main.adb
++ <TAB>gcc -c -m32 -Os -o '$@' -Wall -Wextra '$<'

[...]

-- rm -f *.elf *.o iso/boot/*.elf *.img
++ rm -f *.ali *.elf *.o iso/boot/*.elf *.img

NOTE: Mind the tabs (indicated with <TAB>) before gcc. make is picky on this subject!

I then again subsequently invoked make and then make run to see a QEMU window pop up, but now showing the text:

Ada says: Hello world!

This Ada program executed bare-metal (in IA-32 Real Mode)! I then took the demonstration even further by converting main.img to a VirtualBox disk (VDI) using

VBoxManage convertfromraw main.img main.vdi --variant Fixed

and then created a simple VM (of type "other" and version "other/unknown") with main.vdi as its disk. I booted the VM and (once again) saw the text "Ada says: Hello world!" pop up.

Hence, given the result of above, I think that the compiler is not the main problem when programming x86 bare-metal. I rather think that the main challenges are:

  • Obtaining a proper Ada Runtime (e.g. zero footprint; ZFP) that does not link to any OS libraries (e.g. C standard library; libc). I don't know any, but some might exist out-of-the box. I'm not sure if the one on OSDev.org is complete to the level of a ZFP runtime. For simple programs as the one above, you can omit the runtime (as I did in this example) if you're willing to suppress checks (see comment in source code).

  • Getting the x86 processor all up and running (see here for a nice statement on this). The example above remains in 32-bit real mode (if I state correct), but you might want to proceed to protected mode, 64-bit instructions, etc. to benefit of all its power.