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
Method A: Using gprbuild
(Recommended)
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.