한양과학기술고등학교

에듀테크 프로젝트 대시보드

2의 보수 (Two's Complement)

CPU에는 뺄셈 회로가 따로 없다. 5 - 3 도 사실은 5 + (-3) 으로 계산한다. 그러려면 음수도 0과 1로 표현해야 하는데, 거의 모든 컴퓨터가 쓰는 방식이 2의 보수다. 맨 앞 비트(부호 비트)의 자릿값을 음수로 두는 아이디어 하나로, 덧셈기 하나가 뺄셈까지 처리하고 0이 딱 하나가 된다. 아래에서 8비트 수의 비트를 직접 켜고 끄며 부호 있는 값이 어떻게 바뀌는지 확인해 보자.

같은 비트, 두 가지 해석 — 부호 없음 vs 2의 보수

비트 패턴
00101010
부호 없음 (unsigned)
42
2의 보수 (signed)
42
16진수
0x2A

핵심 — 맨 앞 비트의 자릿값만 음수로

보통 8비트의 자릿값은 128 64 32 16 8 4 2 1 이다. 2의 보수는 맨 앞 자리만 −128로 바꾼다. 나머지는 그대로. 그래서 계산법은 똑같이 "켜진 자릿값을 다 더한다" 뿐이다.

비트
자릿값
더할 값

음수 만들기 — 뒤집고(NOT) +1

원래 값 = 19

왜 +1? "뒤집기(1의 보수)"만 하면 x + (~x) = 11111111 = −1 이 된다. −1이 아니라 0이 되게 하려면 한 번 더 더해야 한다 → ~x + 1. 재미있는 사실: −128을 다시 반전하면 오버플로로 그대로 −128이다.

수는 원형으로 돈다 — 오버플로와 래핑

바늘 위치 = 3

8비트는 256칸짜리 시계다. 오른쪽 절반(0~127)은 양수, 왼쪽 절반(128~255)은 음수(−128~−1). 127에서 1을 더하면 −128로 넘어가는 게 부호 오버플로다.

덧셈기 하나로 뺄셈까지 — 5 − 3 = 5 + (−3)

A = 5
B = -3

비트만 보면 그냥 부호 없는 덧셈이다. 8비트를 넘는 자리올림(9번째 비트)은 그냥 버린다. 그래도 아래 8비트를 2의 보수로 읽으면 정답이 나온다. 단, 결과가 −128~127 범위를 벗어나면 오버플로(부호가 뒤집힘)가 난다.

왜 하필 2의 보수인가

0이 하나뿐

부호+절댓값이나 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에서 정의되지 않은 동작이니 주의.