Ada와 어셈블리 섞어쓰기

시스템 프로그래밍, 하드웨어 제어, 또는 성능이 매우 중요한 최적화를 수행할 때, Ada와 같은 고급 언어에서 순수 어셈블리로 내려가야 할 때가 있습니다. 하지만 어떻게 하면 구조적이고 표준을 준수하는 방식으로 이 작업을 수행하고, 또 그것이 제대로 동작하는지 검증할 수 있을까요?

Ada 언어 표준은 이를 위한 두 가지 주요 방법을 제공합니다. GNAT 컴파일러를 사용하여, 컴파일 가능한 완전한 예제컴파일 방법과 함께 두 가지 방법을 모두 살펴보겠습니다.

방법 1: System.Machine_Code를 이용한 인라인 어셈블리

이 방법은 Ada 코드 내에 어셈블리 명령어를 직접 삽입하는 가장 직접적인 방법으로, 짧고 목표 지향적인 하드웨어 상호작용에 적합합니다. 다음 예제는 어셈블리에서 계산을 수행하고, 그 결과를 Ada로 반환한 다음, 화면에 출력할 것입니다.

예제: 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 -> 어셈블리로 전송: " & Integer'image(input_value));

  System.Machine_Code.asm (
    -- 어셈블리 명령어 템플릿:
    -- %0은 첫 번째 출력(Outputs) 피연산자, %1은 첫 번째 입력(Inputs) 피연산자를 가리킵니다.
    "movl %1, %0; addl $23, %0",

    -- [Inputs]
    -- 'input_value' 변수를 어셈블리 코드에 대한 입력으로 지정합니다.
    -- "r": 'r'은 'register'의 약자로, 컴파일러에게 이 변수 값을
    --      아무 범용 레지스터(general-purpose register)에 넣어달라고 요청합니다.
    --      어셈블리 템플릿에서는 %1으로 이 레지스터를 참조할 수 있습니다.
    Inputs => (Integer'asm_input  ("r", input_value)),

    -- [Outputs]
    -- 어셈블리 코드의 결과를 'result_from_asm' 변수에 저장하도록 지정합니다.
    -- "=": 이 피연산자가 출력 전용(write-only)임을 나타내는 제약 조건입니다.
    -- "r": 결과값 또한 범용 레지스터에 저장됨을 의미합니다.
    --      어셈블리 템플릿에서는 %0으로 이 레지스터를 참조할 수 있습니다.
    Outputs => (Integer'asm_output ("=r", result_from_asm))
  );

  Ada.Text_IO.put_line ("  Ada <- 어셈블리로부터 수신: " & Integer'image(result_from_asm));
  Ada.Text_IO.put_line ("------------------------------------");

  if result_from_asm = 123 then
    Ada.Text_IO.put_line ("성공: 결과가 정확합니다!");
  else
    Ada.Text_IO.put_line ("실패: 결과가 부정확합니다.");
  end if;

end show_asm_result;

인라인 어셈블리 컴파일하기

gnatmake show_asm_result.adb

방법 2: pragma import와 어셈블러 규약을 통한 연동

더 큰 어셈블리 루틴이 있는 경우, 별도의 .s 파일에 보관하는 것이 더 깔끔합니다. 이 작업은 Assembler 호출 규약을 지정한 pragma import를 사용하여 수행됩니다.

예제 파일

1. 어셈블리 파일: math_ops.s

.global my_add
.type my_add, @function

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

# 이 섹션은 스택이 실행 가능할 필요가 없음을 선언하여,
# 흔한 링커 경고를 해결하고 보안을 향상시킵니다.
.section .note.GNU-stack,"",@progbits

2. Ada 패키지 명세: math_functions.ads

package Math_Functions is
  function my_add (x, y : Integer) return Integer;
private
  -- 'assembler' 규약을 사용하여 어셈블리 루틴임을 명확히 합니다.
  -- (참고: 'C' 규약의 호출 방식 또한 어셈블리 루틴과 호환되는 경우가 많아
  -- 자주 사용되기도 합니다.)
  pragma import (assembler, my_add, "my_add");
end Math_Functions;

3. Ada 메인 프로시저: 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 ("외부 어셈블리 결과: " & Integer'image(result));
end main;

외부 어셈블리 컴파일하기

방법 A: gprbuild 사용 (권장)

여러 언어가 섞인 프로젝트를 컴파일하는 가장 쉬운 방법은 gprbuild와 프로젝트 파일을 사용하는 것입니다.

1. GNAT 프로젝트 파일: 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. 빌드 명령어

gprbuild -P my_project.gpr

방법 B: 수동 컴파일 (gprbuild 없이)

gprbuild가 없거나 수동 접근 방식을 선호한다면, 두 단계로 나누어 파일을 컴파일할 수 있습니다.

1. 어셈블리 파일 어셈블하기

먼저 gcc를 사용하여 .s 파일을 오브젝트 파일(.o)로 어셈블합니다.

gcc -c math_ops.s -o math_ops.o

2. Ada 코드 컴파일 및 링크하기

이제 gnatmake로 Ada 코드를 컴파일하고, -largs 스위치를 사용하여 어셈블리 오브젝트 파일을 링커에 전달합니다.

gnatmake main.adb -largs math_ops.o

결론

Ada는 어셈블리와 통합하기 위한 두 가지 견고한 방법을 제공합니다.

  • 작은 인라인 코드 조각에는 System.Machine_Code.asm을 사용하십시오.
  • 더 큰 외부 어셈블리 루틴에는 pragma import를 사용하십시오.