비밀

비밀

학습 연습 문제

소개

비트 조작

정수의 각 비트는 이진 값을 저장하는 데 사용할 수 있어요. 참 또는 거짓, 포함 또는 제외, 켜짐 또는 꺼짐처럼 많은 상황이 이진 정보를 다루기 때문에, N비트 정수의 이진 표현은 N개 항목의 이진 상태를 간결하게 인코딩하는 방법을 제공해요. 덕분에 어셈블리에서는 비트와 바이트를 다루는 능력이 필수적이에요. x86-64 명령어 집합은 매우 다양한 비트 조작 명령어를 제공해요.

단일 비트 조작

이 명령어들은 피연산자의 단일 비트를 다뤄요.

모두 두 개의 피연산자를 받는데, 두 번째 피연산자는 첫 번째 피연산자에서 조작할 비트의 인덱스를 나타내요. 그리고 모두 선택한 비트를 **캐리 플래그(CF)**로 복사해요.

이름 설명
bt 어떤 피연산자도 수정하지 않고 비트를 CF로 복사해요
bts 비트를 CF로 복사하고, 대상 피연산자에서 그 비트를 설정해요
btr 비트를 CF로 복사하고, 대상 피연산자에서 그 비트를 해제해요
btc 비트를 CF로 복사하고, 대상 피연산자에서 그 비트를 보수(반전)해요

비트 연산

비트 연산은 피연산자의 모든 비트에 대해 수행해요.

수행하는 비트 연산과 이름이 같은 명령어가 각각 있어요:

이름 설명
and 두 비트가 모두 1이면 1
or 비트 중 적어도 하나가 1이면 1
xor 두 비트가 다르면 1
not 비트가 0이었으면 1, 1이었으면 0

대부분 두 개의 피연산자를 받아 두 피연산자에 비트 연산을 수행하고, 그 결과를 대상 피연산자에 저장해요. 예외는 not인데, 이는 대상 피연산자 하나만 받아요.

마스크

1과 0을 각각 포함과 제외로 해석할 때, 그 정수를 비트마스크(또는 간단히 마스크)라고 해요.

비트마스크는 항목을 "가려내는데", i번째 비트의 0은 i번째 항목을 제외하고 1은 포함하기 때문이에요. 또한 정수의 특정 비트는 포함하고 나머지는 제외하기 위해 비트마스크를 흔히 사용해요.

예를 들어, 이진 표현이 다음과 같은 정수를 A라고 해봐요:

인덱스 7 6 5 4 3 2 1 0
비트 1 0 0 1 0 1 0 1

또한 이진 표현이 다음과 같은 정수를 M이라고 해봐요:

인덱스 7 6 5 4 3 2 1 0
비트 0 0 0 0 1 1 0 1

둘 다 8비트 정수예요. 이 경우, M은 A의 0, 2, 3번째 비트를 선택하고 나머지는 제외한다고 말할 수 있어요.

앞서 살펴본 비트 연산 명령어들은 마스크를 사용해 정수를 조작할 때 유용해요. 예를 들어:

  • M이 선택하지 않은 A의 비트를 해제하려면, 비트 AND 연산을 해요: A AND M.
  • M이 선택한 A의 비트를 설정하려면, 비트 OR 연산을 해요: A OR M.

TEST 명령어

test 명령어는 두 피연산자 사이에 비트 AND 연산을 수행하고, 그 결과에 따라 플래그를 설정해요.

A가 첫 번째 피연산자이고 B가 두 번째 피연산자라면:

플래그 설정되는 조건
CF 항상 해제됨
ZF A AND B == 0
SF A AND B의 부호 비트가 설정됨
OF 항상 해제됨

이 명령어는 두 개의 피연산자를 받아 플래그를 갱신하지만, 피연산자를 수정하지는 않아요.

시프트 연산

이 명령어들은 두 번째 피연산자로 지정한 위치 수만큼 대상 피연산자의 비트를 이동해요. 두 번째 피연산자는 상수(즉, immediate)이거나 cl 레지스터(rcx의 가장 낮은 8비트)여야 해요.

이름 설명
shl/sal 비트를 왼쪽으로 시프트해요
shr/sar 비트를 오른쪽으로 시프트해요

두 번째 피연산자의 이동 횟수는 5비트로 마스킹되는데, 64비트 대상 피연산자일 때는 6비트로 마스킹된다는 점에 유의해요. 그 이후의 비트는 사실상 무시돼요. 즉, 최대 시프트 횟수는 31이고, 64비트 피연산자일 때는 63이에요.

Shl / Sal

shl과 sal은 완전히 같은 연산을 수행하며, 하나는 다른 하나의 별칭이에요.

왼쪽 시프트가 일어날 때마다, 수열의 끝에서 시프트 길이만큼 넘어가는 비트들은 먼저 CF로 이동한 뒤 버려져요. 반대로, 시프트 길이와 같은 수의 새로운 0 비트가 수열의 앞쪽에 추가돼요.

