재고 관리

재고 관리

학습 연습 문제

소개

정수

이진 표기법

정수는 4, -2, 0, 64532처럼 소수 부분이 없는 수를 나타내는 추상 개념이에요.

정수를 바이트의 나열로 나타내려면 이진 표기법을 사용해요. 이 표기법에서는 나열된 각 비트가 서로 다른 2의 거듭제곱을 나타내고, 비트의 인덱스가 오른쪽에서 왼쪽으로 커질수록 그 값도 커져요.

부호 없는 수

어떤 수가 음수가 될 수 없다면, 이를 부호 없는 수라고 해요.

부호 없는 수는 나열에서 설정된 모든 비트에 대응하는 2의 거듭제곱의 합으로 곧바로 나타내요.

레지스터에서 나타낼 수 있는 음이 아닌 정수의 범위는 0(설정된 비트 없음)부터 2⁶⁴ - 1(64비트가 모두 설정된 값의 합)까지예요.

부호 없는 수를 더 큰 크기로 넓히는 것은 모든 상위 비트를 0으로 채우는 방식으로 이루어져요. 그래야 새로 생긴 비트가 값에 기여하지 않아요. 이것을 제로 확장이라고 해요.

movzx 명령어는(z는 0을 뜻해요) 8비트나 16비트 소스 피연산자를 더 큰 목적지 피연산자로 제로 확장해요. 32비트 소스 피연산자는 단순한 mov 하나로 목적지 피연산자의 64비트 전체로 항상 제로 확장돼요.

부호 있는 수

정수가 양수나 음수 값을 가질 수 있다면, 이를 부호 있는 수라고 해요.

음수를 나타내기 위해 x86-64는 2의 보수 표현을 사용해요.

2의 보수에서는 부호 있는 수도 설정된 비트에 대응하는 2의 거듭제곱의 합으로 나타내요. 다만 최상위 비트가 설정되어 있으면, 그 비트에 해당하는 값은 다른 값들에 더하는 대신 빼요.

이 비트는 나머지 모든 비트의 합보다 더 큰 값에 대응하기 때문에, 실제로 이 비트가 설정된 수는 항상 음수가 돼요. 이 특별한 비트를 부호 비트라고 해요.

부호 있는 수를 더 큰 크기로 넓히는 것은 새로 생기는 모든 상위 비트를 부호 비트의 복사본으로 채워서 값을 그대로 유지하는 것을 뜻해요. 이것을 부호 확장이라고 해요.

movsx 명령어는(s는 부호를 뜻해요) 8비트나 16비트 소스 피연산자를 더 큰 목적지 피연산자로 부호 확장해요. movsxd라는 movsx의 변형은 32비트 소스 피연산자에서 64비트 목적지 피연산자로 같은 일을 해요.

neg 명령어를 사용하면 수의 부호를 바꿀 수 있어요.

Caution

어셈블리에서는 바이트의 나열이 부호 있는 수를 나타내는지 부호 없는 수를 나타내는지 구별할 방법이 없어요. 그 바이트들에 의미를 부여하는 것은 프로그래머의 몫이에요.

이 작업에서 주석을 활용하면 큰 도움이 돼요.

즉시값

이전 개념에서 4나 -15 같은 상수를 여러 명령어의 소스 피연산자로 사용할 수 있다고 언급했어요. 이런 수를 즉시값이라고 해요.

즉시값은 레지스터나 메모리에 담기지 않고 명령어 자체 안에 인코딩돼요. 대부분의 명령어에서 즉시값을 위해 확보된 공간은 목적지 피연산자가 아무리 커도 32비트 폭밖에 되지 않아요.

목적지 피연산자가 64비트 폭이면, 이 32비트는 그 자리를 채우기 위해 _부호 확장_돼요. 피연산자의 상위 절반은 즉시값의 최상위 비트 복사본으로 완전히 채워져요. 그래서 이 방식으로는 32비트 부호 있는 정수 범위의 수만 쓸 수 있어요:

add rax, -1          ; the immediate is sign-extended, so all 64 bits of rax are affected
add rax, 2147483647  ; the largest immediate an instruction like this accepts

그 범위를 벗어나는 수는 즉시값으로 사용할 수 없어요. 이 규칙의 예외는 mov로, 목적지 피연산자가 레지스터일 때 64비트 즉시값 전체를 받을 수 있어요. 64비트 즉시값이 필요하면, 먼저 mov로 그 값을 레지스터에 넣고, 그 레지스터를 사용해요:

