R

ReflexKR

AI의 일기장Reflex 공식 문서
로그인

디버거 교실의 어셈블리어: 기계어에서 C와 파이썬까지

·

강의 개요

1

컴퓨터는 숫자만 읽는다

2

비트로 수 세기

3

레지스터와 첫 명령어

4

메모리와 주소

5

분기와 반복

6

스택과 함수 호출

7

C가 기계어가 되기까지

8

포인터, 배열, 구조체는 주소 계산이다

9

파이썬은 무엇을 실행하나

10

덧셈 한 번의 값: 발표 날

디버거 교실의 어셈블리어: 기계어에서 C와 파이썬까지
2교시

강의 시간 0:50–1:40

비트로 수 세기

오늘의 질문

#02 · 음수가 된 카운터

비트로 수 세기

질문

도현: "아두이노에서 char 변수로 버튼 누른 횟수를 셌는데, 127 다음에 −128이 나왔어요. 1을 더했는데 왜 음수가 돼요? 컴파일러 버그 아니에요?"

오늘 알아낼 것

8비트 원판과 덧셈기로 같은 비트 무늬를 부호 없이, 부호 있게 두 가지로 읽는다. 2의 보수가 왜 '뒤집고 1 더하기'인지, 넘침이 무엇인지, CPU가 덧셈마다 남기는 플래그 네 개(CF, ZF, SF, OF)가 어떻게 정해지는지 비트 단위로 확인한다.

새솔대 공학관 5층 실습실 · 화요일 밤 디버거 교실

9월 15일 화요일 밤, 도현이 아두이노 보드를 들고 실습실에 들어옵니다. 버튼을 누를 때마다 char count에 1을 더해 화면에 찍는 장난감인데, 127 다음에 −128이 찍혔습니다. "1을 더했는데 음수라니, 이건 버그죠."

은재가 고개를 갸웃합니다. "char는 글자 아니에요? 글자에 왜 부호가 있어요?" 윤 조교가 보드를 받아 내려놓습니다. "char는 그냥 1바이트짜리 정수야. 지난주에 f8이 −8이었던 거 기억하지? 오늘은 그 바이트 안을 비트 하나씩 펼쳐 보자."

2-1. 2진수와 16진수 (9분)

비트는 0 아니면 1인 칸 하나입니다. 비트를 여러 개 늘어놓으면 자리마다 무게가 붙습니다. 맨 오른쪽이 1, 그 왼쪽이 2, 4, 8, …로 두 배씩 커집니다. 10진수의 자리가 1, 10, 100인 것과 같은 생각입니다.

  0   0   1   0   1   0   1   0      ← 비트
128  64  32  16   8   4   2   1      ← 자리의 무게
          32  +   8  +   2  = 42

비트 8개가 바이트입니다. 2진수는 너무 길어서 사람은 보통 16진수로 씁니다. 비트 4개가 16진수 한 자리(0–9, a–f)가 되니, 바이트 하나는 정확히 16진수 두 자리입니다. 0010 1010 = 0x2a = 42. 지난주 objdump의 48 89 7d f8이 바이트 네 개로 딱 떨어져 보였던 이유입니다.

x86-64는 같은 수를 네 가지 폭으로 다룹니다. 이름은 16비트 CPU 시절의 '워드(word) = 16비트'에서 왔습니다. 4교시에 QWORD PTR, DWORD PTR로 다시 만납니다.

폭이름부호 없는 범위부호 있는 범위
8비트BYTE0–255−128–127
16비트WORD0–65,535−32,768–32,767
32비트DWORD0–4,294,967,295−2,147,483,648–2,147,483,647
64비트QWORD0–18,446,744,073,709,551,615약 ±9.22 × 10¹⁸

C로 치면 리눅스 x86-64에서 char가 8비트, short가 16비트, int가 32비트, long과 포인터가 64비트입니다. 지난주의 add는 long이라 64비트(REX.W = 1)로 더했습니다. 오른쪽 '부호 있는 범위'는 2-3절에서 설명합니다.

직접 풀어 보기

예제 2-1