정수의 각 비트는 2의 거듭제곱을 나타내므로, n 자리만큼 왼쪽으로 시프트하면 정수에 2ⁿ을 곱하는 효과가 있어요.

Shr / Sar

비트를 오른쪽으로 이동하는 명령어는 shr과 sar 두 가지가 있어요.

두 명령어 중 어느 것을 사용하든, 수열의 시작 쪽에서 시프트 길이만큼 넘어가는 비트들은 먼저 CF로 이동한 뒤 버려져요. 반대로, 시프트 길이와 같은 수의 새로운 비트가 끝에 추가돼요.

두 명령어의 차이는, shr은 왼쪽 끝에 0 비트를 채우고, sar은 최상위 비트가 설정되어 있으면 1을, 그렇지 않으면 0을 채운다는 점이에요. 즉, sar은 부호 있는 정수를 시프트할 때 부호를 보존해요.

정수의 각 비트는 2의 거듭제곱을 나타내므로, shr로 n 자리만큼 오른쪽으로 시프트하면 2ⁿ으로 부호 없는 나눗셈을 하는 효과가 있어요.

마찬가지로, sar로 n 자리만큼 오른쪽으로 시프트하면 2ⁿ으로 부호 있는 나눗셈을 하는 효과가 있어요.

회전 연산

이 명령어들은 두 번째 피연산자로 지정한 위치 수만큼 대상 피연산자의 비트를 이동해요. 두 번째 피연산자는 상수(즉, immediate)이거나 cl 레지스터(rcx의 가장 낮은 8비트)여야 해요.

회전과 시프트의 차이는, 회전은 비트를 버리거나 추가하지 않는다는 점이에요. 시프트라면 버려졌을 비트들이 대신 반대쪽 끝으로 이동해요. 따라서 모든 비트가 남아 있고, 모두 자리를 바꿔요.

이름 설명
rol 비트를 왼쪽으로 회전해요
ror 비트를 오른쪽으로 회전해요

두 번째 피연산자의 이동 횟수는 5비트로 마스킹되는데, 64비트 대상 피연산자일 때는 6비트로 마스킹된다는 점에 유의해요. 그 이후의 비트는 사실상 무시돼요. 즉, 최대 회전 횟수는 31이고, 64비트 피연산자일 때는 63이에요.

기타 비트 조작 명령어

그 밖에도 유용한 비트 조작 명령어가 있어요:

이름 설명
popcnt 설정된 비트의 개수를 세요
bsr 설정된 비트 중 가장 높은 자리의 인덱스를 구해요. 설정된 비트가 없으면 결과는 정의되지 않아요
bsf 설정된 비트 중 가장 낮은 자리의 인덱스를 구해요. 설정된 비트가 없으면 결과는 정의되지 않아요

이 명령어들은 모두 두 개의 16비트, 32비트 또는 64비트 피연산자와 함께 동작해요. 8비트 피연산자와는 함께 사용할 수 없어요.

지침

친구가 방금 중요한 비밀이 담긴 메시지를 보냈어요. 다른 사람이 쉽게 읽지 못하게 하려고, 그 메시지는 일련의 비트 조작을 거쳐 암호화되었어요. 여러분은 메시지를 복호화하는 데 도움이 될 메서드들을 작성해야 해요.

Note

이 개념에서 언급된 단일 비트 명령어들은 다음과 같아요:

이름 설명
bt 어떤 피연산자도 수정하지 않고 비트를 CF로 복사해요
bts 비트를 CF로 복사하고, 대상 피연산자에서 그 비트를 설정해요
btr 비트를 CF로 복사하고, 대상 피연산자에서 그 비트를 지워요
btc 비트를 CF로 복사하고, 대상 피연산자에서 그 비트를 보수(뒤집기)해요

이 개념에서 언급된 비트 연산 명령어들은 다음과 같아요:

이름 설명
and 양쪽 비트가 모두 1이면 1
or 비트 중 적어도 하나가 1이면 1
xor 비트가 서로 다르면 1
not 비트가 0이었으면 1; 1이었으면 0

이 개념에서 언급된 시프트 명령어들은 다음과 같아요:

이름 설명
shl/sal 비트를 왼쪽으로 시프트해요
shr/sar 비트를 오른쪽으로 시프트해요

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

이름 설명
rol 비트를 왼쪽으로 회전해요
ror 비트를 오른쪽으로 회전해요

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

이름 설명
popcnt 설정된 비트의 개수를 세요
bsr 가장 높은 자리의 설정된 비트의 인덱스를 구해요. 설정된 비트가 없으면 결과는 정의되지 않아요
bsf 가장 낮은 자리의 설정된 비트의 인덱스를 구해요. 설정된 비트가 없으면 결과는 정의되지 않아요

1. 마스크 추출

메시지는 16비트 정수로 인코딩되어 있어요. 하지만 그중 상위 8비트는 실제로 메시지의 일부가 아니라, 복호화에 사용해야 하는 마스크예요.

