راهنمای معماری های x86 و x86_64 برای برنامه نویسان سیستمی - قسمت دوم
این آموزش در ادامه سری آموزش های راهنمای معماری های x86 و x86_64 برای برنامه نویسان سیستمی قسمت صفر و اول می باشد. موضوع این آموزش در مورد مدیریت حافظه و روش هایی که برای مدیریت حافظه در این خصوص در پردازنده های معماری x86 و x86_64 ارائه شده است می باشد. این روش ها عبارتند از Segmentation و Paging که در ادامه در مورد آنها صحبت خواهیم کرد.
سرفصل های این آموزش
- Segmentation
- آدرس منطقی (Logical Address)
- آدرس خطی (Linear Address)
- تبدیل آدرس منطقی به آدرس فیزیکی در Segmentation
- Segment Selector
- جداول GDT و LDT
- ساختار Segment Descriptor
- رجیسترهای GDTR و LDTR
- چطور می توان Segmentation را فعال کرد
- Segmentation در IA-32e Mode (در حالت ۶۴ بیتی)
- Paging
Segmentation
Segmentation یکی از روش هایی است که برای مدیریت حافظه می توان از آن استفاده کرد. برخلاف روش Paging که در مد محافظت شده اختیاری می باشد (جلوتر در مورد این موضوع توضیح خواهم داد)، Segmentation اختیاری نیست و در صورتی که بخواهیم حافظه را مدیریت کنیم حتما باید فعالش کرد. در Segmentation حافظه به بخش های کوچکتر به نام segment تقسیم می شود و برای هر segment ما می توانیم مشخص کنیم که چه دسترسی داشته باشیم مثلا خواندن، نوشتن یا اجرایی و همچنین اندازه segment را مشخص کنیم که این اندازه می توان متغییر باشد.
آدرس منطقی (Logical Address)
قبل از اینکه ادامه بدهیم لازم است کمی در مورد آدرس دهی در پردازنده های مورد بحث صحبت کنیم. لازم است اشاره کنم که ابتدا موضوع را در مورد پردازه های ۳۲ بیتی توضیح می دهم بعد تفاوت هایی که در پردازنده های ۶۴ بیتی وجود دارد را خواهم گفت.
در Protected Mode تمام آدرس هایی که استفاده می شود آدرس های منطقی هستند و به فرمتشان بصورت زیر می باشد.
همانطور که می بینید آدرس از دو بخش تشکیل شده است.
- segment: به این قسمت Segment Selector گفته می شود که می تواند یکی از رجیسترهای CS, DS, ES, SS, FS, GS باشد. لازم به ذکر است که مقداری که در این رجبیستر قرار می گیرد با مقداری که در آدرس های Real Mode داریم متفاوت می باشد.
- offset: یک مقدار ۳۲ بیتی می باشد.
می توانیم چند مثال اسمبلی هم بیاوریم که در عمل این آدرس های منطقی را ببینیم.
1. mov ds:[eax], 10h 2. add edx, es:[12345678h] 3. lea ebp, ss:[00067890h]
در همه مثال هایی که آورده ایم یکی از عملوند (operand) ها به یک ناحیه حافظه اشاره میکند. قسمت آدرس برای دستور اول ds:[eax]
، دوم es:[12345678h]
و سوم ss:[00067890h]
می باشد.
آدرس خطی (Linear Address)
چه روش Segmentation استفاده شود و یا Paging در هر دو حالت آدرس منطقی به آدرس دیگری که به آن آدرس خطی گفته می شود تبدیل می شود. آدرس خطی در واقع تمام فضای آدرسی است که پردازنده می تواند آدرسی دهی کند. آدرس ها در معماری IA-32 به صورت ۳۲ بیتی هستند و در معماری IA-32e آدرس ها به صورت ۶۴ بیتی مب باشند. جلوتر که تبدیل آدرس ها به آدرس فیزیکی را توضیح داده ام تا این موضوع روشن تر شود.
تبدیل آدرس منطقی به آدرس فیزیکی در Segmentation
حالا پردازنده در هنگام اجرای دستورات آدرس منطقی استفاده شده را به آدرس خطی تبدیل میکند در صورتی که روش Segmentation استفاده شده باشد این آدرس عینا به آدرس فیزیکی تبدیل می کند (در واقع روی باس آدرس قرار می گیرد). در شکل پایین می توانید ببینید یک آدرس منطقی در حالت Segmentation چطور به آدرس فیزیکی تبدیل می شود. همانطور که می بینید ما در سمت چپ ترکیب segment selector و offset را داریم.
حالا برای تبدیل آدرس منطقی به فیزیکی (آدرس خطی) این اتفاق ها می افتد
- خواندن مقدار رجیستر segment مربوطه یعنی یکی از رجیسترهای CS, DS, ... (که منظور همان Segment Selector در تصویر است)
- این مقدار یک index است که به یک خانه در جدول GDT یا LDT اشاره میکند (جلوتر در مورد این جدول ها توضیح میدهم). از داخل جداول ذکر شده مقداری که به آن آدرس پایه (Base Address) می گویند به دست می آید. این آدرس در واقع آدرس فیزیکی شروع ناحیه ای است که به آن می خواهیم دسترسی داشته باشیم.
- آدرس فیزیکی پایه که بدست آمد با offset جمع می شود و نتیجه آدرس فیزیک (آدرس خطی) معادل بدست می آید.
Segment Selector
همانطور که بالاتر اشاره کردیم رجیستر segment (Segment Selector)
حاوی index ای است که به یک خانه از جداول GDT یا LDT اشاره می کند. خانه های این جداول حاوی ساختار داده ای به نام Segment Descriptor می باشند که جلوتر در موردش توضیح می دهم . این رجیستر ۱۶ بیتی است و می تواند یکی از رجیسترهای CS, DS, ES, SS, FS, GS باشد. در حالتی که در مد محافظت شده باشیم ساختار این سگمنت به صورتی که در شکل نمایش داده شده است.
توضیح در مورد قسمت های Segment Selector
-
RPL (Requested Privilege Level)
: بیت صفر و یک این رجیستر مقدار RPL را مشخص کرد و می تواند مقداری بین عدد صفر تا سه باشد (مربوط به Ring ها که در آموزش اول توضیح دادیم). RPL در واقع سطح دسترسی (Privilege Level) دستور درخواست کننده را مشخص میکند. برای دسترسی به یک ناحیه حافظه باید سطح دسترسی دستور درخواست کننده نیز کافی باشد تا بتواند به آن ناحیه دسترسی داشته باشد.
-
TI (Table Indicator) flag
: بیت دوم مشخص میکند که index ما به کدام یک از جداول GDT یا LDT اشاره میکند. در صورتی که این مقدار صفر باشد index به جدول GDT اشاره میکند و در صورتی که مقدار یک باشد به LDT. -
Index
: از بیت سوم تا پانزدهم مقدار index قرار می گیرد. این index به یکی از ۸۱۹۲ خانه از جداول GDT یا LDT می تواند اشاره کند.
جداول GDT و LDT
برای استفاده از Segmentation یکسری جدول است که سیستم عامل، یا هر نرم افزاری که قرار است از مدیریت حافظه استفاده کند(BIOS, UEFI, Boot Manager, ...)، موظف است آن ها را ایجاد کند. در یک سیستم باید یک نمونه جدول GDT (Global Descriptor Table)
و در صورت نیاز یک یا چند جدول LDT (Local Descriptor Table)
را تعریف گردد. این جداول در واقع یک آرایه هستند که عناصر آنها ساختار داده ای است به نام Segment Descriptor (جلوتر در مورد ساختار Segment Descriptor توضیح می دهم). هر کدام از این Segment Descriptor ها می توانند به یک ناحیه از حافظه اشاره کنند که این ناحیه می تواند برای نگهداری Code, Data, Stack مورد استفاده قرار گیرند. همچنین ناحیه حافظه اشاره شده توسط Segment Descriptor می تواند بخشی یا تمام حافظه اصلی باشد. به علاوه روی این ناحیه های حافظه که در این جداول تعریف شده اند می توان مشخص کرد چه سطح دسترسی داشته باشند در نتیجه در هنگام دسترسی به یک ناحیه حافظه این مساله توسط پردازنده بررسی می شود.
برای مثال در شکل زیر فرض می گیریم سیستم عامل جدول GDT را مانند شکل تعریف کرده است. به این صورت که Segment Descriptor 1 به ناحیه ای به آدرس 0xC0000000 تا 0xE0000000 اشاره می کند و Segment Descriptor 2 نیز به ناحیه ای به آدرس 0x00800000 تا 0x00900000 اشاره می کند. به ترتیب ناحیه اول برای نگهداری کد به اندازه 0x20000000 بایت و ناحیه دوم برای نگهداری داده به اندازه 0x100000 بایت مقدار دهی شده است. به این نکته توجه کنید که این آدرس ها فیزیکی هستند.
لازم به ذکر است که برای استفاده از امکانات مدیریت حافظه در پردازنده های x86 و x86_64 حتما باید یک جدول GDT تعریف شود و جدول LDT کاملا اختیاری است.
ساختار Segment Descriptor
همانطور که گفتیم جداول GDT و LDT چیزی نیستن جز یک آرایه از ساختار داده ای به نام Segment Descriptor در نتیجه لازم است این ساختار نیز مورد بررسی قرار گیرد. در شکل زیر می توانید ساختار Segment Descriptor را ببینید.
این ساختار ۶۴ بیت طول دارد و بیت ها در ادامه شرح داده شده اند:
-
Segment Limit field
: اندازه segment را مشخص میکند. همانطور که میبینید بخشی از این مقدار از بیت صفر تا ۱۵ مربوط به ۴ بایت اول و بخش دیگر از بیت ۱۶ تا ۱۹ مربوط به ۴ بایت دوم قرار گرفته است. پردازنده این دو بخش را به هم متصل می کند و یک مقدار ۲۰ بیتی بدست می آید. -
Base Address field
: آدرس فیزیکی پایه ناحیه حافظه را مشخص میکند. و همانطور که می بینید به سه قسمت تقسیم شده که از کنار هم قرار دادن این سه تیکه یک مقدار ۳۲ بیتی بدست می آید. -
Type field
: این فیلد بسته به مقدار فلگS (descriptor type)
به طور متفاوت تقسیر خواهد شد. به این صورت که- اگر فلگ S برابر یک باشد مقادیر این جدول مربوط به دسترسی سگمنت کد یا داده خواهد بود. برای دیدن مقادیری که در این حالت قابل استفاده است به جدولی که پایین آمده با عنوان Table 3-1. Code- and Data-Segment Types رجوع کنید.
- اگر فلگ S برابر صفر باشد مقادیر این جدول نوع سگمنت System یا Gate را مشخص می کند (در این آموزش وارد این موضوع نخواهیم شد). برای دیدن مقادیری که در این حالت قابل استفاده است به جدول پایین با عنوان Table 3-2. System-Segment and Gate-Descriptor Types رجوع کنید.
-
S (descriptor type) flag
: این بیت مشخص می کند که Segment Descriptor برای تعریف کد یا داده است (اگر بیت برابر یک باشد) یا برای تعریف System Sescriptor (اگر بیت برابر صفر باشد) -
DPL (Descriptor Privilege Level) field
: Privilege Level سگمنت را مشخص میکند. در این رابطه رجوع کنید به آموزش اول که شرحی داده ام که Privilege Level به چه منظور در سیستم عامل استفاده می شود. -
P (segment-present) flag
: این فیلد مشخص میکند که سگمنت داخل حافظه است یا خیر. اگر سگمنت در حافظه باشد این فیلد مقدار یک و در غیر این صورت مقدار صفر خواهد داشت. در صورتی که دسترسی به آدرس در سگمنتی صورت پذیرد که در حافظه نباشد پردازنده یک Exception تولید می کند که در این حالت سیستم عامل می تواند سگمنت مربوط را بار گذاری کند. -
AVL (Available bit)
: این فیلد مشخص میکند که سگمنت قابل استفاده برای نرم افزار سیستمی هست یا خیر. در واقع این فیلد مشخص می کند که نرم افزار سیستمی (مثلا یک سیستم عامل یا BIOS) فیلد های دیگر Segment Descriptor را بخواند یا خیر. -
L (64-bit code segment) flag
: یک بودن این بیت مشخص می کند که دستوراتی که در این سگمنت قرار گرفته است ۶۴ بیتی هستند. در غیر این صورت compatibility mode در نظر گرفته می شود. (توضیح در مورد مد IA32e) -
D/B
: -
G (Granularity)
: این مقدار روی فیلد Segment Limit تاثیر می گذارد. به این صورت که اگر مقدار این فیلد صفر باشد عددی که در Segment Limit قرار می گیرد به صورت بایت در نظر گرفته می شود که در واقع ما هر مقداری در Segment Limit قرار دهیم ضرب در یک بایت می شود. اگر این فیلد یک باشد مقدار Segment Limit به صورت 4 کیلوبایت در نظر گرفته می شود در نتیجه ما هر مقداری در Segment Limit قرار دهیم ضرب در 4 کیلوبایت می شود.
جدول مربوط به مقادیر فیلد Type در حالتی که فلگ S برابر یک باشد.
جدول مربوط به مقادیر Type در حالتی که فلگ S برابر صفر باشد. خیلی از این موارد احتمالا نا آشنا خواهد بود براتون امیدوارم در آینده وقتی باشد تا در مورد این موارد مفصل توضیح بدهم.
رجیسترهای GDTR و LDTR
تا اینجای آموزش در مورد جداول GDT و LDT و همچنین Segment Selector توضیحاتی داده ایم. مساله ای که فعلا صحیتی در موردش نکردیم آدرس شروع این جداول است که پردازنده با آنها کار دارد. پردازنده از طریق دو درجیستر GDTR و LDTR آدرس شروع جداول را بدست می آورد. این رجیسترها ساختاری به صورت شکل زیر دارند. (اولی برای ۳۲ بیتی و دومی برای ۶۴ بیتی)
همانطور که مشاهده می کنید هر رجیستر دارای دو فیلد است.
- Limit: حد اندازه جدول که ۱۶ بیت از این رجیستر برای این منظور در نظر گرفته شده است.
- Base Address: آدرس خطی جدول. این آدرس می تواند ۳۲ یا ۶۴ بیتی باشد (با توجه به مد پردازنده Protected یا IA-32e)
برای خواندن و نوشتن در این رجیستر ها دستورات خاصی وجود دارد.
- LGDT: برای نوشتن در رجیستر GDTR
- SGDT: در خواندن رجیستر GDTR
- LLDT: برای نوشتن در رجیستر LDTR
- SLDT: برای خواندن رجیستر LDTR
اگر آموزش اول - بخش سوییچ به مد محافظت شده از این سری آموزش را خوانده باشید از دستور LGDT برای نوشتن در رجیستر GDTR استفاده شده بود. البته جزئیاتی وجود دارد که در آموزش اول به آن اشاره نکردم و در این آموزش بخش فعال کردن Segmentation به آن پرداخته ایم.
چطور میتوان Segmentation را فعال کرد
در ادامه مراحلی که باید انجام داد تا بتوان از مدیریت حافظه Segmentation استفاده کرد شرح داده ایم.
- ایجاد جدول GDT (در صورت لزوم جدول LDT). کافیه Segment Descriptor های لازم برای ناحیه خافظه ای که می خواهید تعریف کرد.
- سوییچ به Protected Mode (رجوع به آموزش قبلی)
- مقدار دهی رجیستر GDTR (در صورت استفاده از LDT مقدار دهی رجیستر LDTR) که در این مورد هم در کدی که در آموزش اول آمده این موضوع را می توانید ببینید. این کار با دستور LGDT انجام می شود.
- مقداری دهی رجیسترهای CS, DS, ES, SS, FS, GS به عنوان Segment Selector که به مقداری در جدول GDT اشاره میکنند. این کار لازم است از آنجایی که تقریبا تمام دستورات که با حافظه کار دارند از یکی از این رجیسترها بصورت ضمنی استفاده میکنند.
طبق قراری که گذاشبم قسمت های مرتبطی از سورس freebsd را اینجا هم میاورم که مثالی باشد برای درک بهتر موضوع. البته این قسمت freebsd یکم ممکن است گنگ باشد بخاطر اینکه یکسری آدرس های که در دستورات استفاده شده صفر مقدار گرفته اند و در کامنت ها توضیح داده که mpInstallTramp()
این قسمت را تغییر می دهد. با این وجود کلیت موضوع روشن است و به نظرم بدون مشکلی بتوانید از کدها بهره ببرید.
توضیح کد
مسیر کد مورد بحث: freebsd/sys/i386/i386/mpboot.s
بخش هایی از این قسمت را از آموزش اول برداشتم که مربوط به بارگذاری آدرس جدول GDT در رجیستر GDTR است. در نهایت یک پرش انجام می شود تا سوییچ شده به Protected Mode کامل انجام شود. اگر در کد توجه کنید برای پرش دو دستور push داریم و یک دستور ret. دستور push اول در واقع یک Segment Selector دارد در stack قرار می گیرد و دستور push دوم offset است. با توجه به اینکه push اول Segment Selector است می توانیم از مقدار 0x18 (یا 24 دسیمال) اینطور برداشت کنیم که مقدار RPL برابر صفر است که به یعنی Ring 0 است، با توجه به بیت دوم جدول GDT مشخص شده و باقی بیت ها index به یک خانه در جدول GDT است که برابر ۳ می باشد. index اشاره میکند به bootcode (توضیحات پایین تر نشان داده ام که هر خانه در این جدول که در کد freebsd آمده به چه منظور هستند)
/* Now load the global descriptor table */ lgdt MP_GDTptr-bootMP ... ... /* * make intrasegment jump to flush the processor pipeline and * reload CS register */ pushl $0x18 pushl $(protmode-bootMP) lretl
این قسمت از کد در Protected Mode اجرا می شود. مجموعه دستورها که با mov $0x10, %ebx
شروع شده است رجیستر های DS, ES, FS, GS, SS را با مقدار 0x10 مقدار داده است. مقدار 0x10 در واقع Segement Selector است (که بالاتر ساختارش را توضیح داده ام) که دو بیت اول مقدار RPL را مشخص می کند که مقدارش صفراست (یعنی Ring 0)، بیت ۲ توع جدول را مشخص میکند که مقدار صفر است (یعنی جدول GDT)، و باقی بیت ها index به یک خانه از جدول GDT را مشخص کرده است که مقدار index در مثال ما برابر ۲ است که اشاره میکند به خانه سوم از این جدول که در واقع kerneldata می شود.
.code32 protmode: CHECKPOINT(0x35, 2) /* * we are NOW running for the first time with %eip * having the full physical address, BUT we still * are using a segment descriptor with the origin * not matching the booting kernel. * * SO NOW... for the BIG Jump into kernel's segment * and physical text above 1 Meg. */ mov $0x10, %ebx movw %bx, %ds movw %bx, %es movw %bx, %fs movw %bx, %gs movw %bx, %ss ... ...
این قسمت از کد هم Segment Descriptor ها را تعریف کرده است. دراین مثال پنچ Descriptor داریم
- nulldesc: همیشه جدول GDT خانه اول باید صفر باشد
- kernelcode: خیلی واضح هست کدهای سطح کرنل
- kerneldata: داده های سطح کرنل
- bootcode: کدهای bootloader
- bootdata: دادده های مربوط به bootloader
با توضیحاتی که بالا در مورد Descriptor داده ام مشخص کنید هر فیلد چه مقداری دارد، اندازه Segment را مشخص کنید و ...
البته در این قسمت هم segment base صفر مقدار گرفته و اینجا هم mpInstallTramp()
باید مقدار دهیش کند. که البته خیلی هم برای ما مهم نیست چون ما می دانیم در نهایت یک آدرس در این قسمت قرار می گیرد. در نظر بگیرید که segment base میتواند صفر باقی بماند که در واقع تمام فضای آدرس که با آدرس ۳۲ بیتی آدرس داد
/* * MP boot strap Global Descriptor Table */ .p2align 4 .globl MP_GDT .globl bootCodeSeg .globl bootDataSeg MP_GDT: nulldesc: /* offset = 0x0 */ .word 0x0 .word 0x0 .byte 0x0 .byte 0x0 .byte 0x0 .byte 0x0 kernelcode: /* offset = 0x08 */ .word 0xffff /* segment limit 0..15 */ .word 0x0000 /* segment base 0..15 */ .byte 0x0 /* segment base 16..23; set for 0K */ .byte 0x9f /* flags; Type */ .byte 0xcf /* flags; Limit */ .byte 0x0 /* segment base 24..32 */ kerneldata: /* offset = 0x10 */ .word 0xffff /* segment limit 0..15 */ .word 0x0000 /* segment base 0..15 */ .byte 0x0 /* segment base 16..23; set for 0k */ .byte 0x93 /* flags; Type */ .byte 0xcf /* flags; Limit */ .byte 0x0 /* segment base 24..32 */ bootcode: /* offset = 0x18 */ .word 0xffff /* segment limit 0..15 */ bootCodeSeg: /* this will be modified by mpInstallTramp() */ .word 0x0000 /* segment base 0..15 */ .byte 0x00 /* segment base 16...23; set for 0x000xx000 */ .byte 0x9e /* flags; Type */ .byte 0xcf /* flags; Limit */ .byte 0x0 /*segment base 24..32 */ bootdata: /* offset = 0x20 */ .word 0xffff bootDataSeg: /* this will be modified by mpInstallTramp() */ .word 0x0000 /* segment base 0..15 */ .byte 0x00 /* segment base 16...23; set for 0x000xx000 */ .byte 0x92 .byte 0xcf .byte 0x0
در ادامه همین کد مقداری که در رجیستر GDTR ریخته می شود تعریف شده. در اینجا می بینیم که GDT Limit برابر 0x28 است که در واقع پنج تا Segment Descriptor می شود که هر کدام ۸ بایت است (بالاتر به ساختار رجوع کنید). مقدار GDT Base هم صفر هست که توسط mpInstallTramp()
مقدار می گیرد. مطمعن نیستم mpInstallTramp دقیقا چه کار می کند احتمالا شامل روال بوت در سیستم عامل FreeBSD باشد که در نهایت GDT Base به MP_GDT که بالاتر دیدیم اشاره خواهد کرد.
/* * GDT pointer for the lgdt call */ .globl mp_gdtbase MP_GDTptr: mp_gdtlimit: .word 0x0028 mp_gdtbase: /* this will be modified by mpInstallTramp() */ .long 0
Segmentation در IA-32e Mode (در حالت ۶۴ بیتی)
Segmentation در IA-32e Mode بستگی دارد که در چه حالتی هستیم Compatibility Mode یا 64-Bit Mode
(رجوع به آموزش اول برای توضیح بیشتر). در حالت Compatibility Mode هیچ فرقی با آدرس دهی در حالت ۳۲ بیتی ندارد. در حالی که در حالت 64-Bit Mode
Segmentation تقریبا غیر فعال است (ولی نه کاملا). در این مد پردازنده رجیسترهای CS, DS, ES, SS را صفر فرض میگیرد. در این حالت اندازه segment اگر تعریف شده باشد نادیده گرفته می شود. البته در مورد رجیستر های FS و GS استثنا وجود دارد و میتوان از این دو رجیستر برای آٔدرس دهی استفاده کرد.
در این مد واضح که آدرس ها نیز ۶۴ بیتی می شوند. با توجه به اینکه رجیسترهای segment در نظر گرفته نمی شوند قسمت offset معادل آدرس خطی است.
Paging
میرسیم به روش دیگر برای مدیریت حافظه که Paging است. مدیریت حافظه در Paging به این صورت است که کل حافظه به بلاک های کوچک و یک اندازه مثلا 4 KB
تقسیم می شود که به این بلاک ها اصطلاحا Page گفته می شود و دسترسی و خصوصیات مستقلی دارند و همانطور که قبلا اشاره کرده بودم استفاده از Paging اختیاری است. در پردازنده های مورد بحث Paging خود سه Mode دارد که شامل موراد زیر می شود.
32-Bit Paging
PAE Paging
IA-32e Paging
در Paging نگهداری تنظمیات هر Page در قالب تعدادی جدول جدید است که در ادامه در مورد آنها توضیح خواهیم داد. از آنجایی که جزئیات در مورد Paging زیاد است در این آموزش فقط به بخش هایی از این موضوع می پردازیم.
در جدول زیر مشخصاتی که نشاه داده شده عبارتند از مقادیر بیت از رجیسترها برای فعال کردن Mode های Paging، و همچنین طول آدرس خطی، طول آدرس فیزیکی، اندازه Page، و یکسری مقادیر دیگر که خیلی برای آموزش ما مهم نیستند. (4-Level
منظور همان IA-32e Paging
است)
همانطور که می بینید از طریق بیت های PG در رجیستر CR0، و PAE در رجیستر CR4، و LME در رجیستر IA32_EFER می توان Mode مورد نظر را فعال کرد. اگر آموزش اول را دیده باشید بخش سوییچ به IA-32e Mode در مورد این رجیسترها مختصری صحبت کرده بودیم. در آن مثال در واقع 4-Level
یا IA-32e Paging
فعال شده بود.
جداول PTE, PDE, PDPTE, PML4E
بالاتر اشاره کردیم که مشخصات Page که شامل دسترسی ها، آدرس فیزیکی، سطح اجرایی و ... می شد در یکسری جدول نگهداری شده اند. در هر Mode (مربوط به Paging) یک تعداد از این جدول ها مورد استفاده قرار می گیرد. در جدول زیر می توانید ارتباط هر Mode و جدول مورد استفاده را ببینید. این جداول یک ساختار طبقه ای (hierarchy) را تشکیل می دهند.
مثلا مشاهده می کنیم در هر Mode کدام جدول استفاده شده (با توجه به ستون Entry Name)
- در
32-Bit Paging
فقط جدول PTE و PDE استفاده شده است. - در
PAE Paging
جدول PTE, PDE و PDPTE استفاده شده است. - در
IA-32e (4-Level) Paging
جدول PTE, PDE, PDPTE و PML4E استفاده شده است.
بالاتر اشاره کردیم که جداول ساختار طبقه ای را ایجاد می کنند به این صورت که خانه های بعضی جداول به آدرس شروع جدول دیگر اشاره می کند تا در نهایت به Page برسد. برای مثال از جدول بالا می توان برداشت کرد که (با توجه به ستون Physical Address of Structure):
- در
32-Bit Paging
خانه های جدول PDE به یک جدول PTE اشاره می کنند. همچنین رجیستر CR3 به جدول PDE اشاره می کند که در واقع آدرس فیزیکی این جدول را در خود دارد. - در
PAE Paging
مانند مورد قبل خانه های جدول PDE به یک جدول PTE اشاره می کنند. خانه های جدول PDPTE به یک جدول PDE اشاره می کنند و در نهایت رجیستر CR3 آدرس فیزیکی شروع جدول PDPTE را در خود دارد. - در
IA-32e (4-Level) Paging
مانند مورد قبل به اضافه اینکه خانه های جدول PML4E به یک جدول PDPTE اشاره می کنند. همچنین رجیستر CR3 آدرس فیزیکی شروع جدول PML4E را نگه داشته است.
همانطور که از توضیحات می توان برداشت کرد در همه موارد CR3 آدرس فیزیکی شروع یکی از جدول ها با توجه به Mode در خود دارد. در ادامه ساختار این رجیستر را خواهیم دید.
همانطور که قبلا هم گفتیم جزئیات جدول ها زیاد است و در این آموزش خیلی کوتاه به آنها اشاره میکنیم برای گرفتن اطلاعات بیشتر به مستدات معرفی شده در این لینک رجوع کنید.
رجیستر CR3
رجیستر CR3 آدرس فیزیکی شروع بالاترین جدول در مدیریت حافظه Paging را در خود نگه می دارد که پردازنده در تبدیل یک آدرس منطقی به آدرس فیزیکی از این نقطه شروع می کند. در هر Mode جدولی که CR3 به آن اشاره میکند متفاوت خواهد بود (بالاتر به این موضوع اشاره کردیم). در ادامه ساختاری که این رجیستر دارد را می توانیم بررسی کنیم. همانطور که مشاهده میکنید آدرس شروع جدول در بیت 12 تا 31 نگه داری می شود. باقی بیت های آدرس صفر در نظر گرفته می شوند. دلیل اینکه آدرس از بیت ۱۲ شروع شده این است که آدرس مضربی از 4 KB
بشود.
تبدیل آدرس منطقی به آدرس فیزیکی در Paging
در تبدیل آدرس منطقی به فیزیکی در Paging بخشی از مراحل مشابه روش Segmentation است یعنی را رسیدن به آدرس خطی با این تفاوت که بجای اینکه آدرس خطی عینا آدرس فیزیکی در نظر گرفته بشود اتفاق دیگری می افتد. قبل تر گفتیم اگر Paging فعال باشد یک تعداد جدول جدید خواهیم داشت با توجه به Paging Mode. اینکه کدام خانه ها از این جداول خوانده شود از طریق بخش هایی از آدرس خطی بدست آمده انجام می شود. در این آموزش برای کاهش پیچیدگی مساله فقط یکی از Mode را توضیح می دهم چون برای دیگر Mode ها خیلی شبه همین حالت هستند.
در مد 32-Bit Paging
چیدمان بیت های آدرس خطی بعد از تبدیل از آدرس منطقی به صورت زیر است.
شروع بیت | پایان بیت | توضیح |
0 | 11 | مقدار آفست داخل Page |
12 | 21 | مقدار index به جدول PTE |
22 | 31 | مقدار index به جدول PDE |
به صورت یک شکل بخواهیم کل موضوع تبدیل آدرس منطقی به آدرس فیزیکی در 32-Bit Paging Mode
را نمایش دهیم به صورت زیر می شود که از آدرس منطقی شروع به آدرس خطی می رسد و هر بخش آدرس خطی index ای می شود به جدول های PDE و PTE و در نهایت اشاره به افستی در Page بدست آمده می شود.
با توجه به تعداد بیت ها برای index به جدول مورد بحث، می توان تعداد خانه های جداول را حساب کرد. برای مثال همین شکلی که بالاتر نمایش دادیم در 32-Bit Paging Mode
برای جداول PTE و PDE برای حالتی که Page برابر 4 KB
باشد تعداد 10 بیت در نظر گرفته شده است که به عبارتی 1024 خانه در هر جدول می شود (در واقع 210 ). در نتیجه دو جدول داریم که هر کدام 1024 خانه دارند و اندازه هر Page هم 4096 بایت است که ضرب این اعداد می شود 1024 * 1024 * 4096 = 4 GB
به این طریق 4 GB
حافظه را میتوان آدرس دهی کرد. لازم به ذکر است که با توجه به اندازه Page اندازه بیت ها در هر Mode متفاوت می شود. برای اطلاعات بیشتر دراین زمینه به مستندات پردازنده ها رجوع کنید.
ساختار جداول PTE و PDE
قبلتر در مورد جداول PTE, PDE, PDPTE, PML4E صحیت کردیم. در مورد ساختار جداول من فقط مختصری در مورد PDE و PTE توضیح میدهم در حالت 32-Bit Paging Mode
که اندازه Page برابر 4 KB
است. دلیل این کار جزییات زیاد این جداول است که به نظرم وبلاگ مکان مناسبی برای مطرح کردن این همه جزئیات نیست و از آنجایی که هدف من بیشتر آموزش است با آوردن جزئیات خیلی زیاد از این هدف دور می شوم. بهترین راه برای یادگیری تمام موضوع مطالعه مستندات مربوط به است.
در شکل های زیر ساختار جدول PTE و PDE را می توانید ببینید. (که خیلی شبیه هم هستند فقط در برخی بیت ها با هم فرق دارند)
توضیح برخی بیت ها (مربوط به هر دو جدول)
- P: این بیت مشخص می کند که Page در حافظه قرار گرفته است و قابل استفاده است.
- R/W: دسترسی به Page را مشخص می کند. اگر صفر باشد دسترسی نوشتن وجود ندارد. اگر ک باشد دسترسی خواندن و نوشتن داریم
- U/S: این بیت مشخص میکند Page مربوط به User-Mode است یا Supervisor که این موضوع از دید سیستم عامل سطح کاربر و سطح کرنل می شود که مختصری در آموزش اول بخش Privilege و مساله Ring ها توضیحاتی داده بودم. این بیت اگر یک باشد یعنی User-Mode در غیر این صورت Supervisor است.
- PWT:
- PCD:
- A:
- PS:
- Avail:
- Page Table Base Address: اشاره به آدرس فیزیکی Page مربوط به جدول PTE
فعال کردن Paging
برای فعال کردن Paging مراحل زیر باید طی شوند. در ادامه برای گفتن "بیت x از رجیستر Y" از عبارت Y.x استفاده می کنیم
- پردازنده باید در Protected Mode باشد که قبلا در موردش توضیح داده ایم.
- مقدار دقی رجیستر CR3 به آدرس فیزیکی جدول مورد نظر با توجه به Mode ای که برای Paging قرار است داشته باشیم. یعنی یکی از حالت های
32-Bit Paging
یاPAE Paging
یاIA-32e Paging
. - فعال کردن Mode مورد نظر به همراه بیت PG در رجیستر CR0.
- برای فعال کرد
32-Bit Paging
فقط کافی است بیت CR0.PG یک شود. - برای فعال کردن
PAE Paging
باید هم بیت CR4.PAE و CR0.PG هر دو یک شوند. (نمی توانیم CR0.PG را زودتر از CR4.PAE یک کنیم) - برای فعال کردن
IA-32e (4-Level) Paging
باید سه بیت CR0.PG و CR4.PG و IA32_EFER مقدارشان یک شود. به ترتیب اول IA32.EFER بعد CR4.PAE و در آخر CR0.PG باید مقدارشان یک شود.
این بخش از کد freebsd برای فعال کردن Paging مورد استفاده قرار گرفته است. البته کد freebsd ویژگی های دیگری را هم فعال می کند که در این آموزش به آنها اشاره ای نکردم مثل فعال کردن PSE و PGE و VME در صورتی که کرنل بصورتی کانفیگ شده باشد که این موارد فعال باشند.
مسیر کد مورد بحث: freebsd/sys/i386/i386/mpboot.s
#if defined(PAE) || defined(PAE_TABLES) movl R(IdlePDPT), %eax movl %eax, %cr3 //* مرحله دوم */ movl %cr4, %eax orl $CR4_PAE, %eax /* مرحله سوم */ movl %eax, %cr4 #else movl R(IdlePTD), %eax movl %eax,%cr3 #endif movl %cr0,%eax orl $CR0_PE|CR0_PG,%eax /* ادامه مرحله سوم */ movl %eax,%cr0
به ترتیب فعال کردن بیت ها توجه کنید.
پایان
- ۹۶/۱۲/۲۷
- ۲۴۱۳ نمایش