정수의 각 비트는 이진 값을 저장하는 데 사용할 수 있어요. 참 또는 거짓, 포함 또는 제외, 켜짐 또는 꺼짐처럼 많은 상황이 이진 정보를 다루기 때문에, 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 명령어는 두 피연산자 사이에 비트 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은 완전히 같은 연산을 수행하며, 하나는 다른 하나의 별칭이에요.
왼쪽 시프트가 일어날 때마다, 수열의 끝에서 시프트 길이만큼 넘어가는 비트들은 먼저 CF로 이동한 뒤 버려져요.
반대로, 시프트 길이와 같은 수의 새로운 0 비트가 수열의 앞쪽에 추가돼요.
정수의 각 비트는 2의 거듭제곱을 나타내므로, n 자리만큼 왼쪽으로 시프트하면 정수에 2ⁿ을 곱하는 효과가 있어요.
비트를 오른쪽으로 이동하는 명령어는 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비트 피연산자와는 함께 사용할 수 없어요.
친구가 방금 중요한 비밀이 담긴 메시지를 보냈어요. 다른 사람이 쉽게 읽지 못하게 하려고, 그 메시지는 일련의 비트 조작을 거쳐 암호화되었어요. 여러분은 메시지를 복호화하는 데 도움이 될 메서드들을 작성해야 해요.
이 개념에서 언급된 단일 비트 명령어들은 다음과 같아요:
| 이름 | 설명 |
|---|---|
| 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 | 가장 낮은 자리의 설정된 비트의 인덱스를 구해요. 설정된 비트가 없으면 결과는 정의되지 않아요 |
메시지는 16비트 정수로 인코딩되어 있어요. 하지만 그중 상위 8비트는 실제로 메시지의 일부가 아니라, 복호화에 사용해야 하는 마스크예요.
16비트 정수를 받아 그중 상위 8비트를 반환하는 extract_higher_bits 함수를 구현해요.
extract_higher_bits(0b1010010011000101)
// => 0b10100100
마스크를 추출할 수 있는 것만으로는 충분하지 않아요. 메시지도 분리해야 해요.
16비트 정수를 받아 그중 하위 8비트를 반환하는 extract_lower_bits 함수를 구현해요.
extract_lower_bits(0b1010010011000101);
// => 0b11000101
어떤 비트들은 메시지와 마스크 양쪽에서 설정되어 있어요. 이것은 나중에 사용될 매우 중요한 정보예요.
메시지와 마스크를 모두 인코딩한 16비트 정수를 받아, 중복 비트만 설정된 8비트 정수를 반환하는 extract_redundant_bits를 구현해요.
반환된 숫자에서 어떤 비트는, 메시지와 마스크 양쪽에서도 1인 자리에서 1로 설정되어야 해요.
다른 모든 비트는 지워져야 해요.
extract_redundant_bits(0b1010010011000101);
// => 0b10000100
다음으로, 마스크에 따라 메시지에서 1로 설정해야 하는 비트들이 있어요.
메시지와 마스크를 모두 인코딩한 16비트 정수를 받아, 메시지의 비트를 1로 설정한 결과를 반환하는 set_message_bits 함수를 구현해요.
메시지의 비트는, 마스크의 비트가 1인 자리에서 1로 설정되어야 해요.
다른 모든 비트는 그대로 유지되어야 해요. 이미 설정되어 있었다면 설정된 채로, 이미 지워져 있었다면 지워진 채로 남아요.
set_message_bits(0b1010010011000101);
// => 0b11100101
메시지에 명시되어 있지 않은 퍼즐 조각이 하나 있어요. 바로 16비트 숫자 0b1011001100111100이에요.
이 숫자는 여러분이 공유하는 개인 키이며, 메시지를 복호화하는 데 사용해야 해요.
그러려면 먼저 개인 키의 비트를 특정 위치 수만큼 왼쪽으로 회전해야 해요. 그 위치 수는 메시지와 마스크 양쪽에서 설정된 중복 비트의 개수와 같아요.
메시지와 마스크를 모두 인코딩한 16비트 정수를 받아, 개인 키를 회전한 결과를 반환하는 rotate_private_key 함수를 구현해요.
이 결과는 16비트 정수예요.
rotate_private_key(0b1010010011000101);
// => 0b1100110011110010
NASM(이 트랙에서 사용하는 어셈블러인 The Netwide Assembler)은 0b 접두사가 붙은 이진수 형식의 상수를 지원해요.
또한 가독성을 위해 상수에서 밑줄(_)을 구분자로 사용하는 것도 지원해요:
PRIVATE_KEY equ 0b1011_0011_0011_1100
복호화에 사용하려면, 관련된 비트를 분리하도록 개인 키를 포맷해야 해요.
개인 키를 완전히 포맷하려면 다음을 해야 해요:
뒤집힌 비트는 0이었으면 1이고, 1이었으면 0이에요.
메시지와 마스크를 모두 인코딩한 16비트 정수를 받아, 완전히 포맷된 8비트 개인 키를 반환하는 format_private_key 함수를 구현해요.
format_private_key(0b1010010011000101);
// => 0b11000001
관련된 비트가 모두 설정된 메시지와 포맷된 개인 키를 얻었다면, 이제 둘을 합쳐 최종 메시지를 만들 차례예요.
최종 메시지는 16비트 정수이며, 다음과 같아요:
메시지와 마스크를 모두 인코딩한 16비트 정수를 받아, 메시지가 완전히 복호화된 16비트 정수를 반환하는 decrypt_message 함수를 구현해요.
이 함수는 format_private_key로 생성한 포맷된 개인 키와, set_message_bits로 관련된 비트를 모두 설정한 메시지를 활용해야 해요.
decrypt_message(0b1010010011000101);
// => 0b1100000111100101
Exercism에 가입하고 x86-64 Assembly 트랙을 개념 22개연습 문제 130개, 그리고 실제 사람의 멘토링과 함께 배우고 익혀 보세요. 모두 무료예요.