تفکرات صفر و یکی

نوشته های من در مورد سیستم عامل، درایور نویسی، مهندسی معکوس، امنیت و هر چیز سطح پایین دیگر

تفکرات صفر و یکی

نوشته های من در مورد سیستم عامل، درایور نویسی، مهندسی معکوس، امنیت و هر چیز سطح پایین دیگر

آخرین نظرات

این آموزش در ادامه سری آموزش های "راهنمای معماری های x86 و x86_64 برای برنامه نویسان سیستمی" می باشد. در این قسمت در مورد مباحثی صحبت می کنیم که پیش نیاز برای آموزش های بعدی می باشد. این مباحث عبارت اند از رجیسترهای سیستمی، مدهای عملکرد، و سطوح دسترسی. 

یکسری موضوعات که قبل از ادامه مطلب باید به آنها اشاره کنم:

  • وقتی به اصطلاحات x86 و x86_64 اشاره می کنم، در واقع به ترتیب صحبت از خانواده پردازنده های معماری ۳۲ بیتی و ۶۴ بیتی می کنیم. از آنجایی که در برخی از سایتها از اصطلاح x86 برای اشاره به پردازنده های ۶۴ بیتی نیز استفاده شده، من برای اینکه ابهام ایجاد نشود از این به بعد از "معماری ۳۲ بیتی" و "معماری ۶۴ بیتی" به جای x86 و x86_64 استفاده میکنم.
  • از آنجایی که تفاوت های جزئی در ساختارها و نام گذاری برخی رجیسترها (خصوصا بخش های سیستمی) در پردازنده های Intel و AMD وجود دارد. من در این آموزش ها فقط روی پردازندهای شرکت Intel و مستندات ارائه شده  توسط این شرکت تمرکز میکنم.

سرفصل های این آموزش

مدهای عملکرد (Modes of Operation)

مدهای عملکرد در پرازنده های اینتل:

مد آدرس واقعی (Real-address Mode)

وقتی کامپیوتر خود را روشن یا ریست می کنیم اولین مدی که داخلش قرار می گیریم Real Mode است. این مدی است که کدهای ۱۶ بیتی میتوانند داخلش اجرا شوند مثل کد برنامه های DOS و BIOS و مربوط به دوره ای می شود که پردازنده 8086 وجود داشت، و همچنان با تفاوت اندکی در پردازنده های امروزی نیز پشتیبانی می شود.

ویژگی های Real Mode:

  • اندازه رجیسترهای پردازنده در این مد ۱۶ بیت است.
  • آدرس ها ۲۰ بیتی و آدرسی دهی به صورت  "منطقی" است (ترکیب آفست و سگمنت به صورت segment:offset). با آدرس دهی به صورت منطقی می توان 220 = 1M ناحیه از حافظه را آدرس دهی کرد. فرمول محاسبه آدرس فیزیکی بر اساس آدرس منطقی به صورت ((segment << 4) + offset) که در واقع از شیفت کردن segment به اندازه ۴ بیت به سمت چپ و جمع شدن آن با offset به دست می آید.
  • در این مد هیچ امکان محافظتی روی نواحی حافظه نداریم. در نتیجه هر برنامه ای می تواند به تمام ناحیه حافظه (شامل حافظه مربوط به سیستم عامل) دسترسی داشته باشد و هر تغییری می خواهد بدهد. 
  • در این مد امکان چند وظیفگی (Multitasking) وجود ندارد.

هانطور که گفتم این مد کمی با مدی که در پردازنده های امروزی استفاده می شود فرق دارد. در این مد تغییر کرده امکان دسترسی به حافظه بیشتر از 1M وجود دارد (در این حالت همچنان از آدرس دهی منطقی استفاده می شود). در واقع تکنیکی وجود دارد که می توان از امکانات مدیریت حافظه در مد محافظه شده (Protected Mode) بهره برد و به نواحی دیگر حافظه دسترسی پیدا کرد. اگر با اصطلاحاتی مثل Voodoo Mode یا Unreal Mode روبرو شدید بدانید منظور همین Real Mode تغییر کرده در پردازنده های امروزی است. 

مد محافظت شده (Protected Mode)

بعد از Real Mode که بالا اشاره کردیم اولین کاری که یک سیستم عامل (۳۲ بیتی و ۶۴ بیتی) انجام می دهد سوییچ شدن به Protected Mode است. در این مود خیلی از محدودیت هایی که در Real Mode وجود داشت دیگر جود ندارد.

