컴퓨터가 데이터를 처리하는 가장 근본적인 방식은 0과 1의 조합인 비트(Bit) 단위의 계산이다.
비트 연산은 고수준 언어에서도 성능 최적화, 네트워크 프로토콜 처리, 하드웨어 제어 등에서 필수 요소로 사용된다.
1. 비트 연산의 개념과 논리 구조
비트 연산은 정수를 2진수로 변환한 뒤, 각 자릿수(Bit)별로 논리 게이트의 원리를 적용하는 방식이다.
각 연산자는 고유의 진리표(Truth Table)를 가지며 다음과 같이 동작한다.
주요 비트 연산자 정의
- AND (&): 두 비트가 모두 1일 때만 결과가 1이 된다.
- OR (|): 두 비트 중 하나라도 1이면 결과가 1이 된다.
- XOR (^): 두 비트의 값이 서로 다를 때만 1이 된다.
- NOT (~): 비트의 값을 반전시킨다 (0→1, 1→0).
- Shift (<<, >>): 비트 열을 왼쪽이나 오른쪽으로 이동시킨다.
2. 논리곱(AND, &)과 논리합(OR, |)
두 숫자를 비트 단위로 대조하여 연산을 수행한다.
논리곱 AND:
특정 비트를 '가리기(Masking)' 위해 자주 사용된다.
- AND: 3 & 5
- 3: 0000...0011
- 5: 0000...0101
- 결과: 0000...0001 (10진수 1)
논리합 OR :
특정 비트에 1을 '삽입'하거나 플래그를 결합할 때 사용된다.
- OR : 7 | 10
- 7 : 0000...0111
- 10: 0000...1010
- 결과: 0000...1111 (10진수 15)
3. 배타적 논리합(XOR, ^)
XOR 연산은 두 값이 다를 때만 1을 반환하는 특성이 있어, 암호화 알고리즘이나 두 변수의 값을 임시 변수 없이 교환할 때 쓰인다.
- XOR : 3 ^ 5
- 3: 0000... 0011
- 5: 0000... 0101
- 결과: 0000... 0110 (10진수 6)
4. 비트 반전(NOT, ~)과 음수 표현
비트 반전 연산은 피연산자의 모든 비트를 0은 1로, 1은 0으로 뒤집는 연산이다.
즉, 모든 자릿수의 논리 상태를 반대로 바꾼다.
예를 들어, 8비트 환경에서 숫자 3(0000 0011)에 NOT 연산을 수행하면 1111 1100이 된다.
하지만 이 결과값을 10진수로 출력해 보면 단순히 숫자가 뒤집힌 것이 아니라 -4라는 값이 출력된다.
이 현상을 이해하기 위해서는 컴퓨터가 음수를 저장하는 규칙인 2의 보수를 알아야 한다.
🟢2의 보수(2's Complement)
보수는 '보충해 주는 수'를 의미한다.
10진수에서 7에 대한 '10의 보수'가 3인 것처럼(더해서 10이 되는 수),
2진수에서 2의 보수는 어떤 수와 더했을 때 자릿수 올림이 발생하는 최소의 수를 뜻한다.
🌀왜 컴퓨터는 1을 더해서 2의 보수를 만드는가?
비트를 반전시킨 후 1을 더하는 이유는 '0'의 유일성을 확보하고, 뺄셈을 덧셈기 하나로 처리하기 위해서다.
1️⃣ 0의 중복 문제 해결 (-0의 제거)
비트만 반전시키는 '1의 보수' 방식을 사용하면, 0이 두 개 존재하게 된다.
- 0000 (+0)
- 1111 (-0)
컴퓨터 입장에서 0이 두 개라는 것은 "A와 B가 같은가?"를 비교할 때마다 두 종류의 0을 모두 체크해야 한다는 뜻이며, 이는 회로를 복잡하게 만든다.
하지만 1의 보수에 1을 더하는 순간, 1111은 1 0000이 되고, 지정된 비트 범위를 넘어서는 올림수(Carry)를 버리면 0000 하나로 통합된다.
2️⃣수학적 완결성: A + (-A) = 0
컴퓨터에게 음수란 "양수 A와 더했을 때 결과가 0이 나오게 하는 수"여야 한다.
- 1의 보수만 쓸 경우: 3(0011) + NOT 3(1100) = 1111 (결과가 0이 아님)
- 2의 보수를 쓸 경우: 1의 보수(1100)에 1을 더해 1101을 만든다.
- 3(0011) + (-3)(1101) = 1 0000
- 맨 앞의 1을 버리면 결과는 정확히 0000이 된다.
즉, +1은 '더해서 0이 되는 짝'을 맞추기 위한 최종 조각인 셈이다.
🌀왜 컴퓨터가 해석할 때는 '-1'을 한 후 비트를 반전시키는가?
음수 비트를 다시 10진수로 읽을 때 1을 먼저 빼는 이유는 연산의 가역성(Invertibility) 때문이다.
즉, 간 길을 그대로 되돌아와야 원래 숫자가 나오기 때문이다.
1️⃣역함수의 원리
생성 과정이 다음과 같다면:

이 과정을 되돌려 원래의 양수(절대값)를 알아내려면 정확히 반대 순서로 연산해야 한다.

2️⃣수학적 증명
비트 반전 연산을 NOT(x)라고 할 때, 2의 보수 음수 M은 다음과 같이 정의된다.

(여기서 P는 원래의 양수)
우리가 알고 싶은 것은 P의 값이다. 위 식을 P에 관해 정리하면:
- M - 1 = NOT(P)
- NOT(M - 1) = NOT(NOT(P))
- NOT(M - 1) = P
결국 "음수에서 1을 빼고(M-1) 그 결과를 반전(NOT) 시키면 원래의 양수(P)가 나온다"는 식이 성립한다.
3️⃣NOT (~) 연산자의 기능과 컴퓨터의 숫자 해석 방식은 별개다
다시 NOT(~) 연산자로 돌아와서 비트 연산 ~3의 결과가 -3이 아니라 -4가 되는 과정을 컴퓨터의 관점에서 살펴보자.
- 비트 연산자 (~): "나는 그냥 0은 1로, 1은 0으로 뒤집기만 할게." (단순 노동)
- 컴퓨터의 해석기 (CPU/Language): "오, 결과 비트의 맨 앞이 1이네? 이건 음수니까 2의 보수 규칙대로 읽어서(1을 빼고 비트를 뒤집음) 10진수로 보여줘야지." (해석 규칙)
🟢 결론:
a = 3일 때, ~a가 -4가 되는 과정을 정리하면 다음과 같다.

5. 시프트 연산(Shift Operation)
시프트 연산은 비트를 옆으로 밀어내는 연산으로, 산술적으로는 2의 거듭제곱을 곱하거나 나누는 것과 동일한 효과를 내면서도 연산 속도는 훨씬 빠르다.
1️⃣ 왼쪽 시프트 (<<)
비트를 왼쪽으로 이동시키며, 오른쪽에 생기는 빈 공간은 0으로 채운다.
비트가 n칸 이동할 때마다 값은 2^n배 증가한다.
| 연산 | 2진수 비트 표현 (8-bit) | 10진수 결과 |
| a = 3 | 0 0 0 0 0 0 1 1 | 3 |
| a << 1 | 0 0 0 0 0 1 1 0 | 6 |
| a << 2 | 0 0 0 0 1 1 0 0 | 12 |
| a << 3 | 0 0 0 1 1 0 0 0 | 24 |
2️⃣ 오른쪽 시프트 (>>)
비트를 오른쪽으로 이동시킨다.
비트가 오른쪽으로 한 칸 이동할 때마다 값은 2로 나눈 몫이 된다.
| 연산 | 2진수 비트 표현 (8-bit) | 10진수 결과 |
| a = 12 | 0 0 0 0 1 1 0 0 | 12 |
| a >> 1 | 0 0 0 0 0 1 1 0 | 6 |
| a >> 2 | 0 0 0 0 0 0 1 1 | 3 |
| a >> 3 | 0 0 0 0 0 0 0 1 | 1 |
4. 언어별 구현 예시 (C & Python)
비트 연산은 저수준 언어인 C뿐만 아니라 파이썬과 같은 고수준 언어에서도 동일한 문법으로 지원된다.
C 언어 구현
stdio.h 라이브러리를 사용하여 서식 지정자 %d로 비트 연산 결과를 출력할 수 있다.
#include <stdio.h>
int main()
{
printf("%d\n", 3 & 5); // 결과: 1
printf("%d\n", 7 | 10); // 결과: 15
printf("%d\n", 3 ^ 5); // 결과: 6
return 0;
}
파이썬 인터프리터 활용
파이썬은 별도의 컴파일 없이 대화형 쉘에서 즉시 비트 연산 결과를 확인할 수 있다.
>>> 3 & 5
1
>>> 7 | 10
15
>>> 3 ^ 5
6
6. 정리
비트 연산의 활용성을 다음과 같이 요약할 수 있다.
- 성능 최적화: 곱셈이나 나눗셈 연산보다 시프트 연산이 CPU 자원을 훨씬 적게 소모한다.
- 공간 효율성: 하나의 정수 변수(32비트 기준) 안에 32개의 True/False 상태를 저장하는 '비트 플래그' 기법을 사용할 수 있다.
- 데이터 무결성: XOR 연산은 두 번 수행하면 원래 값으로 돌아오는 성질이 있어 간단한 암호화나 네트워크 패킷의 오류 검출에 활용된다.
'Journey to Security > C언어' 카테고리의 다른 글
| C언어 배열명과 포인터의 차이: GDB로 문자열 저장 방식 뜯어보기 (0) | 2026.02.22 |
|---|---|
| C언어 배열 메모리 레이아웃 (feat. GDB) (0) | 2026.02.20 |
| C 주소 전달 메커니즘: scanf 원형과 작동 원리, 메모리 구조 (0) | 2026.02.14 |
| GDB에서 x 명령어로 메모리 분석하기 (0) | 2026.02.14 |
| C언어 필수 디버깅 도구 gdb 기본 사용법과 16진수 표현 이해하기 (0) | 2026.02.13 |