실 전! 강 의 실 개발자를위한실전 64 비트! 64 비트윈도우커널탐구 3 연 + 재 + 순 + 서 1회 2005. 9 64비트시대를향한첫걸음, 커널모드드라이버포팅 2회 2005. 10 64비트윈도우커널분석 AMD64 3회 2005. 12 64비트윈도우커널분석 IA64 연 + 재 + 가 + 이 + 드운영체제 64비트윈도우 XP, 64비트윈도우 2003 개발도구 윈도우 DDK 최신판기반지식 윈도우커널모드드라이버개발응용분야 응용프로그램및드라이버 64비트포팅 김성현 shkim@ahnlab.com 안철수연구소에서 V3, 스파이제로개발에참여하고있는커널모드드라이버개발자이다. 우리나라에서도평생소트프웨어엔지니어로살아가면서식구들을먹여살릴수있는날이올수있다는꿈을안고그길을닦아나가고있다. 올해의목표로 Code Complete 2nd Edition Windows Internals 4th Edition 읽기, 몸무게 10kg 증량하기, 당구 250 만들기, 외국식당에서영어로원하는거주문하기등을가지고있다. 개발자를위한실전 64비트! 64비트윈도우커널탐구 3 64비트윈도우커널분석 IA64 지난호에살펴본 AMD64용커널에이어 IA64용커널에대해알아보자. AMD64를이해하는것은크게어렵지않은일인반면에 IA64를이해하는것은쉽지않은작업이다. 다양한종류의 CPU에익숙하면그렇지않겠지만 x86에만익숙한사람들에게는새로운도전이될수있을것이다. 이번에다루게될내용은 IA64 CPU의레지스터셋과명령어의이해, IA64 모듈함수테이블소개, IA64용 NT 커널의서비스디스크립터테이블의동작방식분석, CPU에서가상메모리주소가물리메모리주소로변환되는과정인가상주소변환에대한것이다. 는레지스터셋이 x86이나 AMD64와확연히다르다고이미첫번째연재부터설 IA64 명했다. CPU를이해하는가장기본적인스텝이레지스터의이해인만큼어떻게다른지그림으로한번살펴보자. < 그림 1> 은 Intel Itanium Architecture Software Developer s Manual 에나온 IA64 레지스터셋이다 ( 참고자료 ). 일단 x86에서봤던 EAX, EBX, ESP 등의익숙한레지스터이름들이전혀보이지않는다. 당황스러울수있으나정신을가다듬고다시한번살펴보자. 이름들을자세히보면그의미들을약간은이해할수있을것같다. 각그림의왼쪽에아래로나열되어있는알파벳과숫자가붙은명칭들이레지스터의이름이다. 범용레지스터 (General Register) 는 gr0, gr1,, gr127이라는이름으로존재한다. 64비트레지스터가 128개존재한다는것을알수있다. 몇개만더설명하면부동소수점레지스터 (Floating-point Register) 는 fr0, fr1,, fr127이라는이름을가지고명령포인터 (Instruction Pointer) 레지스터는 IP라는이름을가진다. 나머지레지스터에대해서는필자도아직이해하지못하는부분이많기때문에설명을생략하니, 궁금한독자들은 IA64 매뉴얼을참고해서각자공부해보기바란다. 이번기사에서는주로범용레지스터가사용되는상황들에대한예만보일것이다. IA64 명령어구조 IA64의디스어셈블리코드를본적이있는가? < 리스트 1> 은 WinDbg에서보여주는 IA64의디스어셈블리코드이다. 참고로 IA64 레지스터셋중 gr 레지스터들은 WinDbg에서 r로표시되고 br 레지스터들은 b로표시된다. < 리스트 1> 의코드샘플에서보여주고있는명령어들은 adds, movl, nop.m, mov, br.cond.sptk. few이다. IA64 명령어역시처음보면다소당황스럽다. 하지만다행히도명령어들을가만히살펴보면 x86의 add, mov, nop, jmp 등과같은의미라고추측할수있다. 명령어다음에보이는 270 2 0 0 5. 1 2
64 비트윈도우커널분석 IA64 Operand들이약간생소해보이지만이것역시천천히살펴보면의미를대강추측할수있다. adds의경우 f( 상수 )+r0( 레지스터 ) 의결과를 ret0( 레지스터 ) 에대입하라는뜻으로이해할수있다. movl의경우 e0000000`ffa00000( 상수 ) 라는값을 r2( 레지스터 ) 에대입하라는뜻으로이해할수있다 ( 여기서 r0는레지스터셋에서봤던범용레지스터 gr0이고 r2는 gr2이다 ). br.cond.sptk.few은아주복잡해보이는데처음의 br만보면 branch의약자이므로 jmp를의미한다는것을알수있다. br 명령은뒤에붙는옵션들에따라서여러가지의미를가지는데방금보았던 br.cond는 jmp를, br.call은 call, br.ret는 ret 를의미한다. 그뒤의옵션들은동작방식에약간의차이를주기는하지만여기서는중요하지않으므로설명하지않는다. < 리스트 1> 과같은코드들이처음에는굉장히 생뚱맞은 코드로보이지만조금만익숙해지면더듬더듬읽을수있는상태가되므로처음부터너무주눅 들지않아도된다. 시스템프로그래밍을하다보면명령어의구조를이해하고작업해야할경우도있기때문에이번기회에 IA64 명령어의구조에대해좀더자세히알아보고넘어가자. IA64의명령어 (instruction) 구조는 < 그림 2> 와같다. < 리스트 1> IA64 디스어셈블리코드 e0000000`8402ecc0 adds ret0=f, r0 e0000000`8402ecc4 movl r2=e0000000`ffa00000 ;; e0000000`8402ecd0 nop.m 0 e0000000`8402ecd4 mov b6=r2, +0 e0000000`8402ecd8 br.cond.sptk.few b6 ;; < 그림 1> IA64 레지스터셋 gr0 gr1 gr2 Application Register Set General Registers Floating-point Registers Predicates Branch Registers 63 0 nats 81 0 63 0 0 fr0 +0.0 pr0 1 fr1 +1.0 pr1 fr2 pr2 br0 br1 br2 0 application registers 63 0 ar0 KR0 ar7 KR7 gr16 gr31 gr32 gr127 fr31 fr32 fr127 pr15 pr16 pr63 br7 Instruction Pointer 63 0 IP Current Frame Maker 37 0 CFM User Mask 5 0 ar16 ar17 ar18 ar19 ar21 ar24 ar25 ar26 ar27 ar28 ar29 ar30 RSC BSP BSPSTORE RNAT FCR EFLAG CSD SSD CFLG FSR FIR FDR ar32 CCV ar36 UNAT Advanced Load Adress Table cpuid0 cpuid1 Processor Identifiers 63 0 Perfomance Monitor Data Registers 63 0 pmd0 pmd1 ar40 ar44 ar64 ar65 FPSR ITC PFS LC cpuid n pmd m ar66 EC ar127 마이크로소프트웨어 271
실 전! 강 의 실 개발자를위한실전 64 비트! 64 비트윈도우커널탐구 3 세개의명령어가 128비트 (16바이트) 의번들형태로구성된다. 번들은 41비트명령어슬롯 (instruction slot) 3개와 5비트템플릿으로구성된다. x86에서는 Opcode, Operand의길이가명령어마다제각기다르지만 IA64는 RISC 구조를따르기때문에명령어들이고정된크기를가진다. 대부분의명령어들이 41비트의크기를가지므로명령어슬롯에하나씩들어가고번들은 3개의명령어를포함하는단위가된다. 어떤명령어는명령어슬롯을 2개차지하는경우도있다. < 리스트 1> 을보면 16바이트단위로 2줄또는 3줄로명령어들을보여주고있다. 번들이 3개의명령어슬롯을가지므로대부분 WinDbg는 16 바이트마다 3줄로디스어셈블리한내용을보여준다. 두줄로보이는부분은 movl 같이 2개의슬롯을차지하는특별한명령어인경우에만나타난다. 번들이라는구조때문에오해하기쉬운것이 WinDbg가명령어의주소를보여주는부분인데 < 리스트 1> 을보면코드를읽는사람은 e0000000`8402ecc4 위치에 movl 명령어가존재한다고생각할수있다. 하지만엄밀히따지자면 movl은 16바이트번들구조에서 41비트라는애매한경계에걸쳐서존재하기때문에 e0000000`8402ecc4에서시작하지않는다. WinDbg는명령어의주소표시를비트단위로할수없으므로그냥세줄을 0, 4, 8 단위로의미없이나누어보여주고있는것뿐이다. 16바이트마다번들이새로시작한다는것만정확한의미를갖는다. IA64 명령어구조때문에나타나는또하나의부작용은, 같은소스코드라도 IA64용으로컴파일한바이너리는파일크기가 x86이나 AMD64용에비해 2~3배정도증가한다는점이다. RISC 기반이기 < 그림 2> IA64 명령어구조 127 87 86 46 45 5 4 0 instruction slot 2 instruction slot 1 instruction slot 0 template 41 41 41 5 < 그림 3> adds 명령어구조 때문에파일크기가증가하는몇가지이유는있겠지만이상하게도 IA64용코드를분석하다보면명령어들사이에 nop가쓸데없이많이들어가있는것을발견할수있다. 물론컴파일러가알아서잘집어넣은것이라고십분이해해줘도코드공간의낭비가심한게사실이고결과적으로컴파일된파일크기는 x86에비해평균 3배정도로커진다. 나중에라도 IA64용바이너리를빌드한후에놀라지말기바란다. adds 명령어구조슬롯에들어가는각명령어의구조는명령어마다다른데 adds 명령어를예로살펴보면 < 그림 3> 과같다. 명령어는 41비트로크기가제한되어있으므로 41비트를알뜰하게쪼개서 Opcode, Operand를모두표현한다. 41비트가쪼개지는형태는명령어마다다르다. adds 명령어의경우최상위 4비트에 8이라는 Opcode가존재한다. Operand는 r1 = imm14, r3으로표현되는데그림만가지고설명하면다소혼란스러울수있으므로 < 리스트 1> 의첫번째명령어를보면서설명하기로한다. 다음과같이보이는디스어셈블리코드와 < 그림 3> 을일치시켜보자. e0000000`8402ecc0 adds ret0=f, r0 40 37 36 35 34 33 32 27 26 20 19 13 12 6 5 0 A4 8 5 X 2a V e imm 6d r 3 imm 7b r 1 qp < 그림 3> 에서보이는 Operands r1 = imm14, r3과 ret0 = f, r0이일치해야하므로 6~12비트영역인 r1에는 ret0 레지스터를나타내는값이들어있고 20~16비트영역인 r3에는 r0 레지스터를나타내는값이들어있다. 다음에는 imm14를찾아야하는데 imm14가명령어구조그림에서보이지않는다. imm14는 14 비트가연속적으로구성되지않고 1비트, 6비트, 7비트로나누어져있기때문이다. 이제그림을다시보면나누어진비트가보일것이다. 결국 imm14가명령어는 s, imm6d, imm7b로쪼개져서명령어구조상에존재하는것이다. 이제부터어떤값들이들어있길래 adds ret0=f, r0와같은명령어로해석되었는지실제코드영역메모리에있는데이터를보면서확인해보자. 4 1 2 1 6 7 7 7 6 kd> dq e0000000`8402ecc0 e0000000`8402ecc0 00ffa100`003c4005 intsruction operands opcode x 2a extension v e 68001000`40600000 adds addp4 r 1 =imm 14, r 3 8 2 3 0 이제우리는이데이터를보면서이 것은 16 바이트짜리번들이라는것을 272 2 0 0 5. 1 2
64 비트윈도우커널분석 IA64 이해할수있다. < 리스트 1> 을보면이번들에는 adds, movl이들어있다는것을확인할수있다. 우리의관심사는첫번째명령어인 adds 이므로명령어슬롯 0을확인해봐야한다. 앞의 128비트데이터중에서 5~45비트의데이터만취해서비트로표시하면다음과같다. MSB 1000 0100 000000 0000000 0001111 0001000 000000 LSB op Imm6d r3 imm7b r1 qp 여기서 6~12비트인 r1은 0001000으로 8이다. 8은범용레지스터 8을의미하는것이므로 r8 레지스터를가리킨다. 왜 ret0이아니라 r8 일까? < 그림 1> 에서눈을씻고찾아봐도 ret0이라는레지스터는존재하지않는다. ret0은 r8 이맵핑된다른이름일뿐이다. IA64에서는범용레지스터중몇개를특수한목적으로사용하면서특별한이름으로맵핑해놓았다. 어셈블리코드에서보이는 ret0~ret3 레지스터는실제로는 r8~r11 레지스터이다. < 표 1> 에서몇가지맵핑된레지스터를예로들고있다. 20~26비트는 0000000으로 0이다. 0은범용레지스터 0을의미하는것이므로 r0 레지스터를가리킨다. imm14를구하기위해서 36비 트, 27~32비트, 13~19비트를살펴보면 0, 000000, 0001111이다. 14비트값으로계산하면 00000000001111로 f이다. 따라서명령어슬롯의데이터를분석하면정확히 adds ret0=f, r0으로해석되는것을확인할수있다. 이해를돕기위해서명령어메모리를해석하여 imm14에해당하는값을구하는과정을코드로쓰면다음과같다. ULONG GetImm14FromAdds(ULONG_PTR pfunction) { ULONGLONG bundle; ULONGLONG slot0; ULONG_PTR imm14; // 번들의하위 64비트 => 00ffa100`003c4005 bundle = *(ULONGLONG*)pFunction; < 표 1> 범용레지스터의맵핑 맵핑된이름 원래이름 용도 ret0 ~ ret3 r8 ~ r11 정수리턴값 rp b0 리턴포인터 gp r1 글로벌포인터 sp r12 스택포인터 [ IA64 디버그부트옵션설정 ] 보통 WinDbg 로커널디버깅을하려면디버그타겟 ( 디버기 ) 의 C:\boot.ini를수정하여디버그부트옵션을설정하는데 IA64 시스템의 C:\ 에는 boot.ini가없다. 그렇다면디버그부트옵션을어떻게설정할까? OS가제공하는 bootcfg.exe라는툴이그해답이다. bootcfg.exe를이용해서디버그부트옵션을설정하는방법은다음과같다. 명령창에서 bootcfg.exe 실행 C:\>bootcfg Boot Options ------------ Timeout: 30 Defulat: \Device\HarddiskVolume3\Windows CurrentBootEntryID: 1 Boot Entries ------------ Boot entry ID: 1 Os Friendly Name: Windows Server 2003, Enterprise OsLoadOptions: /noexecute=optout Boot entry ID: 3 OS Friendly Name: CD/DVD ROM/Pci(1F 1)/Ata(Primary,Master)/CDROM(Entry0) 현재사용중인윈도우 OS의 Boot entry ID를확인 ( 여기서는 1) bootcfg.exe로디버그부트옵션설정 C:\>bootcfg /Debug ON /PORT COM2 /BAUD 115200 /ID 1 SUCCESS: The OS load options have been changed for the BootID: 1. bootcfg 명령으로확인 C:\>bootcfg Boot Entries ------------ Boot entry ID: 1 Os Friendly Name: Windows Server 2003, Enterprise OsLoadOptions: /noexecute=optout /debug /debugport=com2 /baudrate=115200 Boot entry ID: 2 OS Friendly Name: Red Hat Linux Advanced Server OsLoadOptions 가변경되어있는것을확인할수있다. 리부팅하면적용된다. 마이크로소프트웨어 273
실 전! 강 의 실 개발자를위한실전 64 비트! 64 비트윈도우커널탐구 3 // 템플릿 5비트를제거하고 41비트를취한다. slot0 = (bundle >> 5) & 0x1FFFFFFFFFF; // adds 명령포맷에서 imm7b 7bit, imm6d 6bit, s 1bit로 imm14를만든다. imm14 = ((slot0 >> 13) & 0x7F) (((slot0 >> 27) & 0x3F) << 7) (((slot0 >> 36) & 0x1) << 13); return (ULONG)imm14; } 다른명령어들도이내용을참고하면어렵지않게분석할수있다. 여러가지명령어들의구조는 IA64 매뉴얼을참고하여확인해보기바란다. 이다. < 리스트 2> 는 MyDrv.sys가 NT 커널의 RtlZeroMemory 함수를호출했을때의예를코드로나타내고있다. 코드가자연스럽게분석되는가? 이코드를지금자세히설명하려면복잡하므로그림으로먼저설명하고자세히분석하도록한다. 지금은이코드가자신의임포트테이블 (Import Table) 을참조하여 NT 커널의 RtlZeroMemory 함수로점프하는코드라고만생각하자. 여기에서 x86이나 AMD64에는없는어떤테이블이등장하는데이기사에서는이것을 IA64 모듈함수테이블 이라고정의한다 ( 이정의는공식적인용어는아니라는점을밝혀둔다 ). 이테이블의명칭을알기위해조사해보았지만아직정확한명칭을찾지못해서필자나름대로정의한용어이다. < 그림 4> 를단계별로나누어서설명하면다음과같다. IA64 모듈함수테이블어떤모듈에서다른모듈의익스포트함수를호출할때 x86이나 AMD64에서는볼수없었던재미있는개념이 IA64에있다. 이것은유저모드모듈이나커널모드모듈이나모두동일하게적용되는내용 < 그림 4> 모듈간함수호출흐름 MyDrv.sys Call RtlZeroMemory < 리스트 2> MyDrv.sys 가 NT 커널함수를호출하는코드 e0000165`de3d38c0 addl r2=ffffffff`ffe020b8, gp ;; e0000165`de3d38c4 ld8 r2=[r2] e0000165`de3d38c8 nop.i 0 ;; e0000165`de3d38d0 ld8 r3=[r2], 8 ;; e0000165`de3d38d4 ld8 gp=[r2] e0000165`de3d38d8 mov b6=r3, +0 e0000165`de3d38e0 nop.m 0 e0000165`de3d38e4 nop.i 0 e0000165`de3d38e8 br.cond.sptk.few b6 NT 커널 IA64 모듈함수테이블 xxxxxxxx85006400 f000000045e80000 xxxxxxxx85056900 f000000045e80000 xxxxxxxx85088640 f000000045e80000 xxxxxxxx85088280 f000000045e80000 실제함수위치 xxxxxxxx85006400:. xxxxxxxx85056900 MyDrv.sys에서 RtlZeroMemory 호출 MyDrv.sys에서 RtlZeroMemory를호출하는내부코드는 NT 커널의 IA64 모듈 함수테이블을참조 MyDrv.sys에서 NT 커널 IA64 모듈함수테이블의함수포인터참조 NT 커널의함수포인터로점프 x86이나 AMD64였다면 1번에서바로 4번으로진행되었을것이다. 하지만여기서는중간에 IA64 모듈함수테이블을참조하는과정이 들어간다. IA64에서는이와같이어떤모듈이다른모듈의함수를호 출할때반드시 IA64 모듈함수테이블을거쳐야한다. 코드를작성하 는프로그래머입장에서는 RtlZeroMemory의함수주소를직접호출 했다고생각하지만내부의코드는 NT 커널에서 IA64 모듈함수테이 블을참조해서실제 RtlZeroMemory의함수주소를얻고나서야함 수가호출되는것이다. 이미예상했겠지만 IA64 모듈함수테이블에 는그모듈이제공하는모든익스포트함수들에대한함수주소들이 들어있다. 도대체왜이런테이블이존재하는것일까? 전체흐름을 생각하면서다시 < 리스트 2> 의코드를차근차근분석해보자. e0000165`de3d38c0 addl r2=ffffffff`ffe020b8, gp ;; // Step 1 e0000165`de3d38c4 ld8 r2=[r2] // Step 2 e0000165`de3d38c8 nop.i 0 ;; e0000165`de3d38d0 ld8 r3=[r2], 8 ;; // Step 3 e0000165`de3d38d4 ld8 gp=[r2] // Step 4 e0000165`de3d38d8 mov b6=r3, +0 e0000165`de3d38e0 nop.m 0 e0000165`de3d38e4 nop.i 0 e0000165`de3d38e8 br.cond.sptk.few b6 // Step 5 274 2 0 0 5. 1 2
64 비트윈도우커널분석 IA64 step 1) addl은 ffffffff`ffe020b8와 gp 레지스터를더해서 r2 레지스터에저장한다. step 2) ld8 명령어는 8바이트로드명령어이므로 r2 레지스터에있는포인터를 8바이트읽어서 r2에저장한다. step 3) ld8 명령어마지막에보이는 8이로드후에 r2 에 8을더하라는의미이므로 r2 레지스터에있는포인터를 8바이트읽고 r2 레지스터값을 8 증가시킨다. step 4) 여기서또 8바이트를읽어서 gp 레지스터에저장한다. step 5) b6 에저장된포인터로점프한다. < 그림 4> 의과정을이해했으므로코드의내용을다음과같은의미로해석할수있다. step 1) MyDrv.sys는임포트테이블위치를계산한다. step 2) 임포트테이블을참조하여 nt!rtlzeromemroy 주소를구한다. 구해진 nt!rtlzero Memroy의주소는사실함수포인터가아니라 nt 커널모듈의 IA64 모듈함수테이블주소이다. step 3) nt!rtlzeromemroy 함수테이블을참조하여실제 nt!rtlzeromemory 함수포인터를구한다. step 4) nt!rtlzeromemroy 함수포인터다음에저장된포인터를 gp 레지스터에저장한다. step 5) nt!rtlzeromemroy 함수포인터로점프한다. 이제는 < 리스트 2> 를보면 < 그림 4> 가떠오르면서마지막으로해석한내용들이자연스럽게이해돼야한다. 그런데이렇게한단계씩그림의흐름과맞춰보면함수호출의흐름은대충알겠는데 Step 4는도대체무엇을하는것인지모르겠다. gp 레지스터에저장되는내용은뭘까? gp가궁금하다. 기때문에글로벌포인터값은각모듈마다서로다른고유의값을가지고있다는것이다. MyDrv.sys는 MyDrv.sys 고유의글로벌포인터값을가지고있고 NT 커널은 NT 커널고유의글로벌포인터값을가지고있다. 자신의글로벌포인터를정확히알고있어야자신의전역변수를참조할수있다. Step 1을예로살펴보면 MyDrv.sys 모듈내의임포트테이블을참조할때도 gp 레지스터 + 임포트테이블옵셋으로참조하는것을볼수있다. 이제 Step 4에서하는일이무엇인지알아보자. Step 4에서함수포인터다음에저장된포인터를 gp 레지스터에저장하는동작을볼수있다. gp를변경하고있는것이다. Step 4까지는 gp 레지스터에 MyDrv.sys의글로벌포인터가저장되어있었는데 NT 커널모듈로진입하기위해서 NT 커널모듈의글로벌포인터를구해서 gp 레지스터에저장하는것이다. 이렇게모듈과모듈사이에호출이나점프가일어나는경우는반드시대상모듈의글로벌포인터를알아내서 gp 레지스터에저장해주고대상모듈로진입해야만호출된함수가정상동작할수있다. 따라서대상모듈의글로벌포인터를쉽게얻을수있는방법이필요한데여기서알게된사실은 IA64 모듈함수테이블에는함수포인터다음에그함수가존재하는모듈의글로벌포인터가저장되어있다는것이다. Winnt.h를살펴보면 IA64만을위해정의해놓은구조체중에 PLABEL_DESCRIPTOR라는것을볼수있는데지금설명하고있는 IA64 모듈함수테이블은바로이 PLABEL_ DESCRIPTOR의배열로구성된테이블이다. typedef struct _PLABEL_DESCRIPTOR { ULONGLONG EntryPoint; ULONGLONG GlobalPointer; } PLABEL_DESCRIPTOR, *PPLABEL_DESCRIPTOR; gp 레지스터와 PLABEL_DESCRIPTOR gp 레지스터는실제로는 r1 레지스터를의미한다. < 표 1> 에서도이미 gp 레지스터의맵핑에대해서표시했다. r1 레지스터를어셈블리상에서는 gp 레지스터로표시하는데이것은 r1 레지스터가항상글로벌포인터를저장하는특별한목적으로사용되기때문이다. 그렇다면글로벌포인터란또무엇이란말인가? IA64에서는모듈내부에서전역변수를참조하는경우항상다음과같은형식으로참조하게된다. 글로벌포인터 + 전역변수옵셋 (Offset) 전역변수를절대주소로직접참조하는경우는절대없다. 전역변수는모두글로벌포인터 (gp 레지스터값 ) 에각전역변수의옵셋을더해서참조한다. 여기서반드시기억해야하는개념이나오는데이렇 EntryPoint에함수포인터가들어가고 GlobalPointer에그모듈의글로벌포인터가들어간다. 이구조체의내용이모든익스포트함수들에대해만들어지고테이블로구성된다. < 그림 4> 를보면좀더명확하게확인할수있다. < 그림 4> 에서 IA64 모듈함수테이블을보면함수주소와글로벌포인터들이있는데 PLABEL_DESCRIPTOR들이나열되어있는것이다. 여기에서글로벌포인터들은모두같은값을가지는것을볼수있다. 이글로벌포인터는모두 NT 커널모듈의글로벌포인터값이므로당연히모두같은값을가지게되는것이다. 메모리낭비인것같기도하지만이렇게구성할수밖에없어보이기도한다. 이제야 IA64 모듈함수테이블이도대체왜존재하는가에대한의문이풀리는것같다. IA64에서는글로벌포인터라는개념이도입되어모든모듈이항상이값을참조할수있어야한다. gp 레지스터는 마이크로소프트웨어 275
실 전! 강 의 실 개발자를위한실전 64 비트! 64 비트윈도우커널탐구 3 이글로벌포인터를항상가지고있는용도로사용되고있는것이다. 그렇기때문에수행중인모듈이변경될때마다 gp 레지스터의값도변경되어야하는데다음에수행될모듈의글로벌포인터를쉽게얻기위한방법을제공하는것이바로 IA64 모듈함수테이블인것이다. IA64 서비스테이블분석이제 IA64 서비스테이블에대해살펴보자. 분석에사용된 OS는윈도우서버 2003 64비트에디션이다. 서비스테이블에대한기본개념은이전호에서설명했으므로생략하고 IA64에해당하는내용만살펴본다. 이번에도역시 NT 커널에서익스포트된 KeServiceDescriptor Table을참조하면서시작한다. kd> dq KeServiceDescriptorTable e0000000`842cda80 e0000000`8428e1f0 00000000`00000000 e0000000`842cda90 001efe10`00000128 e0000000`8428eb34 64비트로데이터를봐야하므로 dq 명령어를사용했다. 이내용의이해를돕기위해지난호에서도소개했던구조체를다시보자. struct ServiceDescriptorTable { PVOID ServiceTableBase; PVOID ServiceCounterTable; ULONG NumberOfServices; PVOID ParamTableBase; } 이구조체가 64비트로빌드되어사용되는것이메모리상에서이와같이보이는것이다. 독자들은첫번째와네번째포인터가여기서살펴봐야할핵심내용이라는사실을파악하고있을것이다. 이것들을설명하기전에세번째필드인 NumberOfService만잠깐짚고넘어가면 ULONG 타입이므로 0x128만유효만값이라는점에착오가없어야한다. 상위 4바이트, 0x001efe0은 8바이트정렬을위한패딩 (padding) 공간일뿐이므로그냥쓰레기값을보여주고있는것이다. 이제 ServiceTableBase를살펴보자. 이첫번째포인터는 nt!kiservicetable을가리킨다. kd> dq e0000000`8428e1f0 e0000000`8428e1f0 e0000000`844c47e3 e0000000`842f6263 e0000000`8428e200 e0000000`84006f13 e0000000`843154f9 e0000000`8428e210 e0000000`842fb229 e0000000`842ff9a9 e0000000`8428e220 e0000000`842f4865 e0000000`84327d93 e0000000`8428e230 e0000000`84309844 e0000000`84328d62 e0000000`8428e240 e0000000`842fb004 e0000000`842fb2d2 e0000000`8428e250 e0000165`df09fa80 e0000000`8431a585 e0000000`8428e260 e0000000`84353d05 e0000165`df09f980 8바이트단위로함수포인터들이기록되어있는것을볼수있다. 이제는독자들도 AMD64를분석하던기억을되살려서함수포인터의최하위비트를살펴보고있을것같다. 그런데포인터의하위 4비트가 AMD64처럼 1과 0으로나열되는형태는아니다. 그렇다면모두정상적인함수포인터라는생각이든다. AMD64에서는 1과 0을플래그로사용하려고포인터를손상시키는 꽁수 를썼지만 IA64에서는그런작업을하지않는것으로보인다. 함수를확인해보기위해서이중두개만골라서살펴보면다음과같이나온다. kd> u e0000000`844c47e3 nt!ntmapuserphysicalpagesscatter+0x3: e0000000`844c47e3???????? e0000000`844c47f0 adds r3=ffffffff`ffffe000, sp ;; e0000000`844c47f4 ld8.nta r31=[r3] e0000000`844c47f8 mov r57=pr e0000000`844c4800 addl r3=ffffffff`ffffdcc0, r0 ;; e0000000`844c4804 add sp=r3, sp e0000000`844c4808 dep r58=ffffffff`ffffffff, r0, 0, 33 ;; kd> u e0000000`843154f9 nt!ntreadfile+0x9: e0000000`843154f9???????? e0000000`84315500 adds sp=ffffffff`ffffff60, sp e0000000`84315504 adds r64=0, r0 e0000000`84315508 adds r60=1, r0 ;; e0000000`84315510 ld8.nta r3=[sp] e0000000`84315514 mov r58=pr e0000000`84315518 adds r66=78, sp 두개다 NT 커널함수의특정옵셋으로나오면서 WinDbg가명령어해석을제대로하지못하고있다. 이것을제대로보려면마지막 4 비트를클리어해서 0으로만들면된다. 실제함수주소는 e0000000` 844c47e0, e0000000`843154f0인데서비스테이블에는마지막 4비트에 AMD64에서와같이어떤정보를표시하고있는것이다. 이것이어떤정보인지를알기위해서필자는이번에도 DDK 도움말을뒤져가면서일일이서비스테이블에있는함수포인터에해당하는커널서비스함수의함수원형 ( 프로토타입 ) 을살펴보았다. 그결과필자는이정보가그함수의파라미터개수를나타낸다는것을알게되었다. 앞의예에서보면 NtMapUserPhysicalPagesScatter는파라미터가 3 276 2 0 0 5. 1 2
64 비트윈도우커널분석 IA64 개, NtReadFile는파라미터가 9개라는뜻이다. 이것들을이번에는또어떻게사용하는것일까? 그런데 AMD64에서는최하위에표시한비트로파라미터개수를판단해서동작하는코드를확인할수있었지만안타깝게도 IA64에서는왜이런트릭을사용하는지는확인할수없었다. 잘못된포인터를그대로사용하면오류가발생할테니 AMD64처럼마지막비트들을 0으로클리어해서정상적인함수포인터를만들고호출해주는코드가시스템콜핸들러 (System Call Handler) 에있을줄알았는데이런코드가없었다는말이다. 그렇다면이렇게변형된함수포인터를시스템콜핸들러가그냥호출해도정상적으로호출이될까? 확인해본결과정답은 그렇기도하고아니기도하다 이다. 일단함수테이블을참조해서커널함수를호출하는코드를살펴보자. 시스템콜핸들러에서함수테이블을참조해서 NtCreateFile을호출하는경우의예이다. kd> u e0000000`842404f0 nt!kisystemserviceteb+0xf0: e0000000`842404f0 (p2) lfetch [r17] e0000000`842404f4 nop.i 0 e0000000`842404f8 (p11) br.cond.sptk.few b6 // nt!ntcreatefile+a (e0000000`8430614a) < 그림 5> IA64 가상주소변환 63 61 60 4342 33 32 23 22 13 12 0 VRN Unknown page Directory Pointer Index Page Directory Pointer Table PPE Page Directory Index Page Directory Table 기위해디버거로세번째라인 e0000000`842404f8(nt!kisystem ServiceTeb+0xf8) 을수행하기직전에예외핸들러 (Exception Handler) 에브레이크포인트를설정하고한스텝을진행시켜봤다. 브레이크포인트가걸리면서나타난다음의콜스택에서필자가추측한것이사실임을확인할수있었다. PDE Page Table Index Page Table PTE Page Byte Offset 8 Kbyte Physical Page Physical address 세번째라인에서 NtCreateFile+a로점프하는것을볼수있다. b6 레지스터에는서비스테이블에서구한 NtCreateFile 서비스함수의포인터 nt!ntcreatefile+a (e0000000`8430614a) 가들어있다. NtCreateFile의파라미터가 10개이므로함수포인터마지막에 a가더해진형태로보이고있다. 이것이문제없이호출되는지확인해보기위해디버거로한스텝을진행시켜보면다음과같이함수의정확한위치로진입하는것을볼수있다. nt!ntcreatefile+a(e0000000` 8430614a) 가아닌 nt!ntcreatefile (e0000000`84306140) 로정확하게진입한다는것이다. Child-SP Child-BSP RetAddr Call Site e0000165`dfe08120 e0000165`dfe0a548 e0000000`84015cf0 nt!kidispatchinterrupt e0000165`dfe08120 e0000165`dfe0a4e8 e0000000`840041b0 nt!kicheckforsoftwareinterrupt+0xb0 e0000165`dfe08120 e0000165`dfe0a4c0 e0000000`84113818 nt!kiexternalinterrupthandler+0x2a0 e0000165`dfe08460 e0000165`dfe0a488 e0000000`845a4740 nt!kethawexecution+0x288 e0000165`dfe08460 e0000165`dfe0a440 e0000000`840a7860 nt!kdexitdebugger+0x80 e0000165`dfe08460 e0000165`dfe0a318 e0000000`8407b520 nt!kdptrap+0xc40 e0000165`dfe08580 e0000165`dfe0a260 e0000000`84006010 nt!kidispatchexception+0x440 e0000165`dfe090e0 e0000165`dfe0a238 e0000000`84003c50 nt!kiexceptiondispatch+0x190 e0000165`dfe09280 e0000165`dfe0a230 e0000000`842404f8 nt!kigenericexceptionhandler+0x330 e0000165`dfe095c0 e0000165`dfe0a1e8 00000000`6b01afd0 nt!kisystemserviceteb+0xf8 nt!ntcreatefile: e0000000`84306140 alloc r41=ar.pfs, a, 0, 8, 0 e0000000`84306144 mov r40=rp e0000000`84306148 adds sp=ffffffff`ffffff90, sp 그러므로일차정답은 정상적으로호출이된다 이다. 어떻게점프할주소는 nt!ntcreatefile+a (e0000000`8430614a) 인데 nt!ntcreatefile (e0000000`84306140) 로진입이일어난것일까? 이진행과정에는중간과정이있다. 필자는메모리정렬에도맞지않는이런주소로점프하는명령어가수행즉시예외 (Exception) 를발생시킬것이라고생각했다. 그리고이예외는 OS에의해정확한주소로보정된후에다시호출되는것이라는가정을했다. 이것을확인해보 nt!kisystemserviceteb+0xf8에서호출한주소때문에예외가발생하여예외핸들링이되고있음을볼수있다. 핸들링의내용을구체적으로확인하지는않았지만함수주소를조정하여정확한주소로점프할수있게조정해주는역할을하는것으로예상할수있다. 그러므로두번째정답은 정상적인호출이아니다 인것이다. 정상적으로호출되기보다는특별한핸들링에의해서호출되는과정이기때문이다. 서비스테이블에대한나머지내용은 x86이나 AMD64와동일하기때문에더이상설명하지않는다. 궁금한부분이더있다면독자여러분이터득한서비스테이블에대한지식을이용해서스스로탐구해보기바란다. 마이크로소프트웨어 277
실 전! 강 의 실 개발자를위한실전 64 비트! 64 비트윈도우커널탐구 3 IA64 가상주소변환 가상주소변환 (Virtual Address Translation) 은가상메모리주소로부터물리메모리주소로변환되는과정을말하는것이다. 가상주소변환이라는용어와기본개념은역시 AMD64 분석기사에서이미소개했으므로여기서는생략한다. IA64에서가상주소변환은다음과같이동작한다. < 그림 6> IA64 의 PTE(Page Table Entry) 63 53 52 49 13 12 11 10 9 8 7 6 5 4 2 1 0 E Page frame number (37bits) cw W NX O D A V Exception Reserved Copy on write Reserved Write Execute Owner Dirty Accessed Cache Reserved Valid < 그림 5> 의내용은어떤자료로부터참조한내용이아니라 x86, AMD64 구조를응용하여추정한것이다. 다행히추정한대로적용해보니정확히일치했다. IA64 매뉴얼에서는앞과같은식으로설명하지않는다. x86, AMD64 의가상주소구성과유사성을찾다가알아낸내용을필자가나름대로정리한것이다. 최상위비트인 VRN은 Virtual Region Number의약자이고 3비트로 8개의영역 (Region) 을나타낸다. 각영역마다 61비트주소공간을가진다고이해하면된다. IA64 윈도우에서는영역 0 이유저모드영역으로사용되므로유저모드가상주소의형태는 0xxxxxxx xxxxxxxx가되고영역 7이커널모드영역으로사용되므로커널모드가상주소의형태는 exxxxxxx xxxxxxxx가된다. 43~60비트영역은대부분 0으로채워져있어서사용되지않는것처럼보였다. 확실치않으므로여기서는 Unknown 으로표시했다. [ WinDbg 리모트디버깅 ] IA64는대부분서버장비이다. 크기도크고소음도작지않기때문에일반사무실에서운영하기에는다소무리가있다 ( 하드웨어에욕심이많은개발자들은가끔이런오기를부리기도한다 ). 그래서대체로별도의서버실이나테스트실에두고사용하게되는데이런경우커널디버깅을하려면매번노트북등을가지고들어가서작업해야하므로여간불편한게아니다. 이럴때 WinDbg 리모트디버깅을유용하게사용할수있다. 먼저테스트실에서사용하던 IA64와노트북의디버깅환경은그대로유지한채개발자자리로돌아온다. 이제부터개발자자리에서테스트실의노트북을경유하여 IA64 를디버깅하도록설정해보자. 총 3대의컴퓨터가사용되는것이다. 테스트실 IA64( 디버거 ) 디버그모드로부팅 테스트실노트북 WinDbg 설정 ( 디버거 ) IA64에커널디버그모드로연결한상태에서.server 명령으로리모트디버깅활성화 - 하나의포트를열어놓을경우 ( 포트번호는마음대로설정 ) - 포트범위를지정하여열어놓을경우 ( 다음예는 1000~1005까지지정 ).server tcp:port=1000:1005 개발자 PC WinDbg 설정 ( 리모트디버거 ) File 메뉴의 Connect to Remote Session을선택후다음과같이입력 - 컴퓨터이름으로특정포트에연결할때 ( 컴퓨터이름은앞에 \\ 를붙이지않아야한다 ) tcp:server=testroom_notebook,port=1000 - IP 주소로포트범위에대해서연결할때 ( 비어있는포트로연결됨 ) tcp:server=172.16.100.4,port=1000:1005 개발자 PC에서노트북으로 WinDbg 연결이성공하면개발자 PC의 WinDbg는마치테스트실노트북의 WinDbg인것처럼동작한다. 사실실제디버깅제어는테스트실노트북의 WinDbg가다하는것이고개발자 PC의 WinDbg는명령어를보내고결과를받아서뿌려주는터미널역할만하는것이지만매번테스트실에들락날락하지않아도되니대단히유용한기능이라고할수있다..server tcp:port=1000 278 2 0 0 5. 1 2
64 비트윈도우커널분석 IA64 이다음의하위비트부터 AMD64 설명에서접했던 Page Directory Pointer Index가나오기시작한다. 이것은 Page Directory Pointer Table에나열되어있는 Page Directory Pointer Entry(PPE) 중에어떤것인지가리키는인덱스이다. 64비트가상주소에서 33비트부터 42비트까지 10비트를차지한다. 10비트인덱스를가지므로 1024개의 PPE가존재함을알수있다. 각각의 PPE에는 Page Directory Table을가리키는포인터가존재한다. 이포인터가가리키는테이블에는마찬가지로 Page Directory Entry(PDE) 들이나열되어있는데역시 64비트단위의배열이라고보면된다. 이 PDE의인덱스로사용되는것이 23비트부터 32비트까지의 10비트이다. 이역시 10 비트이므로하나의테이블에 1024개의 PDE가존재함을알수있다. 이후부터는 32비트에서 PTE부터찾아가는방법과동일하게처리된다. AMD64와의차이점이몇가지보이는데첫번째는테이블을따라가는단계가하나줄었다는점이다. 두번째는 AMD64에서는각테이블이 9비트인덱스로 512개의엔트리를가졌는데 IA64에서는 10비트인덱스로 1024개의엔트리를가진다는점이다. 세번째는 x86, AMD64에서 4KB였던한페이지의크기가 IA64에서는 8KB이므로 13비트의페이지바이트옵셋 (Page Byte Offset) 을가진다는점이다. 각엔트리들의구조를이해하기위해서대표적으로 PTE 구조를살펴보자. < 그림 6> 은 Windows Internals 4th Edition 에서발췌한 PTE 구조이다 ( 참고자료 ). 페이지속성을나타내는 0~12비트까지의각플래그들에대한의미는각자알아서확인해보기바란다. 여기서는 13~49비트까지가 PTE가가리키는물리주소를나타낸다는것만알아두자. 가상주소에서물리주소까지디버거를이용해서가상주소부터물리주소까지따라가보자. 이번에도 AMD64에서다뤘던 Ntfs!LfsReleaseLch 함수를살펴본다. 이번에는이함수의가상주소가 e0000165`c8455a30이다. kd> u e0000165`c8455a30 Ntfs!LfsReleaseLch: e0000165`c8455a30 alloc r34=ar.pfs, 4, 0, 1, 0 e0000165`c8455a34 mov r33=rp e0000165`c8455a38 addl r36=ffffffff`ffe02310, gp e0000165`c8455a40 adds r35=0, gp e0000165`c8455a44 nop.m 0 e0000165`c8455a48 nop.f 0 e0000165`c8455a50 adds r32=38, r32 e0000165`c8455a54 nop.m 0 e0000165`c8455a58 nop.b 0 ;; 이가상주소를가상주소의구조대로쪼개보면다음과같이 PPE 인덱스, PDE 인덱스, PTE 인덱스, 페이지옵셋이나온다. 가상주소 : e 0 0 0 0 1 6 5 `c 8 4 5 5 a 3 0 2진수표시 : 1110 0000 0000 0000 0000 0001 0110 0101 1100 1000 0100 0101 0101 1010 0011 0000 인덱스 : 0b2 390 22a 1a30 ---------- ------------ ---------- ------------ 10bit 10bit 10bit 13bit PPE Index PDE Index PTE Index Page Offset x86, AMD64에서는최초테이블의물리주소를구하기위해서 CR3 레지스터를참조하면서시작했지만 IA64에서는이에해당하는정보가어디에있는지찾지못했다. 그래서 Page Directory Pointer Table의물리주소는구할수없었다. 이렇게되면나머지과정을진행하기가어려워진다. 하지만필자는분석을계속진행하기위해다른방법을사용하기로했다. 윈도우커널이제공하는페이지테이블맵핑주소를이용하기로한것이다. IA64에서도역시윈도우커널은물리주소에존재하는각페이지테이블에대해서가상주소로맵핑한주소를제공하고있다. IA64에서의페이지테이블맵핑주소는 < 표 2> 와같다. 여기서문제가하나있는데 Windows Internals 4th Edition 에적혀있는값들과 WinDbg가보여주는값들이다르다는점이다. < 표 2> 에있는값들은 WinDbg가보여주는것이다. WinDbg는디버거라서현재시스템의상태를그대로보여줄테니이값이맞는것같은데책에는다른값이적혀있으니그것은또무슨우여곡절이있는것인지도무지알수없는노릇이었다. 한참을고민하다가결국 MS 뉴스그룹에서얻은답변은 IA64 초기버전과업그레이드된버전의테이블맵핑주소가변경되었다는사실이었다. MS 엔지니어는책에있는주소가 IA64 초기버전소스코드에기록되어있는값이라고설명해주었고현재는그렇지않다고했다. 그러므로 WinDbg가 < 표 2> 에서보여주고있는값이현재윈도우서버 2003 64비트에디션의정확한페이지테이블맵핑주소이다. 이제다시!pte 명령을이용해서가상주소와물리주소의관계를알아보자. kd>!pte e0000165`c8455a30 VA e0000165c8455a30 PPE at FFFFFFFF80000590 PDE at FFFFFE0000165C80 PTE at FFF8000059721150 < 표 2> IA64 페이지테이블맵핑주소 테이블이름 가상주소 Page-Directory-Pointer Table Base FFFFFFFF80000000 Page-Directory Table Base FFFFFE0000000000 Page Table Base FFF8000000000000 마이크로소프트웨어 279
실 전! 강 의 실 개발자를위한실전 64 비트! 64 비트윈도우커널탐구 3 contains 0010000004722661 contains 00100000146F2661 contains 0010000008AC0221 pfn 2391 E--DAKWV pfn a379 E--DAKWV pfn 4560 E---AKRV!pte 명령은 PPE, PDE, PTE가맵핑된가상주소를보여주고그내용을해석해서보여준다. PPE at FFFFFFFF80000590라고나오는부분이 PPE의가상주소가 FFFFFFFF80000590에위치함을표시하는것이다. 그밑줄의 contains는그메모리의내용을보여주고그밑줄은메모리내용을해석해서표시하고있다. 가상주소 FFFFFFFF80000590의메모리를디버거로직접보면!pte 명령이보여주는내용과같음을확인할수있다. kd> dq FFFFFFFF80000590 ffffffff`80000590 00100000`04722661 00000000`00000000 PPE Table Base가 FFFFFFFF80000000인것과 PPE 인덱스가 0x0b2인것을고려하여계산해보면다음과같은결과이다. kd> dq FFFFFFFF80000000 + (b2 * 8) ffffffff`80000590 00100000`04722661 00000000`00000000 이메모리의내용은이미!pte 명령이해석해서잘보여줬다. 하위 13비트가속성이고 E-DAKWV로표시하고있다. 그위의비트들에서 PDE Table Base 물리주소를알수있다. 13~49비트만취하면 0x2391이라는값이얻어지는데!pte 명령에서 pfn이라는값으로보여주고있다. Page frame number(pfn) 은페이지크기 (8KB) 로나누어진값이기때문에정확한물리주소를구하려면 pfn 값에페이지크기를다시곱해줘야한다. pfn 0x2391에페이지크기를곱해서계산하면 0x4722000이라는 PDE Table Base 물리주소가구해진다. PDE 인덱스가 0x390이므로 PDE의물리주소는다음과같다. < 그림 7> 맵핑된영역의메모리영역 FFFFF68000000000 (PTE Base 맵핑 ) FFFFF6FB40000000 (PDE Base 맵핑 ) FFFFF6FB7DBED000 (PPE Base 맵핑 ) kd> dq FFFFFE0000000000 + (b2 * 8 * 400) + (390 * 8) fffffe00`00165c80 00100000`146f2661 00100000`146f4661 PDE Table을참조하는데 PPE 인덱스까지고려하는이유는이미 AMD64에서설명한바와같이 PDE Table에는 PPE가가리킬수있는모든 PDE들이들어있기때문이다. PPE 인덱스에 0x400을곱하는이유는인덱스가 10비트이므로테이블하나에들어가는엔트리의개수가 1024(0x400) 이기때문이다. < 그림 7> 을참고해서조금만생각해보면쉽게이해할수있을것이다. 계속해서 PDE 내용을가지고 PTE Table Base의물리주소를구해보자. 00100000`146f2661에서 13~49비트를취하여 pfn 0xa379를얻을수있고여기에페이지크기를곱한값이 PTE Table Base인 0x146f2000이다. PTE 인덱스가 0x22a이므로 PTE의물리주소는다음과같다. kd> dq /p 146f2000 + (22a * 8) 00000000`146f3150 00100000`08ac0221 00100000`08ac2221 PTE(0) PTE(1) PTE(1024) for PPE(0), PDE(0) PTE(0) PTE(1) PTE(1024) for PPE(0), PDE(1) PTE(0) PTE(1) PTE(1024) for PPE(0), PDE(2) PDE(0) PDE(1) PDE(1024) for PPE(0) PDE(0) PDE(1) PDE(1024) for PPE(1) PDE(0) PDE(1) PDE(1024) for PPE(2) PPE(0) PPE(1) PPE(1024) 이것도!pte 가보여주는 PTE 가상주소의내용과일치한다. kd> dq /p 4722000 + (390 * 8) 00000000`04723c80 00100000`146f2661 00100000`146f4661 kd> dq FFF8000059721150 fff80000`59721150 00100000`08ac0221 00100000`08ac2221!pte 가보여주는 PDE 가상주소의내용과일치한다. kd> dq FFFFFE0000165C80 fffffe00`00165c80 00100000`146f2661 00100000`146f4661 PDE Table Base가 FFFFFE0000000000인것과 PPE, PDE 인덱스를고려하여계산하면다음과같은결과가도출된다. PTE Table Base가 FFF8000000000000인것과 PPE, PDE, PTE 인덱스를고려하여계산하면다음과같다. kd> dq FFF8000000000000 + (b2 * 8 * 400 * 400) + (390 * 8 * 400) + (22a * 8) fff80000`59721150 00100000`08ac0221 00100000`08ac2221 결국이페이지의물리주소는이내용에서 13~49 비트를취하여 280 2 0 0 5. 1 2
64 비트윈도우커널분석 IA64 pfn 0x4560을얻고여기에페이지크기를곱한값인 0x8ac0000이다. 이제이페이지주소에가상주소의마지막 13비트인페이지옵셋 0x1a30을더하면물리주소를정확히구하게된다. kd> dq /p 8AC0000 + 1a30 00000000`08ac1a30 02100580`08151000 99180904`80006200 00000000`08ac1a40 00002100`0201180e 00040000`00000200 00000000`08ac1a50 00002100`40e10019 20000000`00000200 00000000`08ac1a60 02401018`4800f019 20000000`00203080 00000000`08ac1a70 00101418`3c20f80a 070009f0`e0203078 00000000`08ac1a80 00000001`00000019 12800078`00000200 00000000`08ac1a90 00101038`40012000 e2004009`e042008c 00000000`08ac1aa0 00000001`00000019 45000050`07800200 00000000`08ac1a50이라는물리주소가바로가상주소 e0000165`c8455a30가가리키는실제메모리주소이다. 가상주소 e0000165`c8455a30을디스플레이해보면그내용이정확히일치하는것을확인할수있다. kd> dq e0000165`c8455a30 e0000165`c8455a30 02100580`08151000 99180904`80006200 e0000165`c8455a40 00002100`0201180e 00040000`00000200 e0000165`c8455a50 00002100`40e10019 20000000`00000200 e0000165`c8455a60 02401018`4800f019 20000000`00203080 e0000165`c8455a70 00101418`3c20f80a 070009f0`e0203078 e0000165`c8455a80 00000001`00000019 12800078`00000200 e0000165`c8455a90 00101038`40012000 e2004009`e042008c e0000165`c8455aa0 00000001`00000019 45000050`07800200 pphysicalpage = GetPhysicalPage( ppte ); // PTE에서페이지의물리주소를구한다. pphysicaladdr = pphysicalpage + npage_offset; // 페이지주소 + 페이지옵셋 64 비트를준비하는설렘과흥분 64비트시대를준비한다는미명아래시작되었던이번 64비트커널탐구연재는무려 5개월동안이나필자를밤새우게만들었던프로젝트이다. 여름새벽시원한공기에의지해원고를작성하던기억이아직남아있는데어느덧찬바람을맞으며이프로젝트를마무리하고있다. 나름대로는 64비트장비를아직접해보지못한독자들이많을것같아서새롭게경험한내용들을빨리독자들에게전하려고두서없이작업에몰두했던것같다. 연재를마치고나니과연독자들이이해하기쉽게내용을잘정리한것인지에대한의문이생기기도한다. 난해한내용들이많았을것으로생각하지만필자에게는 AMD64, IA64 를처음들여다보는과정이굉장히재미있고흥분되는작업이었기때문에독자들이그느낌을그대로전달받았기를바라는마음뿐이다. 기술에대한설렘을먹고사는사람들이바로개발자들아니던가? 이번연재를통해서 64비트환경을이해하고이에대한준비를본격적으로시작하는독자들이많아진다면필자에게그것보다더좋은결실은없을것이다. m a s o 정리 김세미 semsem@imaso.co.kr 만약가상주소를가지고물리주소를구하는프로그램을작성한다면가상주소로맵핑된 PTE Table Base와 PPE 인덱스, PDE 인덱스, PTE 인덱스, 페이지옵셋만가지고구현할수있다. 이과정을간단하게프로그램코드로적어보면다음과같다. #define IA64_PTE_BASE 0xFFF8000000000000 #define SIZE_PTE 8 // bytes #define SIZE_10BIT 1024 // 0x400 nppe_index = GetPPEIndex( pvirtualaddr ); // pvirtualaddr에서 33~42비트를취한다. npde_index = GetPDEIndex( pvirtualaddr ); // pvirtualaddr에서 23~32비트를취한다. npte_index = GetPTEIndex( pvirtualaddr ); // pvirtualaddr에서 13~22비트를취한다. npage_offset = GetPageOffset( pvirtualaddr ); // pvirtualaddr에서 0~12비트를취한다. ppte = (IA64_PTE_BASE + (nppeindex * SIZE_PTE * SIZE_10BIT * SIZE_10BIT) + (npdeindex * SIZE_PTE * SIZE_10BIT) + (npteindex * SIZE_PTE)); 참 + 고 + 자 + 료 MSDN Magazine, November 2000, UNDER THE HOOD, Programming for 64-bit Windows Windows Internals 4th Edition, Microsoft Press. Intel Itanium Architecture Software Developer s Manual, INTEL. 마이크로소프트웨어 281