11

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.

trashgod
  • 203,806
  • 29
  • 246
  • 1,045
ajxs
  • 3,347
  • 2
  • 18
  • 33
  • 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 Jun 25 '19 at 07:57
  • 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 Jun 25 '19 at 09:10

1 Answers1

11

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.

DeeDee
  • 5,654
  • 7
  • 14
  • 1
    Thank you so much for taking the time to make a very well thought out reply. I think this answers my question. My big questions were whether I'd be able to use the Linux x86-64 compiler targeting bare-metal and whether the runtime is the key challenge, both of which I think are answered here. Thanks again. Any other comments and answers are still welcome of course. – ajxs Jun 24 '19 at 22:47
  • 1
  • 2
    You can also take a look at [LovelaceOS](https://sourceforge.net/projects/lovelaceos/) which is an attempt to have an operating system in Ada for ARM and x86. Please note that it's far from complete :) – Frédéric Praca Jun 26 '19 at 09:39
  • 1
    ...or the [Muen separation kernel (SK)](https://muen.codelabs.ch) which is written in SPARK 2014. This microkernel can also be compiled with GNAT CE 2019 and, according to the website, even allows you to run native Ada subjects (I never tried it, but it sounds interesting). – DeeDee Jun 26 '19 at 21:00
  • 1
    Thanks for that link @FrédéricPraca, I'll take a look! I did find Muen while I was searching online, I'll definitely use that for a reference when investigating integrating SPARK into my work. I've managed to successfully set up a basic ZFP runtime to expand upon your example. My next challenge is to try and adapt this to use GNAT CE. Thanks so much for your great answer. – ajxs Jun 29 '19 at 00:01