برخی ویژگی های Protected Mode:

  • اندازه رجیسترها در این مد ۳۲ بیت است.
  • آدرس ها ۳۲ بیتی و آدرس دهی بصورت "منطقی" است. تبدیل آدرس منطقی به فیزیکی در این مد با Real Mode فرق دارد، جزئیات این تبدیل را در آموزش های جلوتر توضیح می دهم.
  • در این مد مدیریت حافظه پیشرفته تری نسبت به Real Mode وجود دارد و سیستم عامل (یا حالا هر نرم افزاری که در این سطح اجرا می شود)‌ می تواند به دو طریق از امکانات مدیریت حافظه در این معماری بهره ببرد این دو روش عبارت اند از Segmentation و Paging
  • در این مد امکان محافظت از ناحیه های حافظه وجود دارد. با امکانات داده شده در این مد می توان برای مثال سطوح کاربر و کرنل را در یک سیستم عامل تعیین کرد، یا نواحی از حافظه را به عنوان کد (Code) و داده (Data) تعریف کرد.
  • در این مد امکان چند وظیفگی (Multitasking) وجود دارد.

چطور می توان به مد محافظت شده سوییچ کرد؟

در پست قبلی گفته بودم برای برخی مباحث از سورس FreeBSD برای شرح بهتر مساله استفاده میکنم. به همین جهت کدهایی که مربوط به فعال کردن Protected Mode است را اینجا قرار داده ام.

برای سوییچ شدن به Protected Mode به ترتیب کارهای زیر باید انجام شوند:

  1. غیر فعال کردن وقفه های maskable با دستور cli، 
  2. بارگذاری اطلاعات جدول GDT که با دستور LGDT انجام می شود. (این مساله مربوط به مدیریت حافظه است که در آموزش بعدی به این موضوع می پردازم)
  3. سوییچ شدن به Protected Mode با تغییر مقدار بیت PE از رجیستر CR0 به یک (جلوتر ساختار این رجیسترها توضیح داده شده است)
  4.  پرش به کدی که برای Protected Mode نوشته شده
  5. و در نهات فعال کردن وقفه ها با دستور sti

همانطور که می بینید قبل سوییچ شدن یکسری کار دیگر هم انجام می شود که غیر فعال کردن وقفه ها و مقداردهی جدول GDT است. برای اینکه بتوان یک محیط قابل استفاده داشت بعد سوییچ شدن به Protected Mode باید خیلی چیزهای دیگر هم مقدار دهی شوند ولی فعلا به آن موضوعات نمی پردازم.

مسیر کد freebsd/sys/i386/i386/mpboot.s

NON_GPROF_ENTRY(bootMP)
	.code16		
	cli                                      /*** مرحله ۱  ***/
	CHECKPOINT(0x34, 1)
	/* First guarantee a 'clean slate' */
	xorl	%eax, %eax
	movl	%eax, %ebx
	movl	%eax, %ecx
 	movl	%eax, %edx
	movl	%eax, %esi
	movl	%eax, %edi

	/* set up data segments */
	mov	%cs, %ax
	mov	%ax, %ds
	mov	%ax, %es
	mov	%ax, %fs
	mov	%ax, %gs
	mov	%ax, %ss
	mov	$(boot_stk-bootMP), %esp

	/* Now load the global descriptor table */
	lgdt	MP_GDTptr-bootMP               /*** مرحله ۲ ***/

	/* Enable protected mode */
	movl	%cr0, %eax 
	orl	$CR0_PE, %eax                 /*** مرحله ۳ ***/
	movl	%eax, %cr0

	/*
	 * make intrasegment jump to flush the processor pipeline and
	 * reload CS register
	 */
	pushl	$0x18
	pushl	$(protmode-bootMP)
	lretl                              /*** مرحله ۴ ***/

کد بالا یکسری کارهای اضافی دیگه مثل مقدار دهی رجیسترها و سگمنت های عمومی هم کرده است. 

مد 8086 مجازی (Virtual-8086 Mode)

