요약

칸 수가 정해진 0과 1의 줄로는 정해진 개수의 수만 적을 수 있고, 그 개수를 0부터 위로만 쓸지 반을 잘라 음수에 줄지는 정하기 나름이다. 컴퓨터는 맨 앞 칸 하나만 음수 무게를 갖게 하는 방식으로 음수를 적는다. 자동차 주행 기록계가 999에서 한 칸 더 가면 000으로 도는 것과 같은 원리라서, 덧셈 회로 하나로 뺄셈까지 된다. 대신 가장 큰 수에 1을 더하면 가장 작은 음수로 넘어가 버린다.

예시로 보기

4칸짜리 주행 기록계를 떠올린다. 0000에서 뒤로 한 칸 돌리면 9999가 된다. 그러면 9999를 “−1”이라고 부르기로 해도 덧셈이 맞는다. 9999에 1을 더하면 0000이기 때문이다. 다만 진짜 기록계는 음수라는 약속이 없다. 이것은 약속을 하나 얹은 것이다.

2진수 4칸에서도 똑같다. 0000에서 뒤로 한 칸이 1111이니 1111을 −1이라 부른다. 이렇게 16가지 줄을 반으로 갈라 위쪽 8개를 음수에 준다.

줄    : 1000 1001 ... 1110 1111 0000 0001 ... 0110 0111
값    :  -8   -7  ...  -2   -1    0    1  ...   6    7

맨 앞 칸이 1인 줄이 모두 음수다. 그래서 맨 앞 칸만 보면 부호를 안다.

예로 1010을 읽어 본다. 칸의 무게를 왼쪽부터 −8, 4, 2, 1로 둔다.

칸 1 0 1 0
무게 −8 4 2 1
더할 값 −8 0 2 0

\(-8 + 2 = -6\)이다. 맨 앞 칸의 무게만 음수이고 나머지는 보통 2진수와 같다.

검증: 1~10비트에서 표현 범위와 겹치지 않음, 본문의 1010 = −6과 8비트 표 열 줄, 뒤집고 1 더하기(−128만 예외), 8비트 덧셈 전수 검사, 127 + 1 → −128, 카드의 값 — 02_twos-complement_verify.py

정의

유한한 비트. 비트(bit)는 2진수 한 자리다. \(N\)비트로 만들 수 있는 줄은 \(2^N\)가지뿐이다. 그래서 적을 수 있는 수의 범위와 촘촘함에 끝이 있다. 8비트라면 256가지다. 이 256가지에 어떤 수를 짝지을지는 필요에 따라 정한다.

부호 없는 정수. \(N\)비트 줄 \(a_{N-1} a_{N-2} \cdots a_1 a_0\)(왼쪽이 \(a_{N-1}\), 각 \(a_i\)는 0 또는 1)을 보통의 2진수로 읽는다. 값은 0부터 \(2^N - 1\)까지다.

\[u = \sum_{i=0}^{N-1} a_i 2^i\]

2의 보수. 맨 앞 칸의 무게만 \(-2^{N-1}\)로 바꾼다. \(w\)는 그 줄이 뜻하는 정수다.

\[w = -a_{N-1} 2^{N-1} + \sum_{i=0}^{N-2} a_i 2^i\]

값은 \(-2^{N-1}\)부터 \(2^{N-1} - 1\)까지다. 8비트면 −128부터 127까지다. 음수가 양수보다 하나 많다. 0이 양수 쪽 자리를 하나 쓰기 때문이다.

8비트에서 두 읽기를 나란히 놓으면 이렇다.

비트 부호 없이 읽은 값 2의 보수로 읽은 값
0000 0000 0 0
0000 0001 1 1
0111 1111 127 127
1000 0000 128 −128
1000 0001 129 −127
1111 1110 254 −2
1111 1111 255 −1

맨 앞 칸이 0이면 두 값이 같다. 1이면 두 값의 차가 늘 \(2^N = 256\)이다. 무게가 \(+128\)에서 \(-128\)로 바뀌었기 때문이다.

