Global Assembly

Global Assembly

어셈블리(assembly) 표현식이 Namespace 수준의 comptime 블록 안에 등장하면, 그걸 **글로벌 어셈블리(global assembly)**라고 불러요. 인라인 어셈블리(inline assembly)에 익숙한 분이라면 아마 "그럼 인라인 어셈블리와 뭐가 다르지?"라는 생각이 먼저 들 텐데요, 결론부터 말하면 규칙이 상당히 다릅니다. 이번에는 그 차이를 하나씩 짚어볼게요.

출처: Zig Documentation

본문

글로벌 어셈블리는 인라인 어셈블리와는 다른 규칙을 따라요. 주의할 점이 몇 가지 있는데, 정리하면 이렇습니다.

  • 첫째, volatile쓸 수 없어요. 인라인 어셈블리에서 volatile은 "이 어셈블리를 생략하지 마라"는 뜻이지만, 글로벌 어셈블리는 무조건 포함되기 때문에 그런 키워드가 필요 없거든요.
  • 둘째, 인풋(input)·아웃풋(output)·클로버(clobber)가 없어요. 인라인 어셈블리처럼 값을 주고받거나 레지스터를 알려주는 장치가 존재하지 않습니다.
  • 셋째, 모든 글로벌 어셈블리는 그대로(verbatim) 하나의 긴 문자열로 이어붙여 한꺼번에 어셈블됩니다. 그래서 인라인 어셈블리 표현식에 있는 것 같은 %를 둘러싼 템플릿 치환 규칙도 적용되지 않아요.

실제로 쓰는 모습을 볼까요. 다음 코드는 x86-64 리눅스에서 어셈블리로 직접 함수 하나를 정의해 쓰는 예시예요.

const std = @import("std");
const expectEqual = std.testing.expectEqual;

comptime {
    asm (
        \\.global my_func;
        \\.type my_func, @function;
        \\my_func:
        \\  lea (%rdi,%rsi,1),%eax
        \\  retq
    );
}

extern fn my_func(a: i32, b: i32) i32;

test "global assembly" {
    try expectEqual(46, my_func(12, 34));
}
$ zig test test_global_assembly.zig -target x86_64-linux -fllvm
1/1 test_global_assembly.test.global assembly...OK
All 1 tests passed.

comptime 블록 안의 asm(...)이 바로 글로벌 어셈블리예요. 여기서 .global my_func로 심볼을 밖에 노출시키고, my_func: 라벨 아래에서 %rdi(첫 번째 인자)와 %rsi(두 번째 인자)를 더해 %eax로 반환하는 어셈블리를 짜두었죠. 그리고 extern fn my_func(a: i32, b: i32) i32;로 이 함수가 외부(즉 어셈블리)에 정의돼 있다고 선언한 다음에요.

이런 식으로 글로벌 어셈블리는 소스에 등장하는 순서 그대로 결과물에 이어붙어 컴파일 타임에 하나로 합쳐진다는 점이 특징이에요. 인라인 어셈블리처럼 레지스터 할당이나 클로버를 계산해 줄 필요가 없는, "그냥 붙이면 되는" 더 낮은 수준의 어셈블리라고 생각하면 이해가 쉬워요.

더 알아보기

  • Inline Assembly — 인풋·아웃풋·클로버·템플릿 치환 규칙이 있는 인라인 어셈블리
  • comptime — 컴파일 타임에 실행되는 블록으로, 글로벌 어셈블리가 등장하는 위치
  • Namespace — 선언이 모이는 네임스페이스 레벨