라자냐

라자냐

학습 연습 문제

소개

기초

주석

Exercism의 x86-64 트랙은 어셈블러로 NASM(The Netwide Assembler)을 사용해요. NASM에서는 주석 앞에 세미콜론(;)을 붙여요.

주석은 프로그램 어디에나 둘 수 있고, ; 뒤에 오는 모든 내용은 어셈블러가 무시해요.

상수

NASM에서는 equ를 사용해 어셈블 시점의 상수를 정의할 수 있어요. 예를 들어, 다음은 값이 42인 UNIVERSE라는 상수를 정의해요:

UNIVERSE equ 42

상수는 정의될 때 한 번만 평가되고, 나중에 다시 정의할 수 없어요.

범용 레지스터 (GPR)

어셈블리에서는 변수 대신 보통 레지스터를 사용해 값을 저장해요. 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.

Caution

나중에 커리큘럼에서 레지스터를 보존하는 방법을 배우게 돼요. 지금은 두 번째 종류의 레지스터만 사용해요: 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에 위치해요.

함수 선언은 다음과 같이 이루어져요:

  1. 함수 이름이 붙은 레이블, 그 뒤에 :.
  2. 함수를 정의하는 명령어들.
  3. 반환 명령어 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

지침

이 연습 문제에서는 좋아하는 요리책을 보고 멋진 라자냐를 만드는 데 도움이 되는 코드를 작성해 볼 거예요.

라자냐를 조리하는 데 걸리는 시간과 관련된 네 가지 과제가 있어요.

Note

이 개념에서 언급한 명령어들은 다음과 같아요:

명령어 설명
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 함수에서 반환해요

1. 오븐에서의 예상 조리 시간을 분 단위로 정의해요

매개변수를 받지 않고, 라자냐를 오븐에 몇 분 동안 넣어 두어야 하는지 반환하는 expected_minutes_in_oven 함수를 정의해요. 요리책에 따르면 오븐에서의 예상 조리 시간은 40분이에요:

expected_minutes_in_oven();
// => 40

2. 남은 오븐 조리 시간을 분 단위로 계산해요

라자냐가 지금까지 오븐에 있던 실제 시간(분)을 매개변수로 받아, 이전 과제에서 구한 예상 오븐 조리 시간을 기준으로 라자냐가 오븐에 앞으로 몇 분 더 있어야 하는지 반환하는 remaining_minutes_in_oven 함수를 정의해요.

remaining_minutes_in_oven(30);
// => 10

3. 준비 시간을 분 단위로 계산해요

라자냐에 넣은 층의 개수를 매개변수로 받아, 각 층을 준비하는 데 2분이 걸린다고 가정할 때 라자냐를 준비하는 데 몇 분을 썼는지 반환하는 preparation_time_in_minutes 함수를 정의해요.

preparation_time_in_minutes(2);
// => 4

4. 경과한 시간을 분 단위로 계산해요

elapsed_time_in_minutes 함수를 정의해요. 이 함수는 두 개의 매개변수를 다음 순서로 받아요:

  1. 라자냐에 넣은 층의 개수
  2. 라자냐가 오븐에 있던 시간(분)

이 함수는 라자냐 요리에 몇 분을 들였는지 반환해야 해요. 준비 시간(분)과 현재까지 라자냐가 오븐에 있던 시간(분)을 더한 값이에요.

elapsed_time_in_minutes(3, 20);
// => 26
GitHub에서 편집 링크가 새 창이나 탭에서 열려요
x86-64 Assembly Exercism

라자냐 문제를 시작해 볼 준비가 됐나요?

Exercism에 가입하고 x86-64 Assembly 트랙을 개념 22개연습 문제 130개, 그리고 실제 사람의 멘토링과 함께 배우고 익혀 보세요. 모두 무료예요.