Exercism의 x86-64 트랙은 어셈블러로 NASM(The Netwide Assembler)을 사용해요.
NASM에서는 주석 앞에 세미콜론(;)을 붙여요.
주석은 프로그램 어디에나 둘 수 있고, ; 뒤에 오는 모든 내용은 어셈블러가 무시해요.
NASM에서는 equ를 사용해 어셈블 시점의 상수를 정의할 수 있어요.
예를 들어, 다음은 값이 42인 UNIVERSE라는 상수를 정의해요:
UNIVERSE equ 42
상수는 정의될 때 한 번만 평가되고, 나중에 다시 정의할 수 없어요.
어셈블리에서는 변수 대신 보통 레지스터를 사용해 값을 저장해요. CPU 레지스터는 컴퓨터 프로세서 내부에 있는 빠른 메모리예요. 대부분의 계산은 CPU의 레지스터에서 이루어져요.
레지스터 중 일부는 다양한 계산에 사용되고, 이를 **범용 레지스터(GPR)**라고 해요. 나머지는 특별하거나 전용의 목적을 가져요.
x86-64에는 64비트 범용 레지스터(GPR)가 16개 있고, 각각을 32비트, 16비트, 8비트로도 접근할 수 있어요.
GPR은 아래에 나와 있어요. 여기서 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.
나중에 커리큘럼에서 레지스터를 보존하는 방법을 배우게 돼요.
지금은 두 번째 종류의 레지스터만 사용해요: rax, rcx, rdx, rdi, rsi, r8, r9, r10, r11.
명령어는 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에 위치해요.
함수 선언은 다음과 같이 이루어져요:
:.ret.이 트랙은 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
이 연습 문제에서는 좋아하는 요리책을 보고 멋진 라자냐를 만드는 데 도움이 되는 코드를 작성해 볼 거예요.
라자냐를 조리하는 데 걸리는 시간과 관련된 네 가지 과제가 있어요.
이 개념에서 언급한 명령어들은 다음과 같아요:
| 명령어 | 설명 |
|---|---|
| mov a, b | b의 내용을 a로 복사해요 |
| add a, b | a = a + b |
| sub a, b | a = a - b |
| imul a, b | a = a * b |
| call a | 함수 a를 호출해요 |
| ret | 함수에서 반환해요 |
매개변수를 받지 않고, 라자냐를 오븐에 몇 분 동안 넣어 두어야 하는지 반환하는 expected_minutes_in_oven 함수를 정의해요.
요리책에 따르면 오븐에서의 예상 조리 시간은 40분이에요:
expected_minutes_in_oven();
// => 40
라자냐가 지금까지 오븐에 있던 실제 시간(분)을 매개변수로 받아, 이전 과제에서 구한 예상 오븐 조리 시간을 기준으로 라자냐가 오븐에 앞으로 몇 분 더 있어야 하는지 반환하는 remaining_minutes_in_oven 함수를 정의해요.
remaining_minutes_in_oven(30);
// => 10
라자냐에 넣은 층의 개수를 매개변수로 받아, 각 층을 준비하는 데 2분이 걸린다고 가정할 때 라자냐를 준비하는 데 몇 분을 썼는지 반환하는 preparation_time_in_minutes 함수를 정의해요.
preparation_time_in_minutes(2);
// => 4
elapsed_time_in_minutes 함수를 정의해요. 이 함수는 두 개의 매개변수를 다음 순서로 받아요:
이 함수는 라자냐 요리에 몇 분을 들였는지 반환해야 해요. 준비 시간(분)과 현재까지 라자냐가 오븐에 있던 시간(분)을 더한 값이에요.
elapsed_time_in_minutes(3, 20);
// => 26
Exercism에 가입하고 x86-64 Assembly 트랙을 개념 22개연습 문제 130개, 그리고 실제 사람의 멘토링과 함께 배우고 익혀 보세요. 모두 무료예요.