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
를 사용하십시오.