부호 바꾸기. 어떤 수의 부호를 바꾸려면 모든 칸을 뒤집고 1을 더한다1.

뒤집고 1을 더하면 부호가 바뀌는 이유
  1. 줄 \(x\)와 그것을 뒤집은 줄 \(\bar x\)를 더하면 칸마다 0과 1이 하나씩이라 모든 칸이 1이 된다. 모든 칸이 1인 줄은 2의 보수로 −1이다(\(-2^{N-1} + (2^{N-1} - 1) = -1\)).
  2. 그래서 \(x + \bar x = -1\)이고, 양변을 옮기면 \(\bar x + 1 = -x\)다.
  3. 예: \(6 =\) 0000 0110, 뒤집으면 1111 1001, 1을 더하면 1111 1010. 무게로 읽으면 \(-128 + 64 + 32 + 16 + 8 + 2 = -6\)이다.
  4. 예외: 가장 작은 수 1000 0000(−128)은 뒤집고 1을 더해도 자기 자신이다. +128은 8비트에 자리가 없기 때문이다.

예제

\(5 + (-6)\)을 8비트로. 부호를 따지지 않고 그냥 더한다.

  1. 두 수를 적는다: \(5 =\) 0000 0101, \(-6 =\) 1111 1010.
  2. 보통의 2진 덧셈: 0000 0101 + 1111 1010 = 1111 1111.
  3. 읽는다: 1111 1111은 −1이다. \(5 - 6 = -1\)과 같다.

뺄셈 회로가 따로 필요 없다. 빼려는 수의 부호를 바꿔 더하면 된다.

범위를 넘으면. \(127 + 1\)을 8비트로 계산하면 0111 1111 + 0000 0001 = 1000 0000이고, 이것은 −128이다. 주행 기록계가 한 바퀴 돈 것과 같다. 이것을 오버플로라 한다1.

활용

  • Java의 int는 32비트 2의 보수라 \(-2^{31}\)부터 \(2^{31} - 1\)(약 ±21억)까지다. C의 int도 요즘 컴퓨터 대부분에서 같다. 이 범위를 넘는 합이나 곱은 오버플로가 난다. 파이썬의 int는 칸 수를 필요한 만큼 늘려서 오버플로가 없다1.
  • 부호 없는 덧셈과 2의 보수 덧셈은 같은 회로다. 결과 줄을 어느 약속으로 읽느냐만 다르다. 이것이 2의 보수를 쓰는 이유다1.
  • 흔한 실수: 부호 없는 수와 부호 있는 수를 섞어 비교하는 것. 1111 1111은 한쪽에서는 255, 다른 쪽에서는 −1이다.
  • 비트를 직접 다루는 방법: 비트 연산과 비트마스크

연결

과목별 관점

어셈블리어 (2-1학기). 같은 2의 보수를 프로세서가 음수를 담는 실제 방식으로 다룬다. 어셈블리어에서 더 다루는 것은 만드는 절차, 뺄셈을 덧셈으로 바꾸는 법, 16진수로 적힌 값의 부호 읽기, 칸의 크기별 범위다.

  • 만드는 절차. 위의 “뒤집고 1을 더하기”를 두 단계로 적으면 이렇다. 00000001 → 비트를 뒤집어 11111110 → 1을 더해 11111111. 그리고 00000001 + 11111111 = 00000000임을 덧붙인다. 2의 보수는 원래 수와 더해서 0이 되는 짝(덧셈의 역원, additive inverse)이다.
  • 뺄셈은 덧셈이다. \(A - B\)를 계산할 때는 \(B\)를 2의 보수로 바꿔 \(A\)에 더한다.
   0 0 0 0 1 1 0 0   (12)            0 0 0 0 1 1 0 0   (12)
 − 0 0 0 0 0 0 1 1   (3)     →     + 1 1 1 1 1 1 0 1   (−3)
                                    -----------------
                                     0 0 0 0 1 0 0 1   (9)   맨 위로 넘친 올림은 버린다
  • 16진수에서 부호 읽기. 맨 윗글자가 7보다 크면 음수다. 8A, C5, A2, 9D는 모두 음수다. 16진수 한 글자는 비트 네 개이고, 8 이상인 글자(8~F)는 그 네 비트의 맨 앞이 1이기 때문이다2. 값은 차례로 −118, −59, −94, −99다.
  • 칸의 크기별 범위. 맨 윗비트를 부호에 쓰므로 범위가 이렇게 정해진다.