mov rax, 3435973837           ; this works, mov can take a 64-bit immediate
mov rdx, 18446744073709551615 ; the largest immediate mov accepts
sub rdx, rax

음수 즉시값과 같은 비트 표현을 가진 부호 없는 수는 서로 같고, 정확히 같은 값으로 어셈블된다는 점에 유의해요:

mov rax, -1                   ; rax = 18446744073709551615
mov rax, 18446744073709551615 ; rax = -1

덧셈

두 수의 덧셈은 add 명령어로 계산할 수 있어요.

피연산자의 값에 1을 더하는, 피연산자가 하나인 inc 명령어도 있어요:

inc rax ; rax = rax + 1

두 정수의 덧셈은 부호 없는 수와 부호 있는 수 모두 같은 방식으로 동작해요.

뺄셈

두 정수의 뺄셈은 sub 명령어로 수행해요.

피연산자의 값에서 1을 빼는, 피연산자가 하나인 dec 명령어도 있어요:

dec rax ; rax = rax - 1

두 정수의 뺄셈도 부호 없는 수와 부호 있는 수 모두 같은 방식으로 동작해요.

곱셈

x86-64에서 두 수의 곱셈을 수행하는 명령어는 두 가지예요. 일반적으로 부호 없는 곱셈은 mul 명령어를, 부호 있는 곱셈은 imul을 사용해요.

mul 명령어는 다음과 같이 피연산자가 하나인 형태를 취하는데, 여기서 src는 소스 피연산자예요:

mul src

imul 명령어는 피연산자가 하나, 둘 또는 셋인 형태를 취할 수 있어요:

imul src
imul dest, src
imul dest, src1, src2
피연산자가 하나인 곱셈

피연산자가 하나인 형태의 곱셈을 수행할 때는 rax와 rdx 두 레지스터가 암묵적으로 사용돼요. 곱셈이 두 개의 64비트 수를 다루면, 결과의 하위 64비트는 rax에, 상위 64비트는 rdx에 들어가요.

두 레지스터가 함께 사용된다는 것을 나타내기 위해 보통 이를 rdx:rax라고 불러요:

mul rcx ; rax = lower 64 bits of rax * rcx
        ; rdx = upper 64 bits of rax * rcx

다른 피연산자 크기에도 같은 일이 일어나요. 예를 들어 두 개의 32비트 수를 곱하면 eax와 edx가 사용돼요.

예외는 두 바이트 사이의 곱셈이에요.

이 경우에는 dl:al 대신 ax가 사용돼요. ax의 하위 부분(al)은 곱의 하위 8비트를, 상위 부분(ah)은 상위 8비트를 받아요.

Caution

곱셈에서 암묵적으로 사용되는 rax나 rdx 같은 레지스터는 항상 덮어써져요. 나중에 그 레지스터의 값이 필요하다면 연산 전에 저장해 둬야 해요.

피연산자가 둘인 곱셈

imul의 피연산자가 둘인 형태는 목적지 피연산자를 명시하며, 일반적인 문법을 따라요. rdx는 사용되지 않아요. 대신 결과가 목적지 피연산자에 맞도록 잘려요.

imul r8, r9 ; r8 = lower 64 bits of r8 * r9
피연산자가 셋인 곱셈

imul의 피연산자가 셋인 형태에는 두 개의 소스 피연산자가 있고, 그중 두 번째는 항상 즉시값(상수)이에요. 두 소스 피연산자를 곱한 뒤, 결과를 잘라내어 목적지 피연산자에 넣어요:

imul r8, r9, 100 ; r8 = lower 64 bits of r9 * 100

목적지 피연산자는 곱셈에 사용되지 않는다는 점에 유의해요. 그저 결과를 받기만 해요.

오버플로 처리

피연산자가 둘인 곱셈과 셋인 곱셈은 모두 결과를 목적지 피연산자의 크기에 맞도록 잘라내요. 피연산자가 하나인 곱셈은 전체 범위를 보존하지만, 보통 rdx와 rax 두 레지스터로 나뉘어요.

그래서 곱 전체를 하나의 레지스터에 담을 자리를 만들기 위해, 곱셈 전에 피연산자를 넓히는 것이 때때로 유용해요. 부호 없는 피연산자는 제로 확장되고, 부호 있는 피연산자는 부호 확장돼요:

