에듀테크 프로젝트 대시보드
CPU에는 뺄셈 회로가 따로 없다. 5 - 3 도 사실은 5 + (-3) 으로 계산한다.
그러려면 음수도 0과 1로 표현해야 하는데, 거의 모든 컴퓨터가 쓰는 방식이 2의 보수다.
맨 앞 비트(부호 비트)의 자릿값을 음수로 두는 아이디어 하나로,
덧셈기 하나가 뺄셈까지 처리하고 0이 딱 하나가 된다.
아래에서 8비트 수의 비트를 직접 켜고 끄며 부호 있는 값이 어떻게 바뀌는지 확인해 보자.
보통 8비트의 자릿값은 128 64 32 16 8 4 2 1 이다. 2의 보수는 맨 앞 자리만 −128로 바꾼다. 나머지는 그대로. 그래서 계산법은 똑같이 "켜진 자릿값을 다 더한다" 뿐이다.
| 비트 |
|---|
| 자릿값 |
| 더할 값 |
왜 +1? "뒤집기(1의 보수)"만 하면 x + (~x) = 11111111 = −1 이 된다.
−1이 아니라 0이 되게 하려면 한 번 더 더해야 한다 → ~x + 1.
재미있는 사실: −128을 다시 반전하면 오버플로로 그대로 −128이다.
8비트는 256칸짜리 시계다. 오른쪽 절반(0~127)은 양수, 왼쪽 절반(128~255)은 음수(−128~−1). 127에서 1을 더하면 −128로 넘어가는 게 부호 오버플로다.
5 − 3 = 5 + (−3)비트만 보면 그냥 부호 없는 덧셈이다. 8비트를 넘는 자리올림(9번째 비트)은 그냥 버린다. 그래도 아래 8비트를 2의 보수로 읽으면 정답이 나온다. 단, 결과가 −128~127 범위를 벗어나면 오버플로(부호가 뒤집힘)가 난다.
부호+절댓값이나 1의 보수 방식은 +0과 −0이 따로 있다. 2의 보수는 00000000 하나. 비교·분기 회로가 단순해진다.
덧셈기 하나로 뺄셈·음수까지 처리. 부호를 신경 쓰지 않고 그냥 더하고 넘친 비트를 버리면 된다.
N비트: −2^(N−1) ~ 2^(N−1)−1. 음수가 딱 하나 더 많다(−128은 있는데 +128은 없음).
8비트 → 32비트로 늘릴 때 맨 앞 비트를 그대로 복사하면 값이 유지된다(0xFF → 0xFFFFFFFF = −1).
# 8비트 비트패턴 -> 2의 보수 정수 def to_signed8(bits: int) -> int: return bits - 256 if bits & 0x80 else bits # 맨 앞 비트가 1이면 -256 to_signed8(0b11111111) # -> -1 to_signed8(0b10000000) # -> -128 # 정수 -> 8비트 비트패턴 (음수는 자동으로 2의 보수) (-3) & 0xFF # -> 253 == 0b11111101 # 부호 반전 = NOT 후 +1 (8비트 안에서) def negate8(x): return (~x + 1) & 0xFF negate8(19) # -> 237 == to_signed8 -> -19 # 뺄셈 = 덧셈 + 하위 8비트만 취하기 a, b = 5, 3 ((a & 0xFF) + (negate8(b))) & 0xFF # -> 2 # struct로 진짜 바이트 해석 import struct struct.unpack('b', bytes([0xFB]))[0] # -> -5 ('b' = signed char) struct.unpack('B', bytes([0xFB]))[0] # -> 251 ('B' = unsigned char)
※ 파이썬 int는 자릿수 제한이 없어 항상 & 0xFF 같은 마스킹으로 폭을 정해줘야 한다.
C·자바의 int(32비트), long(64비트)은 하드웨어처럼 자동으로 래핑되며,
signed 오버플로는 C에서 정의되지 않은 동작이니 주의.