Mixing Ada and Assembly

For systems programming, hardware control, or performance-critical optimizations, there are times when you need to drop down from a high-level language like Ada to raw assembly. But how do you do this in a structured, standard-compliant way, and how do you verify it works?

The Ada language standard provides two primary methods for this. Let’s explore both methods with compilable, complete examples and compilation methods, using the GNAT compiler.

Method 1: Inline Assembly with System.Machine_Code

This is the most direct way to embed assembly instructions within your Ada code. It’s well-suited for short, targeted hardware interactions. The following example will perform a calculation in assembly, return the result to Ada, and print it to the screen.

Example: show_asm_result.adb

with Ada.Text_IO;
with System.Machine_Code;

procedure show_asm_result is
   input_value     : Integer := 100;
   result_from_asm : Integer;
begin
   Ada.Text_IO.put_line ("  Ada -> Sending to Assembly: " & Integer'image(input_value));

   System.Machine_Code.asm (
      -- Assembly Template:
      -- %0 refers to the first Output operand, %1 to the first Input operand.
      "movl %1, %0; addl $23, %0",

      -- [Inputs]
      -- Specifies the 'input_value' variable as an input to the assembly code.
      -- "r": An abbreviation for 'register', this constraint asks the compiler
      --      to place this variable's value into any general-purpose register.
      --      This input is referenced as %1 in the assembly template.
      Inputs  => (Integer'asm_input  ("r", input_value)),

      -- [Outputs]
      -- Specifies that the result of the assembly code will be stored in the
      -- 'result_from_asm' variable.
      -- "=": A constraint modifier indicating that this operand is write-only.
      -- "r": Means the result will also be in a general-purpose register.
      --      This output is referenced as %0 in the assembly template.
      Outputs => (Integer'asm_output ("=r", result_from_asm))
   );

   Ada.Text_IO.put_line ("  Ada <- Received from Assembly: " & Integer'image(result_from_asm));
   Ada.Text_IO.put_line ("------------------------------------");

   if result_from_asm = 123 then
      Ada.Text_IO.put_line ("Success: The result is correct!");
   else
      Ada.Text_IO.put_line ("Failure: The result is incorrect.");
   end if;

end show_asm_result;

Compiling Inline Assembly

gnatmake show_asm_result.adb

Method 2: Interfacing via pragma import and Assembler Convention

When you have larger assembly routines, it’s cleaner to keep them in separate .s files. This is done using pragma import with the Assembler convention. This explicitly tells the compiler that the routine is written in assembly language.

Example Files

1. Assembly file: math_ops.s

.global my_add
.type my_add, @function

my_add:
    movl %edi, %eax
    addl %esi, %eax
    ret

# This section declares that the stack does not need to be executable,
# resolving a common linker warning and improving security.
.section .note.GNU-stack,"",@progbits

2. Ada package spec: math_functions.ads

package Math_Functions is
   function my_add (x, y : Integer) return Integer;
private
   -- Use Convention 'Assembler' to clearly indicate an assembly routine.
   -- (Note: Convention 'C' is also often used as its calling convention
   -- is frequently compatible with assembly routines.)
   pragma import (Assembler, my_add, "my_add");
end Math_Functions;

3. Ada main procedure: main.adb

with Ada.Text_IO;
with Math_Functions;

procedure main is
   result : Integer;
begin
   result := Math_Functions.my_add(10, 5);
   Ada.Text_IO.put_line ("Result from external assembly: " & Integer'image(result));
end main;

Compiling External Assembly

The easiest way to compile a project with mixed languages is with gprbuild and a project file.

1. GNAT Project File: my_project.gpr

project My_Project is
   for Source_Dirs use (".");
   for Object_Dir use "obj";
   for Main use ("main.adb");
   for Languages use ("Ada", "Assembly");
end My_Project;

2. Build Command

gprbuild -P my_project.gpr

Method B: Manual Compilation (without gprbuild)

If you don’t have gprbuild or prefer a manual approach, you can compile the files in two steps.

1. Assemble the Assembly File

First, use gcc to assemble your .s file into an object file (.o).

gcc -c math_ops.s -o math_ops.o

2. Compile and Link Ada Code

Now, use gnatmake to compile the Ada code and pass the assembly object file to the linker using the -largs switch.

gnatmake main.adb -largs math_ops.o

Conclusion

Ada provides two robust methods for integrating with assembly:

  • Use System.Machine_Code.asm for small, inline code snippets.
  • Use pragma import for larger, external assembly routines.