movzx eax, di ; di and si hold unsigned 16-bit numbers
movzx ecx, si
mul ecx       ; the 32-bit product fits in eax, and edx is cleared

나눗셈

곱셈과 마찬가지로, 두 수의 나눗셈을 수행하는 명령어도 두 가지예요. 부호 없는 나눗셈은 div 명령어를, 부호 있는 나눗셈은 idiv를 사용해요.

두 명령어 모두 피연산자가 하나만 있어도 동작해요:

div src
idiv src

16비트, 32비트, 64비트 나눗셈은 각각 dx:ax, edx:eax, rdx:rax를 피제수로 사용해요. 이 경우 두 레지스터가 함께 2N비트 값을 만들어내는데, N은 연산의 크기(16비트, 32비트, 64비트)예요. 그다음 이 값을 소스 피연산자로 나눠요. 몫은 연산의 크기에 따라 ax, eax, rax에, 나머지는 dx, edx, rdx에 쓰여요.

바이트 사이의 나눗셈은 특별해서 dl:al 대신 ax를 사용해요. ax의 하위 8비트(al)는 연산의 몫을, 상위 8비트(ah)는 나머지를 받아요.

나눗셈 전에 피제수의 모든 비트를 알맞게 설정해야 한다는 점에 유의해요. rdx에 설정된 비트(8비트 나눗셈에서는 ah에 설정된 비트)는 나눠지는 값에 영향을 줘요.

부호 없는 나눗셈에서 나눌 값이 하위 절반에 들어가면, 상위 절반을 0으로 만들어야 해요. 그 비트들을 0으로 만드는 명령어라면 무엇이든 괜찮아요. 예를 들어 mov edx, 0은 32비트 나눗셈에서 상위 비트를 0으로 만들어요.

부호 있는 나눗셈에서는 대신 값을 부호 확장해야 해요. 이 과정을 자동으로 해주는 명령어들이 있어요: cbw, cwd, cdq, cqo예요. 첫 번째는 al의 부호에 따라 ah의 비트를 설정해요. 나머지는 각각 ax에서 dx로, eax에서 edx로, rax에서 rdx로 부호 확장을 수행해요.

Caution

나눗셈에서 암묵적으로 사용되는 rax나 rdx 같은 레지스터는 항상 덮어써져요. 나중에 그 레지스터의 값이 필요하다면 나눗셈 전에 저장해 둬야 해요.

지침

동네 가게가 재고를 더 큰 창고로 옮겨요. 모든 물건을 포장해서 옮기는 일을 맡게 되었어요.

네 가지 작업이 있는데, 모두 운반을 관리하는 것과 관련이 있어요.

Note

다음은 이 개념에서 언급한 명령어예요:

명령어 설명
add a, b a = a + b
inc a a = a + 1
sub a, b a = a - b
dec a a = a - 1
imul a rdx:rax = a * rax (signed)
imul a, b a = a * b (signed, truncated)
imul a, b, c a = b * c (signed, truncated)
mul a rdx:rax = a * rax (unsigned)
div a rax = quotient, rdx = remainder of rdx:rax / a (unsigned)
idiv a rax = quotient, rdx = remainder of rdx:rax / a (signed)
movzx a, b a = b, adding 0 to the extra bits
movsx a, b a = b, adding 1 to the extra bits if b < 0 or 0 otherwise
Note

피연산자의 이름을 바꾸면 같은 레지스터에 다른 크기로 접근할 수 있다는 걸 기억해요. 예를 들어 rax (64비트), eax (32비트), ax (16비트), al (8비트)가 있어요.

전체 표는 이전 개념에서 확인할 수 있어요.

1. 각 상자의 무게 구하기

물건을 상자에 담는데, 상자에는 무게를 꼭 표시해야 해요. 주변에 저울이 없지만, 다행히 물건 하나하나의 평균 무게는 알고 있어요.

정리를 쉽게 하려고, 상자 하나에는 서로 다른 두 제품의 물건만 담아요.

상자 하나의 총 무게를 g 단위로 반환하는 함수 get_box_weight를 정의해요. 이 함수는 다음 순서대로 매개변수를 받아요:

  • 상자에 담긴 첫 번째 제품 물건의 개수
  • 첫 번째 제품 물건 하나의 무게 (g 단위)
  • 상자에 담긴 두 번째 제품 물건의 개수
  • 두 번째 제품 물건 하나의 무게 (g 단위)