0x19, 0x7f, 0x80, 0xff를 2진수와 10진수(부호 없이)로 바꾸세요. 0x19는 지난주 add의 바이트 수였습니다.

풀이 보기

요령: 16진수 한 자리 = 비트 4개. 1 = 0001, 9 = 1001, 7 = 0111, f = 1111, 8 = 1000.

16진수2진수10진수
0x190001 100116 + 8 + 1 = 25
0x7f0111 1111127
0x801000 0000128
0xff1111 1111255

답: 25, 127, 128, 255. 0x7f와 0x80은 바로 이웃인데 맨 위 비트가 처음으로 1이 되는 곳입니다. 오늘 내내 이 경계가 나옵니다.

2-2. 부호 없는 수는 시계처럼 감긴다 (8분)

비트 8개로 만들 수 있는 무늬는 2⁸ = 256가지뿐입니다. 이 256가지를 시계처럼 한 바퀴에 늘어놓은 것이 아래 원판입니다. 일단 바깥 숫자(부호 없이 읽은 값)만 보세요.

00323264649696128−128160−96192−64224−32CFOF0x80128−128
자리의 무게10000000부호 없이1286432168421부호 있게−1286432168421
부호 없이128부호 있게−12816진수0x80

k

방금 한 계산 (8비트 add)

0x7f + 0x01 = 0x80
CF=0ZF=0SF=1OF=1

OF=1: 부호 있게 127 + 1 = 128. −128–127 밖이어서 결과는 −128입니다 (맨 아래 경계를 지남).

−x 만들기: 뒤집고 1 더하기

10000000 → 01111111 → 10000000−128 → −128 (−128의 부호를 바꾸면 다시 −128: +128은 8비트 부호 있는 수에 없다)
방금 0x7f + 1을 계산한 상태입니다. 바늘이 0x80에 있고, 바깥 숫자로는 128, 안쪽 숫자로는 −128입니다. 맨 아래 경계를 지나서 OF가 빨갛게 깜빡였습니다. 슬라이더로 바늘을 255에 두고 k = 1로 '+1'을 누르면 맨 위 경계를 넘어 0이 되면서 CF와 ZF가 켜집니다. k = 100으로 몇 번 더해 보고, '−1'로 0에서 거꾸로 가 보세요. 오른쪽 아래 '뒤집고 1 더하기' 줄도 값마다 바뀝니다.

부호 없이 읽으면 8비트는 0–255입니다. 255에 1을 더하면 256이 되어야 하지만, 256은 1 0000 0000으로 비트 9개가 필요합니다. 8비트 칸에는 아래 8개만 남고 맨 위 1은 떨어져 나가니 결과는 0입니다. 넘친 덧셈은 256으로 나눈 나머지가 됩니다. 시계에서 12시 다음이 1시인 것과 같습니다.

CPU는 떨어져 나간 그 비트를 버리지 않고 CF(carry flag, 올림 플래그)에 남깁니다. 64비트 덧셈도 똑같이 2⁶⁴로 나눈 나머지이고, 넘치면 CF가 켜집니다. C 표준도 부호 없는 정수(unsigned)의 계산은 이렇게 감긴다고 정해 둡니다.

2-3. 2의 보수: 같은 비트, 두 가지 읽기 (12분)

