4

I'm attempting to pass a structure from (x86) assembler to Ada on the stack. I've been able to successfully use this pattern in C to accept to wrap a large number of arguments passed from assembly inside a struct and I'm wondering if this will work in a similar way in Ada.

Here is a (contrived, minimal) example:

When I do this, debugging the callee shows that the passed record contains uninitialised data. It appears that Ada is interpreting the C calling convention differently despite the export directive. The RM contains information about passing structs from Ada to C, saying that it will automatically pass a record as a pointer type, but the inverse does not appear to be true. If you accept a single access type it will simply be filled with the first value on the stack, as one would expect from cdecl.

( Please excuse any minor errors, this isn't my actual code. )

#####################################################################
#  Caller
#
#  This pushes the values onto the stack and calls the Ada function
#####################################################################
.global __example_function
.type __example_function, @function
__example_function:
    push $1
    push $2
    push $3
    push $4
    call accepts_struct
    ret
----------------------------------------------------------------------------
--  Accepts_Struct
--
--  Purpose:
--    Attempts to accept arguments pass on the stack as a struct.
----------------------------------------------------------------------------
procedure Accepts_Struct (
  Struct : Struct_Passed_On_Stack
)
with Export,
  Convention    => C,
  External_Name => "accepts_struct";

----------------------------------------------------------------------------
--  Ideally the four variables passed on the stack would be accepted as
--  the values of this struct.
----------------------------------------------------------------------------
type Struct_Passed_On_Stack is
   record
      A : Unsigned_32;
      B : Unsigned_32;
      C : Unsigned_32;
      D : Unsigned_32;
   end record
with Convention => C;

On the other hand, this works just fine:

procedure Accepts_Struct (
  A : Unsigned_32;
  B : Unsigned_32;
  C : Unsigned_32;
  D : Unsigned_32
)
with Export,
  Convention    => C,
  External_Name => "accepts_struct";

That's not a big deal in this minimal case, but if I'm passing 16 or more variables it gets a bit onerous. If you're wondering why I'm doing this, it's an exception handler where the processor automatically passes variables onto the stack to show register states.

Any help here would be greatly appreciated.

ajxs
  • 3,347
  • 2
  • 18
  • 33
  • 1
    Just out of curiosity: are you referring to the processor behavior described in section B1.5.6 of the [ARMv7-M reference manual](https://static.docs.arm.com/ddi0403/eb/DDI0403E_B_armv7m_arm.pdf)? Are you trying to (re)create a HardFault exception handler (e.g. like [this](https://electronics.stackexchange.com/q/293772) one) in Ada? – DeeDee Oct 16 '19 at 20:17
  • I was trying to implement something much closer to the technique described here: https://wiki.osdev.org/Interrupt_Service_Routines . You can find similar implementations in most operating-systems targeting x86. – ajxs Oct 16 '19 at 21:39

2 Answers2

4

The record version does not work because a record is not stored on the stack. Instead 4 Unsigned_32 elements are stored on the stack. If you really want to work with a record instead of four separate unsigned integer values you can assign the four values to the members of your record within the call to "accepts_struct". Ada expects the first entry in the stack to be a record, not an unsigned_32. The Ada LRM, section 6.4.1 states:

For the evaluation of a parameter_association: The actual parameter is first evaluated. For an access parameter, the access_definition is elaborated, which creates the anonymous access type. For a parameter (of any mode) that is passed by reference (see 6.2), a view conversion of the actual parameter to the nominal subtype of the formal parameter is evaluated, and the formal parameter denotes that conversion. For an in or in out parameter that is passed by copy (see 6.2), the formal parameter object is created, and the value of the actual parameter is converted to the nominal subtype of the formal parameter and assigned to the formal.

Furthermore, the passing mode for parameters is described in section 6.2:

6.2 Formal Parameter Modes

A parameter_specification declares a formal parameter of mode in, in out, or out. Static Semantics

A parameter is passed either by copy or by reference. When a parameter is passed by copy, the formal parameter denotes a separate object from the actual parameter, and any information transfer between the two occurs only before and after executing the subprogram. When a parameter is passed by reference, the formal parameter denotes (a view of) the object denoted by the actual parameter; reads and updates of the formal parameter directly reference the actual parameter object.

A type is a by-copy type if it is an elementary type, or if it is a descendant of a private type whose full type is a by-copy type. A parameter of a by-copy type is passed by copy, unless the formal parameter is explicitly aliased.

A type is a by-reference type if it is a descendant of one of the following:

a tagged type;

a task or protected type;

an explicitly limited record type;

a composite type with a subcomponent of a by-reference type;

a private type whose full type is a by-reference type.

A parameter of a by-reference type is passed by reference, as is an explicitly aliased parameter of any type. Each value of a by-reference type has an associated object. For a parenthesized expression, qualified_expression, or type_conversion, this object is the one associated with the operand. For a conditional_expression, this object is the one associated with the evaluated dependent_expression.

For other parameters, it is unspecified whether the parameter is passed by copy or by reference.

It appears that your compiler is trying to pass the struct by reference rather than by copy. In C all parameters are passed by copy.

Jim Rogers
  • 4,822
  • 1
  • 11
  • 24
  • 1
    I wondered whether `C_Pass_By_Copy` ([ARM B.3(60.13](http://www.ada-auth.org/standards/rm12_w_tc1/html/RM-B-3.html#p60.13)) might do the trick. I couldn’t get it to work, but I’m not really invested in getting a solution! (might have got further if OP had provided something more nearly workable). [flyx’s paper](https://flyx.org/2012/06/14/adabindings2/) should add some more understanding. – Simon Wright Oct 16 '19 at 19:11
  • @Jim Rogers thanks for your explantion above! You're absolutely correct. My misunderstanding is coming from me expecting that declaring the procedure to be exported using the `C` calling convention would cause it to treat a record parameter in the same way as C treats a struct parameter. I had ended up going with a solution similar to what you described. If you don't mind, I'll leave the question open for a day longer, if no one else can solve the issue I'll accept your answer as the correct one! Thanks for your help. – ajxs Oct 16 '19 at 21:50
  • @SimonWright I had already tried using the `C_Pass_By_Copy` convention and it didn't achieve the desired result either. Are you referring to my not providing a full source? I was just attempting to trim it down to only the relevant code to illustrate my problem. Thanks for the link. It seems to describe using that convention for passing a record as a parameter *to* a C function, but not the other way around. – ajxs Oct 16 '19 at 21:53
  • 1
    I think you’re right about the pragma. (full source): I spent some time trying to wrap code round your sample, but could only get SEGVs, & wasn’t sure if it was my code or just the way things are! My experience is an Ada ARM hardfault handler, [spec](https://github.com/simonjwright/cortex-gnat-rts/blob/master/common/hardfault_handling.ads), [body](https://github.com/simonjwright/cortex-gnat-rts/blob/master/common/hardfault_handling.adb); but there you can construct a pointer to the stack frame where the data lies & pass it to the Ada as a reference to a record. – Simon Wright Oct 17 '19 at 07:49
  • @SimonWright That's a good idea too! I can see how I can adapt the method you describe to my own use. I don't have as much experience with ARM I must confess. I ended up making a single wrapper function which takes all the parameters individually and adds them to a record to pass onto other specific exception handlers. In case you're curious, this is for creating Interrupt Service Routines for processor exceptions, the first 31 entries in the IDT. Thanks for taking the time to respond! – ajxs Oct 17 '19 at 11:15
1

Maybe you already solved the problem, but if not, then you might also want to have at look at the interrupt function attribute provided by GCC (see here). I've translated a test of the GCC testsuite which pushes values to the stack (as described in section 6.12 of the Intel SDM) and reads them back in an ISR. The translated Ada version seems to work well. See here for the original C version. See the GCC ChangeLog for some additional info.

main.adb

with PR68037_1;

procedure Main is
begin
   PR68037_1.Run;
end Main;

pr68037_1.ads

package PR68037_1 is 
   procedure Run;
end PR68037_1;

pr68037_1.adb

with System.Machine_Code;
with Ada.Assertions;
with Interfaces.C;
with GNAT.OS_Lib;

package body PR68037_1 is

   --  Ada-like re-implementation of
   --     gcc/testsuite/gcc.dg/guality/pr68037-1.c

   subtype uword_t is Interfaces.C.unsigned_long;    --  for x86-64

   ERROR : constant uword_t := 16#1234567_0#;
   IP    : constant uword_t := 16#1234567_1#;
   CS    : constant uword_t := 16#1234567_2#;
   FLAGS : constant uword_t := 16#1234567_3#;
   SP    : constant uword_t := 16#1234567_4#;
   SS    : constant uword_t := 16#1234567_5#;

   type interrupt_frame is
      record
         ip    : uword_t;
         cs    : uword_t;
         flags : uword_t;
         sp    : uword_t;
         ss    : uword_t;
      end record
     with Convention => C;

   procedure fn (frame : interrupt_frame; error : uword_t)
     with Export, Convention => C, Link_Name => "__fn";

   pragma Machine_Attribute (fn, "interrupt");


   --------
   -- fn --
   --------

   procedure fn (frame : interrupt_frame; error : uword_t) is
      use Ada.Assertions;
      use type uword_t;
   begin

      --  Using the assertion function here. In general, be careful when
      --  calling subprograms from an ISR. For now it's OK as we will not
      --  return from the ISR and not continue the execution of an interrupted
      --  program.

      Assert (frame.ip    = IP   , "Mismatch IP");
      Assert (frame.cs    = CS   , "Mismatch CS");      
      Assert (frame.flags = FLAGS, "Mismatch FLAGS");
      Assert (frame.sp    = SP   , "Mismatch SP");
      Assert (frame.ss    = SS   , "Mismatch SS");

      -- At the end of this function IRET will be executed. This will
      -- result in a segmentation fault as the value for EIP is nonsense.
      -- Hence, abort the program before IRET is executed.

      GNAT.OS_Lib.OS_Exit (0);

   end fn;

   ---------
   -- Run --
   ---------

   procedure Run is
      use System.Machine_Code;
      use ASCII;
   begin

      --  Mimic the processor behavior when an ISR is invoked. See also:
      --
      --    Intel (R) 64 and IA-32 Architectures / Software Developer's Manual
      --    Volume 3 (3A, 3B, 3C & 3D) : System Programming Guide
      --    Section 6.12: Exception and Interrupt Handling
      --
      --  Push the data to the stack and jump unconditionally to the
      --  interrupt service routine.

      Asm
        (Template =>
           "push %0" & LF &
           "push %1" & LF &
           "push %2" & LF &
           "push %3" & LF &
           "push %4" & LF &
           "push %5" & LF &
           "jmp __fn",
         Inputs =>
           (uword_t'Asm_Input ("l", SS),
            uword_t'Asm_Input ("l", SP),
            uword_t'Asm_Input ("l", FLAGS),
            uword_t'Asm_Input ("l", CS),
            uword_t'Asm_Input ("l", IP),
            uword_t'Asm_Input ("l", ERROR)),
         Volatile => True);

   end Run;

end PR68037_1;

I compiled the program in GNAT CE 2019 with compiler options -g -mgeneral-regs-only (copied from the GCC test). Note that the parameter interrupt_frame will be passed by reference (see RM B.3 69/2).

DeeDee
  • 5,654
  • 7
  • 14
  • That's extremely interesting! Thanks for pointing out the existence of this pragma. I'll check this out as soon as I can, it looks very promising. – ajxs Oct 28 '19 at 02:24