Exercism의 x86-64 트랙은 어셈블러로 NASM(The Netwide Assembler)을 사용해요.
NASM에서 주석은 세미콜론(;)으로 시작해요.
주석은 프로그램 어디에나 둘 수 있고, ; 뒤에 오는 모든 내용은 어셈블러가 무시해요.
NASM에서는 equ를 사용해 어셈블 시점의 상수를 정의할 수 있어요.
예를 들어, 다음은 값이 42인 UNIVERSE라는 상수를 정의해요:
UNIVERSE equ 42
상수는 정의될 때 한 번 평가되고, 나중에 다시 정의할 수 없어요.
어셈블리에서는 변수 대신 보통 레지스터를 사용해 값을 저장해요. CPU 레지스터는 컴퓨터 프로세서 안에 있는 빠른 메모리예요. 대부분의 계산은 CPU의 레지스터에서 이루어져요.
일부 레지스터는 다양한 계산에 사용되는데, 이런 레지스터를 **범용 레지스터(General Purpose Registers, GPRs)**라고 해요. 다른 레지스터들은 특별하거나 전용 목적을 가져요.
x86-64에는 64비트 범용 레지스터가 16개 있고, 이들은 32비트, 16비트, 8비트로도 접근할 수 있어요.
범용 레지스터는 아래에 설명되어 있어요. 여기서 rn의 n은 8부터 15까지의 값을 가져요: r8, r9, r10, r11, r12, r13, r14, r15.
| 64-bit | 32-bit | 16-bit | 8-bit |
|---|---|---|---|
| rax | eax | ax | ah/al |
| rbx | ebx | bx | bh/bl |
| rcx | ecx | cx | ch/cl |
| rdx | edx | dx | dh/dl |
| rsi | esi | si | sil |
| rdi | edi | di | dil |
| rbp | ebp | bp | bpl |
| rsp | esp | sp | spl |
| rn | rnd | rnw | rnb |
64비트보다 작은 크기를 사용할 때 접근하는 비트는 보통 레지스터의 아래쪽 부분이에요.
이 규칙의 예외는 ah, bh, ch, dh로, 이들은 레지스터의 16비트 부분에서 위쪽 8비트에 접근해요.
rax 레지스터의 비트에 접근하는 방식을 나타낸 그림이에요:
+--------+---------------------------------------+
| 64-bit | rax |
+--------+-------------------+-------------------+
| 32-bit | | eax |
+--------+-------------------+---------+---------+
| 16-bit | | ax |
+--------+-----------------------------+----+----+
| 8-bit | | ah | al |
+--------+-----------------------------+----+----+
이 중 일부 레지스터는 함수 호출을 거쳐도 보존해야 해요: rbp, rsp, rbx, r12, r13, r14, r15.
이들을 보존하지 않으면 오류가 발생하거나 정의되지 않은 동작이 생길 수 있어요.
나머지 레지스터는 보존되지 않으며 자유롭게 사용할 수 있어요: rax, rcx, rdx, rdi, rsi, r8, r9, r10, r11.
레지스터는 보통 push 명령어를 사용해 스택에 보존해요.
그런 다음 함수가 끝나기 전에 pop 명령어를 사용해 복원해요.
명령어는 CPU가 수행할 수 있는 계산 단위예요. 명령어는 보통 다음과 같은 형태를 가져요:
name destination, source
즉, 명령어의 이름이 먼저 오고, 그다음에 하나 이상의 공백, 목적지 피연산자, 쉼표(,)가 오고, 마지막으로 소스 피연산자가 와요.
소스 피연산자는 보통 명령어에 의해 변경되지 않고, 목적지 피연산자만 변경돼요.
따로 언급되지 않는 한 두 피연산자의 크기는 같아야 해요. 예를 들어, 소스 피연산자가 16비트라면 목적지 피연산자도 16비트여야 해요.
예를 들어, 레지스터에 값을 저장하려면 mov 명령어를 사용할 수 있어요:
mov rax, rdx ; rax = rdx
; mov is the name, rax is the destination operand and rdx is the source operand
위 코드 조각은 소스 피연산자인 rdx의 64비트 전체 내용을, 이 명령어의 목적지 피연산자인 rax의 64비트 전체로 복사해요.
대부분의 명령어에서는 정수 상수를 소스 피연산자로 사용할 수 있어요(목적지 피연산자로는 사용할 수 없어요):
mov rcx, 42 ; rcx = 42
; rcx is the destination operand and 42 is the source operand
32비트 레지스터에 값을 쓰면 위쪽 비트도 함께 지워지므로, mov eax, 42는 mov rax, 42와 같아요.
16비트와 8비트 레지스터에는 해당되지 않아요.
덧셈, 뺄셈, 곱셈 같은 산술 연산에는 add, sub, imul 명령어를 사용할 수 있어요:
add rax, rsi ; rax = rax + rsi
imul rax, rdi ; rax = rax * rdi
sub rax, r8 ; rax = rax - r8
명령어는 함수 단위로 구성돼요. 모든 함수는 소스 파일의 section .text에 위치해요.
함수 선언은 다음과 같이 구성돼요:
:.함수에 인자를 전달하고 함수에서 값을 반환하는 방식에는 특정한 규약이 있어요. 대표적인 두 호출 규약은 System V AMD64 ABI와 Microsoft x64예요.
이 트랙은 System V AMD64 ABI 호출 규약을 사용하며, 처음 여섯 개의 정수 인자가 레지스터를 통해 함수에 전달돼요.
인자는 rdi, rsi, rdx, rcx, r8, r9 순서로 전달돼요.
정수 값은 rax 레지스터를 통해 함수에서 반환돼요.
함수를 호출할 때는 call 명령어를 사용해요.
함수는 같은 소스 파일 어디에서든 호출할 수 있어요.
함수를 다른 소스 파일에서도 볼 수 있게 하려면 global 지시문을 사용해야 해요.
예를 들어, 다음은 sum 함수를 선언해요:
section .text ; functions are placed here
global sum ; sum is visible to other source files
sum:
; first argument is passed in rdi
; second argument is passed in rsi
; return value is placed in rax
mov rax, rdi ; rax is now equal to rdi
add rax, rsi ; rax = rax + rsi
ret ; function return
그리고 다음은 인자 3과 5로 sum 함수를 호출해요:
mov rdi, 3 ; First argument in rdi
mov rsi, 5 ; Second argument in rsi
call sum
; The rax register now contains the value 8 (3 + 5), after sum returns