이름 비트 수 범위 2의 거듭제곱으로
부호 있는 바이트 8 −128 ~ +127 \(-2^7 \sim 2^7 - 1\)
부호 있는 워드 16 −32,768 ~ +32,767 \(-2^{15} \sim 2^{15} - 1\)
부호 있는 더블워드 32 −2,147,483,648 ~ +2,147,483,647 \(-2^{31} \sim 2^{31} - 1\)
부호 있는 쿼드워드 64 −9,223,372,036,854,775,808 ~ +9,223,372,036,854,775,807 \(-2^{63} \sim 2^{63} - 1\)

이 내용으로 할 줄 알아야 하는 계산은 다섯 가지다. 16진수의 2의 보수 구하기, 부호 있는 2진수를 10진수로, 부호 있는 10진수를 2진수로, 부호 있는 10진수를 16진수로, 부호 있는 16진수를 10진수로 바꾸기다. 연습은 16진 계산과 2의 보수 연습의 p4와 p5에 있다. 계산 결과가 범위를 넘었는지를 프로세서가 어떻게 표시하는지는 산술 연산과 상태 플래그와 올림 플래그와 넘침 플래그 비교에서 다룬다.

확인 문제

C1 4비트 1101을 부호 없는 정수와 2의 보수로 각각 읽어라.

답: 부호 없이 \(8 + 4 + 1 = 13\). 2의 보수로 \(-8 + 4 + 1 = -3\).
이유: 맨 앞 칸의 무게만 \(+8\)에서 \(-8\)로 바뀐다. 두 값의 차는 \(2^4 = 16\)이다.

C2 −5를 8비트 2의 보수로 적어라.

답: \(5 =\) 0000 0101 → 뒤집어 1111 1010 → 1을 더해 1111 1011.
흔한 오답: 맨 앞 칸만 1로 바꾼 1000 0101. 이것은 2의 보수로 \(-128 + 5 = -123\)이다.

C3 8비트 2의 보수에서 음수는 −128까지 있는데 양수는 왜 127까지뿐인가? 그리고 \(127 + 1\)은 왜 −128이 되는가?

답: 256가지 줄 중 맨 앞 칸이 1인 128가지가 음수, 0인 128가지가 0과 양수다. 0이 한 자리를 쓰니 양수는 127개다. 0111 1111에 1을 더하면 올림이 맨 앞 칸까지 가서 1000 0000이 되는데, 이 줄에서 맨 앞 칸의 무게는 −128이다. 적을 자리가 없는 +128이 반대쪽 끝으로 돌아간 것이다.

C4 부호 있는 8비트 값이 16진수로 9D라고 적혀 있다. 양수인가 음수인가? 10진수로 얼마인가? 또 00001100 − 00000011을 뺄셈 없이 덧셈으로 계산하라.

답: 음수다. 값은 −99다. 뺄셈은 00001100 + 11111101 = 00001001(9)이다.
이유: 맨 윗글자 9가 7보다 크므로 맨 윗비트가 1이다. 9D를 뒤집으면 62, 1을 더하면 63h = 99이므로 값은 −99다. 00000011을 뒤집고 1을 더하면 11111101이고, 더한 뒤 9번째 비트로 넘친 올림은 버린다2.

  1. 8비트 전수 검사로 확인했다. 내용은 Bryant, O’Hallaron, Computer Systems: A Programmer’s Perspective 3판, 2.2~2.3절에 있다. ↩ ↩2 ↩3 ↩4 ↩5

  2. 01_asm-hex-practice_p1.py로 확인했다(8비트 65,536쌍 모두에서 뺄셈과 2의 보수 덧셈이 같다). ↩ ↩2