16비트 정수를 받아 그중 상위 8비트를 반환하는 extract_higher_bits 함수를 구현해요.

extract_higher_bits(0b1010010011000101)
// => 0b10100100

2. 메시지 추출

마스크를 추출할 수 있는 것만으로는 충분하지 않아요. 메시지도 분리해야 해요.

16비트 정수를 받아 그중 하위 8비트를 반환하는 extract_lower_bits 함수를 구현해요.

extract_lower_bits(0b1010010011000101);
// => 0b11000101

3. 중복 비트 추출

어떤 비트들은 메시지와 마스크 양쪽에서 설정되어 있어요. 이것은 나중에 사용될 매우 중요한 정보예요.

메시지와 마스크를 모두 인코딩한 16비트 정수를 받아, 중복 비트만 설정된 8비트 정수를 반환하는 extract_redundant_bits를 구현해요. 반환된 숫자에서 어떤 비트는, 메시지와 마스크 양쪽에서도 1인 자리에서 1로 설정되어야 해요. 다른 모든 비트는 지워져야 해요.

extract_redundant_bits(0b1010010011000101);
// => 0b10000100

4. 모든 메시지 비트 설정

다음으로, 마스크에 따라 메시지에서 1로 설정해야 하는 비트들이 있어요.

메시지와 마스크를 모두 인코딩한 16비트 정수를 받아, 메시지의 비트를 1로 설정한 결과를 반환하는 set_message_bits 함수를 구현해요. 메시지의 비트는, 마스크의 비트가 1인 자리에서 1로 설정되어야 해요. 다른 모든 비트는 그대로 유지되어야 해요. 이미 설정되어 있었다면 설정된 채로, 이미 지워져 있었다면 지워진 채로 남아요.

set_message_bits(0b1010010011000101);
// => 0b11100101

5. 개인 키 회전

메시지에 명시되어 있지 않은 퍼즐 조각이 하나 있어요. 바로 16비트 숫자 0b1011001100111100이에요. 이 숫자는 여러분이 공유하는 개인 키이며, 메시지를 복호화하는 데 사용해야 해요.

그러려면 먼저 개인 키의 비트를 특정 위치 수만큼 왼쪽으로 회전해야 해요. 그 위치 수는 메시지와 마스크 양쪽에서 설정된 중복 비트의 개수와 같아요.

메시지와 마스크를 모두 인코딩한 16비트 정수를 받아, 개인 키를 회전한 결과를 반환하는 rotate_private_key 함수를 구현해요. 이 결과는 16비트 정수예요.

rotate_private_key(0b1010010011000101);
// => 0b1100110011110010
Note

NASM(이 트랙에서 사용하는 어셈블러인 The Netwide Assembler)은 0b 접두사가 붙은 이진수 형식의 상수를 지원해요. 또한 가독성을 위해 상수에서 밑줄(_)을 구분자로 사용하는 것도 지원해요:

PRIVATE_KEY equ 0b1011_0011_0011_1100

6. 개인 키 포맷

복호화에 사용하려면, 관련된 비트를 분리하도록 개인 키를 포맷해야 해요.

개인 키를 완전히 포맷하려면 다음을 해야 해요:

  • 개인 키를 회전해요.
  • 회전된 개인 키의 하위 8비트 부분을 분리해요. 이것이 기본값이에요.
  • 회전된 개인 키의 상위 8비트 부분을 분리해요. 이것은 기본값에 적용할 마스크예요.
  • 기본값에서, 마스크에도 설정된 비트를 뒤집어요.
  • 결과의 모든 비트를 뒤집어요.

뒤집힌 비트는 0이었으면 1이고, 1이었으면 0이에요.

메시지와 마스크를 모두 인코딩한 16비트 정수를 받아, 완전히 포맷된 8비트 개인 키를 반환하는 format_private_key 함수를 구현해요.

format_private_key(0b1010010011000101);
// => 0b11000001

7. 복호화 마무리

관련된 비트가 모두 설정된 메시지와 포맷된 개인 키를 얻었다면, 이제 둘을 합쳐 최종 메시지를 만들 차례예요.

최종 메시지는 16비트 정수이며, 다음과 같아요:

  • 상위 8비트는 포맷된 개인 키로 채워져요.
  • 하위 8비트는 관련된 비트를 모두 설정한 메시지로 채워져요.

메시지와 마스크를 모두 인코딩한 16비트 정수를 받아, 메시지가 완전히 복호화된 16비트 정수를 반환하는 decrypt_message 함수를 구현해요.

이 함수는 format_private_key로 생성한 포맷된 개인 키와, set_message_bits로 관련된 비트를 모두 설정한 메시지를 활용해야 해요.

decrypt_message(0b1010010011000101);
// => 0b1100000111100101
GitHub에서 편집 링크가 새 창이나 탭에서 열려요
x86-64 Assembly Exercism

비밀 문제를 시작해 볼 준비가 됐나요?

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