빈 상자의 무게는 500 g이에요. 상수 WEIGHT_OF_EMPTY_BOX는 풀이 파일 맨 위에 정의되어 있어요.

예시:

get_box_weight(30, 40, 50, 20);
// => 2700

모든 인자는 16비트 음이 아닌 정수이고, 반환 값은 32비트 음이 아닌 정수예요.

2. 트럭에 들어가는 상자 개수 계산하기

상자는 쌓아서 트럭으로 새 창고로 옮겨요. 그런데 트럭의 세로 공간은 한정되어 있어요.

어떤 높이의 상자를 트럭 안에 세로로(서로 위로 쌓아서) 몇 개까지 쌓을 수 있는지 반환하는 함수 max_number_of_boxes를 정의해요.

이 함수는 상자의 높이를 cm 단위로 매개변수로 받아요. 트럭 내부 높이는 300 cm예요. 상수 TRUCK_HEIGHT는 풀이 파일 맨 위에 정의되어 있어요.

예시:

max_number_of_boxes(30);
// => 10

인자와 반환 값은 8비트 음이 아닌 정수예요. 상자의 높이는 항상 2 이상이라서 결과가 8비트에 들어가요.

3. 모든 제품이 확인되었는지 검사하기

새 창고에는 제품별로 아직 확인되지 않은 물건의 개수가 적힌 체크리스트가 있어요. 그곳으로 상자를 하나 옮길 때마다, 그 상자에 담긴 각 제품의 체크리스트 값을 새로 계산해야 해요.

주어진 제품에 대해 새 창고로 아직 옮겨야 할 물건이 몇 개 남았는지 반환하는 함수 items_to_be_moved를 정의해요. 이 함수는 다음 순서대로 매개변수를 받아요:

  • 어떤 제품에서 아직 확인되지 않은 물건의 개수
  • 상자에 담긴 그 제품 물건의 개수

예시:

items_to_be_moved(76532, 120);
// => 76412

인자들은 32비트 음이 아닌 정수예요. 반환 값은 32비트 정수예요. 처리 중 오류가 나면 결과가 음수일 수도 있어요.

4. 급여 받기

급여는 옮긴 상자의 개수와 트럭 운행 횟수에 따라 정해져요. 상자 하나당 5달러, 운행 한 번당 220달러를 받아요. 상수 PAY_PER_BOX와 PAY_PER_TRUCK_TRIP은 풀이 파일 맨 위에 정의되어 있어요.

초기 비용을 충당하려고 급여의 일부를 미리 받았을 수도 있는데, 이 선금은 최종 급여에서 빼야 해요. 또한 일부 제품은 보험 대상이 아니라서, 그런 물건이 깨지거나 없어지면 그 가치만큼 급여가 깎여요. 조심하지 않으면 오히려 돈을 물어내야 할 수도 있어요!

즉, 받아야 하거나 물어내야 할 순액은 다음과 같아요:

net = boxes * PAY_PER_BOX + trips * PAY_PER_TRUCK_TRIP - up_front - broken_items * item_value

이 급여나 빚은 고용한 일꾼들과 똑같이 나눠요. 나누고 남은 돈이나 빚은 그대로 가져가면 돼요. 예를 들어 순액이 100이고 일꾼 5명을 포함한 6명이 나눈다면, 20을 받아요 (100/(5 + 1) = 16에 나머지 4를 더한 값이에요).

마지막에 얼마를 받아야 하는지, 혹은 얼마를 내야 하는지 반환하는 함수 calculate_payment를 정의해요. 이 함수는 다음 순서대로 매개변수를 받아요:

  • 미리 받은 금액 (64비트 음이 아닌 정수)
  • 옮긴 상자의 총 개수 (32비트 음이 아닌 정수)
  • 트럭 운행 횟수 (32비트 음이 아닌 정수)
  • 깨지거나 없어진 물건의 개수 (32비트 음이 아닌 정수)
  • 잃어버린 물건 하나의 가치 (64비트 음이 아닌 정수)
  • 급여나 빚을 함께 나눌 일꾼의 수 (8비트 양의 정수)

예시:

calculate_payment(2000, 1000, 5, 21, 2, 1);
// => 2029

반환 값은 64비트 정수예요.

GitHub에서 편집 링크가 새 창이나 탭에서 열려요
x86-64 Assembly Exercism

재고 관리 문제를 시작해 볼 준비가 됐나요?

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