5 워드와 주소 — C의 원형
먼저 알아야 할 것
돌아보기
4장에서 바이트 하나는 256가지밖에 구별하지 못하며, 큰 수는 바이트를 여러 개 이어 붙여 담는다고 했다. 그러면 기계는 이어 붙인 바이트들이 “한 덩어리”라는 것을 어떻게 아는가?
답. 모른다 — 이것이 이 장의 출발점이다. 메모리 자체는 어느 칸이 어느 칸과 한 덩어리인지 전혀 기록하지 않는다. 덩어리를 아는 것은 메모리가 아니라 읽는 쪽이다. 프로그램이 “여기서부터 네 칸을 하나의 수로 읽겠다”고 정할 뿐이다. 4장의 오개념 — 기억된 것 자체와 읽는 방법의 분리 — 이 여기서 첫 실전을 치른다.
이 장의 필요성과 맥락
이 장이 끝나면
이 장에서 답할 질문
- 복도의 길이, 그러니까 사물함의 개수는 얼마나 되는가?
- 그러면 64비트 기계에서는 모든 것이 8바이트 단위로 저장되는가?
- 사람 눈에는 빅 엔디안이 자연스러운데, 왜 대부분의 기계는 “뒤집힌” 리틀 엔디안을 택했는가? 무슨 장점이 있는가?
- 내 컴퓨터가 리틀 엔디안이라는 것을 프로그램으로 확인할 수 있는가?
- 그러면 지금까지 배운 사물함 그림은 틀린 것인가?
5.1 사물함 복도
메모리를 그림으로 그리면 이렇다. 긴 복도에 같은 크기의 사물함이 한 줄로 끝없이 이어져 있고, 사물함마다 번호가 붙어 있다. 사물함 한 칸의 크기는 1바이트다. 번호는 0부터 시작해 1씩 늘어난다. 이 번호가 주소(address)다.
01001000 | 01101001 | 00100001 | 00000000 | 11111111 |
100 | 101 | 102 | 103 | 104 |
그림 5.1
칸 안에 든 것은 언제나 그저 비트 여덟 개다. 그 여덟 비트가 수인지, 글자인지, 더 큰 무엇의 조각인지는 칸 자신도 복도도 모른다 — 읽는 쪽의 해석이 정한다.
주소에 관해 첫눈에 안 보이는 중요한 사실이 하나 있다. 주소는 값으로 다룰 수 있다. 사물함 번호는 적어 둘 수 있는 것이고, 적어 둘 수 있으니 다른 사물함에 넣어 둘 수도 있다. “347번 칸에, 512번이라는 번호를 적어 둔다” — 이상할 것이 하나도 없다. 이 평범한 착상이 나중에 C에서 가장 유명한 개념인 포인터가 된다(35장).
여기서 층을 하나 갈라 두자. 지금 그리는 단순 모형에서는 주소를 번호, 곧 정수처럼 그려도 좋다 — 복도와 사물함 그림이 그렇게 그려져 있다. 그러나 C 라는 언어에서 포인터 값은 정수 타입이 아니다. 복사하고 비교할 수 있는 값이되, 어떤 타입을 가리키는지와 어디에서 나왔는지가 따라붙는 별도의 종류다. 이 장 뒤쪽(「이 번호는 진짜 주소인가」)에서 그 이유를 보고, 35·37장에서 정식으로 다룬다. 지금 챙길 것은 한 줄이다 — 주소는 값이고, 그 값을 담는 전용 타입이 따로 있다.
문. 복도의 길이, 그러니까 사물함의 개수는 얼마나 되는가?
답. 주소를 몇 비트로 적느냐가 정한다. 주소가 비트 수라면 사물함은 최대 개까지 구별할 수 있다(4장의 이 바로 다시 나왔다). 주소를 32비트로 적던 시절의 복도는 최대 약 43억 칸 — 4GiB — 이었고, 오늘의 64비트 주소는 사실상 다 쓸 수 없을 만큼 긴 복도를 만든다. “32비트 시스템에서 메모리 4GB 한계” 같은 말의 정체가 이것이다.
5.2 워드 — 기계의 자연스러운 한 움큼
사물함은 한 칸씩 있지만, 기계가 일할 때 한 칸씩만 집으면 너무 느리다. 그래서 CPU는 여러 칸을 한 움큼에 집도록 만들어져 있다. 그 움큼의 크기 — 기계가 가장 자연스럽게, 한 번에 다루는 비트 수 — 를 워드(word)라 부른다.
“64비트 컴퓨터”라는 말이 대개 이것을 가리킨다 — 일반 목적 레지스터와 산술의 대표 폭이 64비트, 즉 8바이트라는 뜻이다. 워드는 기계의 “손 크기”라 해도 좋다 — 손이 큰 기계는 한 번에 더 큰 수를 집는다.
다만 이 말은 느슨한 말이라는 것을 함께 알아 두자. 한 CPU 안에도 폭이 다른 레지스터와 통로가 함께 있고(부동소수점·벡터 레지스터는 128·256·512 비트가 흔하다), 주소 폭과 레지스터 폭이 다를 수 있으며, 언어가 쓰는 long이나 포인터의 폭은 또 다른 선택이다(35장에서 그 사례를 본다). 게다가 어떤 ISA 는 “워드”라는 낱말 자체를 다르게 쓴다 — x86 문서에서 word 는 전통적으로 16비트이고, 32비트는 doubleword, 64비트는 quadword다.
그러니 “64비트 기계”는 정확한 정의라기보다 대체로 그 폭으로 일한다는 관용적 표현으로 받아들이는 편이 옳다. 이 책에서 워드라고 쓸 때는 “그 기계가 한 움큼에 다루기 편한 폭”이라는 느슨한 뜻이다.
문. 그러면 64비트 기계에서는 모든 것이 8바이트 단위로 저장되는가?
답. 아니다. 저장은 여전히 바이트 단위로 자유롭다 — 1바이트짜리 글자도, 4바이트짜리 수도 같은 복도에 산다. 워드는 “기계가 가장 편하게 집는 크기”이지 “모든 것의 크기”가 아니다. 실제로 C에는 크기가 다른 여러 수 타입이 있고(25장), 어떤 크기의 데이터를 어디에 두는 것이 편한가라는 문제는 6장(정렬)에서 다시 만난다.
5.3 엔디안 — 여러 칸에 수를 넣는 두 가지 순서
이제 이 장의 하이라이트다. 4바이트짜리 수 하나를 사물함 네 칸에 넣어야 한다. 수를 16진법으로 12 34 56 78이라는 네 바이트 조각으로 나눴다고 하자(16진법 두 자리가 1바이트다). 100번 칸부터 넣는다면 — 어느 조각을 100번 칸에 넣어야 하는가?
두 학파가 있다. 빅 엔디안(big-endian)은 큰 자리부터 넣는다. 사람이 종이에 수를 적는 순서 그대로다.
12 | 34 | 56 | 78 |
100 | 101 | 102 | 103 |
그림 5.2
리틀 엔디안(little-endian)은 작은 자리부터 넣는다. 뒤집힌 것처럼 보이지만, “번째 칸에 번째로 작은 자리가 있다”는 나름의 정연한 규칙이다.
78 | 56 | 34 | 12 |
100 | 101 | 102 | 103 |
그림 5.3
지금 이 글을 읽는 컴퓨터와 스마트폰은 거의 확실히 리틀 엔디안이다(인텔· AMD·대부분의 ARM 운용). 날짜 표기와 똑같은 상황이다 — 2026-08-04라고 적는 나라와 04-08-2026이라고 적는 나라가 있고, 어느 쪽도 틀리지 않았지만, 서로의 편지를 그대로 읽으면 사고가 난다.
문. 사람 눈에는 빅 엔디안이 자연스러운데, 왜 대부분의 기계는 “뒤집힌” 리틀 엔디안을 택했는가? 무슨 장점이 있는가?
답. 두 가지 실속 때문이다.
첫째, 시작 칸이 흔들리지 않는다. 리틀 엔디안에서는 수의 가장 작은 자리가 언제나 시작 주소 그 칸에 있다. 위 그림의 수를 “4바이트 전부”로 읽든 “아래 2바이트만”·“아래 1바이트만”으로 줄여 읽든, 읽기 시작하는 칸은 똑같이 100번이다. 빅 엔디안에서는 읽는 폭을 바꿀 때마다 시작 칸을 옮겨 다시 계산해야 한다. 같은 값을 크기가 다른 눈으로 읽는 일이 잦은 기계에게 “시작 주소 불변”은 꽤 큰 단순함이다.
둘째, 계산의 진행 방향과 맞는다. 손으로 덧셈을 할 때를 떠올리면 된다 — 일의 자리부터 더하고 받아올림을 위로 넘긴다. 여러 바이트짜리 수를 더하는 기계도 마찬가지로 작은 자리부터 처리하는데, 리틀 엔디안 에서는 그것이 “낮은 주소부터 순서대로”와 정확히 일치한다. 초기의 작은 CPU들은 첫 바이트를 가져오자마자 덧셈을 시작할 수 있었고, 이 이점이 초창기 설계(인텔 계열의 조상들)에 스며서 오늘까지 이어졌다.
빅 엔디안의 장점도 공정하게 적으면 — 덤프가 사람 눈에 그대로 읽히고, 바이트를 앞에서부터 통째로 비교하면 수의 대소 비교와 일치한다. 그래서 통신 규약처럼 “사람과 기계가 함께 들여다보는 자리”에는 빅 엔디안이 남았다. 어느 쪽도 우월하지 않다 — 자주 하는 일이 다를 뿐이다.
흔한 오해. “메모리를 앞에서부터 읽으면 적힌 순서대로 수가 보인다”
그럴듯하다 — 종이에 적힌 수는 그렇게 읽으니까. 그러나 리틀 엔디안 기계에서12 34 56 78이라는 수는 메모리에 78 56 34 12 순서로 놓인다. 메모리를 칸 순서대로 들여다보면(디버거나 파일 덤프에서 실제로 이렇게 본다) 수가 “뒤집혀” 보인다. 뒤집힌 것이 아니라, 넣는 순서의 약속이 다를 뿐이다. 기억할 것: 메모리 덤프는 종이에 적은 수가 아니다.실제 사례. NUXI — 순서가 낳은 유령
초기 Unix를 다른 회사의 컴퓨터로 이식하던 엔지니어들이 화면에서 기묘한 것을 보았다.UNIX라고 찍혀야 할 곳에 NUXI가 찍힌 것이다. 두 기계의 엔디안이 달라서, 2바이트 묶음 안의 글자 순서가 통째로 뒤집힌 결과였다. 이 사건은 “NUXI 문제”라는 이름으로 엔디안 사고의 대명사가 됐다. 같은 종류의 사고는 지금도 난다 — 파일을 한 기계에서 저장해 다른 기계에서 읽을 때, 네트워크로 수를 주고받을 때, 엔디안을 정하지 않으면 수가 깨진다. 그래서 인터넷 규약은 “통신할 때는 빅 엔디안으로 보낸다”는 네트워크 바이트 순서를 합의해 두었다.문. 내 컴퓨터가 리틀 엔디안이라는 것을 프로그램으로 확인할 수 있는가?
답. 있다 — 여러 바이트짜리 수를 메모리에 넣고, 그 첫 칸을 1바이트만 따로 읽어 보면 된다. 첫 칸에 작은 자리 조각이 있으면 리틀 엔디안이다. 이 확인을 C로 실제로 해 보이는 것이 48장(공용체)의 시연이다 — 같은 기억을 다른 눈으로 읽는 도구가 그때 생긴다.
5.4 이 번호는 진짜 주소인가
여기서 한 가지를 미리 흔들어 두어야 한다. 지금까지 그린 그림 — 사물함마다 번호가 하나씩 붙어 있고, 그 번호가 곧 기억의 자리 — 은 배우기에 좋은 그림이지 오늘의 컴퓨터를 정확히 그린 것은 아니다.
오늘의 프로그램이 보는 번호는 대개 가상 주소다. 운영체제와 CPU 안의 번역 장치(MMU)가 프로그램마다 따로 만들어 준 번호이고, 그 번호가 실제 DRAM 의 몇 번 칸인지는 프로그램이 알지 못한다. 같은 번호를 두 프로그램이 동시에 써도 서로 다른 물리 칸에 닿는다. 이 번역의 구조는 11장에서 제대로 본다. 지금 필요한 것은 한 문장이다 — 번호는 실체가 아니라 약속이다.
번호가 하나가 아니었던 시절도 있다. 8086 의 세그먼트 방식에서는 주소가 “세그먼트:오프셋” 두 조각이었고, 같은 물리 칸을 가리키는 표기가 여러 개 있었다(0x0000:0x0010과 0x0001:0x0000이 같은 자리다). 어떤 기계는 프로그램용 기억과 자료용 기억이 아예 다른 주소 공간에 있었다(하버드 구조 — 97장의 작은 칩들이 아직 그렇다).
그리고 번호 안에 번호가 아닌 것이 섞여 있기도 하다.
실제 사례. 주소값 안에 실려 다니는 것들
(이 사례들은 외울 필요가 없다. “주소가 늘 번호 하나인 것은 아니다”만 남기면 되고, 이름들은 나중에 그런 기계를 만났을 때 다시 찾아보면 된다.)
남는 끝자리. 정렬(6장) 덕분에 주소의 하위 몇 비트는 늘 0이다. 그 자리에 작은 표시를 끼워 넣는 재주가 널리 쓰인다 — 다음 장의 태그 포인터다.
남는 윗자리. 64비트 주소를 전부 쓰는 기계는 없다. x86-64 는 실제로 48비트(요즘은 57비트)만 쓰고 나머지 상위 비트는 부호 확장 규칙에 묶여 있다. AArch64 의 TBI 는 상위 한 바이트를 아예 무시하도록 정해 두어, 그 자리에 태그를 실을 수 있게 했다(메모리 태깅 MTE 가 이것을 쓴다).
수와 포인터를 한 그릇에. 자바스크립트 엔진들은 실수(NaN)의 남는 비트 패턴 안에 포인터를 숨긴다(NaN 박싱). 하나의 64비트 값이 상황에 따라 수이기도 하고 주소이기도 하다.
주소가 아예 번호가 아닌 기계. CHERI 와 Arm Morello 에서 포인터는 주소와 함께 범위·권한을 담은 128비트 묶음(capability)이다. 하드웨어가 그 범위를 검사하므로, 정수로 바꿔 조작한 뒤 되돌린 값은 권한을 잃어 쓸 수 없다.
그래서 C 는 주소를 “그냥 정수”로 정의하지 않았다. 복사할 수 있고 비교할 수 있는 값이되, 어떤 타입을 가리키는지와 어디에서 나왔는지가 따라붙는 값으로 두었다. 이 결정이 왜 필요했는지는 35장에서 포인터를 배우고 37장에서 프로버넌스를 만나면 분명해진다.
문. 그러면 지금까지 배운 사물함 그림은 틀린 것인가?
답. 틀리지 않았다 — 한 겹 아래를 잠시 접어 둔 그림이다. 프로그램의 눈으로 보면 기억은 여전히 번호가 붙은 칸의 열이고, 이웃한 번호는 이웃한 칸이며, 그 위에서 C 의 배열과 포인터 산술이 정확히 동작한다. 가상 주소든 물리 주소든 그 성질은 유지되도록 만들어져 있다.
접어 둔 것을 펴야 하는 순간은 따로 온다 — 성능을 재고 캐시를 따질 때(11장), 프로그램이 왜 서로를 못 건드리는지 물을 때(3장·11장), 그리고 “주소를 정수로 바꿔 마음대로 만져도 되는가”를 물을 때(37장)다.
5.5 C의 원형이 여기 있다
이 장의 복도 그림은 C라는 언어의 설계도이기도 하다. C가 태어난 시절 (4장), 언어를 설계한 사람들은 기계의 이 모습을 감추는 대신 언어의 개념으로 그대로 들어 올렸다. 메모리는 바이트의 열이라는 것 — C의 모든 데이터는 바이트 단위 크기를 갖는다. 칸마다 주소가 있다는 것 — C에서는 거의 모든 것의 주소를 물을 수 있다. 주소를 값으로 다룬다는 것 — 그 값을 담는 별도의 타입(포인터)이 언어의 한복판에 있다(정수 타입이 아니다). 다른 많은 언어가 “사물함 복도”를 프로그래머에게 보이지 않게 봉인해 둔 것과 대조적이다.
이 정직함은 양날이다. 기계를 훤히 내다보며 일할 수 있는 힘이자, 복도에서 길을 잃으면 그대로 사고가 되는 위험이다. 이 책의 제7부가 그 힘과 위험을 정면으로 다룬다.
다음 장은 이 복도의 구석에 있는 흥미로운 특례들을 구경한다. 0번 사물함 에는 무엇이 있는가, 왜 두 칸짜리 짐은 아무 번호에나 못 넣는가(정렬), 그리고 번호의 끝자리가 항상 0이라는 사실을 이용해 몰래 정보를 숨기는 재주(태그 포인터)까지 — 주소의 세계가 생각보다 훨씬 사람 사는 동네 같다는 것을 보게 된다.