Virtual-8086 Mode مدی است که وقتی داخل Protected Mode هستیم می توانیم به آن سوییچ کنیم. این مد در واقع محیطی معادل Real Mode ایجاد می کند و این امکان را می دهد تا همزمان برنامه های نوشته شده برای Protected Mode و برای Real Mode در کنار هم در Protected Mode اجرا شوند. برای مثال در سیستم عامل ویندوز در صورت اجرای یک برنامه تحت DOS یک پروسه به نام ntvdm.exe اجرا می شود که در واقع ماشین مجازی فایل های ۱۶ بیتی است که از Virtual-8086 Mode برای فراهم کردن این قابلیت استفاده کرده است.

برای سورییچ شدن به این مد کافیه مقدار VM از رجیستر EFLAGS به یک مقدار دهی بشود. یک نکته در مورد سوییچ شدن به این مد این است که مقدار EFLAGS.VM را نمی توان حین اجرای یک برنامه تغییر داد و حتما باید در هنگام ایجاد یک Task انجام شوم.

مد مدیریت سیستم (System Management Mode)

این مدی است که فقط به درد توسعه دهنده های firmware میخورد و کاربردهایش بیشتر مربوط به سخت افزار می شود مثل مدیریت پاور، نظارت بر عملکرد سخت افزارها و در مواقعی تغییر وضیعتشان. و پردازنده در صورت به وجود آمدن وقفه SMI (System Management Interrupt) به این مد سوییچ می کند. SMM این ویژگی ها را دارد:

  • وقتی پردازنده به این مود سوییچ شده باشد هیچ کد دیگری (که شامل سیستم عامل هم می شود) حق اجرا و متوقف کرد این مد را ندارند. به طور دقیقتر هیچ وقفه ای چه نرم افزاری،‌ سخت افزاری، حتی وقفه های non-maskable مانند وقفه NMI (Non-maskable Interrupt) هم نمی توانند وقفه ای در اجرای کد داخل SMM بیاندازند.
  • ناحیه حافظه مربوط به کد SMM از دید سیستم عامل پنهان و محفوظ است در نتیجه سیستم عامل به صورت معمول (در صورتی که توسط firmware قفل شده باشد) هیچ دسترسی (نه خواندن و نه نوشتن) به ناحیه حافظه SMM را ندارد. از دید سیستم عامل و نرم افزارهای داخل آن انگار که اصلا چنین ناحیه حافظه ای وجود ندارد. لازم به ذکر است که داخل پردازنده یک رجیستر (SMRAMC1) وجود دارد که می توان دسترسی ناحیه SMM را تنظیم کرد که در سیستم های جدید تر این ناحیه با احتمال بالایی توسط firmware قفل شده است (حداقل تا جایی که من اطلاع دارم).

همانطور که بالاتر اشاره کردم این مد خیلی بدرد یک برنامه نویس سیستمی سطح سیستم عامل ممکنه نخورد،‌ ولی با این حال به نظرم داشتن دانش در موردش لازمه به خاطر اینکه

  • اجرا شدن کد در این مد زمانی از پردازنده می برد و سیستم عامل هیچ اختیاری برای کنترل SMM ندارد. این مساله می تواند روی کارایی سیستم عامل خصوصا سیستم عامل های real-time تاثیر بگذارد.
  • اشاره کردیم که یکی از کارهای SMM ایجاد یکسری تغییر در وضعیت سخت افزارها است. این تغییرات گاهی برای هماهنگ کردن سخت افزارهای قدیمی با سیستم های امروزی است. این تغییرات می تواند یکسری ناهماهنگی ها را با سیستم عامل نیز ایجاد کند و باعث کرش سیستم شود.

مد IA-32e 

مد IA-32e  یک مد توسعه یافته تر مدهای موجود در معماری ۳۲ بیتی است و تقریبا تمام امکانات مد ۳۲ بیتی را دارد. این مود خود به دو مد دیگر تقسیم شده مد سازگاری (Compatibility Mode) و مد ۶۴ بیتی (64-bit Mode) است. در این مد سیستم عامل باید ۶۴ بیتی باشد. در ادامه در مورد مدها توضیح خواهم داد. 

  • مد سازگاری: این مد برای این وجود دارد که بتوان برنامه های ۳۲ بیتی که برای Protected Mode و ۱۶ بیتی که برای Real Mode نوشته شده اند را در کنار برنامه های ۶۴ بیتی بطور همزمان اجرا کرد. در این حالت برنامه های ۳۲ بیتی و ۱۶ بیتی همان ویژگی های خود در مدهای Protected Mode و Real Mode را همچنان دارند. در مد سازگاری امکان اجرای برنامه های ۶۴ بیتی وجود دارد که اولا، آدرس ها در این مد ۶۴ بیتی می شود، رجیسترهای عمومی بیشتری وجود دارد. دستورهای جدیدتر و معماری هم کمی تغییر می کند. در آموزش های بعدیم به برخی از این تغییرات اشاره می کنم.
  • مد ۶۴ بیتی: در این مد فقط برنامه های ۶۴ بیتی امکان اجرا شدن دارند.

نکته: با وجودی که می گوییم معماری ۶۴ بیتی است ولی از سمت سخت افزار پردازنده فقط ۵۲ بیت را می تواند آدرس دهی کند. و از طرفی آدرس مجازی که قابل دسترسی است ۴۸ بیت است و بیت ۴۸ در بیت های ۴۸-۶۳ کپی می شود. به این نوع آدرس دهی Canonical Addresses گفته می شود.

چطور می توان به IA-32e Mode سوییچ کرد؟

برای سوییچ شدن به IA-32e اول باید در Protected Mode بود بعد از این مد به IA-32e سوییچ کرد. مراحل به صورت زیر است.

  1. سوییچ شدن به Protected Mode در صورتی که در این مد نباشیم
  2. غیر فعال کردن Paging در صورت فعال بودن (مقدار بیت PG از رجیستر CR0 به صفر تغییر کند)
  3. فعال کردن PAE (Physical Address Extensions) (مقدار بیت PAE از رجیستر CR4 به یک تغییر کند)
  4. مقدار دهی رجیستر CR3 با جدول PML4 (مربوط به مدیریت حافظه که در آموزش بعدی توضیح می دهم)
  5. سوییچ شدن به  IA-32e Mode با مقدار دادن بیت LME از رجیستر IA32_EFER به مقدار یک
  6. فعال کردن Paging 

کدی که به IA-32e سوییچ می کند. مسیر کد freebsd/sys/amd64/amd64/mpboot.S

...
... /*** مرحله ۱و۲ ***/
/* * At this point, we are running in 32 bit legacy protected mode. */ .code32 protmode: mov $bootdata-gdt, %eax mov %ax, %ds /* Turn on the PAE bit for when paging is enabled */ mov %cr4, %eax orl $CR4_PAE, %eax mov %eax, %cr4 /*** مرحله ۳ ***/ /* * Enable EFER.LME so that we get long mode when all the prereqs are * in place. In this case, it turns on when CR0_PG is finally enabled. * Pick up a few other EFER bits that we'll use need we're here. */ movl $MSR_EFER, %ecx rdmsr orl $EFER_LME | EFER_SCE, %eax wrmsr /*** مرحله ۴ ***/ /* * Point to the embedded page tables for startup. Note that this * only gets accessed after we're actually in 64 bit mode, however * we can only set the bottom 32 bits of %cr3 in this state. This * means we are required to use a temporary page table that is below * the 4GB limit. %ebx is still our relocation base. We could just * subtract 3 * PAGE_SIZE, but that would be too easy. */ leal mptramp_pagetables-mptramp_start(%ebx),%eax movl (%eax), %eax mov %eax, %cr3 /*** مرحله ۵ ***/ /* * Finally, switch to long bit mode by enabling paging. We have * to be very careful here because all the segmentation disappears * out from underneath us. The spec says we can depend on the * subsequent pipelined branch to execute, but *only if* everything * is still identity mapped. If any mappings change, the pipeline * will flush. */ mov %cr0, %eax orl $CR0_PG, %eax /*** مرحله ۶ ***/ mov %eax, %cr0

سطوح دسترسی (Privilege Levels)

برای محافظت از ناحیه های حافظه و همچنین محدود کردن اجرای دستورات سیستمی چهار سطح دسترسی وجود دارد که به صورت Ring0-Ring3 نامگذاری شده اند. هر چی عدد سطح بزرگتر باشد دسترسی کمتر است. در نتیجه سطح Ring3 دسترسی کمتری نسبت به Ring0 دارد. این سطوح را میتوان به صورت شکل زیر ترسیم کرد. هر چی از حلقه مرکزی به بیرون بیاییم دسترسی نیز کمتر می شود. 

سطوح دسترسی

در سیستم عامل های امروزی فقط از دو تا از این سطوح استفاده می شود Ring0 و Ring3 که از دید سیستم عامل به این سطوح به ترتیب سطح کرنل و سطح کاربر گفته می شود. حالا این ها که گفیم اصلا به چه دردی می خورد. با استفاده از این سطوح سیستم عامل می تواند مشخص کند که چه ناحیه از حافظه متعلق به سطح کاربر است و چه ناحیه هایی مربوط به کرنل. بعد از این عمل پردازنده در اجرای دستورات (مثلا دستوری از Ring3) سطح دسترسی ناحیه حافظه را بررسی می کند اگر دستور سعی کند  آدرسی (مثلا آدرسی از Ring0) را که مربوط به حلقه پایین تر باشد را بخواند یا بنویسد که در این صورت دسترسی کافی ندارد پردازنده  اجازه ادامه اجرا به کد نمی دهد و اصطلاحا یک exception تولید میکند. اینجوری امنیت سیستم و کد های سیستم عامل تامین می شود. 

اینجا چهار سطح وجود دارد چرا از سطوح دیگر استفاده نمی شود؟ قبلا گفته بودیم به دو طریق می توان حافظه را مدیریت کرد Segmentation و Paging. در حالت Segmentation امکان استفاده از هر چهار سطح وجود دارد. ولی در حالت Paging فقط امکان استفاده از دو سطح را داریم. از آنجایی که سیستم عامل های امروزی اکثرا از Paging استفاده می کنند که داخل ساختارهای مربوط به این روش فقط استفاده از دو سطح در نظر گرفته شده است، به همین دلیل دو سطح دیگر استفاده نمی شود.

اصلا چرا این سطوح اضافی معرفی شدند؟ وجود سطح اضافه این امکان را به سیستم عامل میدهد تا بخش هایی از کد خود را که احتمال بروز خطا در آن بیشتر است یا امنیت پایینتری دارد را در سطوحی با دسترسی پایین تر اجرا کند. برای مثال می توان گفت تقریبا 70% تا 80% یک سیستم عامل مربوط می شود به درایورهای مربوط به ارتباط با دیوایس ها. در حالتی که سیستم عامل از دو سطح استفاده کند و در صورت سوء استفاده از یک باگ داخل یکی از درایورها امنیت کل سیستم به خطر میافتد به دلیل اینکه در سیستم عامل های امروزی کد درایورها هم در سطح هسته سیستم عامل یعنی Ring0 اجرا می شوند. در حالی که با داشتن یک سطح اضافه می شود کد درایورها را در سطح با دسترسی پایین تر اجرا کرد و به این صورت سیستم عامل نسبت به سوء استفاده از باگ درایور مصون می ماند. این ویژگی خوبه ولی سیستم عامل های امروزی هیچ کدوم از این ویژگی برخوردار نیستن و دلیلش هم بیشتر بخاطر کارایی (performance) سیستم است به دلیل سوییچ های زیادی که بین سطح ها اتفاق می افتد. اگر اطلاعات بیشتر در این زمینه می خواهید می توانید در مورد سیستم عامل های با معماری Microkernel تحقیق کنید. لازم به ذکر است که برخی سیستم عامل ها معماری هسته ترکیبی دارند که اصطلاحا Hybrid Kernel Architecture نامیده می شوند. در این حالت امکان نوشتن درایور سطح کاربر نیز وجود دارد، ولی همچنان خیلی از درایورهای کلیدی در همان سطح کرنل اجرا می شوند. از جمله این سیستم عامل ها می توان به ویندوز ورژن ویستا به بعد اشاره کرد.

نکته: بالاتر در مورد مد SMM توضیح دادم این مد از Ring0 که در واقع سطح سیستم عامل می شود دسترسی بالاتری دارد و بصورت غیر رسمی به "Ring -2" (حلقه ی منهای دو)معروف است. 

رجیسترهای سیستمی

پردازنده های معماری های ۳۲ بیتی و ۶۴ بیتی رجیسترهای سیستمی مختلفی دارند. در این آموزش من فقط در مورد رجیستر های CR0-CR4 و یکی از رجیسترهای MSR به نام IA32_EFER صحبت می کنم که در این آموزش از آنها مثال زدم. 

رجیستر های کنترل (Control Registers)

در شکل زیر این رجیسترها را می توانید ببینید. خانه هایی که خاکستری هستند رزرو می باشند و نباید داخل آنها مقداری نوشته شود.

Control Registers

این رجیسترها در همه مدهای مربوط به معماری ۳۲ بیتی و مد سازگاری به اندازه ۳۲ بیت است. و در مد ۶۴ بیت به اندازه ۶۴ بیت است.

من در مورد همه بیت ها توضیح نمی دهم و برای توضیحات بیشتر لازمه مستندات پردازنده را ببینید. 

CR0: این رجیستر یکسری وضعیت های مربوط به پردازنده را نگه می دارد. در تمام مقادیر یک به معنی فعال کردن و صفر به معنی غیر فعال می باشد.

  • PE (Protection Enable): این بیت برای فعال کردن Protected Mode است.
  • PG (Paging): برای فعال کردن Paging می باشد 

CR1: رزرو

CR2: در صورت بروز خطای page-fault این رجیستر آدری خط دستوری که خطا را ایجاد کرده نگه می دارد.

CR3: در مدیریت حافظه Paging این رجیستر آدرس فیزیکی اولین جدول را در خود نگه می دارد (توضیحات بیشتر در آموزش بعدی)

CR4: این رجیستر برای فعال یا غیر فعال کردن یکسری امکانات ارائه شده توسط پردازنده است.

  • PAE (Physical Address Extension): این بیت اگر فعال باشد در حالت Paging ادرس های فیزیکی ۳۶ بیت خواهیم داشت. قبل از سوییچ شدن به IA-32e نیز این بیت باید فعال شود.

رجیسترهای MSR

پردازنده های ذکر شده تعداد خیلی زیادی رجیستر به نام MSR دارند. این رجیسترها برای تغییر یا گرفتن یکسری وضعیت ها از پردازنده است. ما در این آموزش از یکی از رجیسترها به نام IA32_EFER استفاده کردیم. به هر کدام از این رجیسترها یک عدد نسبت داده شده.

  • برای خواندن از این رجیسترها اول آن عدد خاص باید داخل رجیستر ECX ریخته شود و بعد با استفاده از دستور RDMSR مقدار رجیستر MSR را خواند. مقدار خوانده شده داخل رجیستر EDX:EAX ریخته می شود.
  • برای نوشتن هم برعکس همین کار مقداری که می خواهیم داخل رجیستر بریزیم داخل EDX:EAX ریخته می شود. و عدد رجیستر داخل ECX و بعد با دستور WRMSR مقدار را داخل رجیستر می ریزیم.

در مثالی که بالا ذکر کردم مقدار رجیستر IA32_EFER برابر مقدار هگزادسیمال 0xc0000080  است و برای فعال کردن IA-32e مقداربیت 8 که IA-32e Mode Enable است را تغییر داده ایم. در تصویر زیر ساختار این رجیستر و مقدار آن قابل مشاهذه است.

IA32_EFER

پایان


1. اطلاعات در مورد SMRAMC در مستندات (datasheet volume 2) ارائه شده که از این لینک قابل دسترسی هستند

نظرات (۳)

  • عماد رضوانی
  • به مطالب خوب بالا اضافه کنم که در گذشته پروسه های مدکاربر برای اینکه بتوانند با مد هسته ارتباط برقرار کنند از وقفه 2E استفاده می کردند. در در معماری های جدید دیگر از این وقفه استفاده نمی شود و بجای اون از دستور SYSENTER استفاده می کنند. توضیحات بیشتر در لینک http://wiki.osdev.org/SYSENTER موجود هست. نکته دیگه اینه که با همین دستورالعمل های خواندن و نوشتن در این ثبات میشه وقفه های سیستمی رو هوک کرد و ... اونایی که باید بگیرند میگیرند !
    پاسخ:
    ممنون بابت توضیحات
    جالبه هنوز کدهای مربوط به وقفه 2E داخل dll ها هست. 
    تو معماری ۶۴ بیتی هم دستور جدید SYSCALL اضافه شده به جای SYSENTER
    در مورد هوک هم ... :)
    سلام ممنون خیلی خوب بود!
    اگه میشه ادامه بدید!
    تشکر مطلب عالی ای بود

    ارسال نظر آزاد است، اما اگر قبلا در بیان ثبت نام کرده اید می توانید ابتدا وارد شوید.
    شما میتوانید از این تگهای html استفاده کنید:
    <b> یا <strong>، <em> یا <i>، <u>، <strike> یا <s>، <sup>، <sub>، <blockquote>، <code>، <pre>، <hr>، <br>، <p>، <a href="" title="">، <span style="">، <div align="">
    تجدید کد امنیتی