대뜸 64비트 어셈블리로 코딩하고 디스어셈블한 소스보면서 디버깅하면서 정신이 없었을것 같습니다. 이쯤해서 약간만 정리하고 넘어가겠습니다.
64비트의 특징은 사실 위키피디아에 너무나 잘 나와있습니다.
https://en.wikipedia.org/wiki/X86-64#AMD64
이전에 말씀드린대로 x86_64에 대해서 위키피디아를 찾아보면 몇가지 항목들이 나오는데 AMD64가 우리가 현재 쓰는 일반 피시의 아키텍쳐입니다.
위키피디아에서 중요한 내용만 가져와보면
- 인텔이 IA-64 (Itanium으로 알려진) 아키텍처를 한답시고 삽질할 때 AMD가 AMD64를 만들어서 산업표준이 되었다.
- 64비트 레지스터를 쓰고 64비트 주소를 쓸 수 있다. 32비트 주소를 쓰면 메인메모레를 4GB까지 쓸 수 있습니다. 약간의 기술을 더해서 더 큰 메모리를 쓸 수 있도록 하긴 했었지만요. 64비트가 되면 2의 64승까지 주소 지정을 할 수 있으니까 어마어마한 메모리를 쓸 수 있게 됩니다.
- 32비트 아키텍쳐는 eax, ebx, ecx, edx, ebp, esp, esi, edi 총 8개의 범용 레지스터가 있는데
- 64비트는 rax, rbx, rcx, rdx, rbp, rsp, rsi, rdi, r8, r9, r10, r11, r12, r13, r14, r15 총 16개의 레지스터를 가지도록 만들었다.
- XMM레지스터 갯수가 32비트는 8개, 64비트는 16개이다.
- 이론상 가능한 주소 범위가 2의 64승이지만 현재 나온 프로세서들은 가상주소로 48비트를 사용하고, 물리주소로 48비트를 사용한다. 이 말의 뜻은 실제 장착할 수 있는 메모리가 48비트, 256TB라는 뜻입니다. 그리고 운영체제 위에서 동작하는 프로그램이 가질 수 있는 주소의 범위도 256TB라는 것이구요. 256TB 이상의 메모리가 필요한 소프트웨어를 만들고싶으신 분은 몇년 더 기다리셔야겠네요.
- 32비트 아키텍쳐보다 보안에 필요한 기능이 추가되었다.
대략 이정도입니다. 그 아래 페이지 테이블과 Long mode라는게 나오는데 이건 다음에 알아보고, 지금은 Canonical address space라는걸 이해해야합니다. 아주 중요한 개념입니다.
64비트는 rip 레지스터가 64비트니까 당연히 64비트 주소를 쓸 수 있고, 주소 범위가 0부터 0xFFFFFFFFFFFFFFFF 가 될 수 있어야 합니다. F가 하도 많아서 몇개인지 잘 안보이실텐데 F가 16개입니다. 8개면 32비트, 4개면 16비트, 2개면 8비트입니다.
그런데 사실은 그렇게 큰 주소공간을 허용하지 않습니다. 왜냐면 64비트 주소 공간을 페이지 갯수로 따지면 한 페이지가 4KB라고 생각해보면 2^64 / 2^12 = 2^52 개의 페이지가 됩니다. 이걸 커버할 수 있는 페이지 테이블을 만드려니 페이지 테이블만 만드는데 들어가는 메모리가 너무 커지는 문제가 생깁니다.
페이지 테이블을 아직 모르신다면 이렇게 생각하시면 됩니다. 간단하게 메모리를 관리하기 위한 데이터 구조의 배열을 생각해 보겠습니다. 운영체제는 현재 시스템에 얼마만큼의 메모리가 있고 각 페이지 단위로 어떤 페이지가 사용중이고 어떤 페이지는 쓸 수 있는 상태인지 등등 그런 상태 정보를 가지고 있어야 합니다. 그래야 페이지 단위로 할당해줄 수 있겠지요. 만약 64비트 주소 공간을 모두 쓴다고하고 데이터 구조의 크기가 20바이트라고 하면 전체 메모리를 관리하기 위해서 20바이트 X 2^52 만큼의 메모리가 필요합니다.
메모리를 관리하기 위한 메모리만 많이 잡아먹는게 아니라, 워낙 방대한 주소가 사용될 수 있으니까 캐시에 주소를 기억하는 공간도 커져야하고 하드웨어적인 문제 소프트웨어적인 문제 복잡해집니다. 그리고 아무리 생각해봐도 당분간은 그렇게 큰 주소공간을 필요로하지도 않습니다. 아무리 슈퍼컴퓨터라고해도 하나의 머신이 256TB의 메모리를 가지고 있다는 이야기는 못들었습니다. 있다고해도 일반적인 용도는 아니겠지요.
그래서 결국은 64비트 전체를 다 쓰는게 아니라 위에서 말씀드린대로 어느정도 타협을 해서 이만하면 넉넉하다라고 결정한게 48비트입니다. 주소 지정은 48비트만 하고 그 위 비트들은 47번 비트를 그대로 복사되도록 하드웨어적으로 만들어놨습니다. 말로는 잘 이해가 안되실텐데 이렇게 테이블을 만들어보겠습니다.
0000 0000 0000 0000 0000 0000 0000 0000 0000 0000 0000 0000 0000 0000 0000 0000
이게 64비트입니다. 0이 64개입니다.
0000 0000 0000 0000 0000 0000 0000 0000 0000 0000 0000 0000 0000 0000 0000 0001
이제 주소값이 1번입니다.
0000 0000 0000 0000 0000 0000 0000 0000 0000 0000 0000 0000 0000 0000 0000 0010
0000 0000 0000 0000 0000 0000 0000 0000 0000 0000 0000 0000 0000 0000 0000 0011
......
계속 주소 값이 증가됩니다.
0000 0000 0000 0000 0111 1111 1111 1111 1111 1111 1111 1111 1111 1111 1111 1111
이렇게되면 0비트부터 46번 비트까지가 1인, 0x0000 7FFF FFFF FFFF 값이 됩니다. 여기서 주소를 1만 더 올리면
1111 1111 1111 1111 1000 0000 0000 0000 0000 0000 0000 0000 0000 0000 0000 0000
이 됩니다. 갑자기 47번 비트가 1이 되는 순간에 그 위 63번 비트까지 1로 바뀌었습니다. 그런 주소 표기를 Canonical address space라고 부릅니다. 현재 이런 주소 표기를 표준으로 약속했다는 의미입니다. 그냥 64비트 전체를 주소로 썼으면 표준이니 약속이니 할 필요가 없습니다. 뭔가 이상한 제약이 있으니 그걸 따라야되고 그러니 그걸 표준이라는 이름으로 정해서 있어보이게 만든 것이지요.
1111 1111 1111 1111 1000 0000 0000 0000 0000 0000 0000 0000 0000 0000 0000 0001
이제 다시 1부터 증가됩니다. 전체 비트가 다 1이 될때까지 계속 증가됩니다. 현재 리눅스등의 운영체제들은 이런 특징을 이용해서 0 ~ 0x0000 7FFF FFFF FFFF를 유저 영역, 0xFFFF 8000 0000 0000 ~ 0xFFFF FFFF FFFF FFFF를 커널 영역으로 정해서 사용하고 있습니다. 만약 프로그램이나 커널 드라이버가 버그로 그 중간영역을 접근하면 문제가 발생했다는 것을 바로 알아차릴 수 있게 되니까 편리해집니다. 32비트에서는 0xc000 0000 이후를 커널 영역으로 지정했었는데 커널의 주소 공간이 1G밖에 안되서 이걸 늘리기위해 HIGHMEM, FIXED MAPPING이니 하는 이상한 기법들이 등장했었습니다. 64비트가 되면 그런 제약이 없어졌습니다. 지금은 디렉토리 이름이 x86으로 통일되었지만 예전에 디렉토리 이름이 AMD64로 나왔을 때 커널의 주소 관리 코드가 얼마나 간단해졌는지 보고 너무나 기뻤었습니다.
제가 이 Canonical address space를 설명하는 이유는 만약 어플리케이션 프로그램을 짜고 있는데 어떤 주소값이 0x7FFF FFFF FFFF보다 크다면 무조건 잘못된 코드라는 것을 말씀드리기 위해서입니다. 커널 드라이버를 짜는데 주소값이 0xFFFF 8000 0000 0000보다 작다면 뭔가가 잘못된 것입니다.
어플리케이션은 0x0 ~ 0x7FFF FFFF FFFF 영역내에서 또 STACK, BSS, DATA, TEXT 등으로 영역을 나눠서 각각의 주소 범위가 있습니다. 그 실제 값은 운영체제마다 다릅니다. 만약 문제가 생겼다면 주소값들이 해당하는 영역에 있지 않는지를 확인해야합니다. rsp 값이 스택영역에 있는게 맞나, rip 값이 텍스트 영역에 있는게 맞나를 gdb로 확인해야한다는 것이지요.