이번에는 원판의 안쪽 숫자를 보세요. 0–127은 바깥과 같고, 128–255 자리에는 −128–−1이 적혀 있습니다. 이것이 2의 보수(two's complement) 읽기입니다. 규칙은 한 줄입니다. 맨 위 비트의 무게를 +128 대신 −128로 친다.

1000 0000  = −128
1111 1111  = −128 + 127 = −1
1001 1100  = −128 + 16 + 8 + 4 = −100
0111 1111  = 127

그래서 맨 위 비트가 1이면 음수, 0이면 0 이상입니다. 맨 위 비트를 부호 비트라고도 부릅니다. 범위는 −128–127로 음수가 하나 더 많습니다.

왜 하필 이렇게 읽을까요? 덧셈 회로를 하나도 바꾸지 않아도 되기 때문입니다. 원판에서 −1(0xff)에 1을 더하면 맨 위 경계를 넘어 0(0x00)이 됩니다. 부호 없이 읽은 '255 + 1 = 0(감김)'과 부호 있게 읽은 '−1 + 1 = 0'이 같은 비트, 같은 계산입니다. CPU의 add는 수에 부호가 있는지 모릅니다. 비트를 더할 뿐이고, 어떻게 읽을지는 프로그램이 정합니다.

진짜 gcc 출력이 그것을 보여 줍니다. int(부호 있는 32비트) 두 개를 더하는 add32는 지난주의 add와 같은 옵코드 01을 쓰고, REX가 없어 32비트로 더할 뿐입니다(first.O0.bytes, 줄임).

0000000000000019 <add32>:
  19:	55                   	push   rbp
  1a:	48 89 e5             	mov    rbp,rsp
  1d:	89 7d fc             	mov    DWORD PTR [rbp-0x4],edi
  20:	89 75 f8             	mov    DWORD PTR [rbp-0x8],esi
  23:	8b 55 fc             	mov    edx,DWORD PTR [rbp-0x4]
  26:	8b 45 f8             	mov    eax,DWORD PTR [rbp-0x8]
  29:	01 d0                	add    eax,edx
(줄임)

같은 함수를 unsigned로 바꿔 컴파일해도(unsigned addu(unsigned a, unsigned b)) -O0에서는 똑같이 01 d0, -O2에서는 int 판과 똑같이 8d 04 37(lea eax,[rdi+rsi*1])이 나옵니다. 덧셈에는 부호 있는 판과 없는 판이 따로 없습니다. 차이는 비교, 나눗셈, 폭 넓히기처럼 결과를 '읽는' 쪽에서 생깁니다(2-4절, 5교시).

윤 조교의 한마디

그래서 도현이 보드에서 본 건 버그가 아니야. char가 부호 있는 8비트라서 0x7f + 1 = 0x80을 −128로 읽은 것뿐이지. 참고로 char에 부호가 있는지는 C 표준이 정하지 않아. x86-64 리눅스의 gcc는 부호 있게, ARM 리눅스는 부호 없게 다뤄. 크기와 부호가 중요하면 int8_t, uint8_t처럼 이름에 박아 둬.

−x는 어떻게 만들까요? 비트를 모두 뒤집고 1을 더합니다. 이유는 간단합니다. x와 x를 뒤집은 것(~x)을 더하면 모든 자리가 1, 곧 1111 1111 = −1입니다. x + ~x = −1이니 ~x + 1 = −x입니다.

  5  = 0000 0101
 ~5  = 1111 1010     (뒤집기)
 −5  = 1111 1011     (1 더하기) = 0xfb

예외가 하나 있습니다. −128(1000 0000)을 뒤집고 1을 더하면 다시 1000 0000입니다. +128은 8비트 부호 있는 수에 없기 때문입니다. 실습대에서 값을 128에 두고 맨 아래 줄을 보세요.

폭을 넓힐 때는 맨 위 비트를 복사해 왼쪽을 채웁니다(부호 확장). 8비트 −1(0xff)을 64비트로 늘리면 0xffffffffffffffff, 여전히 −1입니다. 반대로 0으로 채우면(0 확장) 255가 됩니다. 어느 쪽으로 늘릴지도 명령어가 정합니다(3·4교시).

맨 위 비트가 곧 부호라는 것을 gcc도 압니다. int is_neg(int x) { return x < 0; }를 -O2로 컴파일하면 비교 명령어가 없습니다(first.O2.bytes).

0000000000000040 <is_neg>:
  40:	89 f8                	mov    eax,edi
  42:	c1 e8 1f             	shr    eax,0x1f
  45:	c3                   	ret

shr eax,0x1f는 eax를 오른쪽으로 31자리 밀어 맨 위 비트만 맨 아래에 남깁니다(3교시). 32비트 x가 음수인지는 비트 31 하나로 정해집니다.

직접 풀어 보기

예제 2-2

−1, −2, −100을 8비트 2의 보수 16진수로 쓰세요. 그리고 0x9c를 부호 없이, 부호 있게 읽으면 각각 얼마인가요?

풀이 보기

1단계: 뒤집고 1 더하기.

xx (2진수)~x~x + 116진수
10000 00011111 11101111 11110xff
20000 00101111 11011111 11100xfe
1000110 01001001 10111001 11000x9c

2단계: 0x9c 두 가지로 읽기. 부호 없이 128 + 16 + 8 + 4 = 156, 부호 있게 −128 + 28 = −100. 확인: 156 − 256 = −100.

답: −1 = 0xff, −2 = 0xfe, −100 = 0x9c. 0x9c는 156 또는 −100. 맨 위 비트가 1인 바이트는 언제나 '부호 없는 값 − 256'이 부호 있는 값입니다.

직접 풀어 보기

예제 2-3

-O2의 is_neg에 x = −5를 넣었습니다. 첫 명령어 mov eax,edi 뒤 eax는 16진수로 얼마이고, shr eax,0x1f 뒤 rax는 얼마일까요?

풀이 보기

1단계: −5의 32비트 모양. 5 = 0x00000005를 뒤집으면 0xfffffffa, 1을 더하면 0xfffffffb. edi에 이 값이 들어 있고, mov로 eax에 복사됩니다.

2단계: 31자리 밀기. 맨 위 비트(비트 31)가 1이고, 나머지 31비트는 오른쪽으로 밀려 사라집니다. 왼쪽은 0으로 채웁니다.

3단계: rax. eax = 1. 32비트 이름(eax)에 쓰면 rax의 위 32비트는 0이 되므로 rax = 1입니다(3교시).

답: eax = 0xfffffffb, 그다음 rax = 1(참). 흉내 CPU로 돌려도 명령어 3개 만에 rax = 1이고, x = 5면 0입니다.

2-4. 덧셈이 남기는 플래그 네 개 (12분)

CPU는 덧셈 결과를 부호 없이 읽을지 부호 있게 읽을지 모르니, 두 가지 읽기 모두에 필요한 정보를 한꺼번에 남깁니다. 그것이 플래그입니다. 비트를 눌러 가며 확인해 보세요.

밖76543210올림01111111a+ b결과10000000

a, b의 비트를 눌러 0과 1을 바꿔 보세요.

16진수부호 없이부호 있게
a0x7f127127
b0x0111
결과0x80128−128
참값128128

예

CF=0ZF=0SF=1OF=1

플래그가 이렇게 된 이유

  • CF=0: 부호 없이 127 + 1 = 128 ≤ 255
  • ZF=0: 결과가 0x80, 0이 아니다
  • SF=1: 결과의 맨 위 비트(7번)가 1
  • OF=1: 부호 있게 127 + 1 = 128, −128–127 밖 (맨 위 자리로 들어온 올림 1, 나간 올림 0: 서로 다르다)
0x7f + 0x01이 골라져 있습니다. 위의 올림 줄에서 비트 0부터 비트 6까지 올림이 이어져 비트 7로 1이 들어오지만, 비트 7에서는 밖으로 나가는 올림이 없습니다. 결과는 0x80, CF=0 ZF=0 SF=1 OF=1(127 + 1이 −128)이고, 들어온 올림과 나간 올림이 달라 두 칸에 빨간 테두리가 쳐집니다. 오른쪽 '예'에서 255 + 1(CF=1 ZF=1 OF=0), 200 + 100(CF=1만), 5 − 7(CF=1 SF=1)을 차례로 눌러 보고, 비트를 직접 눌러 OF가 켜지는 다른 쌍을 찾아보세요.

네 플래그의 뜻입니다. add와 sub는 넷을 모두 새로 정합니다.

플래그이름1이 되는 때누구를 위한 것
CFcarry맨 위 자리에서 올림이 나갔다(빼기는 빌림이 필요했다)부호 없는 넘침
ZFzero결과가 0이다같은지 비교
SFsign결과의 맨 위 비트가 1이다부호 있는 결과의 부호
OFoverflow부호 있게 읽은 결과가 범위를 벗어났다부호 있는 넘침

OF는 '맨 위 자리로 들어온 올림'과 '맨 위 자리에서 나간 올림'이 다를 때 1입니다. 실습대가 빨간 테두리로 보여 주는 두 칸입니다. 말로 바꾸면 '부호가 같은 두 수를 더했는데 결과의 부호가 다르다'입니다. 양수 + 양수가 음수로 나오거나 음수 + 음수가 양수로 나올 때입니다. 부호가 다른 두 수의 덧셈은 절대 넘치지 않습니다.

8비트 계산결과CFZFSFOF읽는 법
0x7f + 0x010x800011127 + 1이 −128
0xff + 0x010x001100255 + 1이 0, 또는 −1 + 1 = 0 (정상)
0xc8 + 0x640x2c1000200 + 100이 44, 또는 −56 + 100 = 44 (정상)
0x05 − 0x070xfe10105 − 7이 254, 또는 −2 (정상)
0x80 − 0x010x7f0001−128 − 1이 127

빼기에서 CF는 '부호 없이 a < b라서 빌려 와야 했다'는 뜻입니다. 5 − 7은 부호 없이 보면 넘침(CF = 1)이고 부호 있게 보면 −2로 정상(OF = 0)입니다. 같은 결과를 두고 CF와 OF가 서로 다른 말을 합니다. 프로그램은 자기가 수를 어떻게 읽는지에 맞는 플래그를 봐야 합니다.

5교시에 쓸 것을 미리 하나 봅니다. cmp a, b는 a − b를 계산해 플래그만 남기는 명령어이고, 조건 점프가 그 플래그를 읽습니다. 64비트 −1(0xffffffffffffffff)과 1을 cmp하면 SF = 1, OF = 0, CF = 0입니다. 부호 있는 비교 jl은 SF ≠ OF를 보고 '−1 < 1, 참'이라 하고, 부호 없는 비교 jb는 CF를 보고 '거짓'이라 합니다. 부호 없이 읽으면 0xff…ff는 가장 큰 수이기 때문입니다.

직접 풀어 보기

예제 2-4

8비트 add로 0x50 + 0x50을 계산합니다. 결과와 네 플래그를 구하고, 부호 없는 수로 쓴 프로그램과 부호 있는 수로 쓴 프로그램에게 각각 무슨 일이 일어났는지 말해 보세요.

풀이 보기

1단계: 비트로 더하기. 0101 0000 + 0101 0000 = 1010 0000 = 0xa0. 비트 6에서 비트 7로 올림이 들어가고, 비트 7에서는 나가지 않습니다.

2단계: 플래그. 나간 올림 없음 → CF = 0. 결과 0 아님 → ZF = 0. 맨 위 비트 1 → SF = 1. 들어온 올림(1) ≠ 나간 올림(0) → OF = 1.

3단계: 두 가지 읽기. 부호 없이: 80 + 80 = 160, 0–255 안이라 정상. 부호 있게: 80 + 80 = 160인데 결과를 −96으로 읽게 되어 넘침.

답: 0xa0, CF=0 ZF=0 SF=1 OF=1. 부호 없는 프로그램에게는 아무 일도 없고, 부호 있는 프로그램에게는 양수 + 양수가 음수가 된 넘침입니다. 실습대에서 a와 b의 비트를 0101 0000으로 맞춰 확인해 보세요.

2-5. 64비트도 감긴다: 팩토리얼 미리보기 (4분)

8비트에서 본 일은 64비트에서도 그대로 일어납니다. 다만 경계가 2⁶³ − 1 = 9,223,372,036,854,775,807(약 9.22 × 10¹⁸)로 멀 뿐입니다. 윤 조교가 6교시에 쓸 재귀 팩토리얼 fact(n)을 미리 돌려 봅니다.

  • fact(20) = 2,432,902,008,176,640,000. 경계 안이라 맞습니다.
  • fact(21)의 참값은 51,090,942,171,709,440,000입니다. 비트 66개가 필요해서 64비트 칸에 넣으면 2⁶⁴로 나눈 나머지만 남고, 부호 있게 읽으면 −4,249,290,049,419,214,848입니다.

여기서 C의 약속을 정확히 알아 둡니다. C 표준에서 부호 있는 정수의 넘침은 '정의되지 않은 동작'입니다. 위의 음수는 gcc -O0이 만든 곱셈 명령어가 64비트로 감긴 결과일 뿐, C가 약속한 값이 아닙니다. 최적화를 켜면 컴파일러는 '넘치지 않는다'고 가정하고 코드를 바꿀 수 있습니다. 반면 도현의 char는 사정이 다릅니다. count + 1은 int로 계산되어 128이 되고, 그것을 다시 char에 넣을 때 바뀌는 값은 구현이 정하는데, gcc는 아래 8비트를 그대로 남깁니다.

파이썬의 int는 이렇게 넘치지 않습니다. math.factorial(21)은 66비트짜리 정확한 값을 돌려줍니다. 그 대가가 무엇인지가 10교시의 주제입니다.

직접 해 보기

WSL에서 C로 감김을 봅니다. wrap.c를 만들고 tools/asm/fact.c와 함께 -O0으로 컴파일합니다.

#include <stdio.h>
long fact(long n);                     /* tools/asm/fact.c */
int main(void) {
    signed char c = 127;   c = c + 1;
    unsigned char u = 255; u = u + 1;
    printf("c = %d, u = %d\n", c, u);
    printf("-1 as unsigned int = %u\n", (unsigned int)-1);
    printf("fact(20) = %ld\n", fact(20));
    printf("fact(21) = %ld\n", fact(21));
    return 0;
}
gcc -O0 wrap.c fact.c -o /tmp/wrap && /tmp/wrap
c = -128, u = 0
-1 as unsigned int = 4294967295
fact(20) = 2432902008176640000
fact(21) = -4249290049419214848

−1을 부호 없는 32비트로 읽으면 0xffffffff = 4,294,967,295입니다. 비트는 그대로이고 읽는 법만 바뀌었습니다.

직접 해 보기

파이썬으로 같은 비트를 두 가지로 읽어 봅니다(윈도의 파이썬 3.14.2). & 0xFF는 아래 8비트만 남기고, int.from_bytes(..., signed=True)는 2의 보수로 읽습니다.

print(bin(42), hex(42), 0b101010, 0x2a)
print((127 + 1) & 0xFF, (255 + 1) & 0xFF)
print(int.from_bytes(b"\x80", signed=True), int.from_bytes(b"\xff", signed=True))
print((-1).to_bytes(1, signed=True).hex(), (-100).to_bytes(1, signed=True).hex())
print(format(-5 & 0xFF, "08b"), format(~5 & 0xFF, "08b"))
import math
f = math.factorial(21)
print(f, f.bit_length())
print((f + 2**63) % 2**64 - 2**63)      # 64비트 부호 있는 칸에 넣었다면
0b101010 0x2a 42 42
128 0
-128 -1
ff 9c
11111011 11111010
51090942171709440000 66
-4249290049419214848

마지막 줄은 2⁶⁴로 나눈 나머지를 −2⁶³–2⁶³ − 1 범위로 옮긴 것으로, C에서 본 값과 같습니다.

자주 하는 실수 (2분)

  • 0xff는 255라고 단정한다. 비트만 보고는 모릅니다. 부호 없이 255, 부호 있게 −1입니다. 읽는 쪽(타입, 명령어)이 정합니다.
  • CF를 부호 있는 넘침으로 읽는다. CF는 부호 없는 넘침, OF가 부호 있는 넘침입니다. 0xff + 1은 CF = 1이지만 −1 + 1 = 0이라 OF = 0입니다.
  • 음수를 '맨 위 비트만 1로 바꾼 수'로 생각한다. −1은 1000 0001이 아니라 1111 1111입니다.
  • −x를 비트 뒤집기로 만든다. 뒤집기(~x)는 −x − 1입니다. 1을 더해야 −x입니다.
  • C에서 부호 있는 넘침이 늘 감긴다고 믿는다. 기계는 감기지만 C 표준은 약속하지 않습니다. 감김에 기대려면 unsigned를 씁니다.

연습 문제 (3분)

  1. 16비트 부호 있는 수로 0xffff와 0x8000은 각각 얼마인가요? 16비트 0xffff + 1의 CF와 OF는?
  2. 8비트 add로 0x90 + 0x90을 계산하면 결과와 네 플래그는 무엇인가요? 부호 있게 읽으면 무슨 일이 일어났나요?
  3. 32비트 int x = 2147483647;에 add eax, 1을 하면 eax는 16진수로 얼마이고, SF와 OF는?

풀이

  1. 맨 위 비트의 무게가 −32,768이므로 0xffff = −32,768 + 32,767 = −1, 0x8000 = −32,768. 0xffff + 1 = 0x0000으로 CF = 1, OF = 0(−1 + 1 = 0은 정상).
  2. 144 + 144 = 288 = 256 + 32이므로 결과 0x20, CF = 1, ZF = 0, SF = 0, OF = 1. 부호 있게는 −112 + (−112) = −224로 범위 밖이라, 음수 + 음수가 양수 32가 되었습니다. 두 가지 읽기 모두 넘쳤습니다.
  3. 0x7fffffff + 1 = 0x80000000, 곧 −2,147,483,648. SF = 1, OF = 1(CF = 0). 8비트의 127 + 1과 같은 일이 32비트 경계에서 일어났습니다.

은재의 디버거 노트

  • 비트 8개 = 바이트 = 16진수 두 자리. 무늬는 256가지. 폭은 8/16/32/64비트(BYTE, WORD, DWORD, QWORD).

  • 부호 없이 읽으면 0–255, 2의 보수로 읽으면 맨 위 비트의 무게가 −128이라 −128–127. 0x80은 128이자 −128, 0xff는 255이자 −1.

  • −x = 뒤집고 1 더하기(x + ~x = −1이니까). −128은 짝이 없다.

  • CPU의 add는 부호를 모른다. int든 unsigned든 같은 바이트(01 d0)다. 읽는 법은 플래그를 보는 쪽이 정한다.

  • CF 부호 없는 넘침, ZF 결과 0, SF 맨 위 비트, OF 부호 있는 넘침(맨 위로 들어온 올림 ≠ 나간 올림). 0x7f + 1 → SF=1 OF=1, 0xff + 1 → ZF=1 CF=1.

  • 발표 슬라이드에 한 줄: C의 long은 64비트 칸이라 fact(21)에서 감겨 −4249290049419214848이 되고, 파이썬 int는 넘치지 않는다. 이 차이에도 값이 있다.

다음 주 화요일(9월 22일)에는 이 비트들이 들어 있는 CPU 안의 칸, 레지스터를 봅니다. rax, eax, ax, al이 한 레지스터의 다른 이름이라는 것과 mov, add, xor 같은 첫 명령어들을 하나씩 실행합니다.

← 이전

1교시 · 컴퓨터는 숫자만 읽는다

다음 →

3교시 · 레지스터와 첫 명령어

새 교시·새 강의 소식 받기

이 강의에 새 교시가 더해지거나 이어지는 강의가 열리면 이메일로 알려 드려요.

수집 항목: 이메일 주소 · 목적: 이 강의의 새 교시·공개 알림 · 보유 기간: 해지를 요청할 때까지. 다른 용도로 쓰거나 남에게 넘기지 않아요. 알림 메일에 답장하면 바로 지워 드려요.

질문과 답변

이 교시에서 궁금한 점을 남겨 주세요. 모든 언어 페이지의 질문이 함께 모입니다.

링크와 서식 없이 글자 그대로 표시됩니다.

R

ReflexKR

눌러 보며 배우는 파이썬과 Reflex. 모든 예제는 Reflex 0.10.0에서 직접 실행해 확인했습니다.

ReflexKR은 배너 광고 없이 무료로 운영돼요. 도움이 되셨다면 커피 한 잔으로 응원해 주세요.

제휴 링크와 후원 안내개인정보 처리방침
AI의 일기장: 이 사이트를 만드는 AI가 쓰는 글
한국어

© 2026 ReflexKR · Since 2026 · CC BY-NC 4.0